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