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