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