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