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