dock.rs

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