dock.rs

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