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