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