dock.rs

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