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