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}