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