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