1use crate::persistence::model::DockData;
2use crate::{DraggedDock, Event, ModalLayer, Pane};
3use crate::{Workspace, status_bar::StatusItemView};
4use anyhow::Context as _;
5use client::proto;
6use db::kvp::KeyValueStore;
7
8use gpui::{
9 Action, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle,
10 Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement,
11 Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div,
12 px,
13};
14use serde::{Deserialize, Serialize};
15use settings::SettingsStore;
16use std::sync::Arc;
17use ui::{
18 ContextMenu, CountBadge, Divider, DividerColor, IconButton, Tooltip, prelude::*,
19 right_click_menu,
20};
21use util::ResultExt as _;
22
23pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.);
24
25pub enum PanelEvent {
26 ZoomIn,
27 ZoomOut,
28 Activate,
29 Close,
30}
31
32pub use proto::PanelId;
33
34pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
35 fn persistent_name() -> &'static str;
36 fn panel_key() -> &'static str;
37 fn position(&self, window: &Window, cx: &App) -> DockPosition;
38 fn position_is_valid(&self, position: DockPosition) -> bool;
39 fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context<Self>);
40 fn default_size(&self, window: &Window, cx: &App) -> Pixels;
41 fn initial_size_state(&self, _window: &Window, _cx: &App) -> PanelSizeState {
42 PanelSizeState::default()
43 }
44 fn size_state_changed(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
45 fn supports_flexible_size(&self) -> bool {
46 false
47 }
48 fn has_flexible_size(&self, _window: &Window, _cx: &App) -> bool {
49 false
50 }
51 fn set_flexible_size(
52 &mut self,
53 _flexible: bool,
54 _window: &mut Window,
55 _cx: &mut Context<Self>,
56 ) {
57 }
58 fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
59 fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
60 fn toggle_action(&self) -> Box<dyn Action>;
61 fn icon_label(&self, _window: &Window, _: &App) -> Option<String> {
62 None
63 }
64 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
65 false
66 }
67 fn starts_open(&self, _window: &Window, _cx: &App) -> bool {
68 false
69 }
70 fn set_zoomed(&mut self, _zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
71 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
72 fn pane(&self) -> Option<Entity<Pane>> {
73 None
74 }
75 fn remote_id() -> Option<proto::PanelId> {
76 None
77 }
78 fn activation_priority(&self) -> u32;
79 fn enabled(&self, _cx: &App) -> bool {
80 true
81 }
82}
83
84pub trait PanelHandle: Send + Sync {
85 fn panel_id(&self) -> EntityId;
86 fn persistent_name(&self) -> &'static str;
87 fn panel_key(&self) -> &'static str;
88 fn position(&self, window: &Window, cx: &App) -> DockPosition;
89 fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool;
90 fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App);
91 fn is_zoomed(&self, window: &Window, cx: &App) -> bool;
92 fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App);
93 fn set_active(&self, active: bool, window: &mut Window, cx: &mut App);
94 fn remote_id(&self) -> Option<proto::PanelId>;
95 fn pane(&self, cx: &App) -> Option<Entity<Pane>>;
96 fn default_size(&self, window: &Window, cx: &App) -> Pixels;
97 fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState;
98 fn size_state_changed(&self, window: &mut Window, cx: &mut App);
99 fn supports_flexible_size(&self, cx: &App) -> bool;
100 fn has_flexible_size(&self, window: &Window, cx: &App) -> bool;
101 fn set_flexible_size(&self, flexible: bool, window: &mut Window, cx: &mut App);
102 fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
103 fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
104 fn toggle_action(&self, window: &Window, cx: &App) -> Box<dyn Action>;
105 fn icon_label(&self, window: &Window, cx: &App) -> Option<String>;
106 fn panel_focus_handle(&self, cx: &App) -> FocusHandle;
107 fn to_any(&self) -> AnyView;
108 fn activation_priority(&self, cx: &App) -> u32;
109 fn enabled(&self, cx: &App) -> bool;
110 fn move_to_next_position(&self, window: &mut Window, cx: &mut App) {
111 let current_position = self.position(window, cx);
112 let next_position = [
113 DockPosition::Left,
114 DockPosition::Bottom,
115 DockPosition::Right,
116 ]
117 .into_iter()
118 .filter(|position| self.position_is_valid(*position, cx))
119 .skip_while(|valid_position| *valid_position != current_position)
120 .nth(1)
121 .unwrap_or(DockPosition::Left);
122
123 self.set_position(next_position, window, cx);
124 }
125}
126
127impl<T> PanelHandle for Entity<T>
128where
129 T: Panel,
130{
131 fn panel_id(&self) -> EntityId {
132 Entity::entity_id(self)
133 }
134
135 fn persistent_name(&self) -> &'static str {
136 T::persistent_name()
137 }
138
139 fn panel_key(&self) -> &'static str {
140 T::panel_key()
141 }
142
143 fn position(&self, window: &Window, cx: &App) -> DockPosition {
144 self.read(cx).position(window, cx)
145 }
146
147 fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool {
148 self.read(cx).position_is_valid(position)
149 }
150
151 fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App) {
152 self.update(cx, |this, cx| this.set_position(position, window, cx))
153 }
154
155 fn is_zoomed(&self, window: &Window, cx: &App) -> bool {
156 self.read(cx).is_zoomed(window, cx)
157 }
158
159 fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) {
160 self.update(cx, |this, cx| this.set_zoomed(zoomed, window, cx))
161 }
162
163 fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
164 self.update(cx, |this, cx| this.set_active(active, window, cx))
165 }
166
167 fn pane(&self, cx: &App) -> Option<Entity<Pane>> {
168 self.read(cx).pane()
169 }
170
171 fn remote_id(&self) -> Option<PanelId> {
172 T::remote_id()
173 }
174
175 fn default_size(&self, window: &Window, cx: &App) -> Pixels {
176 self.read(cx).default_size(window, cx)
177 }
178
179 fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState {
180 self.read(cx).initial_size_state(window, cx)
181 }
182
183 fn size_state_changed(&self, window: &mut Window, cx: &mut App) {
184 self.update(cx, |this, cx| this.size_state_changed(window, cx))
185 }
186
187 fn supports_flexible_size(&self, cx: &App) -> bool {
188 self.read(cx).supports_flexible_size()
189 }
190
191 fn has_flexible_size(&self, window: &Window, cx: &App) -> bool {
192 self.read(cx).has_flexible_size(window, cx)
193 }
194
195 fn set_flexible_size(&self, flexible: bool, window: &mut Window, cx: &mut App) {
196 self.update(cx, |this, cx| this.set_flexible_size(flexible, window, cx))
197 }
198
199 fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName> {
200 self.read(cx).icon(window, cx)
201 }
202
203 fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str> {
204 self.read(cx).icon_tooltip(window, cx)
205 }
206
207 fn toggle_action(&self, _: &Window, cx: &App) -> Box<dyn Action> {
208 self.read(cx).toggle_action()
209 }
210
211 fn icon_label(&self, window: &Window, cx: &App) -> Option<String> {
212 self.read(cx).icon_label(window, cx)
213 }
214
215 fn to_any(&self) -> AnyView {
216 self.clone().into()
217 }
218
219 fn panel_focus_handle(&self, cx: &App) -> FocusHandle {
220 self.read(cx).focus_handle(cx)
221 }
222
223 fn activation_priority(&self, cx: &App) -> u32 {
224 self.read(cx).activation_priority()
225 }
226
227 fn enabled(&self, cx: &App) -> bool {
228 self.read(cx).enabled(cx)
229 }
230}
231
232impl From<&dyn PanelHandle> for AnyView {
233 fn from(val: &dyn PanelHandle) -> Self {
234 val.to_any()
235 }
236}
237
238/// A container with a fixed [`DockPosition`] adjacent to a certain widown edge.
239/// Can contain multiple panels and show/hide itself with all contents.
240pub struct Dock {
241 position: DockPosition,
242 panel_entries: Vec<PanelEntry>,
243 workspace: WeakEntity<Workspace>,
244 is_open: bool,
245 active_panel_index: Option<usize>,
246 focus_handle: FocusHandle,
247 pub(crate) serialized_dock: Option<DockData>,
248 zoom_layer_open: bool,
249 modal_layer: Entity<ModalLayer>,
250 _subscriptions: [Subscription; 2],
251}
252
253impl Focusable for Dock {
254 fn focus_handle(&self, _: &App) -> FocusHandle {
255 self.focus_handle.clone()
256 }
257}
258
259#[derive(Copy, Clone, Debug, PartialEq, Eq)]
260pub enum DockPosition {
261 Left,
262 Bottom,
263 Right,
264}
265
266impl From<settings::DockPosition> for DockPosition {
267 fn from(value: settings::DockPosition) -> Self {
268 match value {
269 settings::DockPosition::Left => Self::Left,
270 settings::DockPosition::Bottom => Self::Bottom,
271 settings::DockPosition::Right => Self::Right,
272 }
273 }
274}
275
276impl Into<settings::DockPosition> for DockPosition {
277 fn into(self) -> settings::DockPosition {
278 match self {
279 Self::Left => settings::DockPosition::Left,
280 Self::Bottom => settings::DockPosition::Bottom,
281 Self::Right => settings::DockPosition::Right,
282 }
283 }
284}
285
286impl DockPosition {
287 fn label(&self) -> &'static str {
288 match self {
289 Self::Left => "Left",
290 Self::Bottom => "Bottom",
291 Self::Right => "Right",
292 }
293 }
294
295 pub fn axis(&self) -> Axis {
296 match self {
297 Self::Left | Self::Right => Axis::Horizontal,
298 Self::Bottom => Axis::Vertical,
299 }
300 }
301}
302
303#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
304pub struct PanelSizeState {
305 pub size: Option<Pixels>,
306 #[serde(default)]
307 pub flex: Option<f32>,
308}
309
310struct PanelEntry {
311 panel: Arc<dyn PanelHandle>,
312 size_state: PanelSizeState,
313 _subscriptions: [Subscription; 3],
314}
315
316pub struct PanelButtons {
317 dock: Entity<Dock>,
318 _settings_subscription: Subscription,
319}
320
321pub(crate) const PANEL_SIZE_STATE_KEY: &str = "dock_panel_size";
322
323fn resize_panel_entry(
324 position: DockPosition,
325 entry: &mut PanelEntry,
326 size: Option<Pixels>,
327 flex: Option<f32>,
328 window: &mut Window,
329 cx: &mut App,
330) -> (&'static str, PanelSizeState) {
331 let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
332 let use_flex = entry.panel.has_flexible_size(window, cx) && position.axis() == Axis::Horizontal;
333 if use_flex {
334 entry.size_state.flex = flex;
335 } else {
336 entry.size_state.size = size;
337 }
338 entry.panel.size_state_changed(window, cx);
339 (entry.panel.panel_key(), entry.size_state)
340}
341
342impl Dock {
343 pub fn new(
344 position: DockPosition,
345 modal_layer: Entity<ModalLayer>,
346 window: &mut Window,
347 cx: &mut Context<Workspace>,
348 ) -> Entity<Self> {
349 let focus_handle = cx.focus_handle();
350 let workspace = cx.entity();
351 let dock = cx.new(|cx| {
352 let focus_subscription =
353 cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| {
354 if let Some(active_entry) = dock.active_panel_entry() {
355 active_entry.panel.panel_focus_handle(cx).focus(window, cx)
356 }
357 });
358 let zoom_subscription = cx.subscribe(&workspace, |dock, workspace, e: &Event, cx| {
359 if matches!(e, Event::ZoomChanged) {
360 let is_zoomed = workspace.read(cx).zoomed.is_some();
361 dock.zoom_layer_open = is_zoomed;
362 }
363 });
364 Self {
365 position,
366 workspace: workspace.downgrade(),
367 panel_entries: Default::default(),
368 active_panel_index: None,
369 is_open: false,
370 focus_handle: focus_handle.clone(),
371 _subscriptions: [focus_subscription, zoom_subscription],
372 serialized_dock: None,
373 zoom_layer_open: false,
374 modal_layer,
375 }
376 });
377
378 cx.on_focus_in(&focus_handle, window, {
379 let dock = dock.downgrade();
380 move |workspace, window, cx| {
381 let Some(dock) = dock.upgrade() else {
382 return;
383 };
384 let Some(panel) = dock.read(cx).active_panel() else {
385 return;
386 };
387 if panel.is_zoomed(window, cx) {
388 workspace.zoomed = Some(panel.to_any().downgrade());
389 workspace.zoomed_position = Some(position);
390 } else {
391 workspace.zoomed = None;
392 workspace.zoomed_position = None;
393 }
394 cx.emit(Event::ZoomChanged);
395 workspace.dismiss_zoomed_items_to_reveal(Some(position), window, cx);
396 workspace.update_active_view_for_followers(window, cx)
397 }
398 })
399 .detach();
400
401 cx.observe_in(&dock, window, move |workspace, dock, window, cx| {
402 if dock.read(cx).is_open()
403 && let Some(panel) = dock.read(cx).active_panel()
404 && panel.is_zoomed(window, cx)
405 {
406 workspace.zoomed = Some(panel.to_any().downgrade());
407 workspace.zoomed_position = Some(position);
408 cx.emit(Event::ZoomChanged);
409 return;
410 }
411 if workspace.zoomed_position == Some(position) {
412 workspace.zoomed = None;
413 workspace.zoomed_position = None;
414 cx.emit(Event::ZoomChanged);
415 }
416 })
417 .detach();
418
419 dock
420 }
421
422 pub fn position(&self) -> DockPosition {
423 self.position
424 }
425
426 pub fn is_open(&self) -> bool {
427 self.is_open
428 }
429
430 fn resizable(&self, cx: &App) -> bool {
431 !(self.zoom_layer_open || self.modal_layer.read(cx).has_active_modal())
432 }
433
434 pub fn panel<T: Panel>(&self) -> Option<Entity<T>> {
435 self.panel_entries
436 .iter()
437 .find_map(|entry| entry.panel.to_any().downcast().ok())
438 }
439
440 pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
441 self.panel_entries
442 .iter()
443 .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
444 }
445
446 pub fn panel_index_for_persistent_name(&self, ui_name: &str, _cx: &App) -> Option<usize> {
447 self.panel_entries
448 .iter()
449 .position(|entry| entry.panel.persistent_name() == ui_name)
450 }
451
452 pub fn panel_index_for_proto_id(&self, panel_id: PanelId) -> Option<usize> {
453 self.panel_entries
454 .iter()
455 .position(|entry| entry.panel.remote_id() == Some(panel_id))
456 }
457
458 pub fn panel_for_id(&self, panel_id: EntityId) -> Option<&Arc<dyn PanelHandle>> {
459 self.panel_entries
460 .iter()
461 .find(|entry| entry.panel.panel_id() == panel_id)
462 .map(|entry| &entry.panel)
463 }
464
465 pub fn first_enabled_panel_idx(&mut self, cx: &mut Context<Self>) -> anyhow::Result<usize> {
466 self.panel_entries
467 .iter()
468 .position(|entry| entry.panel.enabled(cx))
469 .with_context(|| {
470 format!(
471 "Couldn't find any enabled panel for the {} dock.",
472 self.position.label()
473 )
474 })
475 }
476
477 fn active_panel_entry(&self) -> Option<&PanelEntry> {
478 self.active_panel_index
479 .and_then(|index| self.panel_entries.get(index))
480 }
481
482 pub fn active_panel_index(&self) -> Option<usize> {
483 self.active_panel_index
484 }
485
486 pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
487 if open != self.is_open {
488 self.is_open = open;
489 if let Some(active_panel) = self.active_panel_entry() {
490 active_panel.panel.set_active(open, window, cx);
491 }
492
493 cx.notify();
494 }
495 }
496
497 pub fn set_panel_zoomed(
498 &mut self,
499 panel: &AnyView,
500 zoomed: bool,
501 window: &mut Window,
502 cx: &mut Context<Self>,
503 ) {
504 for entry in &mut self.panel_entries {
505 if entry.panel.panel_id() == panel.entity_id() {
506 if zoomed != entry.panel.is_zoomed(window, cx) {
507 entry.panel.set_zoomed(zoomed, window, cx);
508 }
509 } else if entry.panel.is_zoomed(window, cx) {
510 entry.panel.set_zoomed(false, window, cx);
511 }
512 }
513
514 self.workspace
515 .update(cx, |workspace, cx| {
516 workspace.serialize_workspace(window, cx);
517 })
518 .ok();
519 cx.notify();
520 }
521
522 pub fn zoom_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
523 for entry in &mut self.panel_entries {
524 if entry.panel.is_zoomed(window, cx) {
525 entry.panel.set_zoomed(false, window, cx);
526 }
527 }
528 }
529
530 pub(crate) fn add_panel<T: Panel>(
531 &mut self,
532 panel: Entity<T>,
533 workspace: WeakEntity<Workspace>,
534 window: &mut Window,
535 cx: &mut Context<Self>,
536 ) -> usize {
537 let subscriptions = [
538 cx.observe(&panel, |_, _, cx| cx.notify()),
539 cx.observe_global_in::<SettingsStore>(window, {
540 let workspace = workspace.clone();
541 let panel = panel.clone();
542
543 move |this, window, cx| {
544 let new_position = panel.read(cx).position(window, cx);
545 if new_position == this.position {
546 return;
547 }
548
549 let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
550 if panel.is_zoomed(window, cx) {
551 workspace.zoomed_position = Some(new_position);
552 }
553 match new_position {
554 DockPosition::Left => &workspace.left_dock,
555 DockPosition::Bottom => &workspace.bottom_dock,
556 DockPosition::Right => &workspace.right_dock,
557 }
558 .clone()
559 }) else {
560 return;
561 };
562
563 let panel_id = Entity::entity_id(&panel);
564 let was_visible = this.is_open()
565 && this
566 .visible_panel()
567 .is_some_and(|active_panel| active_panel.panel_id() == panel_id);
568 let size_state = this
569 .panel_entries
570 .iter()
571 .find(|entry| entry.panel.panel_id() == panel_id)
572 .map(|entry| entry.size_state)
573 .unwrap_or_default();
574
575 let previous_axis = this.position.axis();
576 let next_axis = new_position.axis();
577 let size_state = if previous_axis == next_axis {
578 size_state
579 } else {
580 PanelSizeState::default()
581 };
582
583 if !this.remove_panel(&panel, window, cx) {
584 // Panel was already moved from this dock
585 return;
586 }
587
588 new_dock.update(cx, |new_dock, cx| {
589 let index =
590 new_dock.add_panel(panel.clone(), workspace.clone(), window, cx);
591 if let Some(added_panel) = new_dock.panel_for_id(panel_id).cloned() {
592 new_dock.set_panel_size_state(added_panel.as_ref(), size_state, cx);
593 }
594 if was_visible {
595 new_dock.set_open(true, window, cx);
596 new_dock.activate_panel(index, window, cx);
597 }
598 });
599
600 workspace
601 .update(cx, |workspace, cx| {
602 workspace.serialize_workspace(window, cx);
603 })
604 .ok();
605 }
606 }),
607 cx.subscribe_in(
608 &panel,
609 window,
610 move |this, panel, event, window, cx| match event {
611 PanelEvent::ZoomIn => {
612 this.set_panel_zoomed(&panel.to_any(), true, window, cx);
613 if !PanelHandle::panel_focus_handle(panel, cx).contains_focused(window, cx)
614 {
615 window.focus(&panel.focus_handle(cx), cx);
616 }
617 workspace
618 .update(cx, |workspace, cx| {
619 workspace.zoomed = Some(panel.downgrade().into());
620 workspace.zoomed_position =
621 Some(panel.read(cx).position(window, cx));
622 cx.emit(Event::ZoomChanged);
623 })
624 .ok();
625 }
626 PanelEvent::ZoomOut => {
627 this.set_panel_zoomed(&panel.to_any(), false, window, cx);
628 workspace
629 .update(cx, |workspace, cx| {
630 if workspace.zoomed_position == Some(this.position) {
631 workspace.zoomed = None;
632 workspace.zoomed_position = None;
633 cx.emit(Event::ZoomChanged);
634 }
635 cx.notify();
636 })
637 .ok();
638 }
639 PanelEvent::Activate => {
640 if let Some(ix) = this
641 .panel_entries
642 .iter()
643 .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
644 {
645 this.set_open(true, window, cx);
646 this.activate_panel(ix, window, cx);
647 window.focus(&panel.read(cx).focus_handle(cx), cx);
648 }
649 }
650 PanelEvent::Close => {
651 if this
652 .visible_panel()
653 .is_some_and(|p| p.panel_id() == Entity::entity_id(panel))
654 {
655 this.set_open(false, window, cx);
656 }
657 }
658 },
659 ),
660 ];
661
662 let index = match self
663 .panel_entries
664 .binary_search_by_key(&panel.read(cx).activation_priority(), |entry| {
665 entry.panel.activation_priority(cx)
666 }) {
667 Ok(ix) => {
668 if cfg!(debug_assertions) {
669 panic!(
670 "Panels `{}` and `{}` have the same activation priority. Each panel must have a unique priority so the status bar order is deterministic.",
671 T::panel_key(),
672 self.panel_entries[ix].panel.panel_key()
673 );
674 }
675 ix
676 }
677 Err(ix) => ix,
678 };
679 if let Some(active_index) = self.active_panel_index.as_mut()
680 && *active_index >= index
681 {
682 *active_index += 1;
683 }
684 let size_state = panel.read(cx).initial_size_state(window, cx);
685
686 self.panel_entries.insert(
687 index,
688 PanelEntry {
689 panel: Arc::new(panel.clone()),
690 size_state,
691 _subscriptions: subscriptions,
692 },
693 );
694
695 self.restore_state(window, cx);
696
697 if panel.read(cx).starts_open(window, cx) {
698 self.activate_panel(index, window, cx);
699 self.set_open(true, window, cx);
700 }
701
702 cx.notify();
703 index
704 }
705
706 pub fn restore_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
707 if let Some(serialized) = self.serialized_dock.clone() {
708 if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible)
709 && let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx)
710 {
711 self.activate_panel(idx, window, cx);
712 }
713
714 if serialized.zoom
715 && let Some(panel) = self.active_panel()
716 {
717 panel.set_zoomed(true, window, cx)
718 }
719 self.set_open(serialized.visible, window, cx);
720 return true;
721 }
722 false
723 }
724
725 pub fn remove_panel<T: Panel>(
726 &mut self,
727 panel: &Entity<T>,
728 window: &mut Window,
729 cx: &mut Context<Self>,
730 ) -> bool {
731 if let Some(panel_ix) = self
732 .panel_entries
733 .iter()
734 .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
735 {
736 if let Some(active_panel_index) = self.active_panel_index.as_mut() {
737 match panel_ix.cmp(active_panel_index) {
738 std::cmp::Ordering::Less => {
739 *active_panel_index -= 1;
740 }
741 std::cmp::Ordering::Equal => {
742 self.active_panel_index = None;
743 self.set_open(false, window, cx);
744 }
745 std::cmp::Ordering::Greater => {}
746 }
747 }
748
749 self.panel_entries.remove(panel_ix);
750 cx.notify();
751
752 true
753 } else {
754 false
755 }
756 }
757
758 pub fn panels_len(&self) -> usize {
759 self.panel_entries.len()
760 }
761
762 pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
763 if Some(panel_ix) != self.active_panel_index {
764 if let Some(active_panel) = self.active_panel_entry() {
765 active_panel.panel.set_active(false, window, cx);
766 }
767
768 self.active_panel_index = Some(panel_ix);
769 if let Some(active_panel) = self.active_panel_entry() {
770 active_panel.panel.set_active(true, window, cx);
771 }
772
773 cx.notify();
774 }
775 }
776
777 pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
778 let entry = self.visible_entry()?;
779 Some(&entry.panel)
780 }
781
782 pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
783 let panel_entry = self.active_panel_entry()?;
784 Some(&panel_entry.panel)
785 }
786
787 fn visible_entry(&self) -> Option<&PanelEntry> {
788 if self.is_open {
789 self.active_panel_entry()
790 } else {
791 None
792 }
793 }
794
795 pub fn zoomed_panel(&self, window: &Window, cx: &App) -> Option<Arc<dyn PanelHandle>> {
796 let entry = self.visible_entry()?;
797 if entry.panel.is_zoomed(window, cx) {
798 Some(entry.panel.clone())
799 } else {
800 None
801 }
802 }
803
804 pub fn active_panel_size(&self) -> Option<PanelSizeState> {
805 if self.is_open {
806 self.active_panel_entry().map(|entry| entry.size_state)
807 } else {
808 None
809 }
810 }
811
812 pub fn stored_panel_size(
813 &self,
814 panel: &dyn PanelHandle,
815 window: &Window,
816 cx: &App,
817 ) -> Option<Pixels> {
818 self.panel_entries
819 .iter()
820 .find(|entry| entry.panel.panel_id() == panel.panel_id())
821 .map(|entry| {
822 entry
823 .size_state
824 .size
825 .unwrap_or_else(|| entry.panel.default_size(window, cx))
826 })
827 }
828
829 pub fn stored_panel_size_state(&self, panel: &dyn PanelHandle) -> Option<PanelSizeState> {
830 self.panel_entries
831 .iter()
832 .find(|entry| entry.panel.panel_id() == panel.panel_id())
833 .map(|entry| entry.size_state)
834 }
835
836 pub fn stored_active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
837 if self.is_open {
838 self.active_panel_entry().map(|entry| {
839 entry
840 .size_state
841 .size
842 .unwrap_or_else(|| entry.panel.default_size(window, cx))
843 })
844 } else {
845 None
846 }
847 }
848
849 pub fn set_panel_size_state(
850 &mut self,
851 panel: &dyn PanelHandle,
852 size_state: PanelSizeState,
853 cx: &mut Context<Self>,
854 ) -> bool {
855 if let Some(entry) = self
856 .panel_entries
857 .iter_mut()
858 .find(|entry| entry.panel.panel_id() == panel.panel_id())
859 {
860 entry.size_state = size_state;
861 cx.notify();
862 true
863 } else {
864 false
865 }
866 }
867
868 pub fn toggle_panel_flexible_size(
869 &mut self,
870 panel: &dyn PanelHandle,
871 current_size: Option<Pixels>,
872 current_flex: Option<f32>,
873 window: &mut Window,
874 cx: &mut Context<Self>,
875 ) {
876 let Some(entry) = self
877 .panel_entries
878 .iter_mut()
879 .find(|entry| entry.panel.panel_id() == panel.panel_id())
880 else {
881 return;
882 };
883 let currently_flexible = entry.panel.has_flexible_size(window, cx);
884 if currently_flexible {
885 entry.size_state.size = current_size;
886 } else {
887 entry.size_state.flex = current_flex;
888 }
889 let panel_key = entry.panel.panel_key();
890 let size_state = entry.size_state;
891 let workspace = self.workspace.clone();
892 entry
893 .panel
894 .set_flexible_size(!currently_flexible, window, cx);
895 entry.panel.size_state_changed(window, cx);
896 cx.defer(move |cx| {
897 if let Some(workspace) = workspace.upgrade() {
898 workspace.update(cx, |workspace, cx| {
899 workspace.persist_panel_size_state(panel_key, size_state, cx);
900 });
901 }
902 });
903 cx.notify();
904 }
905
906 pub fn resize_active_panel(
907 &mut self,
908 size: Option<Pixels>,
909 flex: Option<f32>,
910 window: &mut Window,
911 cx: &mut Context<Self>,
912 ) {
913 if let Some(index) = self.active_panel_index
914 && let Some(entry) = self.panel_entries.get_mut(index)
915 {
916 let (panel_key, size_state) =
917 resize_panel_entry(self.position, entry, size, flex, window, cx);
918
919 let workspace = self.workspace.clone();
920 cx.defer(move |cx| {
921 if let Some(workspace) = workspace.upgrade() {
922 workspace.update(cx, |workspace, cx| {
923 workspace.persist_panel_size_state(panel_key, size_state, cx);
924 });
925 }
926 });
927 cx.notify();
928 }
929 }
930
931 pub fn resize_all_panels(
932 &mut self,
933 size: Option<Pixels>,
934 flex: Option<f32>,
935 window: &mut Window,
936 cx: &mut Context<Self>,
937 ) {
938 let size_states_to_persist: Vec<_> = self
939 .panel_entries
940 .iter_mut()
941 .map(|entry| resize_panel_entry(self.position, entry, size, flex, window, cx))
942 .collect();
943
944 let workspace = self.workspace.clone();
945 cx.defer(move |cx| {
946 if let Some(workspace) = workspace.upgrade() {
947 workspace.update(cx, |workspace, cx| {
948 for (panel_key, size_state) in size_states_to_persist {
949 workspace.persist_panel_size_state(panel_key, size_state, cx);
950 }
951 });
952 }
953 });
954
955 cx.notify();
956 }
957
958 pub fn toggle_action(&self) -> Box<dyn Action> {
959 match self.position {
960 DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
961 DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
962 DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
963 }
964 }
965
966 fn dispatch_context() -> KeyContext {
967 let mut dispatch_context = KeyContext::new_with_defaults();
968 dispatch_context.add("Dock");
969
970 dispatch_context
971 }
972
973 pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &Window, cx: &mut App) {
974 let max_size = (max_size - RESIZE_HANDLE_SIZE).abs();
975 for entry in &mut self.panel_entries {
976 let use_flexible = entry.panel.has_flexible_size(window, cx);
977 if use_flexible {
978 continue;
979 }
980
981 let size = entry
982 .size_state
983 .size
984 .unwrap_or_else(|| entry.panel.default_size(window, cx));
985 if size > max_size {
986 entry.size_state.size = Some(max_size.max(RESIZE_HANDLE_SIZE));
987 }
988 }
989 }
990
991 pub(crate) fn load_persisted_size_state(
992 workspace: &Workspace,
993 panel_key: &'static str,
994 cx: &App,
995 ) -> Option<PanelSizeState> {
996 let workspace_id = workspace
997 .database_id()
998 .map(|id| i64::from(id).to_string())
999 .or(workspace.session_id())?;
1000 let kvp = KeyValueStore::global(cx);
1001 let scope = kvp.scoped(PANEL_SIZE_STATE_KEY);
1002 scope
1003 .read(&format!("{workspace_id}:{panel_key}"))
1004 .log_err()
1005 .flatten()
1006 .and_then(|json| serde_json::from_str::<PanelSizeState>(&json).log_err())
1007 }
1008}
1009
1010impl Render for Dock {
1011 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1012 let dispatch_context = Self::dispatch_context();
1013 if let Some(entry) = self.visible_entry() {
1014 let position = self.position;
1015 let create_resize_handle = || {
1016 let handle = div()
1017 .id("resize-handle")
1018 .on_drag(DraggedDock(position), |dock, _, _, cx| {
1019 cx.stop_propagation();
1020 cx.new(|_| dock.clone())
1021 })
1022 .on_mouse_down(
1023 MouseButton::Left,
1024 cx.listener(|_, _: &MouseDownEvent, _, cx| {
1025 cx.stop_propagation();
1026 }),
1027 )
1028 .on_mouse_up(
1029 MouseButton::Left,
1030 cx.listener(|dock, e: &MouseUpEvent, window, cx| {
1031 if e.click_count == 2 {
1032 dock.resize_active_panel(None, None, window, cx);
1033 dock.workspace
1034 .update(cx, |workspace, cx| {
1035 workspace.serialize_workspace(window, cx);
1036 })
1037 .ok();
1038 cx.stop_propagation();
1039 }
1040 }),
1041 )
1042 .occlude();
1043 match self.position() {
1044 DockPosition::Left => deferred(
1045 handle
1046 .absolute()
1047 .right(-RESIZE_HANDLE_SIZE / 2.)
1048 .top(px(0.))
1049 .h_full()
1050 .w(RESIZE_HANDLE_SIZE)
1051 .cursor_col_resize(),
1052 ),
1053 DockPosition::Bottom => deferred(
1054 handle
1055 .absolute()
1056 .top(-RESIZE_HANDLE_SIZE / 2.)
1057 .left(px(0.))
1058 .w_full()
1059 .h(RESIZE_HANDLE_SIZE)
1060 .cursor_row_resize(),
1061 ),
1062 DockPosition::Right => deferred(
1063 handle
1064 .absolute()
1065 .top(px(0.))
1066 .left(-RESIZE_HANDLE_SIZE / 2.)
1067 .h_full()
1068 .w(RESIZE_HANDLE_SIZE)
1069 .cursor_col_resize(),
1070 ),
1071 }
1072 };
1073
1074 div()
1075 .key_context(dispatch_context)
1076 .track_focus(&self.focus_handle(cx))
1077 .flex()
1078 .bg(cx.theme().colors().panel_background)
1079 .border_color(cx.theme().colors().border)
1080 .overflow_hidden()
1081 .map(|this| match self.position().axis() {
1082 // Width and height are always set on the workspace wrapper in
1083 // render_dock, so fill whatever space the wrapper provides.
1084 Axis::Horizontal => this.w_full().h_full().flex_row(),
1085 Axis::Vertical => this.h_full().w_full().flex_col(),
1086 })
1087 .map(|this| match self.position() {
1088 DockPosition::Left => this.border_r_1(),
1089 DockPosition::Right => this.border_l_1(),
1090 DockPosition::Bottom => this.border_t_1(),
1091 })
1092 .child(
1093 div()
1094 .map(|this| match self.position().axis() {
1095 Axis::Horizontal => this.w_full().h_full(),
1096 Axis::Vertical => this.h_full().w_full(),
1097 })
1098 .child(
1099 entry
1100 .panel
1101 .to_any()
1102 .cached(StyleRefinement::default().v_flex().size_full()),
1103 ),
1104 )
1105 .when(self.resizable(cx), |this| {
1106 this.child(create_resize_handle())
1107 })
1108 } else {
1109 div()
1110 .key_context(dispatch_context)
1111 .track_focus(&self.focus_handle(cx))
1112 }
1113 }
1114}
1115
1116impl PanelButtons {
1117 pub fn new(dock: Entity<Dock>, cx: &mut Context<Self>) -> Self {
1118 cx.observe(&dock, |_, _, cx| cx.notify()).detach();
1119 let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| cx.notify());
1120 Self {
1121 dock,
1122 _settings_subscription: settings_subscription,
1123 }
1124 }
1125}
1126
1127impl Render for PanelButtons {
1128 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1129 let dock = self.dock.read(cx);
1130 let active_index = dock.active_panel_index;
1131 let is_open = dock.is_open;
1132 let dock_position = dock.position;
1133
1134 let (menu_anchor, menu_attach) = match dock.position {
1135 DockPosition::Left => (Corner::BottomLeft, Corner::TopLeft),
1136 DockPosition::Bottom | DockPosition::Right => (Corner::BottomRight, Corner::TopRight),
1137 };
1138
1139 let dock_entity = self.dock.clone();
1140 let workspace = dock.workspace.clone();
1141 let mut buttons: Vec<_> = dock
1142 .panel_entries
1143 .iter()
1144 .enumerate()
1145 .filter_map(|(i, entry)| {
1146 let icon = entry.panel.icon(window, cx)?;
1147 let icon_tooltip = entry
1148 .panel
1149 .icon_tooltip(window, cx)
1150 .ok_or_else(|| {
1151 anyhow::anyhow!("can't render a panel button without an icon tooltip")
1152 })
1153 .log_err()?;
1154 let name = entry.panel.persistent_name();
1155 let panel = entry.panel.clone();
1156 let supports_flexible = panel.supports_flexible_size(cx);
1157 let currently_flexible = panel.has_flexible_size(window, cx);
1158 let dock_for_menu = dock_entity.clone();
1159 let workspace_for_menu = workspace.clone();
1160
1161 let is_active_button = Some(i) == active_index && is_open;
1162 let (action, tooltip) = if is_active_button {
1163 let action = dock.toggle_action();
1164
1165 let tooltip: SharedString =
1166 format!("Close {} Dock", dock.position.label()).into();
1167
1168 (action, tooltip)
1169 } else {
1170 let action = entry.panel.toggle_action(window, cx);
1171
1172 (action, icon_tooltip.into())
1173 };
1174
1175 let focus_handle = dock.focus_handle(cx);
1176 let icon_label = entry.panel.icon_label(window, cx);
1177
1178 Some(
1179 right_click_menu(name)
1180 .menu(move |window, cx| {
1181 const POSITIONS: [DockPosition; 3] = [
1182 DockPosition::Left,
1183 DockPosition::Right,
1184 DockPosition::Bottom,
1185 ];
1186
1187 ContextMenu::build(window, cx, |mut menu, _, cx| {
1188 let mut has_position_entries = false;
1189 for position in POSITIONS {
1190 if panel.position_is_valid(position, cx) {
1191 let is_current = position == dock_position;
1192 let panel = panel.clone();
1193 menu = menu.toggleable_entry(
1194 format!("Dock {}", position.label()),
1195 is_current,
1196 IconPosition::Start,
1197 None,
1198 move |window, cx| {
1199 if !is_current {
1200 panel.set_position(position, window, cx);
1201 }
1202 },
1203 );
1204 has_position_entries = true;
1205 }
1206 }
1207 if supports_flexible {
1208 if has_position_entries {
1209 menu = menu.separator();
1210 }
1211 let panel_for_flex = panel.clone();
1212 let dock_for_flex = dock_for_menu.clone();
1213 let workspace_for_flex = workspace_for_menu.clone();
1214 menu = menu.toggleable_entry(
1215 "Flex Width",
1216 currently_flexible,
1217 IconPosition::Start,
1218 None,
1219 move |window, cx| {
1220 if !currently_flexible {
1221 if let Some(ws) = workspace_for_flex.upgrade() {
1222 ws.update(cx, |workspace, cx| {
1223 workspace.toggle_dock_panel_flexible_size(
1224 &dock_for_flex,
1225 panel_for_flex.as_ref(),
1226 window,
1227 cx,
1228 );
1229 });
1230 }
1231 }
1232 },
1233 );
1234 let panel_for_fixed = panel.clone();
1235 let dock_for_fixed = dock_for_menu.clone();
1236 let workspace_for_fixed = workspace_for_menu.clone();
1237 menu = menu.toggleable_entry(
1238 "Fixed Width",
1239 !currently_flexible,
1240 IconPosition::Start,
1241 None,
1242 move |window, cx| {
1243 if currently_flexible {
1244 if let Some(ws) = workspace_for_fixed.upgrade() {
1245 ws.update(cx, |workspace, cx| {
1246 workspace.toggle_dock_panel_flexible_size(
1247 &dock_for_fixed,
1248 panel_for_fixed.as_ref(),
1249 window,
1250 cx,
1251 );
1252 });
1253 }
1254 }
1255 },
1256 );
1257 }
1258 menu
1259 })
1260 })
1261 .anchor(menu_anchor)
1262 .attach(menu_attach)
1263 .trigger(move |is_active, _window, _cx| {
1264 // Include active state in element ID to invalidate the cached
1265 // tooltip when panel state changes (e.g., via keyboard shortcut)
1266 let button = IconButton::new((name, is_active_button as u64), icon)
1267 .icon_size(IconSize::Small)
1268 .toggle_state(is_active_button)
1269 .on_click({
1270 let action = action.boxed_clone();
1271 move |_, window, cx| {
1272 window.focus(&focus_handle, cx);
1273 window.dispatch_action(action.boxed_clone(), cx)
1274 }
1275 })
1276 .when(!is_active, |this| {
1277 this.tooltip(move |_window, cx| {
1278 Tooltip::for_action(tooltip.clone(), &*action, cx)
1279 })
1280 });
1281
1282 div().relative().child(button).when_some(
1283 icon_label
1284 .clone()
1285 .filter(|_| !is_active_button)
1286 .and_then(|label| label.parse::<usize>().ok()),
1287 |this, count| this.child(CountBadge::new(count)),
1288 )
1289 }),
1290 )
1291 })
1292 .collect();
1293
1294 if dock_position == DockPosition::Right {
1295 buttons.reverse();
1296 }
1297
1298 let has_buttons = !buttons.is_empty();
1299
1300 h_flex()
1301 .gap_1()
1302 .when(
1303 has_buttons
1304 && (dock.position == DockPosition::Bottom
1305 || dock.position == DockPosition::Right),
1306 |this| this.child(Divider::vertical().color(DividerColor::Border)),
1307 )
1308 .children(buttons)
1309 .when(has_buttons && dock.position == DockPosition::Left, |this| {
1310 this.child(Divider::vertical().color(DividerColor::Border))
1311 })
1312 }
1313}
1314
1315impl StatusItemView for PanelButtons {
1316 fn set_active_pane_item(
1317 &mut self,
1318 _active_pane_item: Option<&dyn crate::ItemHandle>,
1319 _window: &mut Window,
1320 _cx: &mut Context<Self>,
1321 ) {
1322 // Nothing to do, panel buttons don't depend on the active center item
1323 }
1324}
1325
1326#[cfg(any(test, feature = "test-support"))]
1327pub mod test {
1328 use super::*;
1329 use gpui::{App, Context, Window, actions, div};
1330
1331 pub struct TestPanel {
1332 pub position: DockPosition,
1333 pub zoomed: bool,
1334 pub active: bool,
1335 pub focus_handle: FocusHandle,
1336 pub default_size: Pixels,
1337 pub flexible: bool,
1338 pub activation_priority: u32,
1339 }
1340 actions!(test_only, [ToggleTestPanel]);
1341
1342 impl EventEmitter<PanelEvent> for TestPanel {}
1343
1344 impl TestPanel {
1345 pub fn new(position: DockPosition, activation_priority: u32, cx: &mut App) -> Self {
1346 Self {
1347 position,
1348 zoomed: false,
1349 active: false,
1350 focus_handle: cx.focus_handle(),
1351 default_size: px(300.),
1352 flexible: false,
1353 activation_priority,
1354 }
1355 }
1356
1357 pub fn new_flexible(
1358 position: DockPosition,
1359 activation_priority: u32,
1360 cx: &mut App,
1361 ) -> Self {
1362 Self {
1363 flexible: true,
1364 ..Self::new(position, activation_priority, cx)
1365 }
1366 }
1367 }
1368
1369 impl Render for TestPanel {
1370 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1371 div().id("test").track_focus(&self.focus_handle(cx))
1372 }
1373 }
1374
1375 impl Panel for TestPanel {
1376 fn persistent_name() -> &'static str {
1377 "TestPanel"
1378 }
1379
1380 fn panel_key() -> &'static str {
1381 "TestPanel"
1382 }
1383
1384 fn position(&self, _window: &Window, _: &App) -> super::DockPosition {
1385 self.position
1386 }
1387
1388 fn position_is_valid(&self, _: super::DockPosition) -> bool {
1389 true
1390 }
1391
1392 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1393 self.position = position;
1394 cx.update_global::<SettingsStore, _>(|_, _| {});
1395 }
1396
1397 fn default_size(&self, _window: &Window, _: &App) -> Pixels {
1398 self.default_size
1399 }
1400
1401 fn initial_size_state(&self, _window: &Window, _: &App) -> PanelSizeState {
1402 PanelSizeState {
1403 size: None,
1404 flex: None,
1405 }
1406 }
1407
1408 fn supports_flexible_size(&self) -> bool {
1409 self.flexible
1410 }
1411
1412 fn has_flexible_size(&self, _window: &Window, _: &App) -> bool {
1413 self.flexible
1414 }
1415
1416 fn set_flexible_size(
1417 &mut self,
1418 flexible: bool,
1419 _window: &mut Window,
1420 _cx: &mut Context<Self>,
1421 ) {
1422 self.flexible = flexible;
1423 }
1424
1425 fn icon(&self, _window: &Window, _: &App) -> Option<ui::IconName> {
1426 None
1427 }
1428
1429 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1430 None
1431 }
1432
1433 fn toggle_action(&self) -> Box<dyn Action> {
1434 ToggleTestPanel.boxed_clone()
1435 }
1436
1437 fn is_zoomed(&self, _window: &Window, _: &App) -> bool {
1438 self.zoomed
1439 }
1440
1441 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1442 self.zoomed = zoomed;
1443 }
1444
1445 fn set_active(&mut self, active: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1446 self.active = active;
1447 }
1448
1449 fn activation_priority(&self) -> u32 {
1450 self.activation_priority
1451 }
1452 }
1453
1454 impl Focusable for TestPanel {
1455 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1456 self.focus_handle.clone()
1457 }
1458 }
1459}