dock.rs

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