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