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