1use crate::persistence::model::DockData;
2use crate::{DraggedDock, Event, ModalLayer, Pane};
3use crate::{Workspace, status_bar::StatusItemView};
4use anyhow::Context as _;
5use client::proto;
6
7use gpui::{
8 Action, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle,
9 Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement,
10 Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div,
11 px,
12};
13use settings::SettingsStore;
14use std::sync::Arc;
15use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex};
16use ui::{prelude::*, right_click_menu};
17use util::ResultExt as _;
18
19pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.);
20
21pub enum PanelEvent {
22 ZoomIn,
23 ZoomOut,
24 Activate,
25 Close,
26}
27
28pub use proto::PanelId;
29
30pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
31 fn persistent_name() -> &'static str;
32 fn panel_key() -> &'static str;
33 fn position(&self, window: &Window, cx: &App) -> DockPosition;
34 fn position_is_valid(&self, position: DockPosition) -> bool;
35 fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context<Self>);
36 fn size(&self, window: &Window, cx: &App) -> Pixels;
37 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>);
38 fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
39 fn icon_tooltip(&self, window: &Window, cx: &App) -> &'static str;
40 fn toggle_action(&self) -> Box<dyn Action>;
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 panel_key(&self) -> &'static str;
65 fn position(&self, window: &Window, cx: &App) -> DockPosition;
66 fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool;
67 fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App);
68 fn is_zoomed(&self, window: &Window, cx: &App) -> bool;
69 fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App);
70 fn set_active(&self, active: bool, window: &mut Window, cx: &mut App);
71 fn remote_id(&self) -> Option<proto::PanelId>;
72 fn pane(&self, cx: &App) -> Option<Entity<Pane>>;
73 fn size(&self, window: &Window, cx: &App) -> Pixels;
74 fn set_size(&self, size: Option<Pixels>, window: &mut Window, cx: &mut App);
75 fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
76 fn icon_tooltip(&self, window: &Window, cx: &App) -> &'static str;
77 fn toggle_action(&self, window: &Window, cx: &App) -> Box<dyn Action>;
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 panel_key(&self) -> &'static str {
112 T::panel_key()
113 }
114
115 fn position(&self, window: &Window, cx: &App) -> DockPosition {
116 self.read(cx).position(window, cx)
117 }
118
119 fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool {
120 self.read(cx).position_is_valid(position)
121 }
122
123 fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App) {
124 self.update(cx, |this, cx| this.set_position(position, window, cx))
125 }
126
127 fn is_zoomed(&self, window: &Window, cx: &App) -> bool {
128 self.read(cx).is_zoomed(window, cx)
129 }
130
131 fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) {
132 self.update(cx, |this, cx| this.set_zoomed(zoomed, window, cx))
133 }
134
135 fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
136 self.update(cx, |this, cx| this.set_active(active, window, cx))
137 }
138
139 fn pane(&self, cx: &App) -> Option<Entity<Pane>> {
140 self.read(cx).pane()
141 }
142
143 fn remote_id(&self) -> Option<PanelId> {
144 T::remote_id()
145 }
146
147 fn size(&self, window: &Window, cx: &App) -> Pixels {
148 self.read(cx).size(window, cx)
149 }
150
151 fn set_size(&self, size: Option<Pixels>, window: &mut Window, cx: &mut App) {
152 self.update(cx, |this, cx| this.set_size(size, window, cx))
153 }
154
155 fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName> {
156 self.read(cx).icon(window, cx)
157 }
158
159 fn icon_tooltip(&self, window: &Window, cx: &App) -> &'static str {
160 self.read(cx).icon_tooltip(window, cx)
161 }
162
163 fn toggle_action(&self, _: &Window, cx: &App) -> Box<dyn Action> {
164 self.read(cx).toggle_action()
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
260impl Dock {
261 pub fn new(
262 position: DockPosition,
263 modal_layer: Entity<ModalLayer>,
264 window: &mut Window,
265 cx: &mut Context<Workspace>,
266 ) -> Entity<Self> {
267 let focus_handle = cx.focus_handle();
268 let workspace = cx.entity();
269 let dock = cx.new(|cx| {
270 let focus_subscription =
271 cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| {
272 if let Some(active_entry) = dock.active_panel_entry() {
273 active_entry.panel.panel_focus_handle(cx).focus(window, cx)
274 }
275 });
276 let zoom_subscription = cx.subscribe(&workspace, |dock, workspace, e: &Event, cx| {
277 if matches!(e, Event::ZoomChanged) {
278 let is_zoomed = workspace.read(cx).zoomed.is_some();
279 dock.zoom_layer_open = is_zoomed;
280 }
281 });
282 Self {
283 position,
284 workspace: workspace.downgrade(),
285 panel_entries: Default::default(),
286 active_panel_index: None,
287 is_open: false,
288 focus_handle: focus_handle.clone(),
289 _subscriptions: [focus_subscription, zoom_subscription],
290 serialized_dock: None,
291 zoom_layer_open: false,
292 modal_layer,
293 }
294 });
295
296 cx.on_focus_in(&focus_handle, window, {
297 let dock = dock.downgrade();
298 move |workspace, window, cx| {
299 let Some(dock) = dock.upgrade() else {
300 return;
301 };
302 let Some(panel) = dock.read(cx).active_panel() else {
303 return;
304 };
305 if panel.is_zoomed(window, cx) {
306 workspace.zoomed = Some(panel.to_any().downgrade());
307 workspace.zoomed_position = Some(position);
308 } else {
309 workspace.zoomed = None;
310 workspace.zoomed_position = None;
311 }
312 cx.emit(Event::ZoomChanged);
313 workspace.dismiss_zoomed_items_to_reveal(Some(position), window, cx);
314 workspace.update_active_view_for_followers(window, cx)
315 }
316 })
317 .detach();
318
319 cx.observe_in(&dock, window, move |workspace, dock, window, cx| {
320 if dock.read(cx).is_open()
321 && let Some(panel) = dock.read(cx).active_panel()
322 && panel.is_zoomed(window, cx)
323 {
324 workspace.zoomed = Some(panel.to_any().downgrade());
325 workspace.zoomed_position = Some(position);
326 cx.emit(Event::ZoomChanged);
327 return;
328 }
329 if workspace.zoomed_position == Some(position) {
330 workspace.zoomed = None;
331 workspace.zoomed_position = None;
332 cx.emit(Event::ZoomChanged);
333 }
334 })
335 .detach();
336
337 dock
338 }
339
340 pub fn position(&self) -> DockPosition {
341 self.position
342 }
343
344 pub fn is_open(&self) -> bool {
345 self.is_open
346 }
347
348 fn resizable(&self, cx: &App) -> bool {
349 !(self.zoom_layer_open || self.modal_layer.read(cx).has_active_modal())
350 }
351
352 pub fn panel<T: Panel>(&self) -> Option<Entity<T>> {
353 self.panel_entries
354 .iter()
355 .find_map(|entry| entry.panel.to_any().downcast().ok())
356 }
357
358 pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
359 self.panel_entries
360 .iter()
361 .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
362 }
363
364 pub fn panel_index_for_persistent_name(&self, ui_name: &str, _cx: &App) -> Option<usize> {
365 self.panel_entries
366 .iter()
367 .position(|entry| entry.panel.persistent_name() == ui_name)
368 }
369
370 pub fn panel_index_for_proto_id(&self, panel_id: PanelId) -> Option<usize> {
371 self.panel_entries
372 .iter()
373 .position(|entry| entry.panel.remote_id() == Some(panel_id))
374 }
375
376 pub fn panel_for_id(&self, panel_id: EntityId) -> Option<&Arc<dyn PanelHandle>> {
377 self.panel_entries
378 .iter()
379 .find(|entry| entry.panel.panel_id() == panel_id)
380 .map(|entry| &entry.panel)
381 }
382
383 pub fn first_enabled_panel_idx(&mut self, cx: &mut Context<Self>) -> anyhow::Result<usize> {
384 self.panel_entries
385 .iter()
386 .position(|entry| entry.panel.enabled(cx))
387 .with_context(|| {
388 format!(
389 "Couldn't find any enabled panel for the {} dock.",
390 self.position.label()
391 )
392 })
393 }
394
395 fn active_panel_entry(&self) -> Option<&PanelEntry> {
396 self.active_panel_index
397 .and_then(|index| self.panel_entries.get(index))
398 }
399
400 pub fn active_panel_index(&self) -> Option<usize> {
401 self.active_panel_index
402 }
403
404 pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
405 if open != self.is_open {
406 self.is_open = open;
407 if let Some(active_panel) = self.active_panel_entry() {
408 active_panel.panel.set_active(open, window, cx);
409 }
410
411 cx.notify();
412 }
413 }
414
415 pub fn set_panel_zoomed(
416 &mut self,
417 panel: &AnyView,
418 zoomed: bool,
419 window: &mut Window,
420 cx: &mut Context<Self>,
421 ) {
422 for entry in &mut self.panel_entries {
423 if entry.panel.panel_id() == panel.entity_id() {
424 if zoomed != entry.panel.is_zoomed(window, cx) {
425 entry.panel.set_zoomed(zoomed, window, cx);
426 }
427 } else if entry.panel.is_zoomed(window, cx) {
428 entry.panel.set_zoomed(false, window, cx);
429 }
430 }
431
432 self.workspace
433 .update(cx, |workspace, cx| {
434 workspace.serialize_workspace(window, cx);
435 })
436 .ok();
437 cx.notify();
438 }
439
440 pub fn zoom_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
441 for entry in &mut self.panel_entries {
442 if entry.panel.is_zoomed(window, cx) {
443 entry.panel.set_zoomed(false, window, cx);
444 }
445 }
446 }
447
448 pub(crate) fn add_panel<T: Panel>(
449 &mut self,
450 panel: Entity<T>,
451 workspace: WeakEntity<Workspace>,
452 window: &mut Window,
453 cx: &mut Context<Self>,
454 ) -> usize {
455 let subscriptions = [
456 cx.observe(&panel, |_, _, cx| cx.notify()),
457 cx.observe_global_in::<SettingsStore>(window, {
458 let workspace = workspace.clone();
459 let panel = panel.clone();
460
461 move |this, window, cx| {
462 let new_position = panel.read(cx).position(window, cx);
463 if new_position == this.position {
464 return;
465 }
466
467 let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
468 if panel.is_zoomed(window, cx) {
469 workspace.zoomed_position = Some(new_position);
470 }
471 match new_position {
472 DockPosition::Left => &workspace.left_dock,
473 DockPosition::Bottom => &workspace.bottom_dock,
474 DockPosition::Right => &workspace.right_dock,
475 }
476 .clone()
477 }) else {
478 return;
479 };
480
481 let was_visible = this.is_open()
482 && this.visible_panel().is_some_and(|active_panel| {
483 active_panel.panel_id() == Entity::entity_id(&panel)
484 });
485
486 this.remove_panel(&panel, window, cx);
487
488 new_dock.update(cx, |new_dock, cx| {
489 new_dock.remove_panel(&panel, window, cx);
490 });
491
492 new_dock.update(cx, |new_dock, cx| {
493 let index =
494 new_dock.add_panel(panel.clone(), workspace.clone(), window, cx);
495 if was_visible {
496 new_dock.set_open(true, window, cx);
497 new_dock.activate_panel(index, window, cx);
498 }
499 });
500
501 workspace
502 .update(cx, |workspace, cx| {
503 workspace.serialize_workspace(window, cx);
504 })
505 .ok();
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), 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), 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) => {
569 if cfg!(debug_assertions) {
570 panic!(
571 "Panels `{}` and `{}` have the same activation priority. Each panel must have a unique priority so the status bar order is deterministic.",
572 T::panel_key(),
573 self.panel_entries[ix].panel.panel_key()
574 );
575 }
576 ix
577 }
578 Err(ix) => ix,
579 };
580 if let Some(active_index) = self.active_panel_index.as_mut()
581 && *active_index >= index
582 {
583 *active_index += 1;
584 }
585 self.panel_entries.insert(
586 index,
587 PanelEntry {
588 panel: Arc::new(panel.clone()),
589 _subscriptions: subscriptions,
590 },
591 );
592
593 self.restore_state(window, cx);
594
595 if panel.read(cx).starts_open(window, cx) {
596 self.activate_panel(index, window, cx);
597 self.set_open(true, window, cx);
598 }
599
600 cx.notify();
601 index
602 }
603
604 pub fn restore_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
605 if let Some(serialized) = self.serialized_dock.clone() {
606 if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible)
607 && let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx)
608 {
609 self.activate_panel(idx, window, cx);
610 }
611
612 if serialized.zoom
613 && let Some(panel) = self.active_panel()
614 {
615 panel.set_zoomed(true, window, cx)
616 }
617 self.set_open(serialized.visible, window, cx);
618 return true;
619 }
620 false
621 }
622
623 pub fn remove_panel<T: Panel>(
624 &mut self,
625 panel: &Entity<T>,
626 window: &mut Window,
627 cx: &mut Context<Self>,
628 ) -> bool {
629 if let Some(panel_ix) = self
630 .panel_entries
631 .iter()
632 .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
633 {
634 if let Some(active_panel_index) = self.active_panel_index.as_mut() {
635 match panel_ix.cmp(active_panel_index) {
636 std::cmp::Ordering::Less => {
637 *active_panel_index -= 1;
638 }
639 std::cmp::Ordering::Equal => {
640 self.active_panel_index = None;
641 self.set_open(false, window, cx);
642 }
643 std::cmp::Ordering::Greater => {}
644 }
645 }
646
647 self.panel_entries.remove(panel_ix);
648 cx.notify();
649
650 true
651 } else {
652 false
653 }
654 }
655
656 pub fn panels_len(&self) -> usize {
657 self.panel_entries.len()
658 }
659
660 pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
661 if Some(panel_ix) != self.active_panel_index {
662 if let Some(active_panel) = self.active_panel_entry() {
663 active_panel.panel.set_active(false, window, cx);
664 }
665
666 self.active_panel_index = Some(panel_ix);
667 if let Some(active_panel) = self.active_panel_entry() {
668 active_panel.panel.set_active(true, window, cx);
669 }
670
671 cx.notify();
672 }
673 }
674
675 pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
676 let entry = self.visible_entry()?;
677 Some(&entry.panel)
678 }
679
680 pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
681 let panel_entry = self.active_panel_entry()?;
682 Some(&panel_entry.panel)
683 }
684
685 fn visible_entry(&self) -> Option<&PanelEntry> {
686 if self.is_open {
687 self.active_panel_entry()
688 } else {
689 None
690 }
691 }
692
693 pub fn zoomed_panel(&self, window: &Window, cx: &App) -> Option<Arc<dyn PanelHandle>> {
694 let entry = self.visible_entry()?;
695 if entry.panel.is_zoomed(window, cx) {
696 Some(entry.panel.clone())
697 } else {
698 None
699 }
700 }
701
702 pub fn panel_size(&self, panel: &dyn PanelHandle, window: &Window, cx: &App) -> Option<Pixels> {
703 self.panel_entries
704 .iter()
705 .find(|entry| entry.panel.panel_id() == panel.panel_id())
706 .map(|entry| entry.panel.size(window, cx))
707 }
708
709 pub fn active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
710 if self.is_open {
711 self.active_panel_entry()
712 .map(|entry| entry.panel.size(window, cx))
713 } else {
714 None
715 }
716 }
717
718 pub fn resize_active_panel(
719 &mut self,
720 size: Option<Pixels>,
721 window: &mut Window,
722 cx: &mut Context<Self>,
723 ) {
724 if let Some(entry) = self.active_panel_entry() {
725 let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
726
727 entry.panel.set_size(size, window, cx);
728 cx.notify();
729 }
730 }
731
732 pub fn resize_all_panels(
733 &mut self,
734 size: Option<Pixels>,
735 window: &mut Window,
736 cx: &mut Context<Self>,
737 ) {
738 for entry in &mut self.panel_entries {
739 let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
740 entry.panel.set_size(size, window, cx);
741 }
742 cx.notify();
743 }
744
745 pub fn toggle_action(&self) -> Box<dyn Action> {
746 match self.position {
747 DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
748 DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
749 DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
750 }
751 }
752
753 fn dispatch_context() -> KeyContext {
754 let mut dispatch_context = KeyContext::new_with_defaults();
755 dispatch_context.add("Dock");
756
757 dispatch_context
758 }
759
760 pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &mut Window, cx: &mut App) {
761 let max_size = (max_size - RESIZE_HANDLE_SIZE).abs();
762 for panel in self.panel_entries.iter().map(|entry| &entry.panel) {
763 if panel.size(window, cx) > max_size {
764 panel.set_size(Some(max_size.max(RESIZE_HANDLE_SIZE)), window, cx);
765 }
766 }
767 }
768}
769
770impl Render for Dock {
771 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
772 let dispatch_context = Self::dispatch_context();
773 if let Some(entry) = self.visible_entry() {
774 let size = entry.panel.size(window, cx);
775
776 let position = self.position;
777 let create_resize_handle = || {
778 let handle = div()
779 .id("resize-handle")
780 .on_drag(DraggedDock(position), |dock, _, _, cx| {
781 cx.stop_propagation();
782 cx.new(|_| dock.clone())
783 })
784 .on_mouse_down(
785 MouseButton::Left,
786 cx.listener(|_, _: &MouseDownEvent, _, cx| {
787 cx.stop_propagation();
788 }),
789 )
790 .on_mouse_up(
791 MouseButton::Left,
792 cx.listener(|dock, e: &MouseUpEvent, window, cx| {
793 if e.click_count == 2 {
794 dock.resize_active_panel(None, window, cx);
795 dock.workspace
796 .update(cx, |workspace, cx| {
797 workspace.serialize_workspace(window, cx);
798 })
799 .ok();
800 cx.stop_propagation();
801 }
802 }),
803 )
804 .occlude();
805 match self.position() {
806 DockPosition::Left => deferred(
807 handle
808 .absolute()
809 .right(-RESIZE_HANDLE_SIZE / 2.)
810 .top(px(0.))
811 .h_full()
812 .w(RESIZE_HANDLE_SIZE)
813 .cursor_col_resize(),
814 ),
815 DockPosition::Bottom => deferred(
816 handle
817 .absolute()
818 .top(-RESIZE_HANDLE_SIZE / 2.)
819 .left(px(0.))
820 .w_full()
821 .h(RESIZE_HANDLE_SIZE)
822 .cursor_row_resize(),
823 ),
824 DockPosition::Right => deferred(
825 handle
826 .absolute()
827 .top(px(0.))
828 .left(-RESIZE_HANDLE_SIZE / 2.)
829 .h_full()
830 .w(RESIZE_HANDLE_SIZE)
831 .cursor_col_resize(),
832 ),
833 }
834 };
835
836 div()
837 .key_context(dispatch_context)
838 .track_focus(&self.focus_handle(cx))
839 .flex()
840 .bg(cx.theme().colors().panel_background)
841 .border_color(cx.theme().colors().border)
842 .overflow_hidden()
843 .map(|this| match self.position().axis() {
844 Axis::Horizontal => this.w(size).h_full().flex_row(),
845 Axis::Vertical => this.h(size).w_full().flex_col(),
846 })
847 .map(|this| match self.position() {
848 DockPosition::Left => this.border_r_1(),
849 DockPosition::Right => this.border_l_1(),
850 DockPosition::Bottom => this.border_t_1(),
851 })
852 .child(
853 div()
854 .map(|this| match self.position().axis() {
855 Axis::Horizontal => this.min_w(size).h_full(),
856 Axis::Vertical => this.min_h(size).w_full(),
857 })
858 .child(
859 entry
860 .panel
861 .to_any()
862 .cached(StyleRefinement::default().v_flex().size_full()),
863 ),
864 )
865 .when(self.resizable(cx), |this| {
866 this.child(create_resize_handle())
867 })
868 } else {
869 div()
870 .key_context(dispatch_context)
871 .track_focus(&self.focus_handle(cx))
872 }
873 }
874}
875
876pub struct PanelButtons {
877 dock: Entity<Dock>,
878 _settings_subscription: Subscription,
879}
880
881impl PanelButtons {
882 pub fn new(dock: Entity<Dock>, cx: &mut Context<Self>) -> Self {
883 cx.observe(&dock, |_, _, cx| cx.notify()).detach();
884 let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| cx.notify());
885 Self {
886 dock,
887 _settings_subscription: settings_subscription,
888 }
889 }
890}
891
892impl Render for PanelButtons {
893 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
894 let dock = self.dock.read(cx);
895 let active_index = dock.active_panel_index;
896 let is_open = dock.is_open;
897 let dock_position = dock.position;
898
899 let (menu_anchor, menu_attach) = match dock.position {
900 DockPosition::Left => (Corner::BottomLeft, Corner::TopLeft),
901 DockPosition::Bottom | DockPosition::Right => (Corner::BottomRight, Corner::TopRight),
902 };
903
904 let buttons: Vec<_> = dock
905 .panel_entries
906 .iter()
907 .enumerate()
908 .filter_map(|(i, entry)| {
909 let icon = entry.panel.icon(window, cx)?;
910 let icon_tooltip = entry.panel.icon_tooltip(window, cx);
911 let name = entry.panel.persistent_name();
912 let panel = entry.panel.clone();
913
914 let is_active_button = Some(i) == active_index && is_open;
915 let (action, tooltip) = if is_active_button {
916 let action = dock.toggle_action();
917
918 let tooltip: SharedString =
919 format!("Close {} Dock", dock.position.label()).into();
920
921 (action, tooltip)
922 } else {
923 let action = entry.panel.toggle_action(window, cx);
924
925 (action, icon_tooltip.into())
926 };
927
928 let focus_handle = dock.focus_handle(cx);
929
930 Some(
931 right_click_menu(name)
932 .menu(move |window, cx| {
933 const POSITIONS: [DockPosition; 3] = [
934 DockPosition::Left,
935 DockPosition::Right,
936 DockPosition::Bottom,
937 ];
938
939 ContextMenu::build(window, cx, |mut menu, _, cx| {
940 for position in POSITIONS {
941 if position != dock_position
942 && panel.position_is_valid(position, cx)
943 {
944 let panel = panel.clone();
945 menu = menu.entry(
946 format!("Dock {}", position.label()),
947 None,
948 move |window, cx| {
949 panel.set_position(position, window, cx);
950 },
951 )
952 }
953 }
954 menu
955 })
956 })
957 .anchor(menu_anchor)
958 .attach(menu_attach)
959 .trigger(move |is_active, _window, _cx| {
960 // Include active state in element ID to invalidate the cached
961 // tooltip when panel state changes (e.g., via keyboard shortcut)
962 IconButton::new((name, is_active_button as u64), icon)
963 .icon_size(IconSize::Small)
964 .toggle_state(is_active_button)
965 .on_click({
966 let action = action.boxed_clone();
967 move |_, window, cx| {
968 window.focus(&focus_handle, cx);
969 window.dispatch_action(action.boxed_clone(), cx)
970 }
971 })
972 .when(!is_active, |this| {
973 this.tooltip(move |_window, cx| {
974 Tooltip::for_action(tooltip.clone(), &*action, cx)
975 })
976 })
977 }),
978 )
979 })
980 .collect();
981
982 let has_buttons = !buttons.is_empty();
983
984 h_flex()
985 .gap_1()
986 .when(
987 has_buttons && dock.position == DockPosition::Bottom,
988 |this| this.child(Divider::vertical().color(DividerColor::Border)),
989 )
990 .children(buttons)
991 .when(has_buttons && dock.position == DockPosition::Left, |this| {
992 this.child(Divider::vertical().color(DividerColor::Border))
993 })
994 }
995}
996
997impl StatusItemView for PanelButtons {
998 fn set_active_pane_item(
999 &mut self,
1000 _active_pane_item: Option<&dyn crate::ItemHandle>,
1001 _window: &mut Window,
1002 _cx: &mut Context<Self>,
1003 ) {
1004 // Nothing to do, panel buttons don't depend on the active center item
1005 }
1006}
1007
1008#[cfg(any(test, feature = "test-support"))]
1009pub mod test {
1010 use super::*;
1011 use gpui::{App, Context, Window, actions, div};
1012
1013 pub struct TestPanel {
1014 pub position: DockPosition,
1015 pub zoomed: bool,
1016 pub active: bool,
1017 pub focus_handle: FocusHandle,
1018 pub size: Pixels,
1019 pub activation_priority: u32,
1020 }
1021 actions!(test_only, [ToggleTestPanel]);
1022
1023 impl EventEmitter<PanelEvent> for TestPanel {}
1024
1025 impl TestPanel {
1026 pub fn new(position: DockPosition, activation_priority: u32, cx: &mut App) -> Self {
1027 Self {
1028 position,
1029 zoomed: false,
1030 active: false,
1031 focus_handle: cx.focus_handle(),
1032 size: px(300.),
1033 activation_priority,
1034 }
1035 }
1036 }
1037
1038 impl Render for TestPanel {
1039 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1040 div().id("test").track_focus(&self.focus_handle(cx))
1041 }
1042 }
1043
1044 impl Panel for TestPanel {
1045 fn persistent_name() -> &'static str {
1046 "TestPanel"
1047 }
1048
1049 fn panel_key() -> &'static str {
1050 "TestPanel"
1051 }
1052
1053 fn position(&self, _window: &Window, _: &App) -> super::DockPosition {
1054 self.position
1055 }
1056
1057 fn position_is_valid(&self, _: super::DockPosition) -> bool {
1058 true
1059 }
1060
1061 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1062 self.position = position;
1063 cx.update_global::<SettingsStore, _>(|_, _| {});
1064 }
1065
1066 fn size(&self, _window: &Window, _: &App) -> Pixels {
1067 self.size
1068 }
1069
1070 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _: &mut Context<Self>) {
1071 self.size = size.unwrap_or(px(300.));
1072 }
1073
1074 fn icon(&self, _window: &Window, _: &App) -> Option<ui::IconName> {
1075 None
1076 }
1077
1078 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> &'static str {
1079 "Test Panel"
1080 }
1081
1082 fn toggle_action(&self) -> Box<dyn Action> {
1083 ToggleTestPanel.boxed_clone()
1084 }
1085
1086 fn is_zoomed(&self, _window: &Window, _: &App) -> bool {
1087 self.zoomed
1088 }
1089
1090 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1091 self.zoomed = zoomed;
1092 }
1093
1094 fn set_active(&mut self, active: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1095 self.active = active;
1096 }
1097
1098 fn activation_priority(&self) -> u32 {
1099 self.activation_priority
1100 }
1101 }
1102
1103 impl Focusable for TestPanel {
1104 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1105 self.focus_handle.clone()
1106 }
1107 }
1108}