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