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