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