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