{
+ let state = window.use_state(cx, |_, _| WindowDragState { should_move: false });
+
+ this.window_control_area(WindowControlArea::Drag)
+ .on_mouse_down_out(window.listener_for(&state, |state, _, _, _| {
+ state.should_move = false;
+ }))
+ .on_mouse_down(
+ MouseButton::Left,
+ window.listener_for(&state, |state, _, _, _| {
+ state.should_move = true;
+ }),
+ )
+ .on_mouse_up(
+ MouseButton::Left,
+ window.listener_for(&state, |state, _, _, _| {
+ state.should_move = false;
+ }),
+ )
+ .on_mouse_move(window.listener_for(&state, |state, _, window, _| {
+ if state.should_move {
+ state.should_move = false;
+ window.start_window_move();
+ }
+ }))
+ .on_click(|event, window, _| {
+ if event.click_count() == 2 {
+ if cfg!(target_os = "macos") {
+ window.titlebar_double_click();
+ } else {
+ window.zoom_window();
+ }
+ }
+ })
+}
diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs
new file mode 100644
index 0000000..8d0bf79
--- /dev/null
+++ b/crates/dock/src/tab_panel.rs
@@ -0,0 +1,902 @@
+//! The Signed appearance for a tab group.
+//!
+//! `gpui_base::dock::TabGroup` owns the behavior — membership, the displayed
+//! tab, drag hit-testing, the zoom flag — and draws none of it. Everything
+//! visible is here, ported from the vendored dock: the pill tab bar that
+//! doubles as the window title bar (with window controls, title-bar
+//! dragging, and previous/next tab buttons), the toolbar, the ellipsis menu,
+//! the dock collapse affordance, the drop placeholder, and the styled drag
+//! preview.
+
+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::menu::DropdownMenu as _;
+use gpui_component::{
+ ActiveTheme as _, Disableable as _, IconName, Selectable as _, Sizable as _, h_flex, v_flex,
+};
+
+use crate::dock_area::SkinShared;
+use crate::{
+ ClosePanel, PanelControl, PanelHandle, TAB_BAR_HEIGHT, ToggleZoom, t, title_bar_drag_handlers,
+ window_controls,
+};
+
+/// The size the styled drag preview occupies, reported to base so a drop
+/// placeholder knows where to fly in from.
+const DRAG_PREVIEW_SIZE: gpui::Size
= size(px(96.), px(30.));
+
+/// A panel's title, or its registered name when it reached base without this
+/// crate's handle and so carries no presentation. See [`PanelHandle::of`].
+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.
+///
+/// `gpui_base::dock::DragPanel` is the payload and draws nothing; this is the
+/// appearance half, reintroduced here.
+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))
+ }
+}
+
+/// Where the zoom affordance goes for the group's displayed panel, or `None`
+/// when there is none to offer.
+///
+/// Two questions, and both have to be asked. [`Panel::zoom_control`] says
+/// *where* the control appears; [`gpui_base::dock::Panel::zoomable`] says
+/// whether zooming happens at all, and base refuses a zoom that fails it.
+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 — where a left dock's
+/// collapse affordance goes. Mirrors the old `StackPanel::left_top_tab_panel`.
+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 stacks its children,
+/// so its *first* child is the top one; a horizontal split's last child is
+/// the right-most. Mirrors the old `StackPanel::right_top_tab_panel`.
+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 per group — `DockAreaRenderer::tab_group_renderer` is called once
+/// per container — so the tab bar's scroll position and the measured
+/// title-bar geometry belong to the group they describe.
+pub(crate) struct SignedTabGroupSkin {
+ shared: Rc,
+ scroll_handle: ScrollHandle,
+ /// The displayed tab the last frame drew, so a change scrolls the new tab
+ /// into view.
+ last_active_ix: Cell