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;
   6use db::kvp::KeyValueStore;
   7
   8use gpui::{
   9    Action, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle,
  10    Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement,
  11    Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div,
  12    px,
  13};
  14use serde::{Deserialize, Serialize};
  15use settings::SettingsStore;
  16use std::sync::Arc;
  17use ui::{
  18    ContextMenu, CountBadge, Divider, DividerColor, IconButton, Tooltip, prelude::*,
  19    right_click_menu,
  20};
  21use util::ResultExt as _;
  22
  23pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.);
  24
  25pub enum PanelEvent {
  26    ZoomIn,
  27    ZoomOut,
  28    Activate,
  29    Close,
  30}
  31
  32pub use proto::PanelId;
  33
  34pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
  35    fn persistent_name() -> &'static str;
  36    fn panel_key() -> &'static str;
  37    fn position(&self, window: &Window, cx: &App) -> DockPosition;
  38    fn position_is_valid(&self, position: DockPosition) -> bool;
  39    fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context<Self>);
  40    fn default_size(&self, window: &Window, cx: &App) -> Pixels;
  41    fn initial_size_state(&self, _window: &Window, _cx: &App) -> PanelSizeState {
  42        PanelSizeState::default()
  43    }
  44    fn size_state_changed(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
  45    fn supports_flexible_size(&self) -> bool {
  46        false
  47    }
  48    fn has_flexible_size(&self, _window: &Window, _cx: &App) -> bool {
  49        false
  50    }
  51    fn set_flexible_size(
  52        &mut self,
  53        _flexible: bool,
  54        _window: &mut Window,
  55        _cx: &mut Context<Self>,
  56    ) {
  57    }
  58    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
  59    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
  60    fn toggle_action(&self) -> Box<dyn Action>;
  61    fn icon_label(&self, _window: &Window, _: &App) -> Option<String> {
  62        None
  63    }
  64    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
  65        false
  66    }
  67    fn starts_open(&self, _window: &Window, _cx: &App) -> bool {
  68        false
  69    }
  70    fn set_zoomed(&mut self, _zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
  71    fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
  72    fn pane(&self) -> Option<Entity<Pane>> {
  73        None
  74    }
  75    fn remote_id() -> Option<proto::PanelId> {
  76        None
  77    }
  78    fn activation_priority(&self) -> u32;
  79    fn enabled(&self, _cx: &App) -> bool {
  80        true
  81    }
  82}
  83
  84pub trait PanelHandle: Send + Sync {
  85    fn panel_id(&self) -> EntityId;
  86    fn persistent_name(&self) -> &'static str;
  87    fn panel_key(&self) -> &'static str;
  88    fn position(&self, window: &Window, cx: &App) -> DockPosition;
  89    fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool;
  90    fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App);
  91    fn is_zoomed(&self, window: &Window, cx: &App) -> bool;
  92    fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App);
  93    fn set_active(&self, active: bool, window: &mut Window, cx: &mut App);
  94    fn remote_id(&self) -> Option<proto::PanelId>;
  95    fn pane(&self, cx: &App) -> Option<Entity<Pane>>;
  96    fn default_size(&self, window: &Window, cx: &App) -> Pixels;
  97    fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState;
  98    fn size_state_changed(&self, window: &mut Window, cx: &mut App);
  99    fn supports_flexible_size(&self, cx: &App) -> bool;
 100    fn has_flexible_size(&self, window: &Window, cx: &App) -> bool;
 101    fn set_flexible_size(&self, flexible: bool, window: &mut Window, cx: &mut App);
 102    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
 103    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
 104    fn toggle_action(&self, window: &Window, cx: &App) -> Box<dyn Action>;
 105    fn icon_label(&self, window: &Window, cx: &App) -> Option<String>;
 106    fn panel_focus_handle(&self, cx: &App) -> FocusHandle;
 107    fn to_any(&self) -> AnyView;
 108    fn activation_priority(&self, cx: &App) -> u32;
 109    fn enabled(&self, cx: &App) -> bool;
 110    fn move_to_next_position(&self, window: &mut Window, cx: &mut App) {
 111        let current_position = self.position(window, cx);
 112        let next_position = [
 113            DockPosition::Left,
 114            DockPosition::Bottom,
 115            DockPosition::Right,
 116        ]
 117        .into_iter()
 118        .filter(|position| self.position_is_valid(*position, cx))
 119        .skip_while(|valid_position| *valid_position != current_position)
 120        .nth(1)
 121        .unwrap_or(DockPosition::Left);
 122
 123        self.set_position(next_position, window, cx);
 124    }
 125}
 126
 127impl<T> PanelHandle for Entity<T>
 128where
 129    T: Panel,
 130{
 131    fn panel_id(&self) -> EntityId {
 132        Entity::entity_id(self)
 133    }
 134
 135    fn persistent_name(&self) -> &'static str {
 136        T::persistent_name()
 137    }
 138
 139    fn panel_key(&self) -> &'static str {
 140        T::panel_key()
 141    }
 142
 143    fn position(&self, window: &Window, cx: &App) -> DockPosition {
 144        self.read(cx).position(window, cx)
 145    }
 146
 147    fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool {
 148        self.read(cx).position_is_valid(position)
 149    }
 150
 151    fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App) {
 152        self.update(cx, |this, cx| this.set_position(position, window, cx))
 153    }
 154
 155    fn is_zoomed(&self, window: &Window, cx: &App) -> bool {
 156        self.read(cx).is_zoomed(window, cx)
 157    }
 158
 159    fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) {
 160        self.update(cx, |this, cx| this.set_zoomed(zoomed, window, cx))
 161    }
 162
 163    fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
 164        self.update(cx, |this, cx| this.set_active(active, window, cx))
 165    }
 166
 167    fn pane(&self, cx: &App) -> Option<Entity<Pane>> {
 168        self.read(cx).pane()
 169    }
 170
 171    fn remote_id(&self) -> Option<PanelId> {
 172        T::remote_id()
 173    }
 174
 175    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
 176        self.read(cx).default_size(window, cx)
 177    }
 178
 179    fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState {
 180        self.read(cx).initial_size_state(window, cx)
 181    }
 182
 183    fn size_state_changed(&self, window: &mut Window, cx: &mut App) {
 184        self.update(cx, |this, cx| this.size_state_changed(window, cx))
 185    }
 186
 187    fn supports_flexible_size(&self, cx: &App) -> bool {
 188        self.read(cx).supports_flexible_size()
 189    }
 190
 191    fn has_flexible_size(&self, window: &Window, cx: &App) -> bool {
 192        self.read(cx).has_flexible_size(window, cx)
 193    }
 194
 195    fn set_flexible_size(&self, flexible: bool, window: &mut Window, cx: &mut App) {
 196        self.update(cx, |this, cx| this.set_flexible_size(flexible, window, cx))
 197    }
 198
 199    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName> {
 200        self.read(cx).icon(window, cx)
 201    }
 202
 203    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str> {
 204        self.read(cx).icon_tooltip(window, cx)
 205    }
 206
 207    fn toggle_action(&self, _: &Window, cx: &App) -> Box<dyn Action> {
 208        self.read(cx).toggle_action()
 209    }
 210
 211    fn icon_label(&self, window: &Window, cx: &App) -> Option<String> {
 212        self.read(cx).icon_label(window, cx)
 213    }
 214
 215    fn to_any(&self) -> AnyView {
 216        self.clone().into()
 217    }
 218
 219    fn panel_focus_handle(&self, cx: &App) -> FocusHandle {
 220        self.read(cx).focus_handle(cx)
 221    }
 222
 223    fn activation_priority(&self, cx: &App) -> u32 {
 224        self.read(cx).activation_priority()
 225    }
 226
 227    fn enabled(&self, cx: &App) -> bool {
 228        self.read(cx).enabled(cx)
 229    }
 230}
 231
 232impl From<&dyn PanelHandle> for AnyView {
 233    fn from(val: &dyn PanelHandle) -> Self {
 234        val.to_any()
 235    }
 236}
 237
 238/// A container with a fixed [`DockPosition`] adjacent to a certain widown edge.
 239/// Can contain multiple panels and show/hide itself with all contents.
 240pub struct Dock {
 241    position: DockPosition,
 242    panel_entries: Vec<PanelEntry>,
 243    workspace: WeakEntity<Workspace>,
 244    is_open: bool,
 245    active_panel_index: Option<usize>,
 246    focus_handle: FocusHandle,
 247    pub(crate) serialized_dock: Option<DockData>,
 248    zoom_layer_open: bool,
 249    modal_layer: Entity<ModalLayer>,
 250    _subscriptions: [Subscription; 2],
 251}
 252
 253impl Focusable for Dock {
 254    fn focus_handle(&self, _: &App) -> FocusHandle {
 255        self.focus_handle.clone()
 256    }
 257}
 258
 259#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 260pub enum DockPosition {
 261    Left,
 262    Bottom,
 263    Right,
 264}
 265
 266impl From<settings::DockPosition> for DockPosition {
 267    fn from(value: settings::DockPosition) -> Self {
 268        match value {
 269            settings::DockPosition::Left => Self::Left,
 270            settings::DockPosition::Bottom => Self::Bottom,
 271            settings::DockPosition::Right => Self::Right,
 272        }
 273    }
 274}
 275
 276impl Into<settings::DockPosition> for DockPosition {
 277    fn into(self) -> settings::DockPosition {
 278        match self {
 279            Self::Left => settings::DockPosition::Left,
 280            Self::Bottom => settings::DockPosition::Bottom,
 281            Self::Right => settings::DockPosition::Right,
 282        }
 283    }
 284}
 285
 286impl DockPosition {
 287    fn label(&self) -> &'static str {
 288        match self {
 289            Self::Left => "Left",
 290            Self::Bottom => "Bottom",
 291            Self::Right => "Right",
 292        }
 293    }
 294
 295    pub fn axis(&self) -> Axis {
 296        match self {
 297            Self::Left | Self::Right => Axis::Horizontal,
 298            Self::Bottom => Axis::Vertical,
 299        }
 300    }
 301}
 302
 303#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
 304pub struct PanelSizeState {
 305    pub size: Option<Pixels>,
 306    #[serde(default)]
 307    pub flex: Option<f32>,
 308}
 309
 310struct PanelEntry {
 311    panel: Arc<dyn PanelHandle>,
 312    size_state: PanelSizeState,
 313    _subscriptions: [Subscription; 3],
 314}
 315
 316pub struct PanelButtons {
 317    dock: Entity<Dock>,
 318    _settings_subscription: Subscription,
 319}
 320
 321pub(crate) const PANEL_SIZE_STATE_KEY: &str = "dock_panel_size";
 322
 323fn resize_panel_entry(
 324    position: DockPosition,
 325    entry: &mut PanelEntry,
 326    size: Option<Pixels>,
 327    flex: Option<f32>,
 328    window: &mut Window,
 329    cx: &mut App,
 330) -> (&'static str, PanelSizeState) {
 331    let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
 332    let use_flex = entry.panel.has_flexible_size(window, cx) && position.axis() == Axis::Horizontal;
 333    if use_flex {
 334        entry.size_state.flex = flex;
 335    } else {
 336        entry.size_state.size = size;
 337    }
 338    entry.panel.size_state_changed(window, cx);
 339    (entry.panel.panel_key(), entry.size_state)
 340}
 341
 342impl Dock {
 343    pub fn new(
 344        position: DockPosition,
 345        modal_layer: Entity<ModalLayer>,
 346        window: &mut Window,
 347        cx: &mut Context<Workspace>,
 348    ) -> Entity<Self> {
 349        let focus_handle = cx.focus_handle();
 350        let workspace = cx.entity();
 351        let dock = cx.new(|cx| {
 352            let focus_subscription =
 353                cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| {
 354                    if let Some(active_entry) = dock.active_panel_entry() {
 355                        active_entry.panel.panel_focus_handle(cx).focus(window, cx)
 356                    }
 357                });
 358            let zoom_subscription = cx.subscribe(&workspace, |dock, workspace, e: &Event, cx| {
 359                if matches!(e, Event::ZoomChanged) {
 360                    let is_zoomed = workspace.read(cx).zoomed.is_some();
 361                    dock.zoom_layer_open = is_zoomed;
 362                }
 363            });
 364            Self {
 365                position,
 366                workspace: workspace.downgrade(),
 367                panel_entries: Default::default(),
 368                active_panel_index: None,
 369                is_open: false,
 370                focus_handle: focus_handle.clone(),
 371                _subscriptions: [focus_subscription, zoom_subscription],
 372                serialized_dock: None,
 373                zoom_layer_open: false,
 374                modal_layer,
 375            }
 376        });
 377
 378        cx.on_focus_in(&focus_handle, window, {
 379            let dock = dock.downgrade();
 380            move |workspace, window, cx| {
 381                let Some(dock) = dock.upgrade() else {
 382                    return;
 383                };
 384                let Some(panel) = dock.read(cx).active_panel() else {
 385                    return;
 386                };
 387                if panel.is_zoomed(window, cx) {
 388                    workspace.zoomed = Some(panel.to_any().downgrade());
 389                    workspace.zoomed_position = Some(position);
 390                } else {
 391                    workspace.zoomed = None;
 392                    workspace.zoomed_position = None;
 393                }
 394                cx.emit(Event::ZoomChanged);
 395                workspace.dismiss_zoomed_items_to_reveal(Some(position), window, cx);
 396                workspace.update_active_view_for_followers(window, cx)
 397            }
 398        })
 399        .detach();
 400
 401        cx.observe_in(&dock, window, move |workspace, dock, window, cx| {
 402            if dock.read(cx).is_open()
 403                && let Some(panel) = dock.read(cx).active_panel()
 404                && panel.is_zoomed(window, cx)
 405            {
 406                workspace.zoomed = Some(panel.to_any().downgrade());
 407                workspace.zoomed_position = Some(position);
 408                cx.emit(Event::ZoomChanged);
 409                return;
 410            }
 411            if workspace.zoomed_position == Some(position) {
 412                workspace.zoomed = None;
 413                workspace.zoomed_position = None;
 414                cx.emit(Event::ZoomChanged);
 415            }
 416        })
 417        .detach();
 418
 419        dock
 420    }
 421
 422    pub fn position(&self) -> DockPosition {
 423        self.position
 424    }
 425
 426    pub fn is_open(&self) -> bool {
 427        self.is_open
 428    }
 429
 430    fn resizable(&self, cx: &App) -> bool {
 431        !(self.zoom_layer_open || self.modal_layer.read(cx).has_active_modal())
 432    }
 433
 434    pub fn panel<T: Panel>(&self) -> Option<Entity<T>> {
 435        self.panel_entries
 436            .iter()
 437            .find_map(|entry| entry.panel.to_any().downcast().ok())
 438    }
 439
 440    pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
 441        self.panel_entries
 442            .iter()
 443            .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
 444    }
 445
 446    pub fn panel_index_for_persistent_name(&self, ui_name: &str, _cx: &App) -> Option<usize> {
 447        self.panel_entries
 448            .iter()
 449            .position(|entry| entry.panel.persistent_name() == ui_name)
 450    }
 451
 452    pub fn panel_index_for_proto_id(&self, panel_id: PanelId) -> Option<usize> {
 453        self.panel_entries
 454            .iter()
 455            .position(|entry| entry.panel.remote_id() == Some(panel_id))
 456    }
 457
 458    pub fn panel_for_id(&self, panel_id: EntityId) -> Option<&Arc<dyn PanelHandle>> {
 459        self.panel_entries
 460            .iter()
 461            .find(|entry| entry.panel.panel_id() == panel_id)
 462            .map(|entry| &entry.panel)
 463    }
 464
 465    pub fn first_enabled_panel_idx(&mut self, cx: &mut Context<Self>) -> anyhow::Result<usize> {
 466        self.panel_entries
 467            .iter()
 468            .position(|entry| entry.panel.enabled(cx))
 469            .with_context(|| {
 470                format!(
 471                    "Couldn't find any enabled panel for the {} dock.",
 472                    self.position.label()
 473                )
 474            })
 475    }
 476
 477    fn active_panel_entry(&self) -> Option<&PanelEntry> {
 478        self.active_panel_index
 479            .and_then(|index| self.panel_entries.get(index))
 480    }
 481
 482    pub fn active_panel_index(&self) -> Option<usize> {
 483        self.active_panel_index
 484    }
 485
 486    pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
 487        if open != self.is_open {
 488            self.is_open = open;
 489            if let Some(active_panel) = self.active_panel_entry() {
 490                active_panel.panel.set_active(open, window, cx);
 491            }
 492
 493            cx.notify();
 494        }
 495    }
 496
 497    pub fn set_panel_zoomed(
 498        &mut self,
 499        panel: &AnyView,
 500        zoomed: bool,
 501        window: &mut Window,
 502        cx: &mut Context<Self>,
 503    ) {
 504        for entry in &mut self.panel_entries {
 505            if entry.panel.panel_id() == panel.entity_id() {
 506                if zoomed != entry.panel.is_zoomed(window, cx) {
 507                    entry.panel.set_zoomed(zoomed, window, cx);
 508                }
 509            } else if entry.panel.is_zoomed(window, cx) {
 510                entry.panel.set_zoomed(false, window, cx);
 511            }
 512        }
 513
 514        self.workspace
 515            .update(cx, |workspace, cx| {
 516                workspace.serialize_workspace(window, cx);
 517            })
 518            .ok();
 519        cx.notify();
 520    }
 521
 522    pub fn zoom_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 523        for entry in &mut self.panel_entries {
 524            if entry.panel.is_zoomed(window, cx) {
 525                entry.panel.set_zoomed(false, window, cx);
 526            }
 527        }
 528    }
 529
 530    pub(crate) fn add_panel<T: Panel>(
 531        &mut self,
 532        panel: Entity<T>,
 533        workspace: WeakEntity<Workspace>,
 534        window: &mut Window,
 535        cx: &mut Context<Self>,
 536    ) -> usize {
 537        let subscriptions = [
 538            cx.observe(&panel, |_, _, cx| cx.notify()),
 539            cx.observe_global_in::<SettingsStore>(window, {
 540                let workspace = workspace.clone();
 541                let panel = panel.clone();
 542
 543                move |this, window, cx| {
 544                    let new_position = panel.read(cx).position(window, cx);
 545                    if new_position == this.position {
 546                        return;
 547                    }
 548
 549                    let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
 550                        if panel.is_zoomed(window, cx) {
 551                            workspace.zoomed_position = Some(new_position);
 552                        }
 553                        match new_position {
 554                            DockPosition::Left => &workspace.left_dock,
 555                            DockPosition::Bottom => &workspace.bottom_dock,
 556                            DockPosition::Right => &workspace.right_dock,
 557                        }
 558                        .clone()
 559                    }) else {
 560                        return;
 561                    };
 562
 563                    let panel_id = Entity::entity_id(&panel);
 564                    let was_visible = this.is_open()
 565                        && this
 566                            .visible_panel()
 567                            .is_some_and(|active_panel| active_panel.panel_id() == panel_id);
 568                    let size_state = this
 569                        .panel_entries
 570                        .iter()
 571                        .find(|entry| entry.panel.panel_id() == panel_id)
 572                        .map(|entry| entry.size_state)
 573                        .unwrap_or_default();
 574
 575                    let previous_axis = this.position.axis();
 576                    let next_axis = new_position.axis();
 577                    let size_state = if previous_axis == next_axis {
 578                        size_state
 579                    } else {
 580                        PanelSizeState::default()
 581                    };
 582
 583                    if !this.remove_panel(&panel, window, cx) {
 584                        // Panel was already moved from this dock
 585                        return;
 586                    }
 587
 588                    new_dock.update(cx, |new_dock, cx| {
 589                        let index =
 590                            new_dock.add_panel(panel.clone(), workspace.clone(), window, cx);
 591                        if let Some(added_panel) = new_dock.panel_for_id(panel_id).cloned() {
 592                            new_dock.set_panel_size_state(added_panel.as_ref(), size_state, cx);
 593                        }
 594                        if was_visible {
 595                            new_dock.set_open(true, window, cx);
 596                            new_dock.activate_panel(index, window, cx);
 597                        }
 598                    });
 599
 600                    workspace
 601                        .update(cx, |workspace, cx| {
 602                            workspace.serialize_workspace(window, cx);
 603                        })
 604                        .ok();
 605                }
 606            }),
 607            cx.subscribe_in(
 608                &panel,
 609                window,
 610                move |this, panel, event, window, cx| match event {
 611                    PanelEvent::ZoomIn => {
 612                        this.set_panel_zoomed(&panel.to_any(), true, window, cx);
 613                        if !PanelHandle::panel_focus_handle(panel, cx).contains_focused(window, cx)
 614                        {
 615                            window.focus(&panel.focus_handle(cx), cx);
 616                        }
 617                        workspace
 618                            .update(cx, |workspace, cx| {
 619                                workspace.zoomed = Some(panel.downgrade().into());
 620                                workspace.zoomed_position =
 621                                    Some(panel.read(cx).position(window, cx));
 622                                cx.emit(Event::ZoomChanged);
 623                            })
 624                            .ok();
 625                    }
 626                    PanelEvent::ZoomOut => {
 627                        this.set_panel_zoomed(&panel.to_any(), false, window, cx);
 628                        workspace
 629                            .update(cx, |workspace, cx| {
 630                                if workspace.zoomed_position == Some(this.position) {
 631                                    workspace.zoomed = None;
 632                                    workspace.zoomed_position = None;
 633                                    cx.emit(Event::ZoomChanged);
 634                                }
 635                                cx.notify();
 636                            })
 637                            .ok();
 638                    }
 639                    PanelEvent::Activate => {
 640                        if let Some(ix) = this
 641                            .panel_entries
 642                            .iter()
 643                            .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
 644                        {
 645                            this.set_open(true, window, cx);
 646                            this.activate_panel(ix, window, cx);
 647                            window.focus(&panel.read(cx).focus_handle(cx), cx);
 648                        }
 649                    }
 650                    PanelEvent::Close => {
 651                        if this
 652                            .visible_panel()
 653                            .is_some_and(|p| p.panel_id() == Entity::entity_id(panel))
 654                        {
 655                            this.set_open(false, window, cx);
 656                        }
 657                    }
 658                },
 659            ),
 660        ];
 661
 662        let index = match self
 663            .panel_entries
 664            .binary_search_by_key(&panel.read(cx).activation_priority(), |entry| {
 665                entry.panel.activation_priority(cx)
 666            }) {
 667            Ok(ix) => {
 668                if cfg!(debug_assertions) {
 669                    panic!(
 670                        "Panels `{}` and `{}` have the same activation priority. Each panel must have a unique priority so the status bar order is deterministic.",
 671                        T::panel_key(),
 672                        self.panel_entries[ix].panel.panel_key()
 673                    );
 674                }
 675                ix
 676            }
 677            Err(ix) => ix,
 678        };
 679        if let Some(active_index) = self.active_panel_index.as_mut()
 680            && *active_index >= index
 681        {
 682            *active_index += 1;
 683        }
 684        let size_state = panel.read(cx).initial_size_state(window, cx);
 685
 686        self.panel_entries.insert(
 687            index,
 688            PanelEntry {
 689                panel: Arc::new(panel.clone()),
 690                size_state,
 691                _subscriptions: subscriptions,
 692            },
 693        );
 694
 695        self.restore_state(window, cx);
 696
 697        if panel.read(cx).starts_open(window, cx) {
 698            self.activate_panel(index, window, cx);
 699            self.set_open(true, window, cx);
 700        }
 701
 702        cx.notify();
 703        index
 704    }
 705
 706    pub fn restore_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 707        if let Some(serialized) = self.serialized_dock.clone() {
 708            if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible)
 709                && let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx)
 710            {
 711                self.activate_panel(idx, window, cx);
 712            }
 713
 714            if serialized.zoom
 715                && let Some(panel) = self.active_panel()
 716            {
 717                panel.set_zoomed(true, window, cx)
 718            }
 719            self.set_open(serialized.visible, window, cx);
 720            return true;
 721        }
 722        false
 723    }
 724
 725    pub fn remove_panel<T: Panel>(
 726        &mut self,
 727        panel: &Entity<T>,
 728        window: &mut Window,
 729        cx: &mut Context<Self>,
 730    ) -> bool {
 731        if let Some(panel_ix) = self
 732            .panel_entries
 733            .iter()
 734            .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
 735        {
 736            if let Some(active_panel_index) = self.active_panel_index.as_mut() {
 737                match panel_ix.cmp(active_panel_index) {
 738                    std::cmp::Ordering::Less => {
 739                        *active_panel_index -= 1;
 740                    }
 741                    std::cmp::Ordering::Equal => {
 742                        self.active_panel_index = None;
 743                        self.set_open(false, window, cx);
 744                    }
 745                    std::cmp::Ordering::Greater => {}
 746                }
 747            }
 748
 749            self.panel_entries.remove(panel_ix);
 750            cx.notify();
 751
 752            true
 753        } else {
 754            false
 755        }
 756    }
 757
 758    pub fn panels_len(&self) -> usize {
 759        self.panel_entries.len()
 760    }
 761
 762    pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
 763        if Some(panel_ix) != self.active_panel_index {
 764            if let Some(active_panel) = self.active_panel_entry() {
 765                active_panel.panel.set_active(false, window, cx);
 766            }
 767
 768            self.active_panel_index = Some(panel_ix);
 769            if let Some(active_panel) = self.active_panel_entry() {
 770                active_panel.panel.set_active(true, window, cx);
 771            }
 772
 773            cx.notify();
 774        }
 775    }
 776
 777    pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
 778        let entry = self.visible_entry()?;
 779        Some(&entry.panel)
 780    }
 781
 782    pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
 783        let panel_entry = self.active_panel_entry()?;
 784        Some(&panel_entry.panel)
 785    }
 786
 787    fn visible_entry(&self) -> Option<&PanelEntry> {
 788        if self.is_open {
 789            self.active_panel_entry()
 790        } else {
 791            None
 792        }
 793    }
 794
 795    pub fn zoomed_panel(&self, window: &Window, cx: &App) -> Option<Arc<dyn PanelHandle>> {
 796        let entry = self.visible_entry()?;
 797        if entry.panel.is_zoomed(window, cx) {
 798            Some(entry.panel.clone())
 799        } else {
 800            None
 801        }
 802    }
 803
 804    pub fn active_panel_size(&self) -> Option<PanelSizeState> {
 805        if self.is_open {
 806            self.active_panel_entry().map(|entry| entry.size_state)
 807        } else {
 808            None
 809        }
 810    }
 811
 812    pub fn stored_panel_size(
 813        &self,
 814        panel: &dyn PanelHandle,
 815        window: &Window,
 816        cx: &App,
 817    ) -> Option<Pixels> {
 818        self.panel_entries
 819            .iter()
 820            .find(|entry| entry.panel.panel_id() == panel.panel_id())
 821            .map(|entry| {
 822                entry
 823                    .size_state
 824                    .size
 825                    .unwrap_or_else(|| entry.panel.default_size(window, cx))
 826            })
 827    }
 828
 829    pub fn stored_panel_size_state(&self, panel: &dyn PanelHandle) -> Option<PanelSizeState> {
 830        self.panel_entries
 831            .iter()
 832            .find(|entry| entry.panel.panel_id() == panel.panel_id())
 833            .map(|entry| entry.size_state)
 834    }
 835
 836    pub fn stored_active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
 837        if self.is_open {
 838            self.active_panel_entry().map(|entry| {
 839                entry
 840                    .size_state
 841                    .size
 842                    .unwrap_or_else(|| entry.panel.default_size(window, cx))
 843            })
 844        } else {
 845            None
 846        }
 847    }
 848
 849    pub fn set_panel_size_state(
 850        &mut self,
 851        panel: &dyn PanelHandle,
 852        size_state: PanelSizeState,
 853        cx: &mut Context<Self>,
 854    ) -> bool {
 855        if let Some(entry) = self
 856            .panel_entries
 857            .iter_mut()
 858            .find(|entry| entry.panel.panel_id() == panel.panel_id())
 859        {
 860            entry.size_state = size_state;
 861            cx.notify();
 862            true
 863        } else {
 864            false
 865        }
 866    }
 867
 868    pub fn toggle_panel_flexible_size(
 869        &mut self,
 870        panel: &dyn PanelHandle,
 871        current_size: Option<Pixels>,
 872        current_flex: Option<f32>,
 873        window: &mut Window,
 874        cx: &mut Context<Self>,
 875    ) {
 876        let Some(entry) = self
 877            .panel_entries
 878            .iter_mut()
 879            .find(|entry| entry.panel.panel_id() == panel.panel_id())
 880        else {
 881            return;
 882        };
 883        let currently_flexible = entry.panel.has_flexible_size(window, cx);
 884        if currently_flexible {
 885            entry.size_state.size = current_size;
 886        } else {
 887            entry.size_state.flex = current_flex;
 888        }
 889        let panel_key = entry.panel.panel_key();
 890        let size_state = entry.size_state;
 891        let workspace = self.workspace.clone();
 892        entry
 893            .panel
 894            .set_flexible_size(!currently_flexible, window, cx);
 895        entry.panel.size_state_changed(window, cx);
 896        cx.defer(move |cx| {
 897            if let Some(workspace) = workspace.upgrade() {
 898                workspace.update(cx, |workspace, cx| {
 899                    workspace.persist_panel_size_state(panel_key, size_state, cx);
 900                });
 901            }
 902        });
 903        cx.notify();
 904    }
 905
 906    pub fn resize_active_panel(
 907        &mut self,
 908        size: Option<Pixels>,
 909        flex: Option<f32>,
 910        window: &mut Window,
 911        cx: &mut Context<Self>,
 912    ) {
 913        if let Some(index) = self.active_panel_index
 914            && let Some(entry) = self.panel_entries.get_mut(index)
 915        {
 916            let (panel_key, size_state) =
 917                resize_panel_entry(self.position, entry, size, flex, window, cx);
 918
 919            let workspace = self.workspace.clone();
 920            cx.defer(move |cx| {
 921                if let Some(workspace) = workspace.upgrade() {
 922                    workspace.update(cx, |workspace, cx| {
 923                        workspace.persist_panel_size_state(panel_key, size_state, cx);
 924                    });
 925                }
 926            });
 927            cx.notify();
 928        }
 929    }
 930
 931    pub fn resize_all_panels(
 932        &mut self,
 933        size: Option<Pixels>,
 934        flex: Option<f32>,
 935        window: &mut Window,
 936        cx: &mut Context<Self>,
 937    ) {
 938        let size_states_to_persist: Vec<_> = self
 939            .panel_entries
 940            .iter_mut()
 941            .map(|entry| resize_panel_entry(self.position, entry, size, flex, window, cx))
 942            .collect();
 943
 944        let workspace = self.workspace.clone();
 945        cx.defer(move |cx| {
 946            if let Some(workspace) = workspace.upgrade() {
 947                workspace.update(cx, |workspace, cx| {
 948                    for (panel_key, size_state) in size_states_to_persist {
 949                        workspace.persist_panel_size_state(panel_key, size_state, cx);
 950                    }
 951                });
 952            }
 953        });
 954
 955        cx.notify();
 956    }
 957
 958    pub fn toggle_action(&self) -> Box<dyn Action> {
 959        match self.position {
 960            DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
 961            DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
 962            DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
 963        }
 964    }
 965
 966    fn dispatch_context() -> KeyContext {
 967        let mut dispatch_context = KeyContext::new_with_defaults();
 968        dispatch_context.add("Dock");
 969
 970        dispatch_context
 971    }
 972
 973    pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &Window, cx: &mut App) {
 974        let max_size = (max_size - RESIZE_HANDLE_SIZE).abs();
 975        for entry in &mut self.panel_entries {
 976            let use_flexible = entry.panel.has_flexible_size(window, cx);
 977            if use_flexible {
 978                continue;
 979            }
 980
 981            let size = entry
 982                .size_state
 983                .size
 984                .unwrap_or_else(|| entry.panel.default_size(window, cx));
 985            if size > max_size {
 986                entry.size_state.size = Some(max_size.max(RESIZE_HANDLE_SIZE));
 987            }
 988        }
 989    }
 990
 991    pub(crate) fn load_persisted_size_state(
 992        workspace: &Workspace,
 993        panel_key: &'static str,
 994        cx: &App,
 995    ) -> Option<PanelSizeState> {
 996        let workspace_id = workspace
 997            .database_id()
 998            .map(|id| i64::from(id).to_string())
 999            .or(workspace.session_id())?;
1000        let kvp = KeyValueStore::global(cx);
1001        let scope = kvp.scoped(PANEL_SIZE_STATE_KEY);
1002        scope
1003            .read(&format!("{workspace_id}:{panel_key}"))
1004            .log_err()
1005            .flatten()
1006            .and_then(|json| serde_json::from_str::<PanelSizeState>(&json).log_err())
1007    }
1008}
1009
1010impl Render for Dock {
1011    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1012        let dispatch_context = Self::dispatch_context();
1013        if let Some(entry) = self.visible_entry() {
1014            let position = self.position;
1015            let create_resize_handle = || {
1016                let handle = div()
1017                    .id("resize-handle")
1018                    .on_drag(DraggedDock(position), |dock, _, _, cx| {
1019                        cx.stop_propagation();
1020                        cx.new(|_| dock.clone())
1021                    })
1022                    .on_mouse_down(
1023                        MouseButton::Left,
1024                        cx.listener(|_, _: &MouseDownEvent, _, cx| {
1025                            cx.stop_propagation();
1026                        }),
1027                    )
1028                    .on_mouse_up(
1029                        MouseButton::Left,
1030                        cx.listener(|dock, e: &MouseUpEvent, window, cx| {
1031                            if e.click_count == 2 {
1032                                dock.resize_active_panel(None, None, window, cx);
1033                                dock.workspace
1034                                    .update(cx, |workspace, cx| {
1035                                        workspace.serialize_workspace(window, cx);
1036                                    })
1037                                    .ok();
1038                                cx.stop_propagation();
1039                            }
1040                        }),
1041                    )
1042                    .occlude();
1043                match self.position() {
1044                    DockPosition::Left => deferred(
1045                        handle
1046                            .absolute()
1047                            .right(-RESIZE_HANDLE_SIZE / 2.)
1048                            .top(px(0.))
1049                            .h_full()
1050                            .w(RESIZE_HANDLE_SIZE)
1051                            .cursor_col_resize(),
1052                    ),
1053                    DockPosition::Bottom => deferred(
1054                        handle
1055                            .absolute()
1056                            .top(-RESIZE_HANDLE_SIZE / 2.)
1057                            .left(px(0.))
1058                            .w_full()
1059                            .h(RESIZE_HANDLE_SIZE)
1060                            .cursor_row_resize(),
1061                    ),
1062                    DockPosition::Right => deferred(
1063                        handle
1064                            .absolute()
1065                            .top(px(0.))
1066                            .left(-RESIZE_HANDLE_SIZE / 2.)
1067                            .h_full()
1068                            .w(RESIZE_HANDLE_SIZE)
1069                            .cursor_col_resize(),
1070                    ),
1071                }
1072            };
1073
1074            div()
1075                .key_context(dispatch_context)
1076                .track_focus(&self.focus_handle(cx))
1077                .flex()
1078                .bg(cx.theme().colors().panel_background)
1079                .border_color(cx.theme().colors().border)
1080                .overflow_hidden()
1081                .map(|this| match self.position().axis() {
1082                    // Width and height are always set on the workspace wrapper in
1083                    // render_dock, so fill whatever space the wrapper provides.
1084                    Axis::Horizontal => this.w_full().h_full().flex_row(),
1085                    Axis::Vertical => this.h_full().w_full().flex_col(),
1086                })
1087                .map(|this| match self.position() {
1088                    DockPosition::Left => this.border_r_1(),
1089                    DockPosition::Right => this.border_l_1(),
1090                    DockPosition::Bottom => this.border_t_1(),
1091                })
1092                .child(
1093                    div()
1094                        .map(|this| match self.position().axis() {
1095                            Axis::Horizontal => this.w_full().h_full(),
1096                            Axis::Vertical => this.h_full().w_full(),
1097                        })
1098                        .child(
1099                            entry
1100                                .panel
1101                                .to_any()
1102                                .cached(StyleRefinement::default().v_flex().size_full()),
1103                        ),
1104                )
1105                .when(self.resizable(cx), |this| {
1106                    this.child(create_resize_handle())
1107                })
1108        } else {
1109            div()
1110                .key_context(dispatch_context)
1111                .track_focus(&self.focus_handle(cx))
1112        }
1113    }
1114}
1115
1116impl PanelButtons {
1117    pub fn new(dock: Entity<Dock>, cx: &mut Context<Self>) -> Self {
1118        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
1119        let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| cx.notify());
1120        Self {
1121            dock,
1122            _settings_subscription: settings_subscription,
1123        }
1124    }
1125}
1126
1127impl Render for PanelButtons {
1128    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1129        let dock = self.dock.read(cx);
1130        let active_index = dock.active_panel_index;
1131        let is_open = dock.is_open;
1132        let dock_position = dock.position;
1133
1134        let (menu_anchor, menu_attach) = match dock.position {
1135            DockPosition::Left => (Corner::BottomLeft, Corner::TopLeft),
1136            DockPosition::Bottom | DockPosition::Right => (Corner::BottomRight, Corner::TopRight),
1137        };
1138
1139        let dock_entity = self.dock.clone();
1140        let workspace = dock.workspace.clone();
1141        let mut buttons: Vec<_> = dock
1142            .panel_entries
1143            .iter()
1144            .enumerate()
1145            .filter_map(|(i, entry)| {
1146                let icon = entry.panel.icon(window, cx)?;
1147                let icon_tooltip = entry
1148                    .panel
1149                    .icon_tooltip(window, cx)
1150                    .ok_or_else(|| {
1151                        anyhow::anyhow!("can't render a panel button without an icon tooltip")
1152                    })
1153                    .log_err()?;
1154                let name = entry.panel.persistent_name();
1155                let panel = entry.panel.clone();
1156                let supports_flexible = panel.supports_flexible_size(cx);
1157                let currently_flexible = panel.has_flexible_size(window, cx);
1158                let dock_for_menu = dock_entity.clone();
1159                let workspace_for_menu = workspace.clone();
1160
1161                let is_active_button = Some(i) == active_index && is_open;
1162                let (action, tooltip) = if is_active_button {
1163                    let action = dock.toggle_action();
1164
1165                    let tooltip: SharedString =
1166                        format!("Close {} Dock", dock.position.label()).into();
1167
1168                    (action, tooltip)
1169                } else {
1170                    let action = entry.panel.toggle_action(window, cx);
1171
1172                    (action, icon_tooltip.into())
1173                };
1174
1175                let focus_handle = dock.focus_handle(cx);
1176                let icon_label = entry.panel.icon_label(window, cx);
1177
1178                Some(
1179                    right_click_menu(name)
1180                        .menu(move |window, cx| {
1181                            const POSITIONS: [DockPosition; 3] = [
1182                                DockPosition::Left,
1183                                DockPosition::Right,
1184                                DockPosition::Bottom,
1185                            ];
1186
1187                            ContextMenu::build(window, cx, |mut menu, _, cx| {
1188                                let mut has_position_entries = false;
1189                                for position in POSITIONS {
1190                                    if panel.position_is_valid(position, cx) {
1191                                        let is_current = position == dock_position;
1192                                        let panel = panel.clone();
1193                                        menu = menu.toggleable_entry(
1194                                            format!("Dock {}", position.label()),
1195                                            is_current,
1196                                            IconPosition::Start,
1197                                            None,
1198                                            move |window, cx| {
1199                                                if !is_current {
1200                                                    panel.set_position(position, window, cx);
1201                                                }
1202                                            },
1203                                        );
1204                                        has_position_entries = true;
1205                                    }
1206                                }
1207                                if supports_flexible {
1208                                    if has_position_entries {
1209                                        menu = menu.separator();
1210                                    }
1211                                    let panel_for_flex = panel.clone();
1212                                    let dock_for_flex = dock_for_menu.clone();
1213                                    let workspace_for_flex = workspace_for_menu.clone();
1214                                    menu = menu.toggleable_entry(
1215                                        "Flex Width",
1216                                        currently_flexible,
1217                                        IconPosition::Start,
1218                                        None,
1219                                        move |window, cx| {
1220                                            if !currently_flexible {
1221                                                if let Some(ws) = workspace_for_flex.upgrade() {
1222                                                    ws.update(cx, |workspace, cx| {
1223                                                        workspace.toggle_dock_panel_flexible_size(
1224                                                            &dock_for_flex,
1225                                                            panel_for_flex.as_ref(),
1226                                                            window,
1227                                                            cx,
1228                                                        );
1229                                                    });
1230                                                }
1231                                            }
1232                                        },
1233                                    );
1234                                    let panel_for_fixed = panel.clone();
1235                                    let dock_for_fixed = dock_for_menu.clone();
1236                                    let workspace_for_fixed = workspace_for_menu.clone();
1237                                    menu = menu.toggleable_entry(
1238                                        "Fixed Width",
1239                                        !currently_flexible,
1240                                        IconPosition::Start,
1241                                        None,
1242                                        move |window, cx| {
1243                                            if currently_flexible {
1244                                                if let Some(ws) = workspace_for_fixed.upgrade() {
1245                                                    ws.update(cx, |workspace, cx| {
1246                                                        workspace.toggle_dock_panel_flexible_size(
1247                                                            &dock_for_fixed,
1248                                                            panel_for_fixed.as_ref(),
1249                                                            window,
1250                                                            cx,
1251                                                        );
1252                                                    });
1253                                                }
1254                                            }
1255                                        },
1256                                    );
1257                                }
1258                                menu
1259                            })
1260                        })
1261                        .anchor(menu_anchor)
1262                        .attach(menu_attach)
1263                        .trigger(move |is_active, _window, _cx| {
1264                            // Include active state in element ID to invalidate the cached
1265                            // tooltip when panel state changes (e.g., via keyboard shortcut)
1266                            let button = IconButton::new((name, is_active_button as u64), icon)
1267                                .icon_size(IconSize::Small)
1268                                .toggle_state(is_active_button)
1269                                .on_click({
1270                                    let action = action.boxed_clone();
1271                                    move |_, window, cx| {
1272                                        window.focus(&focus_handle, cx);
1273                                        window.dispatch_action(action.boxed_clone(), cx)
1274                                    }
1275                                })
1276                                .when(!is_active, |this| {
1277                                    this.tooltip(move |_window, cx| {
1278                                        Tooltip::for_action(tooltip.clone(), &*action, cx)
1279                                    })
1280                                });
1281
1282                            div().relative().child(button).when_some(
1283                                icon_label
1284                                    .clone()
1285                                    .filter(|_| !is_active_button)
1286                                    .and_then(|label| label.parse::<usize>().ok()),
1287                                |this, count| this.child(CountBadge::new(count)),
1288                            )
1289                        }),
1290                )
1291            })
1292            .collect();
1293
1294        if dock_position == DockPosition::Right {
1295            buttons.reverse();
1296        }
1297
1298        let has_buttons = !buttons.is_empty();
1299
1300        h_flex()
1301            .gap_1()
1302            .when(
1303                has_buttons
1304                    && (dock.position == DockPosition::Bottom
1305                        || dock.position == DockPosition::Right),
1306                |this| this.child(Divider::vertical().color(DividerColor::Border)),
1307            )
1308            .children(buttons)
1309            .when(has_buttons && dock.position == DockPosition::Left, |this| {
1310                this.child(Divider::vertical().color(DividerColor::Border))
1311            })
1312    }
1313}
1314
1315impl StatusItemView for PanelButtons {
1316    fn set_active_pane_item(
1317        &mut self,
1318        _active_pane_item: Option<&dyn crate::ItemHandle>,
1319        _window: &mut Window,
1320        _cx: &mut Context<Self>,
1321    ) {
1322        // Nothing to do, panel buttons don't depend on the active center item
1323    }
1324}
1325
1326#[cfg(any(test, feature = "test-support"))]
1327pub mod test {
1328    use super::*;
1329    use gpui::{App, Context, Window, actions, div};
1330
1331    pub struct TestPanel {
1332        pub position: DockPosition,
1333        pub zoomed: bool,
1334        pub active: bool,
1335        pub focus_handle: FocusHandle,
1336        pub default_size: Pixels,
1337        pub flexible: bool,
1338        pub activation_priority: u32,
1339    }
1340    actions!(test_only, [ToggleTestPanel]);
1341
1342    impl EventEmitter<PanelEvent> for TestPanel {}
1343
1344    impl TestPanel {
1345        pub fn new(position: DockPosition, activation_priority: u32, cx: &mut App) -> Self {
1346            Self {
1347                position,
1348                zoomed: false,
1349                active: false,
1350                focus_handle: cx.focus_handle(),
1351                default_size: px(300.),
1352                flexible: false,
1353                activation_priority,
1354            }
1355        }
1356
1357        pub fn new_flexible(
1358            position: DockPosition,
1359            activation_priority: u32,
1360            cx: &mut App,
1361        ) -> Self {
1362            Self {
1363                flexible: true,
1364                ..Self::new(position, activation_priority, cx)
1365            }
1366        }
1367    }
1368
1369    impl Render for TestPanel {
1370        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1371            div().id("test").track_focus(&self.focus_handle(cx))
1372        }
1373    }
1374
1375    impl Panel for TestPanel {
1376        fn persistent_name() -> &'static str {
1377            "TestPanel"
1378        }
1379
1380        fn panel_key() -> &'static str {
1381            "TestPanel"
1382        }
1383
1384        fn position(&self, _window: &Window, _: &App) -> super::DockPosition {
1385            self.position
1386        }
1387
1388        fn position_is_valid(&self, _: super::DockPosition) -> bool {
1389            true
1390        }
1391
1392        fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1393            self.position = position;
1394            cx.update_global::<SettingsStore, _>(|_, _| {});
1395        }
1396
1397        fn default_size(&self, _window: &Window, _: &App) -> Pixels {
1398            self.default_size
1399        }
1400
1401        fn initial_size_state(&self, _window: &Window, _: &App) -> PanelSizeState {
1402            PanelSizeState {
1403                size: None,
1404                flex: None,
1405            }
1406        }
1407
1408        fn supports_flexible_size(&self) -> bool {
1409            self.flexible
1410        }
1411
1412        fn has_flexible_size(&self, _window: &Window, _: &App) -> bool {
1413            self.flexible
1414        }
1415
1416        fn set_flexible_size(
1417            &mut self,
1418            flexible: bool,
1419            _window: &mut Window,
1420            _cx: &mut Context<Self>,
1421        ) {
1422            self.flexible = flexible;
1423        }
1424
1425        fn icon(&self, _window: &Window, _: &App) -> Option<ui::IconName> {
1426            None
1427        }
1428
1429        fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1430            None
1431        }
1432
1433        fn toggle_action(&self) -> Box<dyn Action> {
1434            ToggleTestPanel.boxed_clone()
1435        }
1436
1437        fn is_zoomed(&self, _window: &Window, _: &App) -> bool {
1438            self.zoomed
1439        }
1440
1441        fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1442            self.zoomed = zoomed;
1443        }
1444
1445        fn set_active(&mut self, active: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1446            self.active = active;
1447        }
1448
1449        fn activation_priority(&self) -> u32 {
1450            self.activation_priority
1451        }
1452    }
1453
1454    impl Focusable for TestPanel {
1455        fn focus_handle(&self, _cx: &App) -> FocusHandle {
1456            self.focus_handle.clone()
1457        }
1458    }
1459}