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