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