dock.rs

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