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