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