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