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