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