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