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