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