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