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