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