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