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