use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ Anchor, Animation, AnimationExt as _, AnyElement, AnyView, App, AppContext as _, Bounds, Context, Div, Empty, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Point, Render, ScrollHandle, SharedString, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window, div, px, size, }; use gpui_base::dock::{ AnyDrag, DockPlacement, DragPanel, DropIndicator, NodeId, PaneNode, PaneRef, PanelView as BasePanelView, TabGroupContext, TabGroupRenderer, }; use gpui_base::{ElementExt, InteractiveElementExt, Tab, Tabs}; use gpui_component::animation::{Lerp as _, ease_out_cubic}; use gpui_component::button::{Button, ButtonVariants as _}; use gpui_component::dock::{ClosePanel, PanelControl, PanelHandle, ToggleZoom}; use gpui_component::menu::DropdownMenu as _; use gpui_component::{ ActiveTheme as _, Disableable as _, IconName, Selectable as _, Sizable as _, h_flex, v_flex, }; use signed_ui::title_bar_drag_handlers; use crate::dock_area::SkinShared; use crate::{TAB_BAR_HEIGHT, t, window_controls}; /// The drag preview's size, reported to base for the drop placeholder. const DRAG_PREVIEW_SIZE: gpui::Size = size(px(96.), px(30.)); /// A panel's title, or its registered name when the panel has no handle. pub(crate) fn panel_title( panel: &Arc, window: &mut Window, cx: &mut App, ) -> AnyElement { match PanelHandle::of(panel) { Some(handle) => handle.title(window, cx), None => SharedString::from(panel.panel_name(cx)).into_any_element(), } } /// The preview that follows the cursor while a panel is dragged. /// Base's `DragPanel` is the payload and draws nothing, this is the appearance half. struct DragPanelPreview { panel: Arc, } impl Render for DragPanelPreview { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .id("drag-panel") .cursor_grab() .py_1() .px_3() .w_24() .overflow_hidden() .whitespace_nowrap() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .text_color(cx.theme().tab_foreground) .bg(cx.theme().tokens.tab_active) .opacity(0.75) .child(panel_title(&self.panel, window, cx)) } } /// The zoom affordance for the group's displayed panel, if it offers one. /// The panel must offer a control and be zoomable, base refuses a zoom otherwise. fn zoom_control(group: &TabGroupContext, cx: &App) -> Option { let panel = group.active_panel()?; panel .zoomable(cx) .then(|| PanelHandle::of(panel).and_then(|handle| handle.zoom_control(cx))) .flatten() } /// The left-most, top-most tab group in a container. /// A left dock's collapse button lives in this group. fn left_top_group(node: &PaneNode) -> Option { match node.kind() { PaneRef::Tabs { .. } => Some(node.id()), PaneRef::Split { children, .. } => children.first().and_then(left_top_group), PaneRef::Tiles { .. } => None, } } /// The right-most, top-most tab group. /// A vertical split picks its first child, a horizontal split picks its last. fn right_top_group(node: &PaneNode) -> Option { match node.kind() { PaneRef::Tabs { .. } => Some(node.id()), PaneRef::Split { axis, children, .. } => match axis { gpui::Axis::Vertical => children.first(), gpui::Axis::Horizontal => children.last(), } .and_then(right_top_group), PaneRef::Tiles { .. } => None, } } /// One tab group's appearance, built once per container so its geometry is its own. pub(crate) struct SignedTabGroupSkin { shared: Rc, scroll_handle: ScrollHandle, /// The tab shown last frame, so a change scrolls the new one into view. last_active_ix: Cell>, /// Bounds of the title bar row, measured to place the title-bar drag overlay. title_bar_bounds: Rc>>>, /// Bounds of the empty strip after the last tab, where the drag region starts. title_bar_strip_bounds: Rc>>>, /// Bounds of the suffix area, where the drag region ends. title_bar_suffix_bounds: Rc>>>, } impl SignedTabGroupSkin { pub(crate) fn new(shared: Rc) -> Self { Self { shared, scroll_handle: ScrollHandle::default(), last_active_ix: Cell::new(None), title_bar_bounds: Rc::new(Cell::new(None)), title_bar_strip_bounds: Rc::new(Cell::new(None)), title_bar_suffix_bounds: Rc::new(Cell::new(None)), } } /// A group that is the left dock's only group, with one panel, draws no chrome. /// The vendored dock rendered such a panel bare and the sidebar is one. fn is_plain_sidebar_group(&self, group: &TabGroupContext, cx: &mut App) -> bool { let Some(area) = self.shared.area().upgrade() else { return false; }; let area = area.read(cx); let Some(left) = area .layout(DockPlacement::Left) .map(|tree| tree.root().id()) else { return false; }; left == group.node() && group.panels().len() == 1 } /// The bottom or right dock whose root tab group is this one, if any. /// Base keeps a dock's last group, so the skin removes these docks as a whole. fn is_dock_root_group(&self, group: &TabGroupContext, cx: &App) -> Option { let area = self.shared.area().upgrade()?; let area = area.read(cx); [DockPlacement::Bottom, DockPlacement::Right] .into_iter() .find(|placement| { area.layout(*placement) .is_some_and(|tree| tree.root().id() == group.node()) }) } /// The tab's drag payload, or `None` when the group must not be rearranged. /// A locked group never is, a bottom or right dock root always is. fn tab_drag(&self, group: &TabGroupContext, ix: usize, cx: &App) -> Option { if group.is_locked() { return None; } if !group.is_draggable() && self.is_dock_root_group(group, cx).is_none() { return None; } group.drag_panel(ix, cx) } /// A dock's collapse button for this group's bar, or `None` when it does not belong. /// The icon direction depends on whether the dock is open. fn dock_toggle_button( &self, placement: DockPlacement, group: &TabGroupContext, cx: &mut App, ) -> Option