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