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