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