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