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