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