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