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