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 let name = panel.persistent_name().to_string();
435
436 self.panel_entries.push(PanelEntry {
437 panel: Arc::new(panel.clone()),
438 _subscriptions: subscriptions,
439 });
440 if let Some(serialized) = self.serialized_dock.clone() {
441 if serialized.active_panel == Some(name) {
442 self.activate_panel(self.panel_entries.len() - 1, cx);
443 if serialized.visible {
444 self.set_open(true, cx);
445 }
446 if serialized.zoom {
447 if let Some(panel) = self.active_panel() {
448 panel.set_zoomed(true, cx)
449 };
450 }
451 }
452 } else if panel.read(cx).starts_open(cx) {
453 self.activate_panel(self.panel_entries.len() - 1, cx);
454 self.set_open(true, cx);
455 }
456
457 cx.notify()
458 }
459
460 pub fn remove_panel<T: Panel>(&mut self, panel: &View<T>, cx: &mut ViewContext<Self>) {
461 if let Some(panel_ix) = self
462 .panel_entries
463 .iter()
464 .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
465 {
466 if panel_ix == self.active_panel_index {
467 self.active_panel_index = 0;
468 self.set_open(false, cx);
469 } else if panel_ix < self.active_panel_index {
470 self.active_panel_index -= 1;
471 }
472 self.panel_entries.remove(panel_ix);
473 cx.notify();
474 }
475 }
476
477 pub fn panels_len(&self) -> usize {
478 self.panel_entries.len()
479 }
480
481 pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
482 if panel_ix != self.active_panel_index {
483 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
484 active_panel.panel.set_active(false, cx);
485 }
486
487 self.active_panel_index = panel_ix;
488 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
489 active_panel.panel.set_active(true, cx);
490 }
491
492 cx.notify();
493 }
494 }
495
496 pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
497 let entry = self.visible_entry()?;
498 Some(&entry.panel)
499 }
500
501 pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
502 Some(&self.panel_entries.get(self.active_panel_index)?.panel)
503 }
504
505 fn visible_entry(&self) -> Option<&PanelEntry> {
506 if self.is_open {
507 self.panel_entries.get(self.active_panel_index)
508 } else {
509 None
510 }
511 }
512
513 pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Arc<dyn PanelHandle>> {
514 let entry = self.visible_entry()?;
515 if entry.panel.is_zoomed(cx) {
516 Some(entry.panel.clone())
517 } else {
518 None
519 }
520 }
521
522 pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<Pixels> {
523 self.panel_entries
524 .iter()
525 .find(|entry| entry.panel.panel_id() == panel.panel_id())
526 .map(|entry| entry.panel.size(cx))
527 }
528
529 pub fn active_panel_size(&self, cx: &WindowContext) -> Option<Pixels> {
530 if self.is_open {
531 self.panel_entries
532 .get(self.active_panel_index)
533 .map(|entry| entry.panel.size(cx))
534 } else {
535 None
536 }
537 }
538
539 pub fn resize_active_panel(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
540 if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
541 let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
542 entry.panel.set_size(size, cx);
543 cx.notify();
544 }
545 }
546
547 pub fn toggle_action(&self) -> Box<dyn Action> {
548 match self.position {
549 DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
550 DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
551 DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
552 }
553 }
554
555 fn dispatch_context() -> KeyContext {
556 let mut dispatch_context = KeyContext::new_with_defaults();
557 dispatch_context.add("Dock");
558
559 dispatch_context
560 }
561}
562
563impl Render for Dock {
564 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
565 let dispatch_context = Self::dispatch_context();
566 if let Some(entry) = self.visible_entry() {
567 let size = entry.panel.size(cx);
568
569 let position = self.position;
570 let create_resize_handle = || {
571 let handle = div()
572 .id("resize-handle")
573 .on_drag(DraggedDock(position), |dock, cx| {
574 cx.stop_propagation();
575 cx.new_view(|_| dock.clone())
576 })
577 .on_mouse_down(
578 MouseButton::Left,
579 cx.listener(|_, _: &MouseDownEvent, cx| {
580 cx.stop_propagation();
581 }),
582 )
583 .on_mouse_up(
584 MouseButton::Left,
585 cx.listener(|v, e: &MouseUpEvent, cx| {
586 if e.click_count == 2 {
587 v.resize_active_panel(None, cx);
588 cx.stop_propagation();
589 }
590 }),
591 )
592 .occlude();
593 match self.position() {
594 DockPosition::Left => deferred(
595 handle
596 .absolute()
597 .right(-RESIZE_HANDLE_SIZE / 2.)
598 .top(px(0.))
599 .h_full()
600 .w(RESIZE_HANDLE_SIZE)
601 .cursor_col_resize(),
602 ),
603 DockPosition::Bottom => deferred(
604 handle
605 .absolute()
606 .top(-RESIZE_HANDLE_SIZE / 2.)
607 .left(px(0.))
608 .w_full()
609 .h(RESIZE_HANDLE_SIZE)
610 .cursor_row_resize(),
611 ),
612 DockPosition::Right => deferred(
613 handle
614 .absolute()
615 .top(px(0.))
616 .left(-RESIZE_HANDLE_SIZE / 2.)
617 .h_full()
618 .w(RESIZE_HANDLE_SIZE)
619 .cursor_col_resize(),
620 ),
621 }
622 };
623
624 div()
625 .key_context(dispatch_context)
626 .track_focus(&self.focus_handle)
627 .flex()
628 .bg(cx.theme().colors().panel_background)
629 .border_color(cx.theme().colors().border)
630 .overflow_hidden()
631 .map(|this| match self.position().axis() {
632 Axis::Horizontal => this.w(size).h_full().flex_row(),
633 Axis::Vertical => this.h(size).w_full().flex_col(),
634 })
635 .map(|this| match self.position() {
636 DockPosition::Left => this.border_r_1(),
637 DockPosition::Right => this.border_l_1(),
638 DockPosition::Bottom => this.border_t_1(),
639 })
640 .child(
641 div()
642 .map(|this| match self.position().axis() {
643 Axis::Horizontal => this.min_w(size).h_full(),
644 Axis::Vertical => this.min_h(size).w_full(),
645 })
646 .child(
647 entry
648 .panel
649 .to_any()
650 .cached(StyleRefinement::default().v_flex().size_full()),
651 ),
652 )
653 .when(self.resizeable, |this| this.child(create_resize_handle()))
654 } else {
655 div()
656 .key_context(dispatch_context)
657 .track_focus(&self.focus_handle)
658 }
659 }
660}
661
662impl PanelButtons {
663 pub fn new(dock: View<Dock>, cx: &mut ViewContext<Self>) -> Self {
664 cx.observe(&dock, |_, _, cx| cx.notify()).detach();
665 Self { dock }
666 }
667}
668
669impl Render for PanelButtons {
670 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
671 let dock = self.dock.read(cx);
672 let active_index = dock.active_panel_index;
673 let is_open = dock.is_open;
674 let dock_position = dock.position;
675
676 let (menu_anchor, menu_attach) = match dock.position {
677 DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
678 DockPosition::Bottom | DockPosition::Right => {
679 (AnchorCorner::BottomRight, AnchorCorner::TopRight)
680 }
681 };
682
683 let buttons = dock
684 .panel_entries
685 .iter()
686 .enumerate()
687 .filter_map(|(i, entry)| {
688 let icon = entry.panel.icon(cx)?;
689 let icon_tooltip = entry.panel.icon_tooltip(cx)?;
690 let name = entry.panel.persistent_name();
691 let panel = entry.panel.clone();
692
693 let is_active_button = i == active_index && is_open;
694 let (action, tooltip) = if is_active_button {
695 let action = dock.toggle_action();
696
697 let tooltip: SharedString =
698 format!("Close {} dock", dock.position.to_label()).into();
699
700 (action, tooltip)
701 } else {
702 let action = entry.panel.toggle_action(cx);
703
704 (action, icon_tooltip.into())
705 };
706
707 Some(
708 right_click_menu(name)
709 .menu(move |cx| {
710 const POSITIONS: [DockPosition; 3] = [
711 DockPosition::Left,
712 DockPosition::Right,
713 DockPosition::Bottom,
714 ];
715
716 ContextMenu::build(cx, |mut menu, cx| {
717 for position in POSITIONS {
718 if position != dock_position
719 && panel.position_is_valid(position, cx)
720 {
721 let panel = panel.clone();
722 menu = menu.entry(
723 format!("Dock {}", position.to_label()),
724 None,
725 move |cx| {
726 panel.set_position(position, cx);
727 },
728 )
729 }
730 }
731 menu
732 })
733 })
734 .anchor(menu_anchor)
735 .attach(menu_attach)
736 .trigger(
737 IconButton::new(name, icon)
738 .icon_size(IconSize::Small)
739 .selected(is_active_button)
740 .on_click({
741 let action = action.boxed_clone();
742 move |_, cx| cx.dispatch_action(action.boxed_clone())
743 })
744 .tooltip(move |cx| {
745 Tooltip::for_action(tooltip.clone(), &*action, cx)
746 }),
747 ),
748 )
749 });
750
751 h_flex().gap_0p5().children(buttons)
752 }
753}
754
755impl StatusItemView for PanelButtons {
756 fn set_active_pane_item(
757 &mut self,
758 _active_pane_item: Option<&dyn crate::ItemHandle>,
759 _cx: &mut ViewContext<Self>,
760 ) {
761 // Nothing to do, panel buttons don't depend on the active center item
762 }
763}
764
765#[cfg(any(test, feature = "test-support"))]
766pub mod test {
767 use super::*;
768 use gpui::{actions, div, ViewContext, WindowContext};
769
770 pub struct TestPanel {
771 pub position: DockPosition,
772 pub zoomed: bool,
773 pub active: bool,
774 pub focus_handle: FocusHandle,
775 pub size: Pixels,
776 }
777 actions!(test, [ToggleTestPanel]);
778
779 impl EventEmitter<PanelEvent> for TestPanel {}
780
781 impl TestPanel {
782 pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self {
783 Self {
784 position,
785 zoomed: false,
786 active: false,
787 focus_handle: cx.focus_handle(),
788 size: px(300.),
789 }
790 }
791 }
792
793 impl Render for TestPanel {
794 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
795 div().id("test").track_focus(&self.focus_handle)
796 }
797 }
798
799 impl Panel for TestPanel {
800 fn persistent_name() -> &'static str {
801 "TestPanel"
802 }
803
804 fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
805 self.position
806 }
807
808 fn position_is_valid(&self, _: super::DockPosition) -> bool {
809 true
810 }
811
812 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
813 self.position = position;
814 cx.update_global::<SettingsStore, _>(|_, _| {});
815 }
816
817 fn size(&self, _: &WindowContext) -> Pixels {
818 self.size
819 }
820
821 fn set_size(&mut self, size: Option<Pixels>, _: &mut ViewContext<Self>) {
822 self.size = size.unwrap_or(px(300.));
823 }
824
825 fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
826 None
827 }
828
829 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
830 None
831 }
832
833 fn toggle_action(&self) -> Box<dyn Action> {
834 ToggleTestPanel.boxed_clone()
835 }
836
837 fn is_zoomed(&self, _: &WindowContext) -> bool {
838 self.zoomed
839 }
840
841 fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
842 self.zoomed = zoomed;
843 }
844
845 fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
846 self.active = active;
847 }
848 }
849
850 impl FocusableView for TestPanel {
851 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
852 self.focus_handle.clone()
853 }
854 }
855}