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