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