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