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