dock.rs

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