use std::sync::Arc;
use gpui::{
AnyElement, App, AppContext, Bounds, Context, Corner, DismissEvent, Div, DragMoveEvent, Empty,
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
WeakEntity, Window, div, prelude::FluentBuilder,
};
use super::{
super::{
button::{Button, ButtonVariants as _},
menu::{DropdownMenu, PopupMenu},
tab::{Tab, TabBar},
},
ClosePanel, DockArea, Panel, PanelControl, PanelEvent, PanelInfo, PanelState, PanelStyle,
PanelView, StackPanel, ToggleZoom,
};
use crate::{
ActiveTheme, AxisExt, Disableable, Divider, DockPlacement, Icon, IconLabel, IconName, Placement,
Selectable, Size, StyleSized, TabBarDirection, Tooltip, h_flex, translate_woocraft, v_flex,
};
#[derive(Clone)]
struct TabState {
closable: bool,
zoomable: Option<PanelControl>,
draggable: bool,
droppable: bool,
active_panel: Option<Arc<dyn PanelView>>,
}
#[derive(Clone)]
pub(crate) struct DragPanel {
pub(crate) panel: Arc<dyn PanelView>,
pub(crate) tab_panel: Entity<TabPanel>,
size: Size,
}
impl DragPanel {
pub(crate) fn new(panel: Arc<dyn PanelView>, tab_panel: Entity<TabPanel>) -> Self {
Self {
panel,
tab_panel,
size: Size::Medium,
}
}
}
impl_sizable!(DragPanel);
impl Render for DragPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("drag-panel")
.cursor_grab()
.container_py(self.size)
.w_48()
.text_ellipsis()
.whitespace_nowrap()
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.text_color(cx.theme().tab_foreground)
.bg(cx.theme().tab_active)
.opacity(0.75)
.child(
IconLabel::new("drag-panel-label")
.icon(Icon::new(self.panel.icon(cx)))
.label(self.panel.title(cx)),
)
}
}
pub struct TabPanel {
focus_handle: FocusHandle,
dock_area: WeakEntity<DockArea>,
dock: Option<WeakEntity<super::Dock>>,
stack_panel: Option<WeakEntity<StackPanel>>,
pub(crate) panels: Vec<Arc<dyn PanelView>>,
pub(crate) active_ix: usize,
pub(crate) closable: bool,
tab_bar_scroll_handle: ScrollHandle,
zoomed: bool,
will_split_placement: Option<Placement>,
in_tiles: bool,
}
impl Panel for TabPanel {
fn panel_name(&self) -> &'static str {
"TabPanel"
}
fn title(&self, cx: &App) -> SharedString {
self
.active_panel(cx)
.map(|panel| panel.title(cx))
.unwrap_or_else(|| "Empty Tab".into())
}
fn icon(&self, cx: &App) -> IconName {
self
.active_panel(cx)
.map(|panel| panel.icon(cx))
.unwrap_or(IconName::Grid)
}
fn closable(&self, cx: &App) -> bool {
if !self.closable {
return false;
}
if !self.in_tiles && !self.closable_by_layout(cx) {
return false;
}
self
.active_panel(cx)
.map(|panel| panel.closable(cx))
.unwrap_or(false)
}
fn zoomable(&self, cx: &App) -> Option<PanelControl> {
self.active_panel(cx).and_then(|panel| panel.zoomable(cx))
}
fn visible(&self, cx: &App) -> bool {
if self.panels.is_empty() {
return true;
}
self.visible_panels(cx).next().is_some()
}
fn dropdown_menu(
&mut self, menu: PopupMenu, window: &mut Window, cx: &mut Context<Self>,
) -> PopupMenu {
if let Some(panel) = self.active_panel(cx) {
panel.dropdown_menu(menu, window, cx)
} else {
menu
}
}
fn toolbar_buttons(
&mut self, window: &mut Window, cx: &mut Context<Self>,
) -> Option<Vec<Button>> {
self
.active_panel(cx)
.and_then(|panel| panel.toolbar_buttons(window, cx))
}
fn dump(&self, cx: &App) -> PanelState {
let mut state = PanelState::new(self);
for panel in self.panels.iter() {
state.add_child(panel.dump(cx));
state.info = PanelInfo::tabs(self.active_ix);
}
state
}
fn inner_padding(&self, cx: &App) -> bool {
self
.active_panel(cx)
.is_none_or(|panel| panel.inner_padding(cx))
}
}
impl TabPanel {
pub fn new(
stack_panel: Option<WeakEntity<StackPanel>>, dock_area: WeakEntity<DockArea>, _: &mut Window,
cx: &mut Context<Self>,
) -> Self {
Self {
focus_handle: cx.focus_handle(),
dock_area,
dock: None,
stack_panel,
panels: Vec::new(),
active_ix: 0,
tab_bar_scroll_handle: ScrollHandle::new(),
will_split_placement: None,
zoomed: false,
closable: true,
in_tiles: false,
}
}
pub(crate) fn set_dock(&mut self, dock: WeakEntity<super::Dock>) {
self.dock = Some(dock);
}
fn get_tab_bar_direction(&self, cx: &App) -> TabBarDirection {
if let Some(dock) = self.dock.as_ref().and_then(|d| d.upgrade()) {
return dock.read(cx).tab_bar_direction;
}
self
.dock_area
.upgrade()
.map(|dock_area| dock_area.read(cx).tab_bar_direction)
.unwrap_or_default()
}
fn is_dock_collapsed(&self, cx: &App) -> bool {
self
.dock
.as_ref()
.and_then(|d| d.upgrade())
.map(|dock| dock.read(cx).collapsed)
.unwrap_or(false)
}
pub(super) fn set_in_tiles(&mut self, in_tiles: bool) {
self.in_tiles = in_tiles;
}
pub(super) fn set_parent(&mut self, view: WeakEntity<StackPanel>) {
self.stack_panel = Some(view);
}
pub fn active_panel(&self, cx: &App) -> Option<Arc<dyn PanelView>> {
let panel = self.panels.get(self.active_ix);
if let Some(panel) = panel {
if panel.visible(cx) {
Some(panel.clone())
} else {
self.visible_panels(cx).next()
}
} else {
None
}
}
pub fn active_ix(&self) -> usize {
self.active_ix
}
fn panel_index_by_id(&self, panel_id: &str, cx: &App) -> Option<usize> {
self
.panels
.iter()
.position(|panel| panel.panel_id(cx) == panel_id)
}
pub(crate) fn panel_by_id(&self, panel_id: &str, cx: &App) -> Option<Arc<dyn PanelView>> {
self
.panel_index_by_id(panel_id, cx)
.and_then(|ix| self.panels.get(ix).cloned())
}
pub(crate) fn activate_panel_by_id(
&mut self, panel_id: &str, window: &mut Window, cx: &mut Context<Self>,
) -> bool {
let Some(ix) = self.panel_index_by_id(panel_id, cx) else {
return false;
};
self.handle_tab_click(ix, window, cx);
true
}
pub(crate) fn close_panel_by_id(
&mut self, panel_id: &str, dock_area_locked: bool, window: &mut Window, cx: &mut Context<Self>,
) -> bool {
let Some(ix) = self.panel_index_by_id(panel_id, cx) else {
return false;
};
if !self.closable_by_layout_with_dock_area_locked(dock_area_locked, cx) {
return false;
}
let Some(panel) = self.panels.get(ix).cloned() else {
return false;
};
if !panel.closable(cx) {
return false;
}
self.remove_panel(panel, window, cx);
true
}
fn set_active_ix(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
if ix == self.active_ix {
return;
}
let last_active_ix = self.active_ix;
self.active_ix = ix;
self.tab_bar_scroll_handle.scroll_to_item(ix);
self.focus_active_panel(window, cx);
cx.spawn_in(window, async move |view, cx| {
_ = cx.update(|window, cx| {
_ = view.update(cx, |view, cx| {
if let Some(last_active) = view.panels.get(last_active_ix) {
last_active.set_active(false, window, cx);
}
if let Some(active) = view.panels.get(view.active_ix) {
active.set_active(true, window, cx);
}
});
});
})
.detach();
cx.notify();
}
fn handle_tab_click(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
let is_dock_collapsed = self.is_dock_collapsed(cx);
let has_dock = self.dock.is_some();
if !has_dock {
if ix != self.active_ix {
self.set_active_ix(ix, window, cx);
}
return;
}
if is_dock_collapsed {
if let Some(dock) = self.dock.as_ref().and_then(|d| d.upgrade()) {
dock.update(cx, |dock, cx| {
dock.set_collapsed(false, window, cx);
});
}
if ix != self.active_ix {
self.set_active_ix(ix, window, cx);
} else {
cx.notify();
}
} else if ix == self.active_ix {
if let Some(dock) = self.dock.as_ref().and_then(|d| d.upgrade()) {
dock.update(cx, |dock, cx| {
dock.set_collapsed(true, window, cx);
});
}
} else {
self.set_active_ix(ix, window, cx);
}
}
pub fn add_panel(
&mut self, panel: Arc<dyn PanelView>, window: &mut Window, cx: &mut Context<Self>,
) {
self.add_panel_with_active(panel, true, window, cx);
}
fn add_panel_with_active(
&mut self, panel: Arc<dyn PanelView>, active: bool, window: &mut Window, cx: &mut Context<Self>,
) {
assert_ne!(
panel.panel_name(cx),
"StackPanel",
"can not allows add `StackPanel` to `TabPanel`"
);
if self
.panels
.iter()
.any(|p| p.view().entity_id() == panel.view().entity_id())
{
return;
}
panel.on_added_to(cx.entity().downgrade(), window, cx);
self.panels.push(panel);
if active {
self.set_active_ix(self.panels.len() - 1, window, cx);
}
cx.emit(PanelEvent::LayoutChanged);
cx.notify();
}
pub fn add_panel_at(
&mut self, panel: Arc<dyn PanelView>, placement: Placement, size: Option<Pixels>,
window: &mut Window, cx: &mut Context<Self>,
) {
cx.spawn_in(window, async move |view, cx| {
cx.update(|window, cx| {
view
.update(cx, |view, cx| {
view.will_split_placement = Some(placement);
view.split_panel(panel, placement, size, window, cx)
})
.ok()
})
.ok()
})
.detach();
cx.emit(PanelEvent::LayoutChanged);
cx.notify();
}
fn insert_panel_at(
&mut self, panel: Arc<dyn PanelView>, ix: usize, window: &mut Window, cx: &mut Context<Self>,
) {
if self
.panels
.iter()
.any(|p| p.view().entity_id() == panel.view().entity_id())
{
return;
}
panel.on_added_to(cx.entity().downgrade(), window, cx);
self.panels.insert(ix, panel);
self.set_active_ix(ix, window, cx);
cx.emit(PanelEvent::LayoutChanged);
cx.notify();
}
pub fn remove_panel(
&mut self, panel: Arc<dyn PanelView>, window: &mut Window, cx: &mut Context<Self>,
) {
self.detach_panel(panel, window, cx);
self.remove_self_if_empty(window, cx);
if self.panels.is_empty()
&& let Some(dock) = self.dock.as_ref().and_then(|d| d.upgrade())
{
dock.update(cx, |dock, cx| {
dock.set_collapsed(true, window, cx);
});
}
cx.emit(PanelEvent::ZoomOut);
cx.emit(PanelEvent::LayoutChanged);
}
fn detach_panel(
&mut self, panel: Arc<dyn PanelView>, window: &mut Window, cx: &mut Context<Self>,
) {
panel.on_removed(window, cx);
let panel_view = panel.view();
self.panels.retain(|p| p.view() != panel_view);
if self.active_ix >= self.panels.len() {
self.set_active_ix(self.panels.len().saturating_sub(1), window, cx)
}
}
fn remove_self_if_empty(&self, window: &mut Window, cx: &mut Context<Self>) {
if !self.panels.is_empty() {
return;
}
let tab_view = cx.entity().clone();
if let Some(stack_panel) = self.stack_panel.as_ref() {
if let Some(stack) = stack_panel.upgrade() {
let stack_ref = stack.read(cx);
if stack_ref.parent.is_none() && stack_ref.panels_len() <= 1 {
cx.notify();
return;
}
}
_ = stack_panel.update(cx, |view, cx| {
view.remove_panel(Arc::new(tab_view), window, cx);
});
return;
}
if self.dock.is_some() {
cx.notify();
}
}
pub(super) fn set_collapsed(
&mut self, collapsed: bool, window: &mut Window, cx: &mut Context<Self>,
) {
if let Some(panel) = self.panels.get(self.active_ix) {
panel.set_active(!collapsed, window, cx);
}
cx.notify();
}
fn dock_area_locked(&self, cx: &App) -> bool {
let Some(dock_area) = self.dock_area.upgrade() else {
return true;
};
dock_area.read(cx).is_locked()
}
fn is_locked(&self, cx: &App) -> bool {
self.is_locked_with_dock_area_locked(self.dock_area_locked(cx))
}
fn is_locked_with_dock_area_locked(&self, dock_area_locked: bool) -> bool {
if dock_area_locked {
return true;
}
if self.zoomed {
return true;
}
if self.in_tiles {
return true;
}
self.stack_panel.is_none() && self.dock.is_none()
}
fn allows_split_drop(&self) -> bool {
self.dock.is_none()
&& !self.panels.is_empty()
&& self
.stack_panel
.as_ref()
.and_then(|stack| stack.upgrade())
.is_some()
}
fn is_empty_tab_panel(&self) -> bool {
self.panels.is_empty()
}
fn clear_split_preview(&mut self, cx: &mut Context<Self>) {
self.will_split_placement = None;
if let Some(dock_area) = self.dock_area.upgrade() {
dock_area.update(cx, |dock_area, cx| dock_area.clear_split_preview(cx));
}
}
fn update_split_preview(
&mut self, bounds: Bounds<Pixels>, placement: Option<Placement>, cx: &mut Context<Self>,
) {
if self.will_split_placement == placement {
return;
}
self.will_split_placement = placement;
if let Some(dock_area) = self.dock_area.upgrade() {
dock_area.update(cx, |dock_area, cx| {
if let Some(placement) = placement {
dock_area.set_split_preview(bounds, placement, cx);
} else {
dock_area.clear_split_preview(cx);
}
});
}
}
fn closable_by_layout(&self, cx: &App) -> bool {
self.closable_by_layout_with_dock_area_locked(self.dock_area_locked(cx), cx)
}
fn closable_by_layout_with_dock_area_locked(&self, dock_area_locked: bool, cx: &App) -> bool {
if self.is_locked_with_dock_area_locked(dock_area_locked) {
return false;
}
if self.dock.is_none() && self.stack_panel.is_some() {
return true;
}
!self.is_last_panel(cx)
}
fn is_last_panel(&self, cx: &App) -> bool {
if let Some(parent) = &self.stack_panel
&& let Some(stack_panel) = parent.upgrade()
&& !stack_panel.read(cx).is_last_panel(cx)
{
return false;
}
self.panels.len() <= 1
}
fn visible_panels<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = Arc<dyn PanelView>> + 'a {
self.panels.iter().filter_map(|panel| {
if panel.visible(cx) {
Some(panel.clone())
} else {
None
}
})
}
fn draggable(&self, cx: &App) -> bool {
if self.is_locked(cx) {
return false;
}
if self.dock.is_some() {
return !self.panels.is_empty();
}
!self.panels.is_empty()
}
fn droppable(&self, cx: &App) -> bool {
!self.is_locked(cx)
}
fn render_toolbar(
&mut self, state: &TabState, window: &mut Window, cx: &mut Context<Self>,
) -> impl IntoElement {
if self.is_dock_collapsed(cx) {
return div();
}
let zoomed = self.zoomed;
let view = cx.entity().clone();
let zoomable_toolbar_visible = state.zoomable.is_some_and(|v| v.toolbar_visible());
h_flex()
.flex_none()
.flex_shrink_0()
.container_gap(Size::Medium)
.items_center()
.occlude()
.when_some(self.toolbar_buttons(window, cx), |this, buttons| {
this.children(buttons.into_iter().map(|btn| btn.flat().tab_stop(false)))
})
.map(|this| {
let value = if zoomed {
Some((
"zoom-out",
IconName::ArrowMinimize,
translate_woocraft("dock.zoom_out"),
))
} else if zoomable_toolbar_visible {
Some((
"zoom-in",
IconName::Maximize,
translate_woocraft("dock.zoom_in"),
))
} else {
None
};
if let Some((id, icon, tooltip)) = value {
let tooltip = SharedString::from(tooltip.to_string());
this.child(
Button::new(id)
.icon(Icon::new(icon))
.flat()
.tab_stop(false)
.tooltip(move |window, cx| Tooltip::new(tooltip.clone()).build(window, cx))
.selected(zoomed)
.on_click(cx.listener(|view, _, window, cx| {
view.on_action_toggle_zoom(&ToggleZoom, window, cx)
})),
)
} else {
this
}
})
.child(
Button::new("menu")
.icon(Icon::new(IconName::MoreHorizontal))
.flat()
.tab_stop(false)
.dropdown_menu({
let zoomable = state.zoomable.is_some_and(|v| v.menu_visible());
let closable = state.closable;
move |menu, window, cx| {
view.update(cx, |this, cx| {
this
.dropdown_menu(menu, window, cx)
.separator()
.menu_with_icon_and_disabled(
if zoomed {
translate_woocraft("dock.zoom_out")
} else {
translate_woocraft("dock.zoom_in")
},
if zoomed {
IconName::ArrowMinimize
} else {
IconName::Maximize
},
Box::new(ToggleZoom),
!zoomable,
)
.when(closable, |this| {
this.separator().menu_with_icon(
translate_woocraft("dock.close"),
IconName::Dismiss,
Box::new(ClosePanel),
)
})
})
}
})
.anchor(Corner::TopRight),
)
}
fn render_dock_toggle_button(
&self, placement: DockPlacement, _: &mut Window, cx: &mut Context<Self>,
) -> Option<Button> {
if self.zoomed {
return None;
}
if matches!(placement, DockPlacement::Left | DockPlacement::Right) {
return None;
}
let dock_area = self.dock_area.upgrade()?.read(cx);
if !dock_area.toggle_button_visible {
return None;
}
let view_entity_id = cx.entity().entity_id();
let toggle_button_panels = dock_area.toggle_button_panels;
if !match placement {
DockPlacement::Left => toggle_button_panels.left == Some(view_entity_id),
DockPlacement::Right => toggle_button_panels.right == Some(view_entity_id),
DockPlacement::Bottom => toggle_button_panels.bottom == Some(view_entity_id),
DockPlacement::Center => unreachable!(),
} {
return None;
}
let is_collapsed = dock_area.is_dock_collapsed(placement, cx);
let dock_has_panels = !self.panels.is_empty();
let icon = match placement {
DockPlacement::Left => {
if is_collapsed {
IconName::PanelLeftExpand
} else {
IconName::PanelLeft
}
}
DockPlacement::Right => {
if is_collapsed {
IconName::PanelRightContract
} else {
IconName::PanelRight
}
}
DockPlacement::Bottom => {
if is_collapsed {
IconName::ArrowMaximize
} else {
IconName::ArrowMinimize
}
}
DockPlacement::Center => unreachable!(),
};
Some(
Button::new(SharedString::from(format!("toggle-dock:{:?}", placement)))
.icon(Icon::new(icon))
.flat()
.tab_stop(false)
.disabled(!dock_has_panels)
.tooltip({
let label = SharedString::from(
if is_collapsed {
translate_woocraft("dock.expand")
} else {
translate_woocraft("dock.collapse")
}
.to_string(),
);
move |window, cx| Tooltip::new(label.clone()).build(window, cx)
})
.when(dock_has_panels, |this: Button| {
this.on_click(cx.listener({
let dock_area = self.dock_area.clone();
move |_, _, window, cx| {
_ = dock_area.update(cx, |dock_area, cx| {
dock_area.toggle_dock(placement, window, cx);
});
}
}))
}),
)
}
fn render_dock_collapse_button(&self, _: &mut Window, cx: &mut Context<Self>) -> Option<Button> {
if self.zoomed {
return None;
}
let dock = self.dock.as_ref()?.upgrade()?;
let dock_ref = dock.read(cx);
let placement = dock_ref.placement;
if !matches!(placement, DockPlacement::Left | DockPlacement::Right) {
return None;
}
let is_collapsed = dock_ref.collapsed;
let has_panels = !self.panels.is_empty();
let icon = if is_collapsed {
IconName::ArrowMaximize
} else {
IconName::ArrowMinimize
};
Some(
Button::new(SharedString::from("collapse-dock-content"))
.icon(Icon::new(icon))
.flat()
.tab_stop(false)
.disabled(!has_panels)
.tooltip({
let label = SharedString::from(
if is_collapsed {
translate_woocraft("dock.expand")
} else {
translate_woocraft("dock.collapse")
}
.to_string(),
);
move |window, cx| Tooltip::new(label.clone()).build(window, cx)
})
.when(has_panels, |this: Button| {
this.on_click(cx.listener({
move |this, _, window, cx| {
if let Some(dock) = this.dock.as_ref().and_then(|d| d.upgrade()) {
dock.update(cx, |dock, cx| {
dock.toggle_collapsed(window, cx);
});
}
}
}))
}),
)
}
fn render_title_bar(
&mut self, state: &TabState, window: &mut Window, cx: &mut Context<Self>,
) -> AnyElement {
let view = cx.entity().clone();
let Some(dock_area) = self.dock_area.upgrade() else {
return div().into_any_element();
};
let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx);
let is_bottom_dock_collapsed = bottom_dock_button.is_some()
&& dock_area
.read(cx)
.is_dock_collapsed(DockPlacement::Bottom, cx);
let panel_style = dock_area.read(cx).panel_style;
let tab_bar_direction = self.get_tab_bar_direction(cx);
let visible_panels = self.visible_panels(cx).collect::<Vec<_>>();
let show_single_title = tab_bar_direction.is_vertical()
|| (visible_panels.len() <= 1 && panel_style == PanelStyle::default());
let show_bottom_divider = !is_bottom_dock_collapsed || tab_bar_direction.is_bottom();
if show_single_title {
let Some(panel) = self.active_panel(cx) else {
if !self.is_empty_tab_panel() {
return div().into_any_element();
}
let title_content = h_flex()
.w_full()
.items_center()
.min_w_0()
.overflow_hidden()
.gap_1()
.container_size(Size::Medium)
.container_h(Size::Medium)
.when_some(bottom_dock_button, |this, btn| this.child(btn))
.child(
div()
.id("tab")
.flex_1()
.h_full()
.min_w_0()
.overflow_hidden()
.when(state.droppable, |this| {
this
.drag_over::<DragPanel>({
let view = view.clone();
move |this, _, _, cx| {
view.update(cx, |view, cx| view.clear_split_preview(cx));
this
.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
}
})
.on_drop(cx.listener(|this, drag: &DragPanel, window, cx| {
this.clear_split_preview(cx);
this.on_drop(drag, Some(0), true, window, cx)
}))
}),
);
return v_flex()
.w_full()
.min_w_0()
.overflow_hidden()
.when(!show_bottom_divider, |this| {
this.child(Divider::horizontal())
})
.child(title_content)
.when(show_bottom_divider, |this| {
this.child(Divider::horizontal())
})
.into_any_element();
};
if !panel.visible(cx) {
return div().into_any_element();
}
let icon = panel.icon(cx);
let title = panel.title(cx);
let title_content = h_flex()
.w_full()
.min_w_0()
.overflow_hidden()
.items_center()
.gap_1()
.container_size(Size::Medium)
.container_h(Size::Medium)
.when_some(bottom_dock_button, |this, btn| this.child(btn))
.child(
div()
.id("tab")
.flex_1()
.min_w_0()
.overflow_hidden()
.child(
IconLabel::new("panel-title")
.flex_1()
.label_flex_1()
.icon(Icon::new(icon))
.label(title),
)
.when(state.draggable, |this| {
this.on_drag(
DragPanel {
panel: panel.clone(),
tab_panel: view.clone(),
size: Size::Medium,
},
|drag, _, _, cx| {
cx.stop_propagation();
cx.new(|_| drag.clone())
},
)
})
.when(state.droppable, |this| {
this
.drag_over::<DragPanel>({
let view = view.clone();
move |this, _, _, cx| {
view.update(cx, |view, cx| view.clear_split_preview(cx));
this
.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
}
})
.on_drop(cx.listener(move |this, drag: &DragPanel, window, cx| {
this.clear_split_preview(cx);
this.on_drop(drag, Some(0), true, window, cx)
}))
}),
)
.children(panel.title_suffix(window, cx))
.child(self.render_toolbar(state, window, cx));
return v_flex()
.w_full()
.min_w_0()
.overflow_hidden()
.when(!show_bottom_divider, |this| {
this.child(Divider::horizontal())
})
.child(title_content)
.when(show_bottom_divider, |this| {
this.child(Divider::horizontal())
})
.into_any_element();
}
let tabs_count = self.panels.len();
let tab_bar = TabBar::new("tab-bar")
.track_scroll(&self.tab_bar_scroll_handle)
.when_some(bottom_dock_button, |this, btn| {
this.prefix(h_flex().items_center().top_0().right_0().child(btn))
})
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
let mut active = state.active_panel.as_ref() == Some(panel);
let is_collapsed = self.is_dock_collapsed(cx);
if !panel.visible(cx) {
return None;
}
if is_collapsed {
active = false;
}
Some(
Tab::new()
.ix(ix)
.map(|this| {
if let Some(tab_name) = panel.tab_name(cx) {
this.icon(panel.icon(cx)).label(tab_name)
} else {
this.icon(panel.icon(cx)).label(panel.title(cx))
}
})
.selected(active)
.when(panel.closable(cx), |this| {
let panel = panel.clone();
this.closable(true).on_close(cx.listener({
move |this, _, window, cx| {
this.remove_panel(panel.clone(), window, cx);
}
}))
})
.on_click(cx.listener({
move |view, _, window, cx| {
view.handle_tab_click(ix, window, cx);
}
}))
.when(state.draggable, |this| {
this.on_drag(
DragPanel::new(panel.clone(), view.clone()),
|drag, _, _, cx| {
cx.stop_propagation();
cx.new(|_| drag.clone())
},
)
})
.when(state.droppable, |this| {
this
.drag_over::<DragPanel>({
let view = view.clone();
move |this, _, _, cx| {
view.update(cx, |view, cx| view.clear_split_preview(cx));
this
.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
}
})
.on_drop(cx.listener(move |this, drag: &DragPanel, window, cx| {
this.clear_split_preview(cx);
this.on_drop(drag, Some(ix), true, window, cx)
}))
}),
)
}))
.last_empty_space(
div()
.id("tab-bar-empty-space")
.h_full()
.flex_grow()
.min_w_16()
.when(state.droppable, |this| {
this
.drag_over::<DragPanel>({
let view = view.clone();
move |this, _, _, cx| {
view.update(cx, |view, cx| view.clear_split_preview(cx));
this.bg(cx.theme().drop_target)
}
})
.on_drop(cx.listener(move |this, drag: &DragPanel, window, cx| {
this.clear_split_preview(cx);
let ix = if drag.tab_panel == view {
Some(tabs_count - 1)
} else {
None
};
this.on_drop(drag, ix, false, window, cx)
}))
}),
)
.when(!self.is_dock_collapsed(cx), |this| {
this.suffix(
h_flex()
.flex_none()
.flex_shrink_0()
.items_center()
.top_0()
.right_0()
.children(
self
.active_panel(cx)
.and_then(|panel| panel.title_suffix(window, cx)),
)
.child(self.render_toolbar(state, window, cx)),
)
});
v_flex()
.w_full()
.min_w_0()
.overflow_hidden()
.when(!show_bottom_divider, |this| {
this.child(Divider::horizontal())
})
.child(tab_bar)
.when(show_bottom_divider, |this| {
this.child(Divider::horizontal())
})
.into_any_element()
}
fn render_vertical_tab_bar(
&mut self, state: &TabState, window: &mut Window, cx: &mut Context<Self>,
) -> AnyElement {
let view = cx.entity().clone();
let tabs_count = self.panels.len();
let fallback_drop_ix = if tabs_count > 0 {
Some(tabs_count.saturating_sub(1))
} else {
None
};
let view_for_tab_drops = view.clone();
let view_for_bar_drop = view.clone();
let collapse_button = self.render_dock_collapse_button(window, cx);
let is_dock_collapsed = self.is_dock_collapsed(cx);
let tab_bar = TabBar::new("vertical-tab-bar")
.vertical(true)
.h_full()
.when_some(collapse_button, |this, btn| this.suffix(btn))
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
if !panel.visible(cx) {
return None;
}
let active = state.active_panel.as_ref() == Some(panel);
let is_active = if is_dock_collapsed { false } else { active };
Some(
Tab::new()
.ix(ix)
.icon(panel.icon(cx))
.label(panel.tab_name(cx).unwrap_or_else(|| panel.title(cx)))
.selected(is_active)
.on_click(cx.listener({
move |view, _, window, cx| {
view.handle_tab_click(ix, window, cx);
}
}))
.when(state.draggable, |this| {
this.on_drag(
DragPanel::new(panel.clone(), view.clone()),
|drag, _, _, cx| {
cx.stop_propagation();
cx.new(|_| drag.clone())
},
)
})
.when(state.droppable, |this| {
this
.drag_over::<DragPanel>({
let view = view.clone();
move |this, _, _, cx| {
view.update(cx, |view, cx| view.clear_split_preview(cx));
this.bg(cx.theme().drop_target)
}
})
.on_drop(cx.listener(move |this, drag: &DragPanel, window, cx| {
this.clear_split_preview(cx);
this.on_drop(drag, Some(ix), true, window, cx);
}))
}),
)
}))
.last_empty_space(
div()
.id("vertical-tab-bar-empty-space")
.w_full()
.flex_grow()
.min_h_16()
.when(state.droppable, |this| {
this
.drag_over::<DragPanel>({
let view = view.clone();
move |this, _, _, cx| {
view.update(cx, |view, cx| view.clear_split_preview(cx));
this.bg(cx.theme().drop_target)
}
})
.on_drop(cx.listener(move |this, drag: &DragPanel, window, cx| {
this.clear_split_preview(cx);
let ix = if drag.tab_panel == view_for_tab_drops {
Some(tabs_count.saturating_sub(1))
} else {
None
};
this.on_drop(drag, ix, false, window, cx);
}))
}),
);
let tab_bar = div()
.h_full()
.child(tab_bar)
.when(state.droppable, |this| {
this
.drag_over::<DragPanel>({
let view = view.clone();
move |this, _, _, cx| {
view.update(cx, |view, cx| view.clear_split_preview(cx));
this.bg(cx.theme().drop_target)
}
})
.on_drop(cx.listener(move |this, drag: &DragPanel, window, cx| {
this.clear_split_preview(cx);
let ix = if drag.tab_panel == view_for_bar_drop {
fallback_drop_ix
} else {
None
};
this.on_drop(drag, ix, false, window, cx);
}))
})
.into_any_element();
if self.get_tab_bar_direction(cx).is_left() {
h_flex()
.h_full()
.child(tab_bar)
.child(Divider::vertical())
.into_any_element()
} else if self.get_tab_bar_direction(cx).is_right() {
h_flex()
.h_full()
.child(Divider::vertical())
.child(tab_bar)
.into_any_element()
} else {
tab_bar.into_any_element()
}
}
fn render_active_panel(
&self, state: &TabState, _: &mut Window, cx: &mut Context<Self>,
) -> AnyElement {
let is_dock_collapsed = self.is_dock_collapsed(cx);
let is_vertical = self.get_tab_bar_direction(cx).is_vertical();
let allows_split_drop = self.allows_split_drop();
if is_dock_collapsed && is_vertical {
return Empty {}.into_any_element();
}
let Some(active_panel) = state.active_panel.as_ref() else {
let center_placeholder = if self.dock.is_none() {
self
.dock_area
.upgrade()
.and_then(|area| area.read(cx).center_placeholder.clone())
} else {
None
};
return v_flex()
.id("active-panel")
.group("")
.flex_1()
.min_w_0()
.overflow_hidden()
.when_some(center_placeholder, |this, view| {
this.child(div().size_full().overflow_hidden().child(view))
})
.when(state.droppable, |this| {
this.child(
div()
.invisible()
.absolute()
.top_0()
.left_0()
.size_full()
.bg(cx.theme().drop_target)
.group_drag_over::<DragPanel>("", |this| this.visible())
.on_drop(cx.listener(|this, drag: &DragPanel, window, cx| {
this.clear_split_preview(cx);
this.on_drop(drag, None, true, window, cx)
})),
)
})
.into_any_element();
};
let is_render_in_tabs = self.panels.len() > 1 && self.inner_padding(cx);
v_flex()
.id("active-panel")
.group("")
.flex_1()
.min_w_0()
.overflow_hidden()
.when(is_render_in_tabs, |this| this.pt_2())
.child(
div()
.id("tab-content")
.min_w_0()
.overflow_y_scroll()
.overflow_x_hidden()
.flex_1()
.child(
active_panel.view(), ),
)
.when(state.droppable, |this| {
this
.when(allows_split_drop, |this| {
this.on_drag_move(cx.listener(Self::on_panel_drag_move))
})
.child(
div()
.invisible()
.absolute()
.top_0()
.left_0()
.size_full()
.map(|this| {
if !allows_split_drop || self.will_split_placement.is_none() {
this.bg(cx.theme().drop_target)
} else {
this.bg(cx.theme().drop_target.opacity(0.01))
}
})
.group_drag_over::<DragPanel>("", |this| this.visible())
.on_drop(cx.listener(|this, drag: &DragPanel, window, cx| {
this.on_drop(drag, None, true, window, cx)
})),
)
})
.into_any_element()
}
fn on_panel_drag_move(
&mut self, drag: &DragMoveEvent<DragPanel>, _: &mut Window, cx: &mut Context<Self>,
) {
if !self.allows_split_drop() {
self.clear_split_preview(cx);
return;
}
let bounds = drag.bounds;
let position = drag.event.position;
let new_placement = if position.x < bounds.left() + bounds.size.width * 0.35 {
Some(Placement::Left)
} else if position.x > bounds.left() + bounds.size.width * 0.65 {
Some(Placement::Right)
} else if position.y < bounds.top() + bounds.size.height * 0.35 {
Some(Placement::Top)
} else if position.y > bounds.top() + bounds.size.height * 0.65 {
Some(Placement::Bottom)
} else {
None
};
self.update_split_preview(bounds, new_placement, cx);
}
fn on_drop(
&mut self, drag: &DragPanel, ix: Option<usize>, active: bool, window: &mut Window,
cx: &mut Context<Self>,
) {
let panel = drag.panel.clone();
let is_same_tab = drag.tab_panel == cx.entity();
let split_placement = if self.allows_split_drop() {
self.will_split_placement
} else {
None
};
self.clear_split_preview(cx);
if is_same_tab && ix.is_none() && (split_placement.is_none() || self.panels.len() == 1) {
return;
}
let source_dock = if !is_same_tab {
drag
.tab_panel
.read(cx)
.dock
.as_ref()
.and_then(|d| d.upgrade())
} else {
None
};
if is_same_tab {
self.detach_panel(panel.clone(), window, cx);
} else {
drag.tab_panel.update(cx, |view, cx| {
view.detach_panel(panel.clone(), window, cx);
view.remove_self_if_empty(window, cx);
});
}
if let Some(placement) = split_placement {
self.split_panel(panel, placement, None, window, cx);
} else if let Some(ix) = ix {
self.insert_panel_at(panel, ix, window, cx)
} else {
self.add_panel_with_active(panel, active, window, cx)
}
if let Some(dock) = self.dock.as_ref().and_then(|d| d.upgrade())
&& dock.read(cx).collapsed
{
dock.update(cx, |dock, cx| {
dock.set_collapsed(false, window, cx);
});
}
if let Some(source_dock) = source_dock
&& !source_dock.read(cx).has_panels(cx)
{
source_dock.update(cx, |dock, cx| {
dock.set_collapsed(true, window, cx);
});
}
self.remove_self_if_empty(window, cx);
}
fn split_panel(
&self, panel: Arc<dyn PanelView>, placement: Placement, size: Option<Pixels>,
window: &mut Window, cx: &mut Context<Self>,
) {
let dock_area = self.dock_area.clone();
let new_tab_panel = cx.new(|cx| Self::new(None, dock_area.clone(), window, cx));
new_tab_panel.update(cx, |view, cx| {
view.add_panel(panel, window, cx);
});
let stack_panel = match self.stack_panel.as_ref().and_then(|panel| panel.upgrade()) {
Some(panel) => panel,
None => return,
};
let parent_axis = stack_panel.read(cx).axis;
let ix = stack_panel
.read(cx)
.index_of_panel(Arc::new(cx.entity().clone()))
.unwrap_or_default();
if parent_axis.is_vertical() && placement.is_vertical() {
stack_panel.update(cx, |view, cx| {
view.insert_panel_at(
Arc::new(new_tab_panel),
ix,
placement,
size,
dock_area.clone(),
window,
cx,
);
});
} else if parent_axis.is_horizontal() && placement.is_horizontal() {
stack_panel.update(cx, |view, cx| {
view.insert_panel_at(
Arc::new(new_tab_panel),
ix,
placement,
size,
dock_area.clone(),
window,
cx,
);
});
} else {
let tab_panel = cx.entity().clone();
let new_stack_panel = if stack_panel.read(cx).panels_len() <= 1 {
stack_panel.update(cx, |view, cx| {
view.remove_all_panels(window, cx);
view.set_axis(placement.axis(), window, cx);
});
stack_panel.clone()
} else {
cx.new(|cx| {
let mut panel = StackPanel::new(placement.axis(), window, cx);
panel.parent = Some(stack_panel.downgrade());
panel.set_dock_area(dock_area.clone());
panel
})
};
new_stack_panel.update(cx, |view, cx| match placement {
Placement::Left | Placement::Top => {
view.add_panel(Arc::new(new_tab_panel), size, dock_area.clone(), window, cx);
view.add_panel(
Arc::new(tab_panel.clone()),
None,
dock_area.clone(),
window,
cx,
);
}
Placement::Right | Placement::Bottom => {
view.add_panel(
Arc::new(tab_panel.clone()),
None,
dock_area.clone(),
window,
cx,
);
view.add_panel(Arc::new(new_tab_panel), size, dock_area.clone(), window, cx);
}
});
if stack_panel != new_stack_panel {
stack_panel.update(cx, |view, cx| {
view.replace_panel(
Arc::new(tab_panel.clone()),
new_stack_panel.clone(),
window,
cx,
);
});
}
cx.spawn_in(window, async move |_, cx| {
cx.update(|window, cx| {
tab_panel.update(cx, |view, cx| view.remove_self_if_empty(window, cx))
})
})
.detach()
}
cx.emit(PanelEvent::LayoutChanged);
}
fn focus_active_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(active_panel) = self.active_panel(cx) {
active_panel.focus_handle(cx).focus(window);
}
}
fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
if self.zoomable(cx).is_none() {
return;
}
if !self.zoomed {
cx.emit(PanelEvent::ZoomIn)
} else {
cx.emit(PanelEvent::ZoomOut)
}
self.zoomed = !self.zoomed;
cx.spawn_in(window, {
let zoomed = self.zoomed;
async move |view, cx| {
_ = cx.update(|window, cx| {
_ = view.update(cx, |view, cx| {
view.set_zoomed(zoomed, window, cx);
});
});
}
})
.detach();
}
fn on_action_close_panel(&mut self, _: &ClosePanel, window: &mut Window, cx: &mut Context<Self>) {
if !self.closable(cx) {
return;
}
if let Some(panel) = self.active_panel(cx) {
self.remove_panel(panel, window, cx);
}
if self.panels.is_empty() && self.in_tiles {
let tab_panel = Arc::new(cx.entity());
window.defer(cx, {
let dock_area = self.dock_area.clone();
move |window, cx| {
_ = dock_area.update(cx, |this, cx| {
this.remove_panel_from_all_docks(tab_panel, window, cx);
});
}
});
}
}
fn bind_actions(&self, cx: &mut Context<Self>) -> Div {
v_flex().when(!self.is_dock_collapsed(cx), |this| {
this
.on_action(cx.listener(Self::on_action_toggle_zoom))
.on_action(cx.listener(Self::on_action_close_panel))
})
}
}
impl Focusable for TabPanel {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
if let Some(active_panel) = self.active_panel(cx) {
active_panel.focus_handle(cx)
} else {
self.focus_handle.clone()
}
}
}
impl EventEmitter<DismissEvent> for TabPanel {}
impl EventEmitter<PanelEvent> for TabPanel {}
impl Render for TabPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
let focus_handle = self.focus_handle(cx);
let active_panel = self.active_panel(cx);
let state = TabState {
closable: self.closable(cx),
draggable: self.draggable(cx),
droppable: self.droppable(cx),
zoomable: self.zoomable(cx),
active_panel,
};
let direction = self.get_tab_bar_direction(cx);
if direction.is_vertical() {
let is_dock_collapsed = self.is_dock_collapsed(cx);
let vertical_tab_bar = self.render_vertical_tab_bar(&state, window, cx);
if is_dock_collapsed {
return self
.bind_actions(cx)
.id("tab-panel")
.track_focus(&focus_handle)
.tab_group()
.size_full()
.overflow_hidden()
.child(vertical_tab_bar)
.into_any_element();
}
let title_bar = self.render_title_bar(&state, window, cx);
let active_panel_content = self.render_active_panel(&state, window, cx);
let content = v_flex()
.flex_1()
.size_full()
.min_w_0()
.overflow_hidden()
.child(title_bar)
.child(active_panel_content);
let main_content = if direction.is_left() {
h_flex()
.size_full()
.min_w_0()
.overflow_hidden()
.child(vertical_tab_bar)
.child(content)
} else {
h_flex()
.size_full()
.min_w_0()
.overflow_hidden()
.child(content)
.child(vertical_tab_bar)
};
return self
.bind_actions(cx)
.id("tab-panel")
.track_focus(&focus_handle)
.tab_group()
.size_full()
.overflow_hidden()
.bg(cx.theme().background)
.child(main_content)
.into_any_element();
}
let title_bar = self.render_title_bar(&state, window, cx);
let active_panel_content = self.render_active_panel(&state, window, cx);
let main_content = if direction.is_bottom() {
v_flex()
.size_full()
.min_w_0()
.overflow_hidden()
.child(active_panel_content)
.child(title_bar)
} else {
v_flex()
.size_full()
.min_w_0()
.overflow_hidden()
.child(title_bar)
.child(active_panel_content)
};
self
.bind_actions(cx)
.id("tab-panel")
.track_focus(&focus_handle)
.tab_group()
.size_full()
.overflow_hidden()
.bg(cx.theme().background)
.child(main_content)
.into_any_element()
}
}