diff --git a/Cargo.lock b/Cargo.lock index ef3cb86..450a341 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1363,15 +1363,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -1723,7 +1714,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case 0.10.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -1827,14 +1818,9 @@ dependencies = [ name = "dock" version = "1.0.0" dependencies = [ - "anyhow", "gpui", "gpui-base", "gpui-component", - "itertools 0.13.0", - "serde", - "serde_json", - "smallvec", ] [[package]] @@ -3500,7 +3486,6 @@ dependencies = [ "anyhow", "async-channel", "async-task", - "backtrace", "bindgen", "bitflags 2.13.1", "chrono", @@ -3531,7 +3516,6 @@ dependencies = [ "pollster 0.4.0", "postage", "profiling", - "proptest", "rand 0.9.5", "raw-window-handle", "refineable", @@ -4514,7 +4498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ "byteorder-lite", - "quick-error 2.0.1", + "quick-error", ] [[package]] @@ -6629,36 +6613,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "proptest" -version = "1.10.0" -source = "git+https://github.com/proptest-rs/proptest?rev=3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b#3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b" -dependencies = [ - "bit-set 0.8.0", - "bit-vec 0.8.0", - "bitflags 2.13.1", - "num-traits", - "proptest-macro", - "rand 0.9.5", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "proptest-macro" -version = "0.5.0" -source = "git+https://github.com/proptest-rs/proptest?rev=3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b#3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b" -dependencies = [ - "convert_case 0.11.0", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "psm" version = "0.1.32" @@ -6706,12 +6660,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-error" version = "2.0.1" @@ -6890,15 +6838,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - [[package]] name = "range-alloc" version = "0.1.5" @@ -6955,7 +6894,7 @@ dependencies = [ "avif-serialize", "imgref", "loop9", - "quick-error 2.0.1", + "quick-error", "rav1e", "rayon", "rgb", @@ -7507,18 +7446,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", -] - [[package]] name = "rustybuzz" version = "0.20.1" @@ -8665,7 +8592,7 @@ dependencies = [ "fax", "flate2", "half", - "quick-error 2.0.1", + "quick-error", "weezl", "zune-jpeg 0.5.15", ] @@ -9471,12 +9398,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicase" version = "2.9.0" @@ -9780,15 +9701,6 @@ dependencies = [ "libc", ] -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - [[package]] name = "waker-fn" version = "1.2.0" diff --git a/crates/dock/Cargo.toml b/crates/dock/Cargo.toml index 555d6fb..2218da5 100644 --- a/crates/dock/Cargo.toml +++ b/crates/dock/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dock" -description = "Dock (DockArea / Dock / Panel) components vendored from gpui-component." +description = "The Signed dock skin over gpui-component's upstream dock (gpui_base::dock engine + renderer traits)." version.workspace = true edition.workspace = true publish.workspace = true @@ -9,12 +9,3 @@ publish.workspace = true gpui.workspace = true gpui-component.workspace = true gpui-base.workspace = true - -anyhow.workspace = true -serde.workspace = true -serde_json.workspace = true -itertools = "0.13.0" -smallvec = "1" - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/dock/src/dock.rs b/crates/dock/src/dock.rs deleted file mode 100644 index 1b40184..0000000 --- a/crates/dock/src/dock.rs +++ /dev/null @@ -1,429 +0,0 @@ -use std::ops::Deref; -use std::sync::Arc; - -use gpui::prelude::FluentBuilder as _; -use gpui::{ - App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent, - MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _, - WeakEntity, Window, div, px, -}; -use gpui_component::{Side, StyledExt}; -use serde::{Deserialize, Serialize}; - -use super::{DockArea, DockEvent, DockItem, PanelView, TabPanel}; -use crate::resize_handle::{PANEL_MIN_SIZE, resize_handle}; - -#[derive(Clone)] -struct ResizePanel; - -impl Render for ResizePanel { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - Empty - } -} - -/// Where to place a panel. -/// -/// The [`DockArea`] has a fixed left dock and a center area. `Left` targets -/// the left dock; `Center` adds a tab to the center; `Right` and `Bottom` -/// split the center so the new panel lands on the given side of the existing -/// center content. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -pub enum DockPlacement { - #[serde(rename = "center")] - Center, - #[serde(rename = "left")] - Left, - #[serde(rename = "bottom")] - Bottom, - #[serde(rename = "right")] - Right, -} - -impl DockPlacement { - /// The split axis used when the placement splits the center area. - pub(crate) fn axis(&self) -> Axis { - match self { - Self::Left | Self::Right => Axis::Horizontal, - Self::Bottom => Axis::Vertical, - Self::Center => unreachable!(), - } - } - - pub fn is_left(&self) -> bool { - matches!(self, Self::Left) - } -} - -/// The Dock is a fixed container that places at the left side of the window. -/// -/// This is unlike Panel, it can't be move or add any other panel. -pub struct Dock { - pub(super) placement: DockPlacement, - dock_area: WeakEntity, - pub(crate) panel: DockItem, - /// The width of the dock. - pub(super) size: Pixels, - pub(super) open: bool, - /// Whether the Dock is collapsible, default: true - pub(super) collapsible: bool, - - // Runtime state - /// Whether the Dock is resizing - resizing: bool, -} - -impl Dock { - pub(crate) fn new( - dock_area: WeakEntity, - placement: DockPlacement, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let panel = cx.new(|cx| { - let mut tab = TabPanel::new(None, dock_area.clone(), window, cx); - tab.closable = false; - tab - }); - - let panel = DockItem::Tabs { - size: None, - items: Vec::new(), - active_ix: 0, - view: panel.clone(), - }; - - Self::subscribe_panel_events(dock_area.clone(), &panel, window, cx); - - Self { - placement, - dock_area, - panel, - open: true, - collapsible: true, - size: px(200.0), - resizing: false, - } - } - - pub fn left( - dock_area: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self::new(dock_area, DockPlacement::Left, window, cx) - } - - /// Update the Dock to be collapsible or not. - /// - /// And if the Dock is not collapsible, it will be open. - pub fn set_collapsible(&mut self, collapsible: bool, _: &mut Window, cx: &mut Context) { - self.collapsible = collapsible; - if !collapsible { - self.open = true - } - cx.notify(); - } - - pub(super) fn from_state( - dock_area: WeakEntity, - placement: DockPlacement, - size: Pixels, - panel: DockItem, - open: bool, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self::subscribe_panel_events(dock_area.clone(), &panel, window, cx); - - if !open { - match panel.clone() { - DockItem::Tabs { view, .. } => { - view.update(cx, |panel, cx| { - panel.set_collapsed(true, window, cx); - }); - } - DockItem::Split { items, .. } => { - for item in items { - item.set_collapsed(true, window, cx); - } - } - _ => {} - } - } - - Self { - placement, - dock_area, - panel, - open, - size, - collapsible: true, - resizing: false, - } - } - - fn subscribe_panel_events( - dock_area: WeakEntity, - panel: &DockItem, - window: &mut Window, - cx: &mut Context, - ) { - match panel { - DockItem::Tabs { view, .. } => { - window.defer(cx, { - let view = view.clone(); - move |window, cx| { - _ = dock_area.update(cx, |this, cx| { - this.subscribe_panel(&view, window, cx); - }); - } - }); - } - DockItem::Split { items, view, .. } => { - for item in items { - Self::subscribe_panel_events(dock_area.clone(), item, window, cx); - } - window.defer(cx, { - let view = view.clone(); - move |window, cx| { - _ = dock_area.update(cx, |this, cx| { - this.subscribe_panel(&view, window, cx); - }); - } - }); - } - DockItem::Panel { .. } => { - // Not supported - } - } - } - - pub fn set_panel(&mut self, panel: DockItem, _: &mut Window, cx: &mut Context) { - self.panel = panel; - cx.notify(); - } - - pub fn panel(&self) -> &DockItem { - &self.panel - } - - pub fn is_open(&self) -> bool { - self.open - } - - pub fn toggle_open(&mut self, window: &mut Window, cx: &mut Context) { - self.set_open(!self.open, window, cx); - } - - /// Returns the size of the Dock, the size is means the width or height of - /// the Dock, if the placement is left or right, the size is width, - /// otherwise the size is height. - pub fn size(&self) -> Pixels { - self.size - } - - /// Set the size of the Dock. - pub fn set_size(&mut self, size: Pixels, _: &mut Window, cx: &mut Context) { - self.size = size.max(PANEL_MIN_SIZE); - cx.notify(); - } - - /// Set the open state of the Dock. - pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context) { - self.open = open; - let item = self.panel.clone(); - cx.defer_in(window, move |_, window, cx| { - item.set_collapsed(!open, window, cx); - }); - cx.notify(); - } - - /// Add item to the Dock. - pub fn add_panel( - &mut self, - panel: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.panel.add_panel(panel, &self.dock_area, window, cx); - cx.notify(); - } - - /// Remove item from the Dock. - pub fn remove_panel( - &mut self, - panel: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.panel.remove_panel(panel, window, cx); - cx.notify(); - } - - fn render_resize_handle(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let axis = self.placement.axis(); - let view = cx.entity().clone(); - - resize_handle("resize-handle", axis) - .when(self.placement == DockPlacement::Left, |this| { - this.placement(Side::Left) - }) - .on_drag(ResizePanel {}, move |info, _, _, cx| { - cx.stop_propagation(); - view.update(cx, |view, _| { - view.resizing = true; - }); - cx.new(|_| info.deref().clone()) - }) - } - - fn resize( - &mut self, - mouse_position: Point, - window: &mut Window, - cx: &mut Context, - ) { - if !self.resizing { - return; - } - - if !self.open { - self.set_open(true, window, cx); - } - - let dock_area = self - .dock_area - .upgrade() - .expect("DockArea is missing") - .read(cx); - let area_bounds = dock_area.bounds; - - let size = mouse_position.x - area_bounds.left(); - let max_size = (area_bounds.size.width - PANEL_MIN_SIZE).max(PANEL_MIN_SIZE); - self.size = size.clamp(PANEL_MIN_SIZE, max_size); - - cx.notify(); - } - - fn done_resizing(&mut self, _window: &mut Window, cx: &mut Context) { - if !self.resizing { - return; - } - self.resizing = false; - - // Dragging the dock's resize handle finished, bubble a layout change - // so subscribers can persist the new dock size. - _ = self.dock_area.update(cx, |_, cx| { - cx.emit(DockEvent::LayoutChanged); - }); - } -} - -impl Render for Dock { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { - if !self.open { - return div(); - } - - let cache_style = StyleRefinement::default().absolute().size_full(); - - div() - .relative() - .overflow_hidden() - .h_flex() - .h_full() - .w(self.size) - .map(|this| match &self.panel { - DockItem::Split { view, .. } => this.child(view.clone()), - DockItem::Tabs { view, .. } => this.child(view.clone()), - DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)), - }) - .child(self.render_resize_handle(window, cx)) - .child(DockElement { - view: cx.entity().clone(), - }) - } -} - -struct DockElement { - view: Entity, -} - -impl IntoElement for DockElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for DockElement { - type PrepaintState = (); - type RequestLayoutState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - window: &mut gpui::Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - (window.request_layout(Style::default(), None, cx), ()) - } - - fn prepaint( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _: gpui::Bounds, - _: &mut Self::RequestLayoutState, - _window: &mut gpui::Window, - _cx: &mut App, - ) -> Self::PrepaintState { - } - - fn paint( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _: gpui::Bounds, - _: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut gpui::Window, - cx: &mut App, - ) { - window.on_mouse_event({ - let view = self.view.clone(); - let resizing = view.read(cx).resizing; - move |e: &MouseMoveEvent, phase, window, cx| { - if !resizing { - return; - } - if !phase.bubble() { - return; - } - - view.update(cx, |view, cx| view.resize(e.position, window, cx)) - } - }); - - // When any mouse up, stop dragging - window.on_mouse_event({ - let view = self.view.clone(); - move |_: &MouseUpEvent, phase, window, cx| { - if phase.bubble() { - view.update(cx, |view, cx| view.done_resizing(window, cx)); - } - } - }) - } -} diff --git a/crates/dock/src/dock_area.rs b/crates/dock/src/dock_area.rs new file mode 100644 index 0000000..7b3f3df --- /dev/null +++ b/crates/dock/src/dock_area.rs @@ -0,0 +1,350 @@ +//! The dock-area appearance: the outer frame, the split frames, and one +//! dock's chrome. Ported from the vendored dock's `DockArea`/`Dock` render +//! onto `gpui_base::dock::DockAreaRenderer`. + +use std::cell::Cell; +use std::ops::Deref as _; +use std::rc::Rc; +use std::sync::Arc; + +use gpui::prelude::FluentBuilder as _; +use gpui::{ + AnyElement, App, AppContext as _, Axis, Context, Div, Element, Empty, InteractiveElement as _, + IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, Stateful, Style, + Styled as _, WeakEntity, Window, div, px, +}; +use gpui_base::dock::{ + DockArea, DockAreaRenderer, DockContext, DockEvent, DockPlacement, NodeId, PanelState, + PanelView, TabGroupRenderer, TilesRenderer, +}; +use gpui_base::resize_handle; +use gpui_component::scroll::ScrollbarMode; +use gpui_component::{ActiveTheme as _, Side, StyledExt as _}; + +use crate::invalid_panel::InvalidPanel; +use crate::tab_panel::SignedTabGroupSkin; +use crate::tiles::SignedTilesSkin; +use crate::{TAB_BAR_HEIGHT, panel_handle}; + +/// What every part of the skin reads, and the dock area it belongs to. +/// +/// The renderer is the only skin-owned object in the picture, so the settings +/// the old `DockArea` carried live here. It is shared by reference with the +/// per-container renderers, which are built once each and outlive any one +/// frame. +pub(crate) struct SkinShared { + area: WeakEntity, + toggle_button_visible: Cell, + tiles_scrollbar_mode: Cell>, + /// The dock whose resize handle is being dragged, if any. Only one can be. + resizing_dock: Cell>, +} + +impl SkinShared { + pub(crate) fn area(&self) -> &WeakEntity { + &self.area + } + + pub(crate) fn is_toggle_button_visible(&self) -> bool { + self.toggle_button_visible.get() + } + + pub(crate) fn tiles_scrollbar_mode(&self) -> Option { + self.tiles_scrollbar_mode.get() + } + + pub(crate) fn resizing_dock(&self) -> &Cell> { + &self.resizing_dock + } + + /// Redraw the area after a setting changed. The skin is not an entity, so + /// nothing else would notice. + pub(crate) fn notify(&self, cx: &mut App) { + _ = self.area.update(cx, |_, cx| cx.notify()); + } +} + +/// The Signed appearance for a [`DockArea`]. +/// +/// Install it at construction, where the area's own weak handle is available: +/// +/// ```ignore +/// let dock = cx.new(|cx| { +/// let skin = SignedDockSkin::new(cx); +/// DockArea::new("dock", Some(1), window, cx).with_renderer(skin) +/// }); +/// ``` +pub struct SignedDockSkin { + shared: Rc, +} + +impl SignedDockSkin { + pub fn new(cx: &mut Context) -> Rc { + Rc::new(Self { + shared: Rc::new(SkinShared { + area: cx.weak_entity(), + toggle_button_visible: Cell::new(true), + tiles_scrollbar_mode: Cell::new(None), + resizing_dock: Cell::new(None), + }), + }) + } + + pub(crate) fn shared(&self) -> &Rc { + &self.shared + } + + /// Whether tab bars offer the affordance that collapses a neighbouring + /// dock. + pub fn is_toggle_button_visible(&self) -> bool { + self.shared.is_toggle_button_visible() + } + + pub fn set_toggle_button_visible(&self, visible: bool, cx: &mut App) { + self.shared.toggle_button_visible.set(visible); + self.shared.notify(cx); + } + + /// When a tiles canvas shows its scrollbar. `None` follows the theme. + pub fn tiles_scrollbar_mode(&self) -> Option { + self.shared.tiles_scrollbar_mode() + } + + pub fn set_tiles_scrollbar_mode(&self, mode: Option, cx: &mut App) { + self.shared.tiles_scrollbar_mode.set(mode); + self.shared.notify(cx); + } +} + +/// The payload a dock's resize handle drags. It draws nothing: the handle +/// itself is the affordance. +#[derive(Clone)] +struct ResizePanel; + +impl Render for ResizePanel { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + Empty + } +} + +impl DockAreaRenderer for SignedDockSkin { + fn frame(&self, _: &mut Window, _: &mut App) -> Stateful
{ + div() + .id("dock-area") + .relative() + .size_full() + .overflow_hidden() + .flex() + .flex_row() + } + + fn center_frame(&self, _: &mut Window, _: &mut App) -> Stateful
{ + div() + .id("dock-area-center") + .flex() + .flex_1() + .flex_col() + .overflow_hidden() + } + + fn split_frame(&self, node: NodeId, _: Axis, _: &mut Window, cx: &mut App) -> Stateful
{ + // `size_full` is what the old `StackPanel::render` carried; `flex_1` + // is belt and braces so the frame never collapses to zero height in + // an unsizing parent. + div() + .id(("dock-split-frame", node.as_u64())) + .size_full() + .flex_1() + .min_h(px(0.)) + .overflow_hidden() + .bg(cx.theme().tokens.tab_bar) + } + + fn render_dock( + &self, + dock: &DockContext, + content: AnyElement, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let placement = dock.placement(); + let open = dock.is_open(); + + // A closed left or right dock takes no space at all; a closed bottom + // dock keeps a strip so its tab bar stays clickable. + if !open && !placement.is_bottom() { + return div().into_any_element(); + } + + div() + .flex() + .flex_none() + .relative() + .overflow_hidden() + .map(|this| match placement { + DockPlacement::Left | DockPlacement::Right => this.h_flex().h_full().w(dock.size()), + DockPlacement::Bottom => this.w_full().h(dock.size()), + // Base never builds a dock for the centre. + DockPlacement::Center => this, + }) + // The closed bottom dock's strip is the tab bar itself, which is + // a full tab bar tall. + .when(!open && placement.is_bottom(), |this| { + this.h(TAB_BAR_HEIGHT) + }) + .child(content) + .child(self.render_resize_handle(dock, window, cx)) + .child(DockResizeTracker { + dock: dock.clone(), + shared: self.shared().clone(), + }) + .into_any_element() + } + + /// The "unknown panel" message the old `InvalidPanel` drew. + /// + /// It answers `dump` with the state it was handed, so a layout written by + /// a build that knows the panel survives a load and save here. + fn build_placeholder( + &self, + state: &PanelState, + _: &mut Window, + cx: &mut App, + ) -> Option> { + let state = state.clone(); + Some(panel_handle(cx.new(|cx| { + InvalidPanel::new(state.panel_name.clone(), state, cx) + }))) + } + + fn tab_group_renderer(&self) -> Rc { + Rc::new(SignedTabGroupSkin::new(self.shared().clone())) + } + + fn tiles_renderer(&self) -> Rc { + Rc::new(SignedTilesSkin::new(self.shared().clone())) + } +} + +impl SignedDockSkin { + fn render_resize_handle( + &self, + dock: &DockContext, + _: &mut Window, + _: &mut App, + ) -> impl IntoElement { + let placement = dock.placement(); + let shared = self.shared().clone(); + + resize_handle("resize-handle", placement.axis()) + .when(placement.is_left(), |this| this.placement(Side::Left)) + .on_drag(ResizePanel, move |info, _, _, cx| { + cx.stop_propagation(); + shared.resizing_dock().set(Some(placement)); + cx.new(|_| info.deref().clone()) + }) + } +} + +/// Turns the window's mouse stream into dock resizing. +/// +/// A resize is driven by pointer moves that land anywhere in the window, not +/// only on the handle, so it cannot be expressed as a listener on the handle +/// itself. This element paints nothing and exists for its `paint` hook, which +/// is the only place a window-level mouse listener can be registered. +struct DockResizeTracker { + dock: DockContext, + shared: Rc, +} + +impl IntoElement for DockResizeTracker { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for DockResizeTracker { + type PrepaintState = (); + type RequestLayoutState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: Option<&gpui::InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + (window.request_layout(Style::default(), None, cx), ()) + } + + fn prepaint( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: Option<&gpui::InspectorElementId>, + _: gpui::Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Window, + _: &mut App, + ) -> Self::PrepaintState { + } + + fn paint( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: Option<&gpui::InspectorElementId>, + _: gpui::Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + _: &mut App, + ) { + let placement = self.dock.placement(); + + window.on_mouse_event({ + let dock = self.dock.clone(); + let shared = self.shared.clone(); + move |event: &MouseMoveEvent, phase, window, cx| { + if !phase.bubble() || shared.resizing_dock().get() != Some(placement) { + return; + } + // Dragging a closed dock's handle reopens it, as the old dock + // did. The live state is read rather than the render-time + // snapshot in `dock`, which would still say closed for the + // rest of the frame and toggle it shut again on the next move. + let open = shared + .area() + .upgrade() + .is_some_and(|area| area.read(cx).is_dock_open(placement)); + if !open { + dock.toggle(window, cx); + } + dock.resize_to(event.position, window, cx); + } + }); + + window.on_mouse_event({ + let shared = self.shared.clone(); + move |_: &MouseUpEvent, phase, _, cx| { + if !phase.bubble() || shared.resizing_dock().get() != Some(placement) { + return; + } + shared.resizing_dock().set(None); + // The size lives on the dock, not in the layout tree, so + // nothing else tells a subscriber to persist it. + _ = shared + .area() + .update(cx, |_, cx| cx.emit(DockEvent::LayoutChanged)); + } + }); + } +} diff --git a/crates/dock/src/fixtures/layout.json b/crates/dock/src/fixtures/layout.json deleted file mode 100644 index d1ceb41..0000000 --- a/crates/dock/src/fixtures/layout.json +++ /dev/null @@ -1,205 +0,0 @@ -{ - "center": { - "panel_name": "StackPanel", - "children": [ - { - "panel_name": "TabPanel", - "children": [ - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "ButtonStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "InputStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "TextStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "SelectStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "DialogStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "SwitchStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "ProgressStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "DataTableStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "ImageStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "IconStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "TooltipStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "ProgressStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "CalendarStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "ResizableStory" - } - } - }, - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "ScrollbarStory" - } - } - } - ], - "info": { - "tabs": { - "active_index": 0 - } - } - }, - { - "panel_name": "TabPanel", - "children": [ - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "PopupStory" - } - } - } - ], - "info": { - "tabs": { - "active_index": 0 - } - } - } - ], - "info": { - "stack": { - "sizes": [ - 704.0, - 263.0 - ], - "axis": 1 - } - } - }, - "left_dock": { - "panel": { - "panel_name": "TabPanel", - "children": [ - { - "panel_name": "StoryContainer", - "children": [], - "info": { - "panel": { - "story_klass": "ListStory" - } - } - } - ], - "info": { - "tabs": { - "active_index": 0 - } - } - }, - "placement": "left", - "size": 350.0, - "open": true, - "resizeable": true - } -} \ No newline at end of file diff --git a/crates/dock/src/invalid_panel.rs b/crates/dock/src/invalid_panel.rs index ff5c8c7..da78f36 100644 --- a/crates/dock/src/invalid_panel.rs +++ b/crates/dock/src/invalid_panel.rs @@ -1,11 +1,18 @@ use gpui::{ - App, EventEmitter, FocusHandle, Focusable, ParentElement as _, Render, SharedString, - Styled as _, Window, + App, Context, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement as _, Render, + SharedString, Styled as _, Window, div, }; +use gpui_base::dock::{PanelEvent, PanelState}; use gpui_component::ActiveTheme as _; -use super::{Panel, PanelEvent, PanelState}; +use crate::Panel; +/// Stands in for a panel this build cannot construct — one whose `panel_name` +/// no [`PanelRegistry`](gpui_base::dock::PanelRegistry) builder answers to. +/// +/// It reports the original [`PanelState`] from +/// [`dump`](gpui_base::dock::Panel::dump), so a layout written by a build that +/// knows the panel survives a load and a save here rather than losing it. pub(crate) struct InvalidPanel { name: SharedString, focus_handle: FocusHandle, @@ -13,36 +20,42 @@ pub(crate) struct InvalidPanel { } impl InvalidPanel { - pub(crate) fn new(name: &str, state: PanelState, _: &mut Window, cx: &mut App) -> Self { + pub(crate) fn new( + name: impl Into, + state: PanelState, + cx: &mut Context, + ) -> Self { Self { focus_handle: cx.focus_handle(), - name: SharedString::from(name.to_owned()), + name: name.into(), old_state: state, } } } -impl Panel for InvalidPanel { + +impl gpui_base::dock::Panel for InvalidPanel { fn panel_name(&self) -> &'static str { "InvalidPanel" } - fn dump(&self, _cx: &App) -> super::PanelState { + fn dump(&self, _: &App) -> PanelState { self.old_state.clone() } } + +impl Panel for InvalidPanel {} + impl EventEmitter for InvalidPanel {} + impl Focusable for InvalidPanel { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } + impl Render for InvalidPanel { - fn render( - &mut self, - _: &mut gpui::Window, - cx: &mut gpui::Context, - ) -> impl gpui::IntoElement { - gpui::div() + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + div() .size_full() .my_6() .flex() diff --git a/crates/dock/src/lib.rs b/crates/dock/src/lib.rs index 6a57ce4..16f2192 100644 --- a/crates/dock/src/lib.rs +++ b/crates/dock/src/lib.rs @@ -1,46 +1,40 @@ -mod dock; +//! The Signed dock skin. +//! +//! The dock engine lives upstream: `gpui_base::dock` owns the layout tree, +//! the drags, the zoom and the persistence, and `gpui_component::dock` +//! supplies the default appearance. This crate is the appearance the app +//! used to vendor from gpui-component — a 44px tab bar that doubles as the +//! window title bar, with pill tabs, window controls, title-bar dragging and +//! previous/next tab buttons — re-implemented against upstream's renderer +//! traits. +//! +//! Everything `gpui_component::dock` exports is re-exported here, so the app +//! keeps importing the dock from a single place. + +use gpui::{ + App, Div, InteractiveElement as _, MouseButton, Pixels, Stateful, + StatefulInteractiveElement as _, Window, WindowControlArea, px, +}; + +mod dock_area; mod invalid_panel; -mod panel; -mod resize_handle; -mod stack_panel; -mod state; -mod tab_bar; mod tab_panel; +mod tiles; mod window_controls; -use std::sync::Arc; - -use anyhow::Result; -pub use dock::*; -use gpui::prelude::FluentBuilder; -use gpui::{ - AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity, EntityId, - EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, - SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px, +pub use dock_area::SignedDockSkin; +pub use gpui_component::dock::{ + AnyDrag, BasePanel, BasePanelView, ClosePanel, DockArea, DockAreaState, DockContext, DockEvent, + DockLayout, DockPlacement, DockState, DragPanel, DropIndicator, DropPlaceholderBounds, + DropTarget, Panel, PanelControl, PanelEvent, PanelHandle, PanelInfo, PanelState, PanelStyle, + PanelView, TitleStyle, ToggleZoom, panel_handle, register_panel, }; -use gpui_component::{ElementExt, Placement}; -pub use panel::*; -pub use stack_panel::*; -pub use state::*; -pub use tab_panel::*; -/// Initialize the dock, registering the [`PanelRegistry`] global. -/// -/// Call this from your app entry point, before building any [`DockArea`]. -/// It is idempotent, so it is safe to call alongside `gpui_component::init`. -pub fn init(cx: &mut App) { - PanelRegistry::init(cx); -} +/// The fixed height of the tab bar, which doubles as the window title bar. +pub const TAB_BAR_HEIGHT: Pixels = px(44.); -// Note: the action group name must not collide with gpui-component's own -// `dock::` actions, which are linked into the same binary while the app still -// depends on gpui-component (action names are registered globally per App). -actions!(signed_dock, [ToggleZoom, ClosePanel]); - -/// Minimal i18n shim replacing gpui-component's `rust_i18n::t!()`. -/// -/// The upstream dock used `t!("Dock.*")` keys; we keep the same keys but -/// resolve them to the English strings so the crate has no i18n dependency. +/// Minimal i18n shim replacing gpui-component's `rust_i18n::t!()`, keeping the +/// same `Dock.*` keys resolved to English so the crate has no i18n dependency. pub(crate) fn t(key: &'static str) -> &'static str { match key { "Dock.Unnamed" => "Unnamed", @@ -53,977 +47,53 @@ pub(crate) fn t(key: &'static str) -> &'static str { } } -/// The fixed height of the tab bar, which doubles as the window title bar. -pub const TAB_BAR_HEIGHT: Pixels = px(44.); - -/// A host-owned drag item, dragged into the dock by the application. -#[derive(Clone, Debug)] -pub struct AnyDrag { - pub value: Arc, +/// State used to move the window when the title bar area is dragged. +struct WindowDragState { + should_move: bool, } -impl AnyDrag { - pub fn new(value: T) -> Self { - Self { - value: Arc::new(value), - } - } -} +/// Make an element behave like a window title bar: dragging it moves the +/// window, and double-clicking zooms the window (or performs the platform's +/// default title-bar double-click action on macOS). +/// +/// Only the bar's non-interactive areas should get this — tabs are draggable +/// (to reorder panels) and must not move the window. +pub fn title_bar_drag_handlers( + this: Stateful
, + window: &mut Window, + cx: &mut App, +) -> Stateful
{ + let state = window.use_state(cx, |_, _| WindowDragState { should_move: false }); -pub enum DockEvent { - /// The layout of the dock has changed, subscribers this to save the layout. - /// - /// This event is emitted when every time the layout of the dock has changed, - /// So it emits may be too frequently, you may want to debounce the event. - LayoutChanged, - - /// A host-owned drag item ([`AnyDrag`]) was dropped inside the dock. - DragDrop { item: AnyDrag, target: DropTarget }, -} - -/// Where a host-owned drag landed, and how much the container can say about it. -#[derive(Clone, Debug)] -pub enum DropTarget { - /// Dropped on a [`TabPanel`] in a split layout. A split layout has no free - /// coordinates, so the container reports the panel and the edge it resolved - /// from the cursor instead. - /// - /// `placement` is `None` for the centre zone, meaning merge into the tab - /// group rather than split. - Panel { - tab_panel: Entity, - placement: Option, - }, -} - -/// The main area of the dock. -pub struct DockArea { - id: SharedString, - /// The version is used to special the default layout, this is like the `panel_version` in [`Panel`](Panel). - version: Option, - pub(crate) bounds: Bounds, - - /// The center view of the dock_area. - center: DockItem, - /// The left dock of the dock_area. - left_dock: Option>, - - /// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed, - toggle_button_panels: Edges>, - - /// Whether to show the toggle button. - toggle_button_visible: bool, - /// The top zoom view of the dock_area, if any. - zoom_view: Option, - - /// Lock panels layout, but allow to resize. - locked: bool, - - _subscriptions: Vec, -} - -/// DockItem is a tree structure that represents the layout of the dock. -#[derive(Clone)] -pub enum DockItem { - /// Split layout - Split { - axis: Axis, - /// Self size, only used for build split panels - size: Option, - items: Vec, - /// Items sizes - sizes: Vec>, - view: Entity, - }, - /// Tab layout - Tabs { - /// Self size, only used for build split panels - size: Option, - items: Vec>, - active_ix: usize, - view: Entity, - }, - /// Panel layout - Panel { - /// Self size, only used for build split panels - size: Option, - view: Arc, - }, -} - -impl std::fmt::Debug for DockItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DockItem::Split { - axis, items, sizes, .. - } => f - .debug_struct("Split") - .field("axis", axis) - .field("items", &items.len()) - .field("sizes", sizes) - .finish(), - DockItem::Tabs { - items, active_ix, .. - } => f - .debug_struct("Tabs") - .field("items", &items.len()) - .field("active_ix", active_ix) - .finish(), - DockItem::Panel { .. } => f.debug_struct("Panel").finish(), - } - } -} - -impl DockItem { - /// Get the size of the DockItem. - fn get_size(&self) -> Option { - match self { - Self::Split { size, .. } => *size, - Self::Tabs { size, .. } => *size, - Self::Panel { size, .. } => *size, - } - } - - /// Set size for the DockItem. - pub fn size(mut self, new_size: impl Into) -> Self { - let new_size: Option = Some(new_size.into()); - match self { - Self::Split { ref mut size, .. } => *size = new_size, - Self::Tabs { ref mut size, .. } => *size = new_size, - Self::Panel { ref mut size, .. } => *size = new_size, - } - self - } - - /// Set active index for the DockItem, only valid for [`DockItem::Tabs`]. - pub fn active_index(mut self, new_active_ix: usize, cx: &mut App) -> Self { - debug_assert!( - matches!(self, Self::Tabs { .. }), - "active_ix can only be set for DockItem::Tabs" - ); - - if let Self::Tabs { - ref mut active_ix, - ref mut view, - .. - } = self - { - *active_ix = new_active_ix; - view.update(cx, |tab_panel, _| { - tab_panel.active_ix = new_active_ix; - }); - } - self - } - - /// Create DockItem::Split with given split layout. - pub fn split( - axis: Axis, - items: Vec, - dock_area: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Self { - let sizes = items.iter().map(|item| item.get_size()).collect(); - Self::split_with_sizes(axis, items, sizes, dock_area, window, cx) - } - - /// Create DockItem with vertical split layout. - pub fn v_split( - items: Vec, - dock_area: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Self { - Self::split(Axis::Vertical, items, dock_area, window, cx) - } - - /// Create DockItem with horizontal split layout. - pub fn h_split( - items: Vec, - dock_area: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Self { - Self::split(Axis::Horizontal, items, dock_area, window, cx) - } - - /// Create DockItem with split layout, each item of panel have specified size. - /// - /// Please note that the `items` and `sizes` must have the same length. - /// Set `None` in `sizes` to make the index of panel have auto size. - pub fn split_with_sizes( - axis: Axis, - items: Vec, - sizes: Vec>, - dock_area: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Self { - let stack_panel = cx.new(|cx| { - let mut stack_panel = StackPanel::new(axis, window, cx); - for (i, item) in items.iter().enumerate() { - let view = item.view(); - let size = sizes.get(i).copied().flatten(); - stack_panel.add_panel(view.clone(), size, dock_area.clone(), window, cx) + 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(); } - - stack_panel - }); - - window.defer(cx, { - let stack_panel = stack_panel.clone(); - let dock_area = dock_area.clone(); - move |window, cx| { - _ = dock_area.update(cx, |this, cx| { - this.subscribe_panel(&stack_panel, window, cx); - }); - } - }); - - Self::Split { - axis, - size: None, - items, - sizes, - view: stack_panel, - } - } - - /// Create DockItem with panel layout - pub fn panel(panel: Arc) -> Self { - Self::Panel { - size: None, - view: panel, - } - } - - /// Create DockItem with tabs layout, items are displayed as tabs. - /// - /// The `active_ix` is the index of the active tab, if `None` the first tab is active. - pub fn tabs( - items: Vec>, - dock_area: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Self { - let mut new_items: Vec> = vec![]; - for item in items.into_iter() { - new_items.push(item) - } - Self::new_tabs(new_items, None, dock_area, window, cx) - } - - pub fn tab( - item: Entity

, - dock_area: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Self { - Self::new_tabs(vec![Arc::new(item.clone())], None, dock_area, window, cx) - } - - fn new_tabs( - items: Vec>, - active_ix: Option, - dock_area: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Self { - let active_ix = active_ix.unwrap_or(0); - let tab_panel = cx.new(|cx| { - let mut tab_panel = TabPanel::new(None, dock_area.clone(), window, cx); - for item in items.iter() { - tab_panel.add_panel(item.clone(), window, cx) - } - tab_panel.active_ix = active_ix; - tab_panel - }); - - Self::Tabs { - size: None, - items, - active_ix, - view: tab_panel, - } - } - - /// Returns the views of the dock item. - pub fn view(&self) -> Arc { - match self { - Self::Split { view, .. } => Arc::new(view.clone()), - Self::Tabs { view, .. } => Arc::new(view.clone()), - Self::Panel { view, .. } => view.clone(), - } - } - - /// Whether this dock item currently holds no visible panel. - /// - /// Walks the live panel entities, not `items`: [`Self::add_panel`] only - /// pushes into them, nothing ever removes, and splitting does not touch - /// them at all. - /// - /// A container is empty when every child is, so a fresh one is empty. A - /// leaf counts as empty while it is hidden, matching the render path, which - /// skips panels whose [`Panel::visible`] is `false`. - pub fn is_empty(&self, cx: &App) -> bool { - fn is_empty(panel: &Arc, cx: &App) -> bool { - let view = panel.view(); - - if let Ok(stack) = view.clone().downcast::() { - return stack - .read(cx) - .panels - .iter() - .all(|panel| is_empty(panel, cx)); - } - if let Ok(tabs) = view.clone().downcast::() { - return tabs.read(cx).panels.iter().all(|panel| is_empty(panel, cx)); - } - - !panel.visible(cx) - } - - is_empty(&self.view(), cx) - } - - /// Find existing panel in the dock item. - pub fn find_panel(&self, panel: Arc) -> Option> { - match self { - Self::Split { items, .. } => { - items.iter().find_map(|item| item.find_panel(panel.clone())) - } - Self::Tabs { items, .. } => items.iter().find(|item| *item == &panel).cloned(), - Self::Panel { view, .. } => Some(view.clone()), - } - } - - /// Add a panel to the dock item. - pub fn add_panel( - &mut self, - panel: Arc, - dock_area: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) { - match self { - Self::Tabs { view, items, .. } => { - items.push(panel.clone()); - view.update(cx, |tab_panel, cx| { - tab_panel.add_panel(panel, window, cx); - }); - } - Self::Split { view, items, .. } => { - // Iter items to add panel to the first tabs - for item in items.iter_mut() { - if let DockItem::Tabs { view, .. } = item { - view.update(cx, |tab_panel, cx| { - tab_panel.add_panel(panel.clone(), window, cx); - }); - return; - } - } - - // Unable to find tabs, create new tabs - let new_item = Self::tabs(vec![panel.clone()], dock_area, window, cx); - items.push(new_item.clone()); - view.update(cx, |stack_panel, cx| { - stack_panel.add_panel(new_item.view(), None, dock_area.clone(), window, cx); - }); - } - Self::Panel { .. } => {} - } - } - - /// Remove a panel from the dock item. - pub fn remove_panel(&self, panel: Arc, window: &mut Window, cx: &mut App) { - match self { - DockItem::Tabs { view, .. } => { - view.update(cx, |tab_panel, cx| { - tab_panel.remove_panel(panel, window, cx); - }); - } - DockItem::Split { items, view, .. } => { - // For each child item, set collapsed state - for item in items { - item.remove_panel(panel.clone(), window, cx); - } - view.update(cx, |split, cx| { - split.remove_panel(panel, window, cx); - }); - } - DockItem::Panel { .. } => {} - } - } - - pub fn set_collapsed(&self, collapsed: bool, window: &mut Window, cx: &mut App) { - match self { - DockItem::Tabs { view, .. } => { - view.update(cx, |tab_panel, cx| { - tab_panel.set_collapsed(collapsed, window, cx); - }); - } - DockItem::Split { items, .. } => { - // For each child item, set collapsed state - for item in items { - item.set_collapsed(collapsed, window, cx); - } - } - DockItem::Panel { view, .. } => view.set_active(!collapsed, window, cx), - } - } - - /// Recursively traverses to find the left-most and top-most TabPanel. - pub(crate) fn left_top_tab_panel(&self, cx: &App) -> Option> { - match self { - DockItem::Tabs { view, .. } => Some(view.clone()), - DockItem::Split { view, .. } => view.read(cx).left_top_tab_panel(true, cx), - DockItem::Panel { .. } => None, - } - } -} - -impl DockArea { - pub fn new( - id: impl Into, - version: Option, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let stack_panel = cx.new(|cx| StackPanel::new(Axis::Horizontal, window, cx)); - - let dock_item = DockItem::Split { - axis: Axis::Horizontal, - size: None, - items: vec![], - sizes: vec![], - view: stack_panel.clone(), - }; - - let mut this = Self { - id: id.into(), - version, - bounds: Bounds::default(), - center: dock_item, - left_dock: None, - zoom_view: None, - toggle_button_panels: Edges::default(), - toggle_button_visible: true, - locked: false, - _subscriptions: vec![], - }; - - this.subscribe_panel(&stack_panel, window, cx); - - this - } - - /// Return the bounds of the dock area. - pub fn bounds(&self) -> Bounds { - self.bounds - } - - /// Set version of the dock area. - pub fn set_version(&mut self, version: usize, _: &mut Window, cx: &mut Context) { - self.version = Some(version); - cx.notify(); - } - - /// Return the center dock item. - pub fn center(&self) -> &DockItem { - &self.center - } - - /// Whether the center area currently holds no visible panel. - /// - /// See [`DockItem::is_empty`]. Ask a dock the same question with - /// [`Dock::panel`]. - pub fn is_center_empty(&self, cx: &App) -> bool { - self.center.is_empty(cx) - } - - /// Return the left dock item. - pub fn left_dock(&self) -> Option<&Entity> { - self.left_dock.as_ref() - } - - /// Remove the left dock. - pub fn remove_left_dock(&mut self, _: &mut Window, _: &mut Context) { - self.left_dock = None; - } - - /// The the DockItem as the center of the dock area. - /// - /// This is used to render at the Center of the DockArea. - pub fn set_center(&mut self, center: DockItem, window: &mut Window, cx: &mut Context) { - self.subscribe_item(¢er, window, cx); - self.center = center; - self.update_toggle_button_tab_panels(window, cx); - cx.notify(); - } - - pub fn set_left_dock( - &mut self, - panel: DockItem, - size: Option, - open: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.subscribe_item(&panel, window, cx); - let weak_self = cx.entity().downgrade(); - self.left_dock = Some(cx.new(|cx| { - let mut dock = Dock::left(weak_self.clone(), window, cx); - if let Some(size) = size { - dock.set_size(size, window, cx); - } - dock.set_panel(panel, window, cx); - dock.set_open(open, window, cx); - dock - })); - self.update_toggle_button_tab_panels(window, cx); - } - - /// Set locked state of the dock area, if locked, the dock area cannot be split or move, but allows to resize panels. - pub fn set_locked(&mut self, locked: bool, _window: &mut Window, _cx: &mut App) { - self.locked = locked; - } - - /// Determine if the dock area is locked. - #[inline] - pub fn is_locked(&self) -> bool { - self.locked - } - - /// Determine if the dock area has a dock at the given placement. - pub fn has_dock(&self, placement: DockPlacement) -> bool { - match placement { - DockPlacement::Left => self.left_dock.is_some(), - DockPlacement::Center | DockPlacement::Right | DockPlacement::Bottom => false, - } - } - - /// Determine if the dock at the given placement is open. - pub fn is_dock_open(&self, placement: DockPlacement, cx: &App) -> bool { - match placement { - DockPlacement::Left => self - .left_dock - .as_ref() - .map(|dock| dock.read(cx).is_open()) - .unwrap_or(false), - DockPlacement::Center | DockPlacement::Right | DockPlacement::Bottom => false, - } - } - - /// Set the dock at the given placement to be open or closed. - /// - /// Only the left dock can be toggled. - pub fn set_dock_collapsible( - &mut self, - collapsible_edges: Edges, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(left_dock) = self.left_dock.as_ref() { - left_dock.update(cx, |dock, cx| { - dock.set_collapsible(collapsible_edges.left, window, cx); - }); - } - } - - /// Determine if the dock at the given placement is collapsible. - pub fn is_dock_collapsible(&self, placement: DockPlacement, cx: &App) -> bool { - match placement { - DockPlacement::Left => self - .left_dock - .as_ref() - .map(|dock| dock.read(cx).collapsible) - .unwrap_or(false), - DockPlacement::Center | DockPlacement::Right | DockPlacement::Bottom => false, - } - } - - /// Toggle the dock at the given placement. - pub fn toggle_dock( - &self, - placement: DockPlacement, - window: &mut Window, - cx: &mut Context, - ) { - if let DockPlacement::Left = placement - && let Some(dock) = self.left_dock.as_ref() - { - dock.update(cx, |view, cx| { - view.toggle_open(window, cx); - }) - } - } - - /// Set the visibility of the toggle button. - pub fn set_toggle_button_visible(&mut self, visible: bool, _: &mut Context) { - self.toggle_button_visible = visible; - } - - /// Add a panel item to the dock area at the given placement. - /// - /// - [`DockPlacement::Left`] adds the panel to the left dock (creating it - /// if needed). - /// - [`DockPlacement::Center`] adds the panel as a tab in the center. - /// - [`DockPlacement::Right`] and [`DockPlacement::Bottom`] split the - /// center so the panel lands on the given side of the existing center - /// content. - pub fn add_panel( - &mut self, - panel: Arc, - placement: DockPlacement, - window: &mut Window, - cx: &mut Context, - ) { - let weak_self = cx.entity().downgrade(); - match placement { - DockPlacement::Left => { - if let Some(dock) = self.left_dock.as_ref() { - dock.update(cx, |dock, cx| dock.add_panel(panel, window, cx)) + })) + .on_click(|event, window, _| { + if event.click_count() == 2 { + if cfg!(target_os = "macos") { + window.titlebar_double_click(); } else { - self.set_left_dock( - DockItem::tabs(vec![panel], &weak_self, window, cx), - None, - true, - window, - cx, - ); + window.zoom_window(); } } - DockPlacement::Right | DockPlacement::Bottom => { - self.add_panel_to_center_side(panel, placement, window, cx); - } - DockPlacement::Center => { - self.center - .add_panel(panel, &cx.entity().downgrade(), window, cx); - } - } - } - - /// Split the center so the new panel lands on the given side of the - /// existing center content (right of it for [`DockPlacement::Right`], - /// below it for [`DockPlacement::Bottom`]). - /// - /// The existing center keeps its place. Repeating adds with the same axis - /// appends further tab groups to the split; a different axis wraps the - /// current center in a new split. An empty center is added to directly. - fn add_panel_to_center_side( - &mut self, - panel: Arc, - placement: DockPlacement, - window: &mut Window, - cx: &mut Context, - ) { - debug_assert!(matches!( - placement, - DockPlacement::Right | DockPlacement::Bottom - )); - - let weak_self = cx.entity().downgrade(); - - if self.center.is_empty(cx) { - self.center.add_panel(panel, &weak_self, window, cx); - } else if let DockItem::Split { - axis, - size: _, - items, - sizes, - view, - } = &mut self.center - && *axis == placement.axis() - { - // The center is already split along this axis: append a new tab group. - let new_item = DockItem::tabs(vec![panel], &weak_self, window, cx); - items.push(new_item.clone()); - sizes.push(None); - view.update(cx, |stack, cx| { - stack.add_panel(new_item.view(), None, weak_self.clone(), window, cx); - }); - } else { - // Wrap the existing center in a new split. - let existing = self.center.clone(); - let new_item = DockItem::tabs(vec![panel], &weak_self, window, cx); - let items = vec![existing, new_item]; - let sizes = vec![None; items.len()]; - self.center = - DockItem::split_with_sizes(placement.axis(), items, sizes, &weak_self, window, cx); - } - - self.update_toggle_button_tab_panels(window, cx); - cx.notify(); - } - - /// Remove panel from the DockArea at the given placement. - pub fn remove_panel( - &mut self, - panel: Arc, - placement: DockPlacement, - window: &mut Window, - cx: &mut Context, - ) { - match placement { - DockPlacement::Left => { - if let Some(dock) = self.left_dock.as_mut() { - dock.update(cx, |dock, cx| { - dock.remove_panel(panel, window, cx); - }); - } - } - DockPlacement::Center => { - self.center.remove_panel(panel, window, cx); - } - DockPlacement::Right | DockPlacement::Bottom => { - // The panel lives in the center; splits are part of it. - self.center.remove_panel(panel, window, cx); - } - } - cx.notify(); - } - - /// Remove a panel from all docks. - pub fn remove_panel_from_all_docks( - &mut self, - panel: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.remove_panel(panel.clone(), DockPlacement::Center, window, cx); - self.remove_panel(panel.clone(), DockPlacement::Left, window, cx); - } - - /// Load the state of the DockArea from the DockAreaState. - /// - /// See also [DockeArea::dump]. - pub fn load( - &mut self, - state: DockAreaState, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - self.version = state.version; - let weak_self = cx.entity().downgrade(); - - if let Some(left_dock_state) = state.left_dock { - self.left_dock = Some(left_dock_state.to_dock(weak_self.clone(), window, cx)); - } - - self.center = state.center.to_item(weak_self, window, cx); - self.update_toggle_button_tab_panels(window, cx); - Ok(()) - } - - /// Dump the dock panels layout to PanelState. - /// - /// See also [DockArea::load]. - pub fn dump(&self, cx: &App) -> DockAreaState { - let root = self.center.view(); - let center = root.dump(cx); - - let left_dock = self - .left_dock - .as_ref() - .map(|dock| DockState::new(dock.clone(), cx)); - - DockAreaState { - version: self.version, - center, - left_dock, - } - } - - /// Subscribe event on the panels - #[allow(clippy::only_used_in_recursion)] - fn subscribe_item(&mut self, item: &DockItem, window: &mut Window, cx: &mut Context) { - match item { - DockItem::Split { items, view, .. } => { - for item in items { - self.subscribe_item(item, window, cx); - } - - self._subscriptions.push(cx.subscribe_in( - view, - window, - move |_, _, event, window, cx| { - if let PanelEvent::LayoutChanged = event { - cx.spawn_in(window, async move |view, window| { - _ = view.update_in(window, |view, window, cx| { - view.update_toggle_button_tab_panels(window, cx) - }); - }) - .detach(); - cx.emit(DockEvent::LayoutChanged); - } - }, - )); - } - DockItem::Tabs { .. } => { - // We subscribe to the tab panel event in StackPanel's insert_panel - } - DockItem::Panel { .. } => { - // Not supported - } - } - } - - /// Subscribe zoom event on the panel - pub(crate) fn subscribe_panel( - &mut self, - view: &Entity

, - window: &mut Window, - cx: &mut Context, - ) { - let subscription = - cx.subscribe_in( - view, - window, - move |_, panel, event, window, cx| match event { - PanelEvent::ZoomIn => { - let panel = panel.clone(); - cx.spawn_in(window, async move |view, window| { - _ = view.update_in(window, |view, window, cx| { - view.set_zoomed_in(panel, window, cx); - cx.notify(); - }); - }) - .detach(); - } - PanelEvent::ZoomOut => cx - .spawn_in(window, async move |view, window| { - _ = view.update_in(window, |view, window, cx| { - view.set_zoomed_out(window, cx); - }); - }) - .detach(), - PanelEvent::LayoutChanged => { - cx.spawn_in(window, async move |view, window| { - _ = view.update_in(window, |view, window, cx| { - view.update_toggle_button_tab_panels(window, cx) - }); - }) - .detach(); - cx.emit(DockEvent::LayoutChanged); - } - }, - ); - - self._subscriptions.push(subscription); - } - - /// Returns the ID of the dock area. - pub fn id(&self) -> SharedString { - self.id.clone() - } - - pub fn set_zoomed_in( - &mut self, - panel: Entity

, - _: &mut Window, - cx: &mut Context, - ) { - self.zoom_view = Some(panel.into()); - cx.notify(); - } - - pub fn set_zoomed_out(&mut self, _: &mut Window, cx: &mut Context) { - self.zoom_view = None; - cx.notify(); - } - - fn render_items(&self, _window: &mut Window, _cx: &mut Context) -> AnyElement { - match &self.center { - DockItem::Split { view, .. } => view.clone().into_any_element(), - DockItem::Tabs { view, .. } => view.clone().into_any_element(), - DockItem::Panel { view, .. } => view.clone().view().into_any_element(), - } - } - - pub fn update_toggle_button_tab_panels(&mut self, _: &mut Window, cx: &mut Context) { - // The left dock's toggle button lives in the center's top-left tab panel. - self.toggle_button_panels.left = self - .center - .left_top_tab_panel(cx) - .map(|view| view.entity_id()); - } -} - -impl EventEmitter for DockArea {} - -impl Render for DockArea { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let view = cx.entity().clone(); - - div() - .id("dock-area") - .relative() - .size_full() - .overflow_hidden() - .on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds)) - .map(|this| { - if let Some(zoom_view) = self.zoom_view.clone() { - this.child(zoom_view) - } else { - // render dock - this.child( - div() - .flex() - .flex_row() - .h_full() - // Left dock - .when_some(self.left_dock.clone(), |this, dock| { - this.child(div().flex().flex_none().child(dock)) - }) - // Center - .child( - div() - .flex_1() - .overflow_hidden() - .child(self.render_items(window, cx)), - ), - ) - } - }) - } -} - -#[cfg(test)] -mod tests { - use gpui::TestAppContext; - - use super::*; - - #[gpui::test] - fn split_with_sizes_adds_each_child_once(cx: &mut TestAppContext) { - cx.update(|cx| { - cx.set_global(gpui_component::Theme::default()); - cx.open_window(Default::default(), |window, cx| { - let dock_area = cx.new(|cx| DockArea::new("test-dock", None, window, cx)); - let weak_dock_area = dock_area.downgrade(); - let children = vec![ - DockItem::tabs(Vec::new(), &weak_dock_area, window, cx), - DockItem::tabs(Vec::new(), &weak_dock_area, window, cx), - ]; - - let split = DockItem::split_with_sizes( - Axis::Horizontal, - children, - vec![None, None], - &weak_dock_area, - window, - cx, - ); - - let DockItem::Split { view, .. } = split else { - unreachable!("split_with_sizes must return DockItem::Split"); - }; - assert_eq!(view.read(cx).panels_len(), 2); - - cx.new(|cx| gpui_component::Root::new(dock_area, window, cx)) - }) - .unwrap(); - }); - } + }) } diff --git a/crates/dock/src/panel.rs b/crates/dock/src/panel.rs deleted file mode 100644 index 0e2fa0e..0000000 --- a/crates/dock/src/panel.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use gpui::{ - AnyElement, AnyView, App, AppContext as _, Context, Entity, EntityId, EventEmitter, - FocusHandle, Focusable, Global, IntoElement, Render, SharedString, WeakEntity, Window, -}; -use gpui_component::button::Button; -use gpui_component::menu::PopupMenu; - -use super::{DockArea, PanelInfo, PanelState, TabPanel}; -use crate::invalid_panel::InvalidPanel; -use crate::t; - -pub enum PanelEvent { - ZoomIn, - ZoomOut, - LayoutChanged, -} - -#[derive(Clone, Copy, Default)] -pub enum PanelControl { - Both, - #[default] - Menu, - Toolbar, -} - -impl PanelControl { - #[inline] - pub fn toolbar_visible(&self) -> bool { - matches!(self, PanelControl::Both | PanelControl::Toolbar) - } - - #[inline] - pub fn menu_visible(&self) -> bool { - matches!(self, PanelControl::Both | PanelControl::Menu) - } -} - -/// The Panel trait used to define the panel. -#[allow(unused_variables)] -pub trait Panel: EventEmitter + Render + Focusable { - /// The name of the panel used to serialize, deserialize and identify the panel. - /// - /// This is used to identify the panel when deserializing the panel. - /// Once you have defined a panel name, this must not be changed. - fn panel_name(&self) -> &'static str; - - /// The name of the tab of the panel, default is `None`. - /// - /// Used to display in the already collapsed tab panel. - fn tab_name(&self, cx: &App) -> Option { - None - } - - /// The title of the panel - fn title(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - t("Dock.Unnamed") - } - - /// The suffix of the panel title, default is `None`. - /// - /// This is used to add a suffix element to the panel title. - fn title_suffix( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - None:: - } - - /// Whether the panel can be closed, default is `true`. - /// - /// This method called in Panel render, we should make sure it is fast. - fn closable(&self, cx: &App) -> bool { - true - } - - /// Return `PanelControl` if the panel is zoomable, default is `PanelControl::Menu`. - /// - /// This method called in Panel render, we should make sure it is fast. - fn zoomable(&self, cx: &App) -> Option { - Some(PanelControl::Menu) - } - - /// Return false to hide panel, true to show panel, default is `true`. - /// - /// This method called in Panel render, we should make sure it is fast. - fn visible(&self, cx: &App) -> bool { - true - } - - /// Set active state of the panel. - /// - /// Called with the frame-end net state when this panel becomes (or stops - /// being) the displayed tab of its tab group: exactly one notification - /// per edge, delivered on the next tick after the change — never - /// same-value repeats nor false→true flips within one frame. - /// - /// A panel removed from its group is NOT told `false`; [`Panel::on_removed`] - /// is the deactivation signal. A hidden panel occupying `active_ix` still - /// receives `true` even though rendering falls back to the first visible - /// panel, and panels inside a bare `DockItem::Panel` (no tab group) are - /// outside this contract. - fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) {} - - /// Set zoomed state of the panel. - /// - /// This method will be called when the panel is zoomed or unzoomed. - /// - /// Only current Panel will touch this method. - fn set_zoomed(&mut self, zoomed: bool, window: &mut Window, cx: &mut Context) {} - - /// When this Panel is added to a TabPanel, this will be called. - fn on_added_to( - &mut self, - tab_panel: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - } - - /// When this Panel is removed from a TabPanel, this will be called. - fn on_removed(&mut self, window: &mut Window, cx: &mut Context) {} - - /// The addition dropdown menu of the panel, default is `None`. - fn dropdown_menu( - &mut self, - this: PopupMenu, - window: &mut Window, - cx: &mut Context, - ) -> PopupMenu { - this - } - - /// The addition toolbar buttons of the panel used to show in the right of the title bar, default is `None`. - fn toolbar_buttons( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - None - } - - /// Dump the panel, used to serialize the panel. - fn dump(&self, cx: &App) -> PanelState { - PanelState::new(self) - } -} - -/// The PanelView trait used to define the panel view. -#[allow(unused_variables)] -pub trait PanelView: 'static + Send + Sync { - fn panel_name(&self, cx: &App) -> &'static str; - fn panel_id(&self, cx: &App) -> EntityId; - fn tab_name(&self, cx: &App) -> Option; - fn title(&self, window: &mut Window, cx: &mut App) -> AnyElement; - fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option; - fn closable(&self, cx: &App) -> bool; - fn zoomable(&self, cx: &App) -> Option; - fn visible(&self, cx: &App) -> bool; - fn set_active(&self, active: bool, window: &mut Window, cx: &mut App); - fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App); - fn on_added_to(&self, tab_panel: WeakEntity, window: &mut Window, cx: &mut App); - fn on_removed(&self, window: &mut Window, cx: &mut App); - fn dropdown_menu(&self, menu: PopupMenu, window: &mut Window, cx: &mut App) -> PopupMenu; - fn toolbar_buttons(&self, window: &mut Window, cx: &mut App) -> Option>; - fn view(&self) -> AnyView; - fn focus_handle(&self, cx: &App) -> FocusHandle; - fn dump(&self, cx: &App) -> PanelState; -} - -impl PanelView for Entity { - fn panel_name(&self, cx: &App) -> &'static str { - self.read(cx).panel_name() - } - - fn panel_id(&self, _: &App) -> EntityId { - self.entity_id() - } - - fn tab_name(&self, cx: &App) -> Option { - self.read(cx).tab_name(cx) - } - - fn title(&self, window: &mut Window, cx: &mut App) -> AnyElement { - self.update(cx, |this, cx| this.title(window, cx).into_any_element()) - } - - fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option { - self.update(cx, |this, cx| { - this.title_suffix(window, cx) - .map(|el| el.into_any_element()) - }) - } - - fn closable(&self, cx: &App) -> bool { - self.read(cx).closable(cx) - } - - fn zoomable(&self, cx: &App) -> Option { - self.read(cx).zoomable(cx) - } - - fn visible(&self, cx: &App) -> bool { - self.read(cx).visible(cx) - } - - fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.set_active(active, window, cx); - }) - } - - fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.set_zoomed(zoomed, window, cx); - }) - } - - fn on_added_to(&self, tab_panel: WeakEntity, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.on_added_to(tab_panel, window, cx)); - } - - fn on_removed(&self, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.on_removed(window, cx)); - } - - fn dropdown_menu(&self, menu: PopupMenu, window: &mut Window, cx: &mut App) -> PopupMenu { - self.update(cx, |this, cx| this.dropdown_menu(menu, window, cx)) - } - - fn toolbar_buttons(&self, window: &mut Window, cx: &mut App) -> Option> { - self.update(cx, |this, cx| this.toolbar_buttons(window, cx)) - } - - fn view(&self) -> AnyView { - self.clone().into() - } - - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx) - } - - fn dump(&self, cx: &App) -> PanelState { - self.read(cx).dump(cx) - } -} - -impl From<&dyn PanelView> for AnyView { - fn from(handle: &dyn PanelView) -> Self { - handle.view() - } -} - -impl From<&dyn PanelView> for Entity { - fn from(value: &dyn PanelView) -> Self { - value.view().downcast::().unwrap() - } -} - -impl PartialEq for dyn PanelView { - fn eq(&self, other: &Self) -> bool { - self.view() == other.view() - } -} - -/// The deserializer used by [`PanelRegistry`] to rebuild a panel from its -/// persisted [`PanelState`]. -type PanelBuilder = dyn Fn( - WeakEntity, - &PanelState, - &PanelInfo, - &mut Window, - &mut App, -) -> Box; - -pub struct PanelRegistry { - pub(super) items: HashMap>, -} -impl PanelRegistry { - /// Initialize the panel registry. - pub(crate) fn init(cx: &mut App) { - if cx.try_global::().is_none() { - cx.set_global(PanelRegistry::new()); - } - } - - pub fn new() -> Self { - Self { - items: HashMap::new(), - } - } - - pub fn global(cx: &App) -> &Self { - cx.global::() - } - - pub fn global_mut(cx: &mut App) -> &mut Self { - cx.global_mut::() - } - - /// Build a panel by name. - /// - /// If not registered, return InvalidPanel. - pub fn build_panel( - panel_name: &str, - dock_area: WeakEntity, - panel_state: &PanelState, - panel_info: &PanelInfo, - window: &mut Window, - cx: &mut App, - ) -> Box { - if let Some(view) = Self::global(cx) - .items - .get(panel_name) - .cloned() - .map(|f| f(dock_area, panel_state, panel_info, window, cx)) - { - view - } else { - // Show an invalid panel if the panel is not registered. - Box::new(cx.new(|cx| InvalidPanel::new(panel_name, panel_state.clone(), window, cx))) - } - } -} -impl Default for PanelRegistry { - fn default() -> Self { - Self::new() - } -} -impl Global for PanelRegistry {} - -/// Register the Panel init by panel_name to global registry. -pub fn register_panel(cx: &mut App, panel_name: &str, deserialize: F) -where - F: Fn( - WeakEntity, - &PanelState, - &PanelInfo, - &mut Window, - &mut App, - ) -> Box - + 'static, -{ - PanelRegistry::init(cx); - PanelRegistry::global_mut(cx) - .items - .insert(panel_name.to_string(), Arc::new(deserialize)); -} diff --git a/crates/dock/src/resize_handle.rs b/crates/dock/src/resize_handle.rs deleted file mode 100644 index d60fdb3..0000000 --- a/crates/dock/src/resize_handle.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Vendored from `gpui-base`'s private `resizable::resize_handle` module -//! (v0.5.2, rev 9e3a29dcbdebc318632bf68203f26c33e9f0e902). gpui-component -//! keeps this and [`PANEL_MIN_SIZE`] crate-private, so the dock crate carries -//! its own copy. - -use std::cell::Cell; -use std::rc::Rc; - -use gpui::prelude::FluentBuilder as _; -use gpui::{ - AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement, - IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, - StatefulInteractiveElement, Styled as _, Window, div, px, -}; -use gpui_component::{ActiveTheme as _, AxisExt as _, Side}; - -pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.); -pub(crate) const HANDLE_PADDING: Pixels = px(4.); -pub(crate) const HANDLE_SIZE: Pixels = px(1.); - -/// Create a resize handle for a resizable panel. -#[doc(hidden)] -pub fn resize_handle( - id: impl Into, - axis: Axis, -) -> ResizeHandle { - ResizeHandle::new(id, axis) -} - -type DragHandler = dyn Fn(&Point, &mut Window, &mut App) -> Entity; - -#[doc(hidden)] -pub struct ResizeHandle { - id: ElementId, - axis: Axis, - drag_value: Option>, - placement: Option, - on_drag: Option>>, -} - -impl ResizeHandle { - fn new(id: impl Into, axis: Axis) -> Self { - let id = id.into(); - Self { - id: id.clone(), - on_drag: None, - drag_value: None, - placement: None, - axis, - } - } - - pub fn on_drag( - mut self, - value: T, - f: impl Fn(Rc, &Point, &mut Window, &mut App) -> Entity + 'static, - ) -> Self { - let value = Rc::new(value); - self.drag_value = Some(value.clone()); - self.on_drag = Some(Rc::new(move |p, window, cx| { - f(value.clone(), p, window, cx) - })); - self - } - - pub fn placement(mut self, placement: Side) -> Self { - self.placement = Some(placement); - self - } -} - -#[derive(Default, Debug, Clone)] -struct ResizeHandleState { - active: Cell, -} - -impl ResizeHandleState { - fn set_active(&self, active: bool) { - self.active.set(active); - } - - fn is_active(&self) -> bool { - self.active.get() - } -} - -impl IntoElement for ResizeHandle { - type Element = ResizeHandle; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for ResizeHandle { - type PrepaintState = (); - type RequestLayoutState = AnyElement; - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - id: Option<&GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let neg_offset = -HANDLE_PADDING; - let axis = self.axis; - - window.with_element_state(id.unwrap(), |state: Option, window| { - let state = state.unwrap_or_default(); - - let bg_color = if state.is_active() { - cx.theme().drag_border - } else { - cx.theme().border - }; - - let mut el = div() - .id(self.id.clone()) - .occlude() - .absolute() - .flex_shrink_0() - .group("handle") - .when_some(self.on_drag.clone(), |this, on_drag| { - this.on_drag( - self.drag_value.clone().unwrap(), - move |_, position, window, cx| on_drag(&position, window, cx), - ) - }) - .map(|this| match self.placement { - Some(Side::Left) => { - // Special for Left Dock - // FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING) - this.cursor_col_resize() - .top_0() - .right(px(1.)) - .h_full() - .w(HANDLE_SIZE) - .pl(HANDLE_PADDING) - } - _ => this - .when(axis.is_horizontal(), |this| { - this.cursor_col_resize() - .top_0() - .left(neg_offset) - .h_full() - .w(HANDLE_SIZE) - .px(HANDLE_PADDING) - }) - .when(axis.is_vertical(), |this| { - this.cursor_row_resize() - .top(neg_offset) - .left_0() - .w_full() - .h(HANDLE_SIZE) - .py(HANDLE_PADDING) - }), - }) - .child( - div() - .bg(bg_color) - .group_hover("handle", |this| this.bg(bg_color)) - .when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE)) - .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), - ) - .into_any_element(); - - let layout_id = el.request_layout(window, cx); - - ((layout_id, el), state) - }) - } - - fn prepaint( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _: gpui::Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - request_layout.prepaint(window, cx); - } - - fn paint( - &mut self, - id: Option<&GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - bounds: gpui::Bounds, - request_layout: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - request_layout.paint(window, cx); - - window.with_element_state(id.unwrap(), |state: Option, window| { - let state = state.unwrap_or_default(); - - window.on_mouse_event({ - let state = state.clone(); - move |ev: &MouseDownEvent, phase, window, _| { - if bounds.contains(&ev.position) && phase.bubble() { - state.set_active(true); - window.refresh(); - } - } - }); - - window.on_mouse_event({ - let state = state.clone(); - move |_: &MouseUpEvent, _, window, _| { - if state.is_active() { - state.set_active(false); - window.refresh(); - } - } - }); - - ((), state) - }); - } -} diff --git a/crates/dock/src/stack_panel.rs b/crates/dock/src/stack_panel.rs deleted file mode 100644 index de47643..0000000 --- a/crates/dock/src/stack_panel.rs +++ /dev/null @@ -1,397 +0,0 @@ -use std::sync::Arc; - -use gpui::{ - App, AppContext as _, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, WeakEntity, - Window, -}; -use gpui_component::{ - ActiveTheme, Placement, ResizablePanelEvent, ResizablePanelGroup, ResizableState, h_flex, - resizable_panel, -}; -use smallvec::SmallVec; - -use super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel}; -use crate::PanelInfo; -use crate::resize_handle::PANEL_MIN_SIZE; - -pub struct StackPanel { - pub(super) parent: Option>, - pub(super) axis: Axis, - focus_handle: FocusHandle, - pub(crate) panels: SmallVec<[Arc; 2]>, - state: Entity, - _subscriptions: Vec, -} - -impl Panel for StackPanel { - fn panel_name(&self) -> &'static str { - "StackPanel" - } - - fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - "StackPanel" - } - - fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { - for panel in &self.panels { - panel.set_active(active, window, cx); - } - } - - fn dump(&self, cx: &App) -> PanelState { - let sizes = self.state.read(cx).sizes().clone(); - let mut state = PanelState::new(self); - state.info = PanelInfo::stack(sizes, self.axis); - for panel in &self.panels { - state.add_child(panel.dump(cx)); - } - - state - } -} - -impl StackPanel { - pub fn new(axis: Axis, _: &mut Window, cx: &mut Context) -> Self { - let state = cx.new(|_| ResizableState::default()); - - let _subscriptions = vec![ - // Bubble up the resize event. - cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| { - cx.emit(PanelEvent::LayoutChanged) - }), - ]; - - Self { - axis, - parent: None, - focus_handle: cx.focus_handle(), - panels: SmallVec::new(), - state, - _subscriptions, - } - } - - /// The first level of the stack panel is root, will not have a parent. - fn is_root(&self) -> bool { - self.parent.is_none() - } - - /// Return true if self or parent only have last panel. - pub(super) fn is_last_panel(&self, cx: &App) -> bool { - if self.panels.len() > 1 { - return false; - } - - if let Some(parent) = &self.parent - && let Some(parent) = parent.upgrade() - { - return parent.read(cx).is_last_panel(cx); - } - - true - } - - pub(super) fn panels_len(&self) -> usize { - self.panels.len() - } - - /// Return the index of the panel. - pub(crate) fn index_of_panel(&self, panel: Arc) -> Option { - self.panels.iter().position(|p| p == &panel) - } - - fn assert_panel_is_valid(&self, panel: &Arc) { - assert!( - panel.view().downcast::().is_ok() - || panel.view().downcast::().is_ok(), - "Panel must be a `TabPanel` or `StackPanel`" - ); - } - - /// Add a panel at the end of the stack. - /// - /// If `size` is `None`, the panel will be given the average size of all panels in the stack. - /// - /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. - pub fn add_panel( - &mut self, - panel: Arc, - size: Option, - dock_area: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - self.insert_panel(panel, self.panels.len(), size, dock_area, window, cx); - } - - /// Add a panel at the [`Placement`]. - /// - /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. - pub fn add_panel_at( - &mut self, - panel: Arc, - placement: Placement, - size: Option, - dock_area: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - self.insert_panel_at( - panel, - self.panels_len(), - placement, - size, - dock_area, - window, - cx, - ); - } - - /// Insert a panel at the index. - /// - /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. - #[allow(clippy::too_many_arguments)] - pub fn insert_panel_at( - &mut self, - panel: Arc, - ix: usize, - placement: Placement, - size: Option, - dock_area: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - match placement { - Placement::Top | Placement::Left => { - self.insert_panel_before(panel, ix, size, dock_area, window, cx) - } - Placement::Right | Placement::Bottom => { - self.insert_panel_after(panel, ix, size, dock_area, window, cx) - } - } - } - - /// Insert a panel at the index. - /// - /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. - pub fn insert_panel_before( - &mut self, - panel: Arc, - ix: usize, - size: Option, - dock_area: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - self.insert_panel(panel, ix, size, dock_area, window, cx); - } - - /// Insert a panel after the index. - /// - /// The `panel` must be a [`TabPanel`] or [`StackPanel`]. - pub fn insert_panel_after( - &mut self, - panel: Arc, - ix: usize, - size: Option, - dock_area: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - self.insert_panel(panel, ix + 1, size, dock_area, window, cx); - } - - fn insert_panel( - &mut self, - panel: Arc, - ix: usize, - size: Option, - dock_area: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - self.assert_panel_is_valid(&panel); - - // If the panel is already in the stack, return. - if self.index_of_panel(panel.clone()).is_some() { - return; - } - - let view = cx.entity().clone(); - window.defer(cx, { - let panel = panel.clone(); - - move |window, cx| { - // If the panel is a TabPanel, set its parent to this. - if let Ok(tab_panel) = panel.view().downcast::() { - tab_panel.update(cx, |tab_panel, _| tab_panel.set_parent(view.downgrade())); - } else if let Ok(stack_panel) = panel.view().downcast::() { - stack_panel.update(cx, |stack_panel, _| { - stack_panel.parent = Some(view.downgrade()) - }); - } - - // Subscribe to the panel's layout change event. - _ = dock_area.update(cx, |this, cx| { - if let Ok(tab_panel) = panel.view().downcast::() { - this.subscribe_panel(&tab_panel, window, cx); - } else if let Ok(stack_panel) = panel.view().downcast::() { - this.subscribe_panel(&stack_panel, window, cx); - } - }); - } - }); - - let ix = if ix > self.panels.len() { - self.panels.len() - } else { - ix - }; - - // Get avg size of all panels to insert new panel, if size is None. - let size = match size { - Some(size) => size, - None => { - let state = self.state.read(cx); - (state.container_size() / (state.sizes().len() + 1) as f32).max(PANEL_MIN_SIZE) - } - }; - - self.panels.insert(ix, panel.clone()); - self.state.update(cx, |state, cx| { - state.insert_panel(Some(size), Some(ix), cx); - }); - cx.emit(PanelEvent::LayoutChanged); - cx.notify(); - } - - /// Remove panel from the stack. - /// - /// If `ix` is not found, do nothing. - pub fn remove_panel( - &mut self, - panel: Arc, - window: &mut Window, - cx: &mut Context, - ) { - let Some(ix) = self.index_of_panel(panel.clone()) else { - return; - }; - - self.panels.remove(ix); - self.state.update(cx, |state, cx| { - state.remove_panel(ix, cx); - }); - - cx.emit(PanelEvent::LayoutChanged); - self.remove_self_if_empty(window, cx); - } - - /// Replace the old panel with the new panel at same index. - pub(super) fn replace_panel( - &mut self, - old_panel: Arc, - new_panel: Entity, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(ix) = self.index_of_panel(old_panel.clone()) { - self.panels[ix] = Arc::new(new_panel.clone()); - - self.state.update(cx, |state, cx| { - state.reset_panel(ix, cx); - }); - cx.emit(PanelEvent::LayoutChanged); - } - } - - /// If children is empty, remove self from parent view. - pub(crate) fn remove_self_if_empty(&mut self, window: &mut Window, cx: &mut Context) { - if self.is_root() { - return; - } - - if !self.panels.is_empty() { - return; - } - - let view = cx.entity().clone(); - if let Some(parent) = self.parent.as_ref() { - _ = parent.update(cx, |parent, cx| { - parent.remove_panel(Arc::new(view.clone()), window, cx); - }); - } - - cx.emit(PanelEvent::LayoutChanged); - cx.notify(); - } - - /// Find the first top left in the stack. - pub(super) fn left_top_tab_panel( - &self, - check_parent: bool, - cx: &App, - ) -> Option> { - if check_parent - && let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) - && let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx) - { - return Some(panel); - } - - let first_panel = self.panels.first(); - if let Some(view) = first_panel { - if let Ok(tab_panel) = view.view().downcast::() { - Some(tab_panel) - } else if let Ok(stack_panel) = view.view().downcast::() { - stack_panel.read(cx).left_top_tab_panel(false, cx) - } else { - None - } - } else { - None - } - } - - /// Remove all panels from the stack. - pub(super) fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context) { - self.panels.clear(); - self.state.update(cx, |state, cx| { - state.clear(); - cx.notify(); - }); - } - - /// Change the axis of the stack panel. - pub(super) fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context) { - self.axis = axis; - cx.notify(); - } -} - -impl Focusable for StackPanel { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} -impl EventEmitter for StackPanel {} -impl EventEmitter for StackPanel {} -impl Render for StackPanel { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .size_full() - .overflow_hidden() - .bg(cx.theme().tokens.tab_bar) - .child( - ResizablePanelGroup::new("stack-panel-group") - .with_state(&self.state) - .axis(self.axis) - .children(self.panels.clone().into_iter().map(|panel| { - resizable_panel() - .child(panel.view()) - .visible(panel.visible(cx)) - })), - ) - } -} diff --git a/crates/dock/src/state.rs b/crates/dock/src/state.rs deleted file mode 100644 index 389bd3c..0000000 --- a/crates/dock/src/state.rs +++ /dev/null @@ -1,233 +0,0 @@ -use gpui::{App, AppContext, Axis, Entity, Pixels, WeakEntity, Window}; -use itertools::Itertools as _; -use serde::{Deserialize, Serialize}; - -use super::{Dock, DockArea, DockItem, DockPlacement, Panel, PanelRegistry}; - -/// Used to serialize and deserialize the DockArea -#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] -pub struct DockAreaState { - /// The version is used to mark this persisted state is compatible with the current version - /// For example, some times we many totally changed the structure of the Panel, - /// then we can compare the version to decide whether we can use the state or ignore. - #[serde(default)] - pub version: Option, - pub center: PanelState, - #[serde(skip_serializing_if = "Option::is_none")] - pub left_dock: Option, -} - -/// Used to serialize and deserialize the Dock -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct DockState { - panel: PanelState, - placement: DockPlacement, - size: Pixels, - open: bool, -} - -impl DockState { - pub fn new(dock: Entity, cx: &App) -> Self { - let dock = dock.read(cx); - - Self { - placement: dock.placement, - size: dock.size, - open: dock.open, - panel: dock.panel.view().dump(cx), - } - } - - /// Convert the DockState to Dock - pub fn to_dock( - &self, - dock_area: WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Entity { - let item = self.panel.to_item(dock_area.clone(), window, cx); - cx.new(|cx| { - Dock::from_state( - dock_area.clone(), - self.placement, - self.size, - item, - self.open, - window, - cx, - ) - }) - } -} - -/// Used to serialize and deserialize the DockerItem -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct PanelState { - pub panel_name: String, - pub children: Vec, - pub info: PanelInfo, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum PanelInfo { - #[serde(rename = "stack")] - Stack { - sizes: Vec, - axis: usize, // 0 for horizontal, 1 for vertical - }, - #[serde(rename = "tabs")] - Tabs { active_index: usize }, - #[serde(rename = "panel")] - Panel(serde_json::Value), -} - -impl PanelInfo { - pub fn stack(sizes: Vec, axis: Axis) -> Self { - Self::Stack { - sizes, - axis: if axis == Axis::Horizontal { 0 } else { 1 }, - } - } - - pub fn tabs(active_index: usize) -> Self { - Self::Tabs { active_index } - } - - pub fn panel(info: serde_json::Value) -> Self { - Self::Panel(info) - } - - pub fn axis(&self) -> Option { - match self { - Self::Stack { axis, .. } => Some(if *axis == 0 { - Axis::Horizontal - } else { - Axis::Vertical - }), - _ => None, - } - } - - pub fn sizes(&self) -> Option<&Vec> { - match self { - Self::Stack { sizes, .. } => Some(sizes), - _ => None, - } - } - - pub fn active_index(&self) -> Option { - match self { - Self::Tabs { active_index } => Some(*active_index), - _ => None, - } - } -} - -impl Default for PanelState { - fn default() -> Self { - Self { - panel_name: "".to_string(), - children: Vec::new(), - info: PanelInfo::Panel(serde_json::Value::Null), - } - } -} - -impl PanelState { - pub fn new(panel: &P) -> Self { - Self { - panel_name: panel.panel_name().to_string(), - ..Default::default() - } - } - - pub fn add_child(&mut self, panel: PanelState) { - self.children.push(panel); - } - - pub fn to_item( - &self, - dock_area: WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> DockItem { - let info = self.info.clone(); - - let items: Vec = self - .children - .iter() - .map(|child| child.to_item(dock_area.clone(), window, cx)) - .collect(); - - match info { - PanelInfo::Stack { sizes, axis } => { - let axis = if axis == 0 { - Axis::Horizontal - } else { - Axis::Vertical - }; - let sizes = sizes.iter().map(|s| Some(*s)).collect_vec(); - DockItem::split_with_sizes(axis, items, sizes, &dock_area, window, cx) - } - PanelInfo::Tabs { active_index } => { - if items.len() == 1 { - return items[0].clone(); - } - - let items = items - .iter() - .flat_map(|item| match item { - DockItem::Tabs { items, .. } => items.clone(), - _ => { - // ignore invalid panels in tabs - vec![] - } - }) - .collect_vec(); - - DockItem::tabs(items, &dock_area, window, cx).active_index(active_index, cx) - } - PanelInfo::Panel(_) => { - let view = PanelRegistry::build_panel( - &self.panel_name, - dock_area.clone(), - self, - &info, - window, - cx, - ); - DockItem::tabs(vec![view.into()], &dock_area, window, cx) - } - } - } -} - -#[cfg(test)] -mod tests { - use gpui::px; - - use super::*; - #[test] - fn test_deserialize_item_state() { - let json = include_str!("fixtures/layout.json"); - let state: DockAreaState = serde_json::from_str(json).unwrap(); - assert_eq!(state.version, None); - assert_eq!(state.center.panel_name, "StackPanel"); - assert_eq!(state.center.children.len(), 2); - assert_eq!(state.center.children[0].panel_name, "TabPanel"); - assert_eq!(state.center.children[1].children.len(), 1); - assert_eq!( - state.center.children[1].children[0].panel_name, - "StoryContainer" - ); - assert_eq!(state.center.children[1].panel_name, "TabPanel"); - - let left_dock = state.left_dock.unwrap(); - assert!(left_dock.open); - assert_eq!(left_dock.size, px(350.0)); - assert_eq!(left_dock.placement, DockPlacement::Left); - assert_eq!(left_dock.panel.panel_name, "TabPanel"); - assert_eq!(left_dock.panel.children.len(), 1); - assert_eq!(left_dock.panel.children[0].panel_name, "StoryContainer"); - } -} diff --git a/crates/dock/src/tab_bar.rs b/crates/dock/src/tab_bar.rs deleted file mode 100644 index b404cfd..0000000 --- a/crates/dock/src/tab_bar.rs +++ /dev/null @@ -1,469 +0,0 @@ -use std::sync::Arc; - -use gpui::prelude::FluentBuilder; -use gpui::{ - AnyElement, App, AppContext, ElementId, Entity, InteractiveElement as _, IntoElement, - ParentElement as _, RenderOnce, ScrollHandle, StatefulInteractiveElement, Styled as _, Window, - div, px, -}; -use gpui_base::{InteractiveElementExt, Tab, Tabs}; -use gpui_component::{ActiveTheme, ElementExt, h_flex}; - -use super::{AnyDrag, DragPanel, PanelView, TabPanel}; -use crate::TAB_BAR_HEIGHT; - -/// The dock's custom tab bar, built on gpui-base's unstyled [`Tab`]/[`Tabs`]. -/// -/// The dock owns the pill presentation; the layout mirrors gpui-component's -/// `TabBar`: a prefix (dock toggle / tab navigation), a scrollable tab strip -/// with a trailing drop target, then a suffix (panel toolbar). -/// -/// Tabs are draggable to move panels between groups and double as drop -/// targets, so panel-level events are forwarded to the owning [`TabPanel`]. -#[derive(IntoElement)] -pub(crate) struct TabBar { - id: ElementId, - panels: Vec>, - active_panel: Option>, - collapsed: bool, - draggable: bool, - droppable: bool, - tab_panel: Entity, - scroll_handle: ScrollHandle, - prefix: Option, - suffix: Option, - empty_space: Option, -} - -impl TabBar { - pub(crate) fn new(id: impl Into, tab_panel: Entity) -> Self { - Self { - id: id.into(), - panels: Vec::new(), - active_panel: None, - collapsed: false, - draggable: false, - droppable: false, - tab_panel, - scroll_handle: ScrollHandle::new(), - prefix: None, - suffix: None, - empty_space: None, - } - } - - /// The panels to show as tabs, in strip order. - pub(crate) fn panels(mut self, panels: Vec>) -> Self { - self.panels = panels; - self - } - - /// The currently active panel; its tab is rendered as the filled pill. - pub(crate) fn active_panel(mut self, active_panel: Option>) -> Self { - self.active_panel = active_panel; - self - } - - /// Collapsed tab panels render no suffix or trailing drop target, and - /// their tabs lose the active style and all interactions. - pub(crate) fn collapsed(mut self, collapsed: bool) -> Self { - self.collapsed = collapsed; - self - } - - /// Whether the tabs can start a panel drag. - pub(crate) fn draggable(mut self, draggable: bool) -> Self { - self.draggable = draggable; - self - } - - /// Whether the tabs and trailing space accept drops. - pub(crate) fn droppable(mut self, droppable: bool) -> Self { - self.droppable = droppable; - self - } - - /// Track the strip's scroll state with the given handle, so callers can - /// scroll a tab into view with [`ScrollHandle::scroll_to_item`]. - pub(crate) fn scroll_handle(mut self, scroll_handle: &ScrollHandle) -> Self { - self.scroll_handle = scroll_handle.clone(); - self - } - - /// Element shown before the tab strip. - pub(crate) fn prefix(mut self, prefix: impl IntoElement) -> Self { - self.prefix = Some(prefix.into_any_element()); - self - } - - /// Element shown after the tab strip. - pub(crate) fn suffix(mut self, suffix: impl IntoElement) -> Self { - self.suffix = Some(suffix.into_any_element()); - self - } - - /// Replace the trailing empty space (the drop target after the last tab). - #[cfg(test)] - pub(crate) fn empty_space(mut self, empty_space: impl IntoElement) -> Self { - self.empty_space = Some(empty_space.into_any_element()); - self - } - - fn render_tab( - &self, - ix: usize, - panel: Arc, - active: bool, - window: &mut Window, - cx: &mut App, - ) -> Tab { - // While collapsed, tabs lose the active style and all interactions. - let droppable = self.collapsed; - let tab_panel = self.tab_panel.clone(); - - // The `ix` element id keeps each tab's identity stable across renders. - Tab::new(ix) - .h_6() - .px_3() - .text_sm() - .whitespace_nowrap() - .flex() - .items_center() - .justify_center() - .gap_1() - .flex_shrink_0() - .overflow_hidden() - .rounded(cx.theme().radius) - .text_color(cx.theme().foreground) - .map(|this| { - if let Some(tab_name) = panel.tab_name(cx) { - this.child(tab_name) - } else { - this.child(panel.title(window, cx)) - } - }) - // Pill presentation: the selected tab is the filled pill, the - // rest are transparent until hovered. - .styles(|styles| { - styles.selected(|style| { - style - .text_color(cx.theme().tab_active_foreground) - .bg(cx.theme().tab_active) - }) - }) - .hover(|this| { - if active { - this - } else { - this.text_color(cx.theme().secondary_foreground) - .bg(cx.theme().secondary_hover) - } - }) - .selected(active) - .on_click(move |_, window, cx| { - tab_panel.update(cx, |view, cx| view.set_active_ix(ix, window, cx)); - }) - .when(!droppable, |this| { - this.when(self.draggable, |this| { - this.on_drag( - DragPanel::new(panel.clone(), self.tab_panel.clone()), - |drag, offset, _, cx| { - cx.stop_propagation(); - drag.drag_offset.set(offset); - cx.new(|_| drag.clone()) - }, - ) - }) - .when(self.droppable, |this| { - this.drag_over::(|this, _, _, cx| { - this.rounded_l_none() - .border_l_2() - .border_r_0() - .border_color(cx.theme().drag_border) - }) - .on_drop({ - let tab_panel = self.tab_panel.clone(); - move |drag: &DragPanel, window, cx| { - tab_panel.update(cx, |view, cx| { - view.will_split_placement = None; - view.on_drop(drag, Some(ix), true, window, cx); - }); - } - }) - .drag_over::(|this, _, _, cx| { - this.rounded_l_none() - .border_l_2() - .border_r_0() - .border_color(cx.theme().drag_border) - }) - .on_drop({ - let tab_panel = self.tab_panel.clone(); - move |item: &AnyDrag, _, cx| { - tab_panel.update(cx, |view, cx| { - view.will_split_placement = None; - view.emit_drag_drop(item, None, cx); - }); - } - }) - }) - }) - } - - fn render_empty_space(&self) -> AnyElement { - let tabs_count = self.panels.len(); - - // The strip after the last tab is a drop target for panels and - // host-owned drag items. Its left edge (right after the last tab) - // marks the start of the title-bar drag overlay. - let mut empty = div() - .id("tab-bar-empty-space") - .h_full() - .flex_grow_1() - .min_w_16() - .on_prepaint({ - let view = self.tab_panel.clone(); - move |bounds, _, cx| { - view.update(cx, |this, cx| { - if this.title_bar_strip_bounds != Some(bounds) { - this.title_bar_strip_bounds = Some(bounds); - cx.notify(); - } - }); - } - }); - - if self.droppable { - empty = empty - .drag_over::(|this, _, _, cx| this.bg(cx.theme().tokens.drop_target)) - .on_drop({ - let view = self.tab_panel.clone(); - move |drag: &DragPanel, window, cx| { - view.update(cx, |this, cx| { - this.will_split_placement = None; - - // Dropping a panel from this same tab group onto - // the strip moves it after the last tab. - let ix = if drag.tab_panel == cx.entity() { - Some(tabs_count - 1) - } else { - None - }; - - this.on_drop(drag, ix, false, window, cx); - }); - } - }) - .drag_over::(|this, _, _, cx| this.bg(cx.theme().tokens.drop_target)) - .on_drop({ - let view = self.tab_panel.clone(); - move |item: &AnyDrag, _, cx| { - view.update(cx, |this, cx| { - this.will_split_placement = None; - this.emit_drag_drop(item, None, cx); - }); - } - }); - } - - empty.into_any_element() - } -} - -impl RenderOnce for TabBar { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let tabs: Vec<_> = self - .panels - .iter() - .enumerate() - .filter_map(|(ix, panel)| { - let mut active = self.active_panel.as_ref() == Some(panel); - if !panel.visible(cx) { - return None; - } - // Always not show active tab style, if the panel is collapsed - if self.collapsed { - active = false; - } - Some(self.render_tab(ix, panel.clone(), active, window, cx)) - }) - .collect(); - - let empty_space = match self.empty_space { - Some(empty_space) => empty_space, - None => self.render_empty_space(), - }; - - Tabs::new(self.id) - .px(px(-1.)) - .h(TAB_BAR_HEIGHT) - .flex() - .items_center() - .text_color(cx.theme().tab_foreground) - .when_some(self.prefix, |this, prefix| this.child(prefix)) - .child( - h_flex().id("tabs").flex_1().overflow_x_hidden().child( - h_flex() - .id("tabs-inner") - .relative() - .gap(px(4.)) - .overflow_x_scroll() - .lock_scroll_axis() - .track_scroll(&self.scroll_handle) - .children(tabs) - .when(!self.collapsed, |this| this.child(empty_space)), - ), - ) - .when_some(self.suffix, |this, suffix| { - this.when(!self.collapsed, |this| this.child(suffix)) - }) - } -} - -#[cfg(test)] -mod tests { - use std::sync::atomic::{AtomicBool, Ordering}; - - use gpui::{ - Context, Entity, MouseButton, Render, TestAppContext, VisualTestContext, WindowOptions, - div, px, size, - }; - use gpui_component::{Root, Theme, v_flex}; - - use super::*; - use crate::DockArea; - use crate::tab_panel::title_bar_drag_handlers; - - #[derive(Default)] - struct ProbeFlags { - empty_down: AtomicBool, - empty_click: AtomicBool, - control_down: AtomicBool, - control_click: AtomicBool, - } - - struct ProbeView { - flags: Entity, - tab_panel: Entity, - } - - impl Render for ProbeView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let flags = self.flags.clone(); - - let empty = title_bar_drag_handlers( - div() - .id("empty-space") - .h(TAB_BAR_HEIGHT) - .flex_grow_1() - .min_w_16(), - window, - cx, - ) - .debug_selector(|| "empty-space".into()) - .on_mouse_down(MouseButton::Left, { - let flags = flags.clone(); - move |_, _, cx| flags.update(cx, |f, _| f.empty_down.store(true, Ordering::SeqCst)) - }) - .on_click({ - let flags = flags.clone(); - move |_, _, cx| flags.update(cx, |f, _| f.empty_click.store(true, Ordering::SeqCst)) - }); - - let control = - title_bar_drag_handlers(div().id("control-space").h_8().flex_grow_1(), window, cx) - .debug_selector(|| "control-space".into()) - .on_mouse_down(MouseButton::Left, { - let flags = flags.clone(); - move |_, _, cx| { - flags.update(cx, |f, _| f.control_down.store(true, Ordering::SeqCst)) - } - }) - .on_click({ - let flags = flags.clone(); - move |_, _, cx| { - flags.update(cx, |f, _| f.control_click.store(true, Ordering::SeqCst)) - } - }); - - v_flex() - .size_full() - .child(TabBar::new("probe-bar", self.tab_panel.clone()).empty_space(empty)) - .child(control) - } - } - - /// Diagnostic: verify that the tab bar's trailing empty space receives - /// mouse events when wrapped by `title_bar_drag_handlers`, i.e. the - /// strip's scroll containers do not swallow them. - #[gpui::test] - fn tab_bar_empty_space_receives_events(cx: &mut TestAppContext) { - let (flags, handle) = cx.update(|cx| { - cx.set_global(Theme::default()); - let flags = cx.new(|_| ProbeFlags::default()); - let handle = cx.open_window( - WindowOptions { - window_bounds: Some(gpui::WindowBounds::Windowed(gpui::Bounds { - origin: gpui::Point::default(), - size: size(px(800.), px(100.)), - })), - ..Default::default() - }, - |window, cx| { - let flags = flags.clone(); - let dock_area = cx.new(|cx| DockArea::new("probe-dock", None, window, cx)); - let tab_panel = - cx.new(|cx| TabPanel::new(None, dock_area.downgrade(), window, cx)); - let content = cx.new(|_| ProbeView { flags, tab_panel }); - cx.new(|cx| Root::new(content, window, cx)) - }, - ); - (flags, handle.unwrap()) - }); - let mut cx = VisualTestContext::from_window(handle.into(), cx); - - cx.run_until_parked(); - cx.update(|window, cx| { - _ = window.draw(cx); - }); - - let empty_bounds = cx - .debug_bounds("empty-space") - .expect("empty-space must be laid out"); - let control_bounds = cx - .debug_bounds("control-space") - .expect("control-space must be laid out"); - let empty_center = empty_bounds.center(); - let control_center = control_bounds.center(); - - // Control: a plain div with the same handlers, outside the tab bar. - cx.simulate_click(control_center, Default::default()); - // Target: the tab bar's empty-space strip. - cx.simulate_click(empty_center, Default::default()); - - let (empty_down, empty_click, control_down, control_click) = cx.read(|cx| { - let flags = flags.read(cx); - ( - flags.empty_down.load(Ordering::SeqCst), - flags.empty_click.load(Ordering::SeqCst), - flags.control_down.load(Ordering::SeqCst), - flags.control_click.load(Ordering::SeqCst), - ) - }); - assert!( - control_down, - "control mouse_down must fire at {control_center:?}" - ); - assert!( - control_click, - "control click must fire at {control_center:?}" - ); - assert!( - empty_down, - "empty-space mouse_down must fire at {empty_center:?}" - ); - assert!( - empty_click, - "empty-space click must fire at {empty_center:?}" - ); - } -} diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs index fad7530..4e77d0b 100644 --- a/crates/dock/src/tab_panel.rs +++ b/crates/dock/src/tab_panel.rs @@ -1,114 +1,69 @@ +//! 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::collections::HashMap; use std::rc::Rc; use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; -use gpui::prelude::FluentBuilder; +use gpui::prelude::FluentBuilder as _; use gpui::{ - Anchor, Animation, AnimationExt as _, App, AppContext, Bounds, Context, DismissEvent, Div, - DragMoveEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle, Focusable, - InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Point, Render, - ScrollHandle, SharedString, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, - WeakEntity, Window, WindowControlArea, div, point, px, + 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_component::animation::{Lerp, ease_out_cubic}; +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, PopupMenu}; +use gpui_component::menu::DropdownMenu as _; use gpui_component::{ - ActiveTheme, AxisExt, Disableable, ElementExt, IconName, Placement, Selectable, Sizable, - h_flex, v_flex, + ActiveTheme as _, Disableable as _, IconName, Selectable as _, Sizable as _, h_flex, v_flex, }; -use super::tab_bar::TabBar; -use super::{ - AnyDrag, ClosePanel, DockArea, DockEvent, DockPlacement, DropTarget, Panel, PanelControl, - PanelEvent, PanelState, PanelView, StackPanel, ToggleZoom, +use crate::dock_area::SkinShared; +use crate::{ + ClosePanel, PanelControl, PanelHandle, TAB_BAR_HEIGHT, ToggleZoom, t, title_bar_drag_handlers, + window_controls, }; -use crate::{PanelInfo, t, window_controls}; -#[derive(Clone)] -struct TabState { - closable: bool, - zoomable: Option, - draggable: bool, - droppable: bool, - active_panel: Option>, -} +/// 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.)); -#[derive(Clone)] -pub(crate) struct DragPanel { - pub(crate) panel: Arc, - pub(crate) tab_panel: Entity, - pub(crate) drag_offset: Rc>>, - drag_session_id: u64, -} - -static NEXT_DRAG_SESSION_ID: AtomicU64 = AtomicU64::new(1); - -/// Stands in for [`DragPanel::drag_session_id`] on host-owned drag items, which -/// carry no session of their own. `NEXT_DRAG_SESSION_ID` starts at 1, so 0 never -/// collides. -const ITEM_DRAG_SESSION_ID: u64 = 0; - -impl DragPanel { - pub(crate) fn new(panel: Arc, tab_panel: Entity) -> Self { - Self { - panel, - tab_panel, - drag_offset: Rc::new(Cell::new(Point::default())), - drag_session_id: NEXT_DRAG_SESSION_ID.fetch_add(1, Ordering::Relaxed), - } +/// 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(), } } -#[derive(Clone, Copy, Debug, PartialEq)] -struct DropPlaceholderBounds { - origin: Point, - size: gpui::Size, +/// 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 DropPlaceholderBounds { - fn for_placement(bounds: gpui::Bounds, placement: Option) -> Self { - let half_width = bounds.size.width * 0.5; - let half_height = bounds.size.height * 0.5; - - match placement { - Some(Placement::Left) => Self { - origin: Point::default(), - size: gpui::size(half_width, bounds.size.height), - }, - Some(Placement::Right) => Self { - origin: point(half_width, px(0.)), - size: gpui::size(half_width, bounds.size.height), - }, - Some(Placement::Top) => Self { - origin: Point::default(), - size: gpui::size(bounds.size.width, half_height), - }, - Some(Placement::Bottom) => Self { - origin: point(px(0.), half_height), - size: gpui::size(bounds.size.width, half_height), - }, - None => Self { - origin: Point::default(), - size: bounds.size, - }, - } - } -} - -#[derive(Clone, Copy, Debug)] -struct DropPlaceholderAnimation { - drag_session_id: u64, - placement: Option, - from: DropPlaceholderBounds, - to: DropPlaceholderBounds, - epoch: u64, -} - -impl Render for DragPanel { +impl Render for DragPanelPreview { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .id("drag-panel") @@ -124,541 +79,254 @@ impl Render for DragPanel { .text_color(cx.theme().tab_foreground) .bg(cx.theme().tokens.tab_active) .opacity(0.75) - .child(self.panel.title(window, cx)) + .child(panel_title(&self.panel, window, cx)) } } -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>, - pub(crate) panels: Vec>, - pub(crate) active_ix: usize, - /// What each panel was last told via `set_active`, keyed by EntityId; absent means `false`. - notified_active: HashMap, - /// Whether an active-state reconcile task is already queued for this frame. - active_sync_scheduled: bool, - /// If this is true, the Panel closable will follow the active panel's closable, - /// otherwise this TabPanel will not able to close - /// - /// This is used for Dock to limit the last TabPanel not able to close, see [`super::Dock::new`]. - pub(crate) closable: bool, +/// 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() +} - tab_bar_scroll_handle: ScrollHandle, - pending_scroll_to_ix: Option, - zoomed: bool, - collapsed: bool, - /// When drag move, will get the placement of the panel to be split - pub(crate) will_split_placement: Option, - drop_placeholder_animation: Option, - drop_placeholder_animation_name: SharedString, +/// The payload for dragging the tab at `ix` out of its group, or `None` when +/// this group must not be rearranged. +fn tab_drag(group: &TabGroupContext, ix: usize, cx: &App) -> Option { + group + .is_draggable() + .then(|| group.drag_panel(ix, 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>, /// Bounds of the title bar row (the wrapper around the tab bar), in /// window coordinates. Measured via `on_prepaint` to position the /// title-bar drag overlay. - title_bar_bounds: Option>, + title_bar_bounds: Rc>>>, /// Bounds of the tab bar's trailing empty space (right after the last /// tab), which marks where the draggable region starts. - pub(crate) title_bar_strip_bounds: Option>, + title_bar_strip_bounds: Rc>>>, /// Bounds of the tab bar's suffix (toolbar) area, which marks where the /// draggable region ends. - title_bar_suffix_bounds: Option>, + title_bar_suffix_bounds: Rc>>>, } -impl Panel for TabPanel { - fn panel_name(&self) -> &'static str { - "TabPanel" - } - - fn title(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - self.active_panel(cx) - .map(|panel| panel.title(window, cx)) - .unwrap_or("Empty Tab".into_any_element()) - } - - fn closable(&self, cx: &App) -> bool { - if !self.closable { - return false; - } - - // The final panel in the dock is not closable. - if !self.draggable(cx) { - return false; - } - - self.active_panel(cx) - .map(|panel| panel.closable(cx)) - .unwrap_or(false) - } - - fn zoomable(&self, cx: &App) -> Option { - self.active_panel(cx).and_then(|panel| panel.zoomable(cx)) - } - - fn visible(&self, cx: &App) -> bool { - self.visible_panels(cx).next().is_some() - } - - fn dropdown_menu( - &mut self, - menu: PopupMenu, - window: &mut Window, - cx: &mut Context, - ) -> 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, - ) -> Option> { - 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 - } -} - -/// State used to move the window when the title bar area is dragged. -struct WindowDragState { - should_move: bool, -} - -/// Make an element behave like a window title bar: dragging it moves the -/// window, and double-clicking zooms the window (or performs the platform's -/// default title-bar double-click action on macOS). -/// -/// Only the bar's non-interactive areas should get this — tabs are draggable -/// (to reorder panels) and must not move the window. -pub fn title_bar_drag_handlers( - this: Stateful

, - window: &mut Window, - cx: &mut App, -) -> Stateful
{ - 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(); - } - } - }) -} - -impl TabPanel { - pub fn new( - stack_panel: Option>, - dock_area: WeakEntity, - _: &mut Window, - cx: &mut Context, - ) -> Self { - let entity_id = cx.entity_id(); +impl SignedTabGroupSkin { + pub(crate) fn new(shared: Rc) -> Self { Self { - focus_handle: cx.focus_handle(), - dock_area, - stack_panel, - panels: Vec::new(), - active_ix: 0, - notified_active: HashMap::new(), - active_sync_scheduled: false, - tab_bar_scroll_handle: ScrollHandle::new(), - pending_scroll_to_ix: None, - will_split_placement: None, - drop_placeholder_animation: None, - drop_placeholder_animation_name: format!("dock-drop-placeholder-{entity_id}").into(), - zoomed: false, - collapsed: false, - closable: true, - title_bar_bounds: None, - title_bar_strip_bounds: None, - title_bar_suffix_bounds: None, + 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)), } } - pub(super) fn set_parent(&mut self, view: WeakEntity) { - self.stack_panel = Some(view); + /// A group that is the left dock's whole content with a single panel + /// draws no chrome at all — 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 } - /// Return current active_panel View - pub fn active_panel(&self, cx: &App) -> Option> { - let panel = self.panels.get(self.active_ix); - - if let Some(panel) = panel { - if panel.visible(cx) { - Some(panel.clone()) - } else { - // Return the first visible panel - self.visible_panels(cx).next() - } - } else { - None - } - } - - pub fn active_ix(&self) -> usize { - self.active_ix - } - - pub(crate) fn set_active_ix(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { - if ix == self.active_ix { - return; + /// Whether a dock's collapse affordance belongs in *this* group's tab + /// bar, and which way it points. `None` means this group draws none. + fn dock_toggle_button( + &self, + placement: DockPlacement, + group: &TabGroupContext, + cx: &mut App, + ) -> Option