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