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 panel_size(&self, panel: &dyn PanelHandle, window: &Window, cx: &App) -> Option<Pixels> {
780 self.panel_entries
781 .iter()
782 .find(|entry| entry.panel.panel_id() == panel.panel_id())
783 .map(|entry| self.resolved_panel_size(entry, window, cx))
784 }
785
786 pub fn active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
787 if self.is_open {
788 self.active_panel_entry()
789 .map(|entry| self.resolved_panel_size(entry, window, cx))
790 } else {
791 None
792 }
793 }
794
795 pub fn stored_panel_size(
796 &self,
797 panel: &dyn PanelHandle,
798 window: &Window,
799 cx: &App,
800 ) -> Option<Pixels> {
801 self.panel_entries
802 .iter()
803 .find(|entry| entry.panel.panel_id() == panel.panel_id())
804 .map(|entry| {
805 entry
806 .size_state
807 .size
808 .unwrap_or_else(|| entry.panel.default_size(window, cx))
809 })
810 }
811
812 pub fn stored_panel_size_state(&self, panel: &dyn PanelHandle) -> Option<PanelSizeState> {
813 self.panel_entries
814 .iter()
815 .find(|entry| entry.panel.panel_id() == panel.panel_id())
816 .map(|entry| entry.size_state)
817 }
818
819 pub fn stored_active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
820 if self.is_open {
821 self.active_panel_entry().map(|entry| {
822 entry
823 .size_state
824 .size
825 .unwrap_or_else(|| entry.panel.default_size(window, cx))
826 })
827 } else {
828 None
829 }
830 }
831
832 pub fn set_panel_size_state(
833 &mut self,
834 panel: &dyn PanelHandle,
835 size_state: PanelSizeState,
836 cx: &mut Context<Self>,
837 ) -> bool {
838 if let Some(entry) = self
839 .panel_entries
840 .iter_mut()
841 .find(|entry| entry.panel.panel_id() == panel.panel_id())
842 {
843 entry.size_state = size_state;
844 cx.notify();
845 true
846 } else {
847 false
848 }
849 }
850
851 pub fn resize_active_panel(
852 &mut self,
853 size: Option<Pixels>,
854 ratio: Option<f32>,
855 window: &mut Window,
856 cx: &mut Context<Self>,
857 ) {
858 if let Some(index) = self.active_panel_index
859 && let Some(entry) = self.panel_entries.get_mut(index)
860 {
861 let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
862
863 if entry.panel.supports_flexible_size(window, cx) {
864 entry.size_state.flexible_size_ratio = ratio;
865 } else {
866 entry.size_state.size = size;
867 }
868
869 let panel_key = entry.panel.panel_key();
870 let size_state = entry.size_state;
871 let workspace = self.workspace.clone();
872 entry.panel.size_state_changed(window, cx);
873 cx.defer(move |cx| {
874 if let Some(workspace) = workspace.upgrade() {
875 workspace.update(cx, |workspace, cx| {
876 workspace.persist_panel_size_state(panel_key, size_state, cx);
877 });
878 }
879 });
880 cx.notify();
881 }
882 }
883
884 pub fn resize_all_panels(
885 &mut self,
886 size: Option<Pixels>,
887 ratio: Option<f32>,
888 window: &mut Window,
889 cx: &mut Context<Self>,
890 ) {
891 let mut size_states_to_persist = Vec::new();
892
893 for entry in &mut self.panel_entries {
894 let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
895 if entry.panel.supports_flexible_size(window, cx) {
896 entry.size_state.flexible_size_ratio = ratio;
897 } else {
898 entry.size_state.size = size;
899 }
900 entry.panel.size_state_changed(window, cx);
901 size_states_to_persist.push((entry.panel.panel_key(), entry.size_state));
902 }
903
904 let workspace = self.workspace.clone();
905 cx.defer(move |cx| {
906 if let Some(workspace) = workspace.upgrade() {
907 workspace.update(cx, |workspace, cx| {
908 for (panel_key, size_state) in size_states_to_persist {
909 workspace.persist_panel_size_state(panel_key, size_state, cx);
910 }
911 });
912 }
913 });
914
915 cx.notify();
916 }
917
918 pub fn toggle_action(&self) -> Box<dyn Action> {
919 match self.position {
920 DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
921 DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
922 DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
923 }
924 }
925
926 fn dispatch_context() -> KeyContext {
927 let mut dispatch_context = KeyContext::new_with_defaults();
928 dispatch_context.add("Dock");
929
930 dispatch_context
931 }
932
933 pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &Window, cx: &mut App) {
934 let max_size = (max_size - RESIZE_HANDLE_SIZE).abs();
935 for entry in &mut self.panel_entries {
936 if entry.panel.supports_flexible_size(window, cx) {
937 continue;
938 }
939
940 let size = entry
941 .size_state
942 .size
943 .unwrap_or_else(|| entry.panel.default_size(window, cx));
944 if size > max_size {
945 entry.size_state.size = Some(max_size.max(RESIZE_HANDLE_SIZE));
946 }
947 }
948 }
949
950 fn resolved_panel_size(&self, entry: &PanelEntry, window: &Window, cx: &App) -> Pixels {
951 if self.position.axis() == Axis::Horizontal
952 && entry.panel.supports_flexible_size(window, cx)
953 {
954 if let Some(workspace) = self.workspace.upgrade() {
955 let workspace = workspace.read(cx);
956 return resolve_panel_size(
957 entry.size_state,
958 entry.panel.as_ref(),
959 self.position,
960 workspace,
961 window,
962 cx,
963 );
964 }
965 }
966 entry
967 .size_state
968 .size
969 .unwrap_or_else(|| entry.panel.default_size(window, cx))
970 }
971
972 pub(crate) fn load_persisted_size_state(
973 workspace: &Workspace,
974 panel_key: &'static str,
975 cx: &App,
976 ) -> Option<PanelSizeState> {
977 let workspace_id = workspace
978 .database_id()
979 .map(|id| i64::from(id).to_string())
980 .or(workspace.session_id())?;
981 let kvp = KeyValueStore::global(cx);
982 let scope = kvp.scoped(PANEL_SIZE_STATE_KEY);
983 scope
984 .read(&format!("{workspace_id}:{panel_key}"))
985 .log_err()
986 .flatten()
987 .and_then(|json| serde_json::from_str::<PanelSizeState>(&json).log_err())
988 }
989}
990
991pub(crate) fn resolve_panel_size(
992 size_state: PanelSizeState,
993 panel: &dyn PanelHandle,
994 position: DockPosition,
995 workspace: &Workspace,
996 window: &Window,
997 cx: &App,
998) -> Pixels {
999 if position.axis() == Axis::Horizontal && panel.supports_flexible_size(window, cx) {
1000 let ratio = size_state
1001 .flexible_size_ratio
1002 .or_else(|| workspace.default_flexible_dock_ratio(position));
1003
1004 if let Some(ratio) = ratio {
1005 return workspace
1006 .flexible_dock_size(position, ratio, window, cx)
1007 .unwrap_or_else(|| {
1008 size_state
1009 .size
1010 .unwrap_or_else(|| panel.default_size(window, cx))
1011 });
1012 }
1013 }
1014
1015 size_state
1016 .size
1017 .unwrap_or_else(|| panel.default_size(window, cx))
1018}
1019
1020impl Render for Dock {
1021 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1022 let dispatch_context = Self::dispatch_context();
1023 if let Some(entry) = self.visible_entry() {
1024 let size = self.resolved_panel_size(entry, window, cx);
1025
1026 let position = self.position;
1027 let create_resize_handle = || {
1028 let handle = div()
1029 .id("resize-handle")
1030 .on_drag(DraggedDock(position), |dock, _, _, cx| {
1031 cx.stop_propagation();
1032 cx.new(|_| dock.clone())
1033 })
1034 .on_mouse_down(
1035 MouseButton::Left,
1036 cx.listener(|_, _: &MouseDownEvent, _, cx| {
1037 cx.stop_propagation();
1038 }),
1039 )
1040 .on_mouse_up(
1041 MouseButton::Left,
1042 cx.listener(|dock, e: &MouseUpEvent, window, cx| {
1043 if e.click_count == 2 {
1044 dock.resize_active_panel(None, None, window, cx);
1045 dock.workspace
1046 .update(cx, |workspace, cx| {
1047 workspace.serialize_workspace(window, cx);
1048 })
1049 .ok();
1050 cx.stop_propagation();
1051 }
1052 }),
1053 )
1054 .occlude();
1055 match self.position() {
1056 DockPosition::Left => deferred(
1057 handle
1058 .absolute()
1059 .right(-RESIZE_HANDLE_SIZE / 2.)
1060 .top(px(0.))
1061 .h_full()
1062 .w(RESIZE_HANDLE_SIZE)
1063 .cursor_col_resize(),
1064 ),
1065 DockPosition::Bottom => deferred(
1066 handle
1067 .absolute()
1068 .top(-RESIZE_HANDLE_SIZE / 2.)
1069 .left(px(0.))
1070 .w_full()
1071 .h(RESIZE_HANDLE_SIZE)
1072 .cursor_row_resize(),
1073 ),
1074 DockPosition::Right => deferred(
1075 handle
1076 .absolute()
1077 .top(px(0.))
1078 .left(-RESIZE_HANDLE_SIZE / 2.)
1079 .h_full()
1080 .w(RESIZE_HANDLE_SIZE)
1081 .cursor_col_resize(),
1082 ),
1083 }
1084 };
1085
1086 div()
1087 .key_context(dispatch_context)
1088 .track_focus(&self.focus_handle(cx))
1089 .flex()
1090 .bg(cx.theme().colors().panel_background)
1091 .border_color(cx.theme().colors().border)
1092 .overflow_hidden()
1093 .map(|this| match self.position().axis() {
1094 Axis::Horizontal => this.w(size).h_full().flex_row(),
1095 Axis::Vertical => this.h(size).w_full().flex_col(),
1096 })
1097 .map(|this| match self.position() {
1098 DockPosition::Left => this.border_r_1(),
1099 DockPosition::Right => this.border_l_1(),
1100 DockPosition::Bottom => this.border_t_1(),
1101 })
1102 .child(
1103 div()
1104 .map(|this| match self.position().axis() {
1105 Axis::Horizontal => this.min_w(size).h_full(),
1106 Axis::Vertical => this.min_h(size).w_full(),
1107 })
1108 .child(
1109 entry
1110 .panel
1111 .to_any()
1112 .cached(StyleRefinement::default().v_flex().size_full()),
1113 ),
1114 )
1115 .when(self.resizable(cx), |this| {
1116 this.child(create_resize_handle())
1117 })
1118 } else {
1119 div()
1120 .key_context(dispatch_context)
1121 .track_focus(&self.focus_handle(cx))
1122 }
1123 }
1124}
1125
1126impl PanelButtons {
1127 pub fn new(dock: Entity<Dock>, cx: &mut Context<Self>) -> Self {
1128 cx.observe(&dock, |_, _, cx| cx.notify()).detach();
1129 let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| cx.notify());
1130 Self {
1131 dock,
1132 _settings_subscription: settings_subscription,
1133 }
1134 }
1135}
1136
1137impl Render for PanelButtons {
1138 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1139 let dock = self.dock.read(cx);
1140 let active_index = dock.active_panel_index;
1141 let is_open = dock.is_open;
1142 let dock_position = dock.position;
1143
1144 let (menu_anchor, menu_attach) = match dock.position {
1145 DockPosition::Left => (Corner::BottomLeft, Corner::TopLeft),
1146 DockPosition::Bottom | DockPosition::Right => (Corner::BottomRight, Corner::TopRight),
1147 };
1148
1149 let mut buttons: Vec<_> = dock
1150 .panel_entries
1151 .iter()
1152 .enumerate()
1153 .filter_map(|(i, entry)| {
1154 let icon = entry.panel.icon(window, cx)?;
1155 let icon_tooltip = entry
1156 .panel
1157 .icon_tooltip(window, cx)
1158 .ok_or_else(|| {
1159 anyhow::anyhow!("can't render a panel button without an icon tooltip")
1160 })
1161 .log_err()?;
1162 let name = entry.panel.persistent_name();
1163 let panel = entry.panel.clone();
1164
1165 let is_active_button = Some(i) == active_index && is_open;
1166 let (action, tooltip) = if is_active_button {
1167 let action = dock.toggle_action();
1168
1169 let tooltip: SharedString =
1170 format!("Close {} Dock", dock.position.label()).into();
1171
1172 (action, tooltip)
1173 } else {
1174 let action = entry.panel.toggle_action(window, cx);
1175
1176 (action, icon_tooltip.into())
1177 };
1178
1179 let focus_handle = dock.focus_handle(cx);
1180 let icon_label = entry.panel.icon_label(window, cx);
1181
1182 Some(
1183 right_click_menu(name)
1184 .menu(move |window, cx| {
1185 const POSITIONS: [DockPosition; 3] = [
1186 DockPosition::Left,
1187 DockPosition::Right,
1188 DockPosition::Bottom,
1189 ];
1190
1191 ContextMenu::build(window, cx, |mut menu, _, cx| {
1192 for position in POSITIONS {
1193 if position != dock_position
1194 && panel.position_is_valid(position, cx)
1195 {
1196 let panel = panel.clone();
1197 menu = menu.entry(
1198 format!("Dock {}", position.label()),
1199 None,
1200 move |window, cx| {
1201 panel.set_position(position, window, cx);
1202 },
1203 )
1204 }
1205 }
1206 menu
1207 })
1208 })
1209 .anchor(menu_anchor)
1210 .attach(menu_attach)
1211 .trigger(move |is_active, _window, _cx| {
1212 // Include active state in element ID to invalidate the cached
1213 // tooltip when panel state changes (e.g., via keyboard shortcut)
1214 let button = IconButton::new((name, is_active_button as u64), icon)
1215 .icon_size(IconSize::Small)
1216 .toggle_state(is_active_button)
1217 .on_click({
1218 let action = action.boxed_clone();
1219 move |_, window, cx| {
1220 window.focus(&focus_handle, cx);
1221 window.dispatch_action(action.boxed_clone(), cx)
1222 }
1223 })
1224 .when(!is_active, |this| {
1225 this.tooltip(move |_window, cx| {
1226 Tooltip::for_action(tooltip.clone(), &*action, cx)
1227 })
1228 });
1229
1230 div().relative().child(button).when_some(
1231 icon_label
1232 .clone()
1233 .filter(|_| !is_active_button)
1234 .and_then(|label| label.parse::<usize>().ok()),
1235 |this, count| this.child(CountBadge::new(count)),
1236 )
1237 }),
1238 )
1239 })
1240 .collect();
1241
1242 if dock_position == DockPosition::Right {
1243 buttons.reverse();
1244 }
1245
1246 let has_buttons = !buttons.is_empty();
1247
1248 h_flex()
1249 .gap_1()
1250 .when(
1251 has_buttons
1252 && (dock.position == DockPosition::Bottom
1253 || dock.position == DockPosition::Right),
1254 |this| this.child(Divider::vertical().color(DividerColor::Border)),
1255 )
1256 .children(buttons)
1257 .when(has_buttons && dock.position == DockPosition::Left, |this| {
1258 this.child(Divider::vertical().color(DividerColor::Border))
1259 })
1260 }
1261}
1262
1263impl StatusItemView for PanelButtons {
1264 fn set_active_pane_item(
1265 &mut self,
1266 _active_pane_item: Option<&dyn crate::ItemHandle>,
1267 _window: &mut Window,
1268 _cx: &mut Context<Self>,
1269 ) {
1270 // Nothing to do, panel buttons don't depend on the active center item
1271 }
1272}
1273
1274#[cfg(any(test, feature = "test-support"))]
1275pub mod test {
1276 use super::*;
1277 use gpui::{App, Context, Window, actions, div};
1278
1279 pub struct TestPanel {
1280 pub position: DockPosition,
1281 pub zoomed: bool,
1282 pub active: bool,
1283 pub focus_handle: FocusHandle,
1284 pub default_size: Pixels,
1285 pub flexible: bool,
1286 pub activation_priority: u32,
1287 }
1288 actions!(test_only, [ToggleTestPanel]);
1289
1290 impl EventEmitter<PanelEvent> for TestPanel {}
1291
1292 impl TestPanel {
1293 pub fn new(position: DockPosition, activation_priority: u32, cx: &mut App) -> Self {
1294 Self {
1295 position,
1296 zoomed: false,
1297 active: false,
1298 focus_handle: cx.focus_handle(),
1299 default_size: px(300.),
1300 flexible: false,
1301 activation_priority,
1302 }
1303 }
1304
1305 pub fn new_flexible(
1306 position: DockPosition,
1307 activation_priority: u32,
1308 cx: &mut App,
1309 ) -> Self {
1310 Self {
1311 flexible: true,
1312 ..Self::new(position, activation_priority, cx)
1313 }
1314 }
1315 }
1316
1317 impl Render for TestPanel {
1318 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1319 div().id("test").track_focus(&self.focus_handle(cx))
1320 }
1321 }
1322
1323 impl Panel for TestPanel {
1324 fn persistent_name() -> &'static str {
1325 "TestPanel"
1326 }
1327
1328 fn panel_key() -> &'static str {
1329 "TestPanel"
1330 }
1331
1332 fn position(&self, _window: &Window, _: &App) -> super::DockPosition {
1333 self.position
1334 }
1335
1336 fn position_is_valid(&self, _: super::DockPosition) -> bool {
1337 true
1338 }
1339
1340 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1341 self.position = position;
1342 cx.update_global::<SettingsStore, _>(|_, _| {});
1343 }
1344
1345 fn default_size(&self, _window: &Window, _: &App) -> Pixels {
1346 self.default_size
1347 }
1348
1349 fn initial_size_state(&self, _window: &Window, _: &App) -> PanelSizeState {
1350 PanelSizeState {
1351 size: None,
1352 flexible_size_ratio: None,
1353 }
1354 }
1355
1356 fn supports_flexible_size(&self, _window: &Window, _: &App) -> bool {
1357 self.flexible
1358 }
1359
1360 fn icon(&self, _window: &Window, _: &App) -> Option<ui::IconName> {
1361 None
1362 }
1363
1364 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1365 None
1366 }
1367
1368 fn toggle_action(&self) -> Box<dyn Action> {
1369 ToggleTestPanel.boxed_clone()
1370 }
1371
1372 fn is_zoomed(&self, _window: &Window, _: &App) -> bool {
1373 self.zoomed
1374 }
1375
1376 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1377 self.zoomed = zoomed;
1378 }
1379
1380 fn set_active(&mut self, active: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1381 self.active = active;
1382 }
1383
1384 fn activation_priority(&self) -> u32 {
1385 self.activation_priority
1386 }
1387 }
1388
1389 impl Focusable for TestPanel {
1390 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1391 self.focus_handle.clone()
1392 }
1393 }
1394}