dock.rs

   1use crate::focus_follows_mouse::FocusFollowsMouse as _;
   2use crate::persistence::model::DockData;
   3use crate::{DraggedDock, Event, FocusFollowsMouse, ModalLayer, Pane, WorkspaceSettings};
   4use crate::{Workspace, status_bar::StatusItemView};
   5use anyhow::Context as _;
   6use client::proto;
   7use db::kvp::KeyValueStore;
   8
   9use gpui::{
  10    Action, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle,
  11    Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement,
  12    Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div,
  13    px,
  14};
  15use serde::{Deserialize, Serialize};
  16use settings::{Settings, SettingsStore};
  17use std::sync::Arc;
  18use ui::{
  19    ContextMenu, CountBadge, Divider, DividerColor, IconButton, Tooltip, prelude::*,
  20    right_click_menu,
  21};
  22use util::ResultExt as _;
  23
  24pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.);
  25
  26pub enum PanelEvent {
  27    ZoomIn,
  28    ZoomOut,
  29    Activate,
  30    Close,
  31}
  32
  33pub use proto::PanelId;
  34
  35pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
  36    fn persistent_name() -> &'static str;
  37    fn panel_key() -> &'static str;
  38    fn position(&self, window: &Window, cx: &App) -> DockPosition;
  39    fn position_is_valid(&self, position: DockPosition) -> bool;
  40    fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context<Self>);
  41    fn default_size(&self, window: &Window, cx: &App) -> Pixels;
  42    fn initial_size_state(&self, _window: &Window, _cx: &App) -> PanelSizeState {
  43        PanelSizeState::default()
  44    }
  45    fn size_state_changed(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
  46    fn supports_flexible_size(&self, _window: &Window, _cx: &App) -> bool {
  47        false
  48    }
  49    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
  50    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
  51    fn toggle_action(&self) -> Box<dyn Action>;
  52    fn icon_label(&self, _window: &Window, _: &App) -> Option<String> {
  53        None
  54    }
  55    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
  56        false
  57    }
  58    fn starts_open(&self, _window: &Window, _cx: &App) -> bool {
  59        false
  60    }
  61    fn set_zoomed(&mut self, _zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
  62    fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
  63    fn pane(&self) -> Option<Entity<Pane>> {
  64        None
  65    }
  66    fn remote_id() -> Option<proto::PanelId> {
  67        None
  68    }
  69    fn activation_priority(&self) -> u32;
  70    fn enabled(&self, _cx: &App) -> bool {
  71        true
  72    }
  73}
  74
  75pub trait PanelHandle: Send + Sync {
  76    fn panel_id(&self) -> EntityId;
  77    fn persistent_name(&self) -> &'static str;
  78    fn panel_key(&self) -> &'static str;
  79    fn position(&self, window: &Window, cx: &App) -> DockPosition;
  80    fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool;
  81    fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App);
  82    fn is_zoomed(&self, window: &Window, cx: &App) -> bool;
  83    fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App);
  84    fn set_active(&self, active: bool, window: &mut Window, cx: &mut App);
  85    fn remote_id(&self) -> Option<proto::PanelId>;
  86    fn pane(&self, cx: &App) -> Option<Entity<Pane>>;
  87    fn default_size(&self, window: &Window, cx: &App) -> Pixels;
  88    fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState;
  89    fn size_state_changed(&self, window: &mut Window, cx: &mut App);
  90    fn supports_flexible_size(&self, window: &Window, cx: &App) -> bool;
  91    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
  92    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
  93    fn toggle_action(&self, window: &Window, cx: &App) -> Box<dyn Action>;
  94    fn icon_label(&self, window: &Window, cx: &App) -> Option<String>;
  95    fn panel_focus_handle(&self, cx: &App) -> FocusHandle;
  96    fn to_any(&self) -> AnyView;
  97    fn activation_priority(&self, cx: &App) -> u32;
  98    fn enabled(&self, cx: &App) -> bool;
  99    fn move_to_next_position(&self, window: &mut Window, cx: &mut App) {
 100        let current_position = self.position(window, cx);
 101        let next_position = [
 102            DockPosition::Left,
 103            DockPosition::Bottom,
 104            DockPosition::Right,
 105        ]
 106        .into_iter()
 107        .filter(|position| self.position_is_valid(*position, cx))
 108        .skip_while(|valid_position| *valid_position != current_position)
 109        .nth(1)
 110        .unwrap_or(DockPosition::Left);
 111
 112        self.set_position(next_position, window, cx);
 113    }
 114}
 115
 116impl<T> PanelHandle for Entity<T>
 117where
 118    T: Panel,
 119{
 120    fn panel_id(&self) -> EntityId {
 121        Entity::entity_id(self)
 122    }
 123
 124    fn persistent_name(&self) -> &'static str {
 125        T::persistent_name()
 126    }
 127
 128    fn panel_key(&self) -> &'static str {
 129        T::panel_key()
 130    }
 131
 132    fn position(&self, window: &Window, cx: &App) -> DockPosition {
 133        self.read(cx).position(window, cx)
 134    }
 135
 136    fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool {
 137        self.read(cx).position_is_valid(position)
 138    }
 139
 140    fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App) {
 141        self.update(cx, |this, cx| this.set_position(position, window, cx))
 142    }
 143
 144    fn is_zoomed(&self, window: &Window, cx: &App) -> bool {
 145        self.read(cx).is_zoomed(window, cx)
 146    }
 147
 148    fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) {
 149        self.update(cx, |this, cx| this.set_zoomed(zoomed, window, cx))
 150    }
 151
 152    fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
 153        self.update(cx, |this, cx| this.set_active(active, window, cx))
 154    }
 155
 156    fn pane(&self, cx: &App) -> Option<Entity<Pane>> {
 157        self.read(cx).pane()
 158    }
 159
 160    fn remote_id(&self) -> Option<PanelId> {
 161        T::remote_id()
 162    }
 163
 164    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
 165        self.read(cx).default_size(window, cx)
 166    }
 167
 168    fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState {
 169        self.read(cx).initial_size_state(window, cx)
 170    }
 171
 172    fn size_state_changed(&self, window: &mut Window, cx: &mut App) {
 173        self.update(cx, |this, cx| this.size_state_changed(window, cx))
 174    }
 175
 176    fn supports_flexible_size(&self, window: &Window, cx: &App) -> bool {
 177        self.read(cx).supports_flexible_size(window, cx)
 178    }
 179
 180    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName> {
 181        self.read(cx).icon(window, cx)
 182    }
 183
 184    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str> {
 185        self.read(cx).icon_tooltip(window, cx)
 186    }
 187
 188    fn toggle_action(&self, _: &Window, cx: &App) -> Box<dyn Action> {
 189        self.read(cx).toggle_action()
 190    }
 191
 192    fn icon_label(&self, window: &Window, cx: &App) -> Option<String> {
 193        self.read(cx).icon_label(window, cx)
 194    }
 195
 196    fn to_any(&self) -> AnyView {
 197        self.clone().into()
 198    }
 199
 200    fn panel_focus_handle(&self, cx: &App) -> FocusHandle {
 201        self.read(cx).focus_handle(cx)
 202    }
 203
 204    fn activation_priority(&self, cx: &App) -> u32 {
 205        self.read(cx).activation_priority()
 206    }
 207
 208    fn enabled(&self, cx: &App) -> bool {
 209        self.read(cx).enabled(cx)
 210    }
 211}
 212
 213impl From<&dyn PanelHandle> for AnyView {
 214    fn from(val: &dyn PanelHandle) -> Self {
 215        val.to_any()
 216    }
 217}
 218
 219/// A container with a fixed [`DockPosition`] adjacent to a certain widown edge.
 220/// Can contain multiple panels and show/hide itself with all contents.
 221pub struct Dock {
 222    position: DockPosition,
 223    panel_entries: Vec<PanelEntry>,
 224    workspace: WeakEntity<Workspace>,
 225    is_open: bool,
 226    active_panel_index: Option<usize>,
 227    focus_handle: FocusHandle,
 228    focus_follows_mouse: FocusFollowsMouse,
 229    pub(crate) serialized_dock: Option<DockData>,
 230    zoom_layer_open: bool,
 231    modal_layer: Entity<ModalLayer>,
 232    _subscriptions: [Subscription; 2],
 233}
 234
 235impl Focusable for Dock {
 236    fn focus_handle(&self, _: &App) -> FocusHandle {
 237        self.focus_handle.clone()
 238    }
 239}
 240
 241#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 242pub enum DockPosition {
 243    Left,
 244    Bottom,
 245    Right,
 246}
 247
 248impl From<settings::DockPosition> for DockPosition {
 249    fn from(value: settings::DockPosition) -> Self {
 250        match value {
 251            settings::DockPosition::Left => Self::Left,
 252            settings::DockPosition::Bottom => Self::Bottom,
 253            settings::DockPosition::Right => Self::Right,
 254        }
 255    }
 256}
 257
 258impl Into<settings::DockPosition> for DockPosition {
 259    fn into(self) -> settings::DockPosition {
 260        match self {
 261            Self::Left => settings::DockPosition::Left,
 262            Self::Bottom => settings::DockPosition::Bottom,
 263            Self::Right => settings::DockPosition::Right,
 264        }
 265    }
 266}
 267
 268impl DockPosition {
 269    fn label(&self) -> &'static str {
 270        match self {
 271            Self::Left => "Left",
 272            Self::Bottom => "Bottom",
 273            Self::Right => "Right",
 274        }
 275    }
 276
 277    pub fn axis(&self) -> Axis {
 278        match self {
 279            Self::Left | Self::Right => Axis::Horizontal,
 280            Self::Bottom => Axis::Vertical,
 281        }
 282    }
 283}
 284
 285#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
 286pub struct PanelSizeState {
 287    pub size: Option<Pixels>,
 288    #[serde(default)]
 289    pub flexible_size_ratio: Option<f32>,
 290}
 291
 292struct PanelEntry {
 293    panel: Arc<dyn PanelHandle>,
 294    size_state: PanelSizeState,
 295    _subscriptions: [Subscription; 3],
 296}
 297
 298pub struct PanelButtons {
 299    dock: Entity<Dock>,
 300    _settings_subscription: Subscription,
 301}
 302
 303pub(crate) const PANEL_SIZE_STATE_KEY: &str = "dock_panel_size";
 304
 305impl Dock {
 306    pub fn new(
 307        position: DockPosition,
 308        modal_layer: Entity<ModalLayer>,
 309        window: &mut Window,
 310        cx: &mut Context<Workspace>,
 311    ) -> Entity<Self> {
 312        let focus_handle = cx.focus_handle();
 313        let workspace = cx.entity();
 314        let dock = cx.new(|cx| {
 315            let focus_subscription =
 316                cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| {
 317                    if let Some(active_entry) = dock.active_panel_entry() {
 318                        active_entry.panel.panel_focus_handle(cx).focus(window, cx)
 319                    }
 320                });
 321            let zoom_subscription = cx.subscribe(&workspace, |dock, workspace, e: &Event, cx| {
 322                if matches!(e, Event::ZoomChanged) {
 323                    let is_zoomed = workspace.read(cx).zoomed.is_some();
 324                    dock.zoom_layer_open = is_zoomed;
 325                }
 326            });
 327            Self {
 328                position,
 329                workspace: workspace.downgrade(),
 330                panel_entries: Default::default(),
 331                active_panel_index: None,
 332                is_open: false,
 333                focus_handle: focus_handle.clone(),
 334                focus_follows_mouse: WorkspaceSettings::get_global(cx).focus_follows_mouse,
 335                _subscriptions: [focus_subscription, zoom_subscription],
 336                serialized_dock: None,
 337                zoom_layer_open: false,
 338                modal_layer,
 339            }
 340        });
 341
 342        cx.on_focus_in(&focus_handle, window, {
 343            let dock = dock.downgrade();
 344            move |workspace, window, cx| {
 345                let Some(dock) = dock.upgrade() else {
 346                    return;
 347                };
 348                let Some(panel) = dock.read(cx).active_panel() else {
 349                    return;
 350                };
 351                if panel.is_zoomed(window, cx) {
 352                    workspace.zoomed = Some(panel.to_any().downgrade());
 353                    workspace.zoomed_position = Some(position);
 354                } else {
 355                    workspace.zoomed = None;
 356                    workspace.zoomed_position = None;
 357                }
 358                cx.emit(Event::ZoomChanged);
 359                workspace.dismiss_zoomed_items_to_reveal(Some(position), window, cx);
 360                workspace.update_active_view_for_followers(window, cx)
 361            }
 362        })
 363        .detach();
 364
 365        cx.observe_in(&dock, window, move |workspace, dock, window, cx| {
 366            if dock.read(cx).is_open()
 367                && let Some(panel) = dock.read(cx).active_panel()
 368                && panel.is_zoomed(window, cx)
 369            {
 370                workspace.zoomed = Some(panel.to_any().downgrade());
 371                workspace.zoomed_position = Some(position);
 372                cx.emit(Event::ZoomChanged);
 373                return;
 374            }
 375            if workspace.zoomed_position == Some(position) {
 376                workspace.zoomed = None;
 377                workspace.zoomed_position = None;
 378                cx.emit(Event::ZoomChanged);
 379            }
 380        })
 381        .detach();
 382
 383        dock
 384    }
 385
 386    pub fn position(&self) -> DockPosition {
 387        self.position
 388    }
 389
 390    pub fn is_open(&self) -> bool {
 391        self.is_open
 392    }
 393
 394    fn resizable(&self, cx: &App) -> bool {
 395        !(self.zoom_layer_open || self.modal_layer.read(cx).has_active_modal())
 396    }
 397
 398    pub fn panel<T: Panel>(&self) -> Option<Entity<T>> {
 399        self.panel_entries
 400            .iter()
 401            .find_map(|entry| entry.panel.to_any().downcast().ok())
 402    }
 403
 404    pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
 405        self.panel_entries
 406            .iter()
 407            .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
 408    }
 409
 410    pub fn panel_index_for_persistent_name(&self, ui_name: &str, _cx: &App) -> Option<usize> {
 411        self.panel_entries
 412            .iter()
 413            .position(|entry| entry.panel.persistent_name() == ui_name)
 414    }
 415
 416    pub fn panel_index_for_proto_id(&self, panel_id: PanelId) -> Option<usize> {
 417        self.panel_entries
 418            .iter()
 419            .position(|entry| entry.panel.remote_id() == Some(panel_id))
 420    }
 421
 422    pub fn panel_for_id(&self, panel_id: EntityId) -> Option<&Arc<dyn PanelHandle>> {
 423        self.panel_entries
 424            .iter()
 425            .find(|entry| entry.panel.panel_id() == panel_id)
 426            .map(|entry| &entry.panel)
 427    }
 428
 429    pub fn first_enabled_panel_idx(&mut self, cx: &mut Context<Self>) -> anyhow::Result<usize> {
 430        self.panel_entries
 431            .iter()
 432            .position(|entry| entry.panel.enabled(cx))
 433            .with_context(|| {
 434                format!(
 435                    "Couldn't find any enabled panel for the {} dock.",
 436                    self.position.label()
 437                )
 438            })
 439    }
 440
 441    fn active_panel_entry(&self) -> Option<&PanelEntry> {
 442        self.active_panel_index
 443            .and_then(|index| self.panel_entries.get(index))
 444    }
 445
 446    pub fn active_panel_index(&self) -> Option<usize> {
 447        self.active_panel_index
 448    }
 449
 450    pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
 451        if open != self.is_open {
 452            self.is_open = open;
 453            if let Some(active_panel) = self.active_panel_entry() {
 454                active_panel.panel.set_active(open, window, cx);
 455            }
 456
 457            cx.notify();
 458        }
 459    }
 460
 461    pub fn set_panel_zoomed(
 462        &mut self,
 463        panel: &AnyView,
 464        zoomed: bool,
 465        window: &mut Window,
 466        cx: &mut Context<Self>,
 467    ) {
 468        for entry in &mut self.panel_entries {
 469            if entry.panel.panel_id() == panel.entity_id() {
 470                if zoomed != entry.panel.is_zoomed(window, cx) {
 471                    entry.panel.set_zoomed(zoomed, window, cx);
 472                }
 473            } else if entry.panel.is_zoomed(window, cx) {
 474                entry.panel.set_zoomed(false, window, cx);
 475            }
 476        }
 477
 478        self.workspace
 479            .update(cx, |workspace, cx| {
 480                workspace.serialize_workspace(window, cx);
 481            })
 482            .ok();
 483        cx.notify();
 484    }
 485
 486    pub fn zoom_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 487        for entry in &mut self.panel_entries {
 488            if entry.panel.is_zoomed(window, cx) {
 489                entry.panel.set_zoomed(false, window, cx);
 490            }
 491        }
 492    }
 493
 494    pub(crate) fn add_panel<T: Panel>(
 495        &mut self,
 496        panel: Entity<T>,
 497        workspace: WeakEntity<Workspace>,
 498        window: &mut Window,
 499        cx: &mut Context<Self>,
 500    ) -> usize {
 501        let subscriptions = [
 502            cx.observe(&panel, |_, _, cx| cx.notify()),
 503            cx.observe_global_in::<SettingsStore>(window, {
 504                let workspace = workspace.clone();
 505                let panel = panel.clone();
 506
 507                move |this, window, cx| {
 508                    let new_position = panel.read(cx).position(window, cx);
 509                    if new_position == this.position {
 510                        return;
 511                    }
 512
 513                    let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
 514                        if panel.is_zoomed(window, cx) {
 515                            workspace.zoomed_position = Some(new_position);
 516                        }
 517                        match new_position {
 518                            DockPosition::Left => &workspace.left_dock,
 519                            DockPosition::Bottom => &workspace.bottom_dock,
 520                            DockPosition::Right => &workspace.right_dock,
 521                        }
 522                        .clone()
 523                    }) else {
 524                        return;
 525                    };
 526
 527                    let panel_id = Entity::entity_id(&panel);
 528                    let was_visible = this.is_open()
 529                        && this
 530                            .visible_panel()
 531                            .is_some_and(|active_panel| active_panel.panel_id() == panel_id);
 532                    let size_state = this
 533                        .panel_entries
 534                        .iter()
 535                        .find(|entry| entry.panel.panel_id() == panel_id)
 536                        .map(|entry| entry.size_state)
 537                        .unwrap_or_default();
 538
 539                    let previous_axis = this.position.axis();
 540                    let next_axis = new_position.axis();
 541                    let size_state = if previous_axis == next_axis {
 542                        size_state
 543                    } else {
 544                        PanelSizeState::default()
 545                    };
 546
 547                    if !this.remove_panel(&panel, window, cx) {
 548                        // Panel was already moved from this dock
 549                        return;
 550                    }
 551
 552                    new_dock.update(cx, |new_dock, cx| {
 553                        let index =
 554                            new_dock.add_panel(panel.clone(), workspace.clone(), window, cx);
 555                        if let Some(added_panel) = new_dock.panel_for_id(panel_id).cloned() {
 556                            new_dock.set_panel_size_state(added_panel.as_ref(), size_state, cx);
 557                        }
 558                        if was_visible {
 559                            new_dock.set_open(true, window, cx);
 560                            new_dock.activate_panel(index, window, cx);
 561                        }
 562                    });
 563
 564                    workspace
 565                        .update(cx, |workspace, cx| {
 566                            workspace.serialize_workspace(window, cx);
 567                        })
 568                        .ok();
 569                }
 570            }),
 571            cx.subscribe_in(
 572                &panel,
 573                window,
 574                move |this, panel, event, window, cx| match event {
 575                    PanelEvent::ZoomIn => {
 576                        this.set_panel_zoomed(&panel.to_any(), true, window, cx);
 577                        if !PanelHandle::panel_focus_handle(panel, cx).contains_focused(window, cx)
 578                        {
 579                            window.focus(&panel.focus_handle(cx), cx);
 580                        }
 581                        workspace
 582                            .update(cx, |workspace, cx| {
 583                                workspace.zoomed = Some(panel.downgrade().into());
 584                                workspace.zoomed_position =
 585                                    Some(panel.read(cx).position(window, cx));
 586                                cx.emit(Event::ZoomChanged);
 587                            })
 588                            .ok();
 589                    }
 590                    PanelEvent::ZoomOut => {
 591                        this.set_panel_zoomed(&panel.to_any(), false, window, cx);
 592                        workspace
 593                            .update(cx, |workspace, cx| {
 594                                if workspace.zoomed_position == Some(this.position) {
 595                                    workspace.zoomed = None;
 596                                    workspace.zoomed_position = None;
 597                                    cx.emit(Event::ZoomChanged);
 598                                }
 599                                cx.notify();
 600                            })
 601                            .ok();
 602                    }
 603                    PanelEvent::Activate => {
 604                        if let Some(ix) = this
 605                            .panel_entries
 606                            .iter()
 607                            .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
 608                        {
 609                            this.set_open(true, window, cx);
 610                            this.activate_panel(ix, window, cx);
 611                            window.focus(&panel.read(cx).focus_handle(cx), cx);
 612                        }
 613                    }
 614                    PanelEvent::Close => {
 615                        if this
 616                            .visible_panel()
 617                            .is_some_and(|p| p.panel_id() == Entity::entity_id(panel))
 618                        {
 619                            this.set_open(false, window, cx);
 620                        }
 621                    }
 622                },
 623            ),
 624        ];
 625
 626        let index = match self
 627            .panel_entries
 628            .binary_search_by_key(&panel.read(cx).activation_priority(), |entry| {
 629                entry.panel.activation_priority(cx)
 630            }) {
 631            Ok(ix) => {
 632                if cfg!(debug_assertions) {
 633                    panic!(
 634                        "Panels `{}` and `{}` have the same activation priority. Each panel must have a unique priority so the status bar order is deterministic.",
 635                        T::panel_key(),
 636                        self.panel_entries[ix].panel.panel_key()
 637                    );
 638                }
 639                ix
 640            }
 641            Err(ix) => ix,
 642        };
 643        if let Some(active_index) = self.active_panel_index.as_mut()
 644            && *active_index >= index
 645        {
 646            *active_index += 1;
 647        }
 648        let size_state = panel.read(cx).initial_size_state(window, cx);
 649
 650        self.panel_entries.insert(
 651            index,
 652            PanelEntry {
 653                panel: Arc::new(panel.clone()),
 654                size_state,
 655                _subscriptions: subscriptions,
 656            },
 657        );
 658
 659        self.restore_state(window, cx);
 660
 661        if panel.read(cx).starts_open(window, cx) {
 662            self.activate_panel(index, window, cx);
 663            self.set_open(true, window, cx);
 664        }
 665
 666        cx.notify();
 667        index
 668    }
 669
 670    pub fn restore_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 671        if let Some(serialized) = self.serialized_dock.clone() {
 672            if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible)
 673                && let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx)
 674            {
 675                self.activate_panel(idx, window, cx);
 676            }
 677
 678            if serialized.zoom
 679                && let Some(panel) = self.active_panel()
 680            {
 681                panel.set_zoomed(true, window, cx)
 682            }
 683            self.set_open(serialized.visible, window, cx);
 684            return true;
 685        }
 686        false
 687    }
 688
 689    pub fn remove_panel<T: Panel>(
 690        &mut self,
 691        panel: &Entity<T>,
 692        window: &mut Window,
 693        cx: &mut Context<Self>,
 694    ) -> bool {
 695        if let Some(panel_ix) = self
 696            .panel_entries
 697            .iter()
 698            .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
 699        {
 700            if let Some(active_panel_index) = self.active_panel_index.as_mut() {
 701                match panel_ix.cmp(active_panel_index) {
 702                    std::cmp::Ordering::Less => {
 703                        *active_panel_index -= 1;
 704                    }
 705                    std::cmp::Ordering::Equal => {
 706                        self.active_panel_index = None;
 707                        self.set_open(false, window, cx);
 708                    }
 709                    std::cmp::Ordering::Greater => {}
 710                }
 711            }
 712
 713            self.panel_entries.remove(panel_ix);
 714            cx.notify();
 715
 716            true
 717        } else {
 718            false
 719        }
 720    }
 721
 722    pub fn panels_len(&self) -> usize {
 723        self.panel_entries.len()
 724    }
 725
 726    pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
 727        if Some(panel_ix) != self.active_panel_index {
 728            if let Some(active_panel) = self.active_panel_entry() {
 729                active_panel.panel.set_active(false, window, cx);
 730            }
 731
 732            self.active_panel_index = Some(panel_ix);
 733            if let Some(active_panel) = self.active_panel_entry() {
 734                active_panel.panel.set_active(true, window, cx);
 735            }
 736
 737            cx.notify();
 738        }
 739    }
 740
 741    pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
 742        let entry = self.visible_entry()?;
 743        Some(&entry.panel)
 744    }
 745
 746    pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
 747        let panel_entry = self.active_panel_entry()?;
 748        Some(&panel_entry.panel)
 749    }
 750
 751    fn visible_entry(&self) -> Option<&PanelEntry> {
 752        if self.is_open {
 753            self.active_panel_entry()
 754        } else {
 755            None
 756        }
 757    }
 758
 759    pub fn zoomed_panel(&self, window: &Window, cx: &App) -> Option<Arc<dyn PanelHandle>> {
 760        let entry = self.visible_entry()?;
 761        if entry.panel.is_zoomed(window, cx) {
 762            Some(entry.panel.clone())
 763        } else {
 764            None
 765        }
 766    }
 767
 768    pub fn panel_size(&self, panel: &dyn PanelHandle, window: &Window, cx: &App) -> Option<Pixels> {
 769        self.panel_entries
 770            .iter()
 771            .find(|entry| entry.panel.panel_id() == panel.panel_id())
 772            .map(|entry| self.resolved_panel_size(entry, window, cx))
 773    }
 774
 775    pub fn active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
 776        if self.is_open {
 777            self.active_panel_entry()
 778                .map(|entry| self.resolved_panel_size(entry, window, cx))
 779        } else {
 780            None
 781        }
 782    }
 783
 784    pub fn stored_panel_size(
 785        &self,
 786        panel: &dyn PanelHandle,
 787        window: &Window,
 788        cx: &App,
 789    ) -> Option<Pixels> {
 790        self.panel_entries
 791            .iter()
 792            .find(|entry| entry.panel.panel_id() == panel.panel_id())
 793            .map(|entry| {
 794                entry
 795                    .size_state
 796                    .size
 797                    .unwrap_or_else(|| entry.panel.default_size(window, cx))
 798            })
 799    }
 800
 801    pub fn stored_panel_size_state(&self, panel: &dyn PanelHandle) -> Option<PanelSizeState> {
 802        self.panel_entries
 803            .iter()
 804            .find(|entry| entry.panel.panel_id() == panel.panel_id())
 805            .map(|entry| entry.size_state)
 806    }
 807
 808    pub fn stored_active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
 809        if self.is_open {
 810            self.active_panel_entry().map(|entry| {
 811                entry
 812                    .size_state
 813                    .size
 814                    .unwrap_or_else(|| entry.panel.default_size(window, cx))
 815            })
 816        } else {
 817            None
 818        }
 819    }
 820
 821    pub fn set_panel_size_state(
 822        &mut self,
 823        panel: &dyn PanelHandle,
 824        size_state: PanelSizeState,
 825        cx: &mut Context<Self>,
 826    ) -> bool {
 827        if let Some(entry) = self
 828            .panel_entries
 829            .iter_mut()
 830            .find(|entry| entry.panel.panel_id() == panel.panel_id())
 831        {
 832            entry.size_state = size_state;
 833            cx.notify();
 834            true
 835        } else {
 836            false
 837        }
 838    }
 839
 840    pub fn resize_active_panel(
 841        &mut self,
 842        size: Option<Pixels>,
 843        ratio: Option<f32>,
 844        window: &mut Window,
 845        cx: &mut Context<Self>,
 846    ) {
 847        if let Some(index) = self.active_panel_index
 848            && let Some(entry) = self.panel_entries.get_mut(index)
 849        {
 850            let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
 851
 852            if entry.panel.supports_flexible_size(window, cx) {
 853                entry.size_state.flexible_size_ratio = ratio;
 854            } else {
 855                entry.size_state.size = size;
 856            }
 857
 858            let panel_key = entry.panel.panel_key();
 859            let size_state = entry.size_state;
 860            let workspace = self.workspace.clone();
 861            entry.panel.size_state_changed(window, cx);
 862            cx.defer(move |cx| {
 863                if let Some(workspace) = workspace.upgrade() {
 864                    workspace.update(cx, |workspace, cx| {
 865                        workspace.persist_panel_size_state(panel_key, size_state, cx);
 866                    });
 867                }
 868            });
 869            cx.notify();
 870        }
 871    }
 872
 873    pub fn resize_all_panels(
 874        &mut self,
 875        size: Option<Pixels>,
 876        ratio: Option<f32>,
 877        window: &mut Window,
 878        cx: &mut Context<Self>,
 879    ) {
 880        let mut size_states_to_persist = Vec::new();
 881
 882        for entry in &mut self.panel_entries {
 883            let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
 884            if entry.panel.supports_flexible_size(window, cx) {
 885                entry.size_state.flexible_size_ratio = ratio;
 886            } else {
 887                entry.size_state.size = size;
 888            }
 889            entry.panel.size_state_changed(window, cx);
 890            size_states_to_persist.push((entry.panel.panel_key(), entry.size_state));
 891        }
 892
 893        let workspace = self.workspace.clone();
 894        cx.defer(move |cx| {
 895            if let Some(workspace) = workspace.upgrade() {
 896                workspace.update(cx, |workspace, cx| {
 897                    for (panel_key, size_state) in size_states_to_persist {
 898                        workspace.persist_panel_size_state(panel_key, size_state, cx);
 899                    }
 900                });
 901            }
 902        });
 903
 904        cx.notify();
 905    }
 906
 907    pub fn toggle_action(&self) -> Box<dyn Action> {
 908        match self.position {
 909            DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
 910            DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
 911            DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
 912        }
 913    }
 914
 915    fn dispatch_context() -> KeyContext {
 916        let mut dispatch_context = KeyContext::new_with_defaults();
 917        dispatch_context.add("Dock");
 918
 919        dispatch_context
 920    }
 921
 922    pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &Window, cx: &mut App) {
 923        let max_size = (max_size - RESIZE_HANDLE_SIZE).abs();
 924        for entry in &mut self.panel_entries {
 925            if entry.panel.supports_flexible_size(window, cx) {
 926                continue;
 927            }
 928
 929            let size = entry
 930                .size_state
 931                .size
 932                .unwrap_or_else(|| entry.panel.default_size(window, cx));
 933            if size > max_size {
 934                entry.size_state.size = Some(max_size.max(RESIZE_HANDLE_SIZE));
 935            }
 936        }
 937    }
 938
 939    fn resolved_panel_size(&self, entry: &PanelEntry, window: &Window, cx: &App) -> Pixels {
 940        if self.position.axis() == Axis::Horizontal
 941            && entry.panel.supports_flexible_size(window, cx)
 942        {
 943            if let Some(workspace) = self.workspace.upgrade() {
 944                let workspace = workspace.read(cx);
 945                return resolve_panel_size(
 946                    entry.size_state,
 947                    entry.panel.as_ref(),
 948                    self.position,
 949                    workspace,
 950                    window,
 951                    cx,
 952                );
 953            }
 954        }
 955        entry
 956            .size_state
 957            .size
 958            .unwrap_or_else(|| entry.panel.default_size(window, cx))
 959    }
 960
 961    pub(crate) fn load_persisted_size_state(
 962        workspace: &Workspace,
 963        panel_key: &'static str,
 964        cx: &App,
 965    ) -> Option<PanelSizeState> {
 966        let workspace_id = workspace
 967            .database_id()
 968            .map(|id| i64::from(id).to_string())
 969            .or(workspace.session_id())?;
 970        let kvp = KeyValueStore::global(cx);
 971        let scope = kvp.scoped(PANEL_SIZE_STATE_KEY);
 972        scope
 973            .read(&format!("{workspace_id}:{panel_key}"))
 974            .log_err()
 975            .flatten()
 976            .and_then(|json| serde_json::from_str::<PanelSizeState>(&json).log_err())
 977    }
 978}
 979
 980pub(crate) fn resolve_panel_size(
 981    size_state: PanelSizeState,
 982    panel: &dyn PanelHandle,
 983    position: DockPosition,
 984    workspace: &Workspace,
 985    window: &Window,
 986    cx: &App,
 987) -> Pixels {
 988    if position.axis() == Axis::Horizontal && panel.supports_flexible_size(window, cx) {
 989        let ratio = size_state
 990            .flexible_size_ratio
 991            .or_else(|| workspace.default_flexible_dock_ratio(position));
 992
 993        if let Some(ratio) = ratio {
 994            return workspace
 995                .flexible_dock_size(position, ratio, window, cx)
 996                .unwrap_or_else(|| {
 997                    size_state
 998                        .size
 999                        .unwrap_or_else(|| panel.default_size(window, cx))
1000                });
1001        }
1002    }
1003
1004    size_state
1005        .size
1006        .unwrap_or_else(|| panel.default_size(window, cx))
1007}
1008
1009impl Render for Dock {
1010    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1011        let dispatch_context = Self::dispatch_context();
1012        if let Some(entry) = self.visible_entry() {
1013            let size = self.resolved_panel_size(entry, window, cx);
1014
1015            let position = self.position;
1016            let create_resize_handle = || {
1017                let handle = div()
1018                    .id("resize-handle")
1019                    .on_drag(DraggedDock(position), |dock, _, _, cx| {
1020                        cx.stop_propagation();
1021                        cx.new(|_| dock.clone())
1022                    })
1023                    .on_mouse_down(
1024                        MouseButton::Left,
1025                        cx.listener(|_, _: &MouseDownEvent, _, cx| {
1026                            cx.stop_propagation();
1027                        }),
1028                    )
1029                    .on_mouse_up(
1030                        MouseButton::Left,
1031                        cx.listener(|dock, e: &MouseUpEvent, window, cx| {
1032                            if e.click_count == 2 {
1033                                dock.resize_active_panel(None, None, window, cx);
1034                                dock.workspace
1035                                    .update(cx, |workspace, cx| {
1036                                        workspace.serialize_workspace(window, cx);
1037                                    })
1038                                    .ok();
1039                                cx.stop_propagation();
1040                            }
1041                        }),
1042                    )
1043                    .occlude();
1044                match self.position() {
1045                    DockPosition::Left => deferred(
1046                        handle
1047                            .absolute()
1048                            .right(-RESIZE_HANDLE_SIZE / 2.)
1049                            .top(px(0.))
1050                            .h_full()
1051                            .w(RESIZE_HANDLE_SIZE)
1052                            .cursor_col_resize(),
1053                    ),
1054                    DockPosition::Bottom => deferred(
1055                        handle
1056                            .absolute()
1057                            .top(-RESIZE_HANDLE_SIZE / 2.)
1058                            .left(px(0.))
1059                            .w_full()
1060                            .h(RESIZE_HANDLE_SIZE)
1061                            .cursor_row_resize(),
1062                    ),
1063                    DockPosition::Right => deferred(
1064                        handle
1065                            .absolute()
1066                            .top(px(0.))
1067                            .left(-RESIZE_HANDLE_SIZE / 2.)
1068                            .h_full()
1069                            .w(RESIZE_HANDLE_SIZE)
1070                            .cursor_col_resize(),
1071                    ),
1072                }
1073            };
1074
1075            div()
1076                .key_context(dispatch_context)
1077                .track_focus(&self.focus_handle(cx))
1078                .flex()
1079                .bg(cx.theme().colors().panel_background)
1080                .border_color(cx.theme().colors().border)
1081                .overflow_hidden()
1082                .map(|this| match self.position().axis() {
1083                    Axis::Horizontal => this.w(size).h_full().flex_row(),
1084                    Axis::Vertical => this.h(size).w_full().flex_col(),
1085                })
1086                .map(|this| match self.position() {
1087                    DockPosition::Left => this.border_r_1(),
1088                    DockPosition::Right => this.border_l_1(),
1089                    DockPosition::Bottom => this.border_t_1(),
1090                })
1091                .child(
1092                    div()
1093                        .id("dock-panel")
1094                        .focus_follows_mouse(self.focus_follows_mouse, cx)
1095                        .map(|this| match self.position().axis() {
1096                            Axis::Horizontal => this.min_w(size).h_full(),
1097                            Axis::Vertical => this.min_h(size).w_full(),
1098                        })
1099                        .child(
1100                            entry
1101                                .panel
1102                                .to_any()
1103                                .cached(StyleRefinement::default().v_flex().size_full()),
1104                        ),
1105                )
1106                .when(self.resizable(cx), |this| {
1107                    this.child(create_resize_handle())
1108                })
1109        } else {
1110            div()
1111                .key_context(dispatch_context)
1112                .track_focus(&self.focus_handle(cx))
1113        }
1114    }
1115}
1116
1117impl PanelButtons {
1118    pub fn new(dock: Entity<Dock>, cx: &mut Context<Self>) -> Self {
1119        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
1120        let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| cx.notify());
1121        Self {
1122            dock,
1123            _settings_subscription: settings_subscription,
1124        }
1125    }
1126}
1127
1128impl Render for PanelButtons {
1129    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1130        let dock = self.dock.read(cx);
1131        let active_index = dock.active_panel_index;
1132        let is_open = dock.is_open;
1133        let dock_position = dock.position;
1134
1135        let (menu_anchor, menu_attach) = match dock.position {
1136            DockPosition::Left => (Corner::BottomLeft, Corner::TopLeft),
1137            DockPosition::Bottom | DockPosition::Right => (Corner::BottomRight, Corner::TopRight),
1138        };
1139
1140        let mut buttons: Vec<_> = dock
1141            .panel_entries
1142            .iter()
1143            .enumerate()
1144            .filter_map(|(i, entry)| {
1145                let icon = entry.panel.icon(window, cx)?;
1146                let icon_tooltip = entry
1147                    .panel
1148                    .icon_tooltip(window, cx)
1149                    .ok_or_else(|| {
1150                        anyhow::anyhow!("can't render a panel button without an icon tooltip")
1151                    })
1152                    .log_err()?;
1153                let name = entry.panel.persistent_name();
1154                let panel = entry.panel.clone();
1155
1156                let is_active_button = Some(i) == active_index && is_open;
1157                let (action, tooltip) = if is_active_button {
1158                    let action = dock.toggle_action();
1159
1160                    let tooltip: SharedString =
1161                        format!("Close {} Dock", dock.position.label()).into();
1162
1163                    (action, tooltip)
1164                } else {
1165                    let action = entry.panel.toggle_action(window, cx);
1166
1167                    (action, icon_tooltip.into())
1168                };
1169
1170                let focus_handle = dock.focus_handle(cx);
1171                let icon_label = entry.panel.icon_label(window, cx);
1172
1173                Some(
1174                    right_click_menu(name)
1175                        .menu(move |window, cx| {
1176                            const POSITIONS: [DockPosition; 3] = [
1177                                DockPosition::Left,
1178                                DockPosition::Right,
1179                                DockPosition::Bottom,
1180                            ];
1181
1182                            ContextMenu::build(window, cx, |mut menu, _, cx| {
1183                                for position in POSITIONS {
1184                                    if position != dock_position
1185                                        && panel.position_is_valid(position, cx)
1186                                    {
1187                                        let panel = panel.clone();
1188                                        menu = menu.entry(
1189                                            format!("Dock {}", position.label()),
1190                                            None,
1191                                            move |window, cx| {
1192                                                panel.set_position(position, window, cx);
1193                                            },
1194                                        )
1195                                    }
1196                                }
1197                                menu
1198                            })
1199                        })
1200                        .anchor(menu_anchor)
1201                        .attach(menu_attach)
1202                        .trigger(move |is_active, _window, _cx| {
1203                            // Include active state in element ID to invalidate the cached
1204                            // tooltip when panel state changes (e.g., via keyboard shortcut)
1205                            let button = IconButton::new((name, is_active_button as u64), icon)
1206                                .icon_size(IconSize::Small)
1207                                .toggle_state(is_active_button)
1208                                .on_click({
1209                                    let action = action.boxed_clone();
1210                                    move |_, window, cx| {
1211                                        window.focus(&focus_handle, cx);
1212                                        window.dispatch_action(action.boxed_clone(), cx)
1213                                    }
1214                                })
1215                                .when(!is_active, |this| {
1216                                    this.tooltip(move |_window, cx| {
1217                                        Tooltip::for_action(tooltip.clone(), &*action, cx)
1218                                    })
1219                                });
1220
1221                            div().relative().child(button).when_some(
1222                                icon_label
1223                                    .clone()
1224                                    .filter(|_| !is_active_button)
1225                                    .and_then(|label| label.parse::<usize>().ok()),
1226                                |this, count| this.child(CountBadge::new(count)),
1227                            )
1228                        }),
1229                )
1230            })
1231            .collect();
1232
1233        if dock_position == DockPosition::Right {
1234            buttons.reverse();
1235        }
1236
1237        let has_buttons = !buttons.is_empty();
1238
1239        h_flex()
1240            .gap_1()
1241            .when(
1242                has_buttons
1243                    && (dock.position == DockPosition::Bottom
1244                        || dock.position == DockPosition::Right),
1245                |this| this.child(Divider::vertical().color(DividerColor::Border)),
1246            )
1247            .children(buttons)
1248            .when(has_buttons && dock.position == DockPosition::Left, |this| {
1249                this.child(Divider::vertical().color(DividerColor::Border))
1250            })
1251    }
1252}
1253
1254impl StatusItemView for PanelButtons {
1255    fn set_active_pane_item(
1256        &mut self,
1257        _active_pane_item: Option<&dyn crate::ItemHandle>,
1258        _window: &mut Window,
1259        _cx: &mut Context<Self>,
1260    ) {
1261        // Nothing to do, panel buttons don't depend on the active center item
1262    }
1263}
1264
1265#[cfg(any(test, feature = "test-support"))]
1266pub mod test {
1267    use super::*;
1268    use gpui::{App, Context, Window, actions, div};
1269
1270    pub struct TestPanel {
1271        pub position: DockPosition,
1272        pub zoomed: bool,
1273        pub active: bool,
1274        pub focus_handle: FocusHandle,
1275        pub default_size: Pixels,
1276        pub flexible: bool,
1277        pub activation_priority: u32,
1278    }
1279    actions!(test_only, [ToggleTestPanel]);
1280
1281    impl EventEmitter<PanelEvent> for TestPanel {}
1282
1283    impl TestPanel {
1284        pub fn new(position: DockPosition, activation_priority: u32, cx: &mut App) -> Self {
1285            Self {
1286                position,
1287                zoomed: false,
1288                active: false,
1289                focus_handle: cx.focus_handle(),
1290                default_size: px(300.),
1291                flexible: false,
1292                activation_priority,
1293            }
1294        }
1295
1296        pub fn new_flexible(
1297            position: DockPosition,
1298            activation_priority: u32,
1299            cx: &mut App,
1300        ) -> Self {
1301            Self {
1302                flexible: true,
1303                ..Self::new(position, activation_priority, cx)
1304            }
1305        }
1306    }
1307
1308    impl Render for TestPanel {
1309        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1310            div().id("test").track_focus(&self.focus_handle(cx))
1311        }
1312    }
1313
1314    impl Panel for TestPanel {
1315        fn persistent_name() -> &'static str {
1316            "TestPanel"
1317        }
1318
1319        fn panel_key() -> &'static str {
1320            "TestPanel"
1321        }
1322
1323        fn position(&self, _window: &Window, _: &App) -> super::DockPosition {
1324            self.position
1325        }
1326
1327        fn position_is_valid(&self, _: super::DockPosition) -> bool {
1328            true
1329        }
1330
1331        fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1332            self.position = position;
1333            cx.update_global::<SettingsStore, _>(|_, _| {});
1334        }
1335
1336        fn default_size(&self, _window: &Window, _: &App) -> Pixels {
1337            self.default_size
1338        }
1339
1340        fn initial_size_state(&self, _window: &Window, _: &App) -> PanelSizeState {
1341            PanelSizeState {
1342                size: None,
1343                flexible_size_ratio: None,
1344            }
1345        }
1346
1347        fn supports_flexible_size(&self, _window: &Window, _: &App) -> bool {
1348            self.flexible
1349        }
1350
1351        fn icon(&self, _window: &Window, _: &App) -> Option<ui::IconName> {
1352            None
1353        }
1354
1355        fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1356            None
1357        }
1358
1359        fn toggle_action(&self) -> Box<dyn Action> {
1360            ToggleTestPanel.boxed_clone()
1361        }
1362
1363        fn is_zoomed(&self, _window: &Window, _: &App) -> bool {
1364            self.zoomed
1365        }
1366
1367        fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1368            self.zoomed = zoomed;
1369        }
1370
1371        fn set_active(&mut self, active: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1372            self.active = active;
1373        }
1374
1375        fn activation_priority(&self) -> u32 {
1376            self.activation_priority
1377        }
1378    }
1379
1380    impl Focusable for TestPanel {
1381        fn focus_handle(&self, _cx: &App) -> FocusHandle {
1382            self.focus_handle.clone()
1383        }
1384    }
1385}