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