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