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