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