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