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