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