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