dock.rs

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