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