1use crate::DraggedDock;
2use crate::{status_bar::StatusItemView, Workspace};
3use gpui::{
4 div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
5 EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
6 SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
7};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11use ui::{h_stack, ContextMenu, IconButton, Tooltip};
12use ui::{prelude::*, right_click_menu};
13
14pub enum PanelEvent {
15 ChangePosition,
16 ZoomIn,
17 ZoomOut,
18 Activate,
19 Close,
20 Focus,
21}
22
23pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
24 fn persistent_name() -> &'static str;
25 fn position(&self, cx: &WindowContext) -> DockPosition;
26 fn position_is_valid(&self, position: DockPosition) -> bool;
27 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
28 fn size(&self, cx: &WindowContext) -> f32;
29 fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
30 // todo!("We should have a icon tooltip method, rather than using persistant_name")
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}
42
43pub trait PanelHandle: Send + Sync {
44 fn panel_id(&self) -> EntityId;
45 fn persistent_name(&self) -> &'static str;
46 fn position(&self, cx: &WindowContext) -> DockPosition;
47 fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
48 fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
49 fn is_zoomed(&self, cx: &WindowContext) -> bool;
50 fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
51 fn set_active(&self, active: bool, cx: &mut WindowContext);
52 fn size(&self, cx: &WindowContext) -> f32;
53 fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
54 fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
55 fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
56 fn icon_label(&self, cx: &WindowContext) -> Option<String>;
57 fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
58 fn to_any(&self) -> AnyView;
59}
60
61impl<T> PanelHandle for View<T>
62where
63 T: Panel,
64{
65 fn panel_id(&self) -> EntityId {
66 Entity::entity_id(self)
67 }
68
69 fn persistent_name(&self) -> &'static str {
70 T::persistent_name()
71 }
72
73 fn position(&self, cx: &WindowContext) -> DockPosition {
74 self.read(cx).position(cx)
75 }
76
77 fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
78 self.read(cx).position_is_valid(position)
79 }
80
81 fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
82 self.update(cx, |this, cx| this.set_position(position, cx))
83 }
84
85 fn is_zoomed(&self, cx: &WindowContext) -> bool {
86 self.read(cx).is_zoomed(cx)
87 }
88
89 fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) {
90 self.update(cx, |this, cx| this.set_zoomed(zoomed, cx))
91 }
92
93 fn set_active(&self, active: bool, cx: &mut WindowContext) {
94 self.update(cx, |this, cx| this.set_active(active, cx))
95 }
96
97 fn size(&self, cx: &WindowContext) -> f32 {
98 self.read(cx).size(cx)
99 }
100
101 fn set_size(&self, size: Option<f32>, cx: &mut WindowContext) {
102 self.update(cx, |this, cx| this.set_size(size, cx))
103 }
104
105 fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
106 self.read(cx).icon(cx)
107 }
108
109 fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action> {
110 self.read(cx).toggle_action()
111 }
112
113 fn icon_label(&self, cx: &WindowContext) -> Option<String> {
114 self.read(cx).icon_label(cx)
115 }
116
117 fn to_any(&self) -> AnyView {
118 self.clone().into()
119 }
120
121 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
122 self.read(cx).focus_handle(cx).clone()
123 }
124}
125
126impl From<&dyn PanelHandle> for AnyView {
127 fn from(val: &dyn PanelHandle) -> Self {
128 val.to_any()
129 }
130}
131
132pub struct Dock {
133 position: DockPosition,
134 panel_entries: Vec<PanelEntry>,
135 is_open: bool,
136 active_panel_index: usize,
137 focus_handle: FocusHandle,
138 _focus_subscription: Subscription,
139}
140
141impl FocusableView for Dock {
142 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
143 self.focus_handle.clone()
144 }
145}
146
147#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
148#[serde(rename_all = "lowercase")]
149pub enum DockPosition {
150 Left,
151 Bottom,
152 Right,
153}
154
155impl DockPosition {
156 fn to_label(&self) -> &'static str {
157 match self {
158 Self::Left => "left",
159 Self::Bottom => "bottom",
160 Self::Right => "right",
161 }
162 }
163
164 // todo!()
165 // fn to_resize_handle_side(self) -> HandleSide {
166 // match self {
167 // Self::Left => HandleSide::Right,
168 // Self::Bottom => HandleSide::Top,
169 // Self::Right => HandleSide::Left,
170 // }
171 // }
172
173 pub fn axis(&self) -> Axis {
174 match self {
175 Self::Left | Self::Right => Axis::Horizontal,
176 Self::Bottom => Axis::Vertical,
177 }
178 }
179}
180
181struct PanelEntry {
182 panel: Arc<dyn PanelHandle>,
183 // todo!()
184 // context_menu: View<ContextMenu>,
185 _subscriptions: [Subscription; 2],
186}
187
188pub struct PanelButtons {
189 dock: View<Dock>,
190}
191
192impl Dock {
193 pub fn new(position: DockPosition, cx: &mut ViewContext<'_, Self>) -> Self {
194 let focus_handle = cx.focus_handle();
195 let focus_subscription = cx.on_focus(&focus_handle, |dock, cx| {
196 if let Some(active_entry) = dock.panel_entries.get(dock.active_panel_index) {
197 active_entry.panel.focus_handle(cx).focus(cx)
198 }
199 });
200 Self {
201 position,
202 panel_entries: Default::default(),
203 active_panel_index: 0,
204 is_open: false,
205 focus_handle,
206 _focus_subscription: focus_subscription,
207 }
208 }
209
210 pub fn position(&self) -> DockPosition {
211 self.position
212 }
213
214 pub fn is_open(&self) -> bool {
215 self.is_open
216 }
217
218 // todo!()
219 // pub fn has_focus(&self, cx: &WindowContext) -> bool {
220 // self.visible_panel()
221 // .map_or(false, |panel| panel.has_focus(cx))
222 // }
223
224 pub fn panel<T: Panel>(&self) -> Option<View<T>> {
225 self.panel_entries
226 .iter()
227 .find_map(|entry| entry.panel.to_any().clone().downcast().ok())
228 }
229
230 pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
231 self.panel_entries
232 .iter()
233 .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
234 }
235
236 pub fn panel_index_for_persistent_name(
237 &self,
238 ui_name: &str,
239 _cx: &AppContext,
240 ) -> Option<usize> {
241 self.panel_entries
242 .iter()
243 .position(|entry| entry.panel.persistent_name() == ui_name)
244 }
245
246 pub fn active_panel_index(&self) -> usize {
247 self.active_panel_index
248 }
249
250 pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
251 if open != self.is_open {
252 self.is_open = open;
253 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
254 active_panel.panel.set_active(open, cx);
255 }
256
257 cx.notify();
258 }
259 }
260
261 pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
262 for entry in &mut self.panel_entries {
263 if entry.panel.panel_id() == panel.entity_id() {
264 if zoomed != entry.panel.is_zoomed(cx) {
265 entry.panel.set_zoomed(zoomed, cx);
266 }
267 } else if entry.panel.is_zoomed(cx) {
268 entry.panel.set_zoomed(false, cx);
269 }
270 }
271
272 cx.notify();
273 }
274
275 pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
276 for entry in &mut self.panel_entries {
277 if entry.panel.is_zoomed(cx) {
278 entry.panel.set_zoomed(false, cx);
279 }
280 }
281 }
282
283 pub(crate) fn add_panel<T: Panel>(
284 &mut self,
285 panel: View<T>,
286 workspace: WeakView<Workspace>,
287 cx: &mut ViewContext<Self>,
288 ) {
289 let subscriptions = [
290 cx.observe(&panel, |_, _, cx| cx.notify()),
291 cx.subscribe(&panel, move |this, panel, event, cx| match event {
292 PanelEvent::ChangePosition => {
293 let new_position = panel.read(cx).position(cx);
294
295 let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
296 if panel.is_zoomed(cx) {
297 workspace.zoomed_position = Some(new_position);
298 }
299 match new_position {
300 DockPosition::Left => &workspace.left_dock,
301 DockPosition::Bottom => &workspace.bottom_dock,
302 DockPosition::Right => &workspace.right_dock,
303 }
304 .clone()
305 }) else {
306 return;
307 };
308
309 let was_visible = this.is_open()
310 && this.visible_panel().map_or(false, |active_panel| {
311 active_panel.panel_id() == Entity::entity_id(&panel)
312 });
313
314 this.remove_panel(&panel, cx);
315
316 new_dock.update(cx, |new_dock, cx| {
317 new_dock.add_panel(panel.clone(), workspace.clone(), cx);
318 if was_visible {
319 new_dock.set_open(true, cx);
320 new_dock.activate_panel(new_dock.panels_len() - 1, cx);
321 }
322 });
323 }
324 PanelEvent::ZoomIn => {
325 this.set_panel_zoomed(&panel.to_any(), true, cx);
326 if !panel.focus_handle(cx).contains_focused(cx) {
327 cx.focus_view(&panel);
328 }
329 workspace
330 .update(cx, |workspace, cx| {
331 workspace.zoomed = Some(panel.downgrade().into());
332 workspace.zoomed_position = Some(panel.read(cx).position(cx));
333 })
334 .ok();
335 }
336 PanelEvent::ZoomOut => {
337 this.set_panel_zoomed(&panel.to_any(), false, cx);
338 workspace
339 .update(cx, |workspace, cx| {
340 if workspace.zoomed_position == Some(this.position) {
341 workspace.zoomed = None;
342 workspace.zoomed_position = None;
343 }
344 cx.notify();
345 })
346 .ok();
347 }
348 // todo!() we do not use this event in the production code (even in zed1), remove it
349 PanelEvent::Activate => {
350 if let Some(ix) = this
351 .panel_entries
352 .iter()
353 .position(|entry| entry.panel.panel_id() == Entity::entity_id(&panel))
354 {
355 this.set_open(true, cx);
356 this.activate_panel(ix, cx);
357 cx.focus_view(&panel);
358 }
359 }
360 PanelEvent::Close => {
361 if this
362 .visible_panel()
363 .map_or(false, |p| p.panel_id() == Entity::entity_id(&panel))
364 {
365 this.set_open(false, cx);
366 }
367 }
368 PanelEvent::Focus => {}
369 }),
370 ];
371
372 // todo!()
373 // let dock_view_id = cx.view_id();
374 self.panel_entries.push(PanelEntry {
375 panel: Arc::new(panel),
376 // todo!()
377 // context_menu: cx.add_view(|cx| {
378 // let mut menu = ContextMenu::new(dock_view_id, cx);
379 // menu.set_position_mode(OverlayPositionMode::Local);
380 // menu
381 // }),
382 _subscriptions: subscriptions,
383 });
384 cx.notify()
385 }
386
387 pub fn remove_panel<T: Panel>(&mut self, panel: &View<T>, cx: &mut ViewContext<Self>) {
388 if let Some(panel_ix) = self
389 .panel_entries
390 .iter()
391 .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
392 {
393 if panel_ix == self.active_panel_index {
394 self.active_panel_index = 0;
395 self.set_open(false, cx);
396 } else if panel_ix < self.active_panel_index {
397 self.active_panel_index -= 1;
398 }
399 self.panel_entries.remove(panel_ix);
400 cx.notify();
401 }
402 }
403
404 pub fn panels_len(&self) -> usize {
405 self.panel_entries.len()
406 }
407
408 pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
409 if panel_ix != self.active_panel_index {
410 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
411 active_panel.panel.set_active(false, cx);
412 }
413
414 self.active_panel_index = panel_ix;
415 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
416 active_panel.panel.set_active(true, cx);
417 }
418
419 cx.notify();
420 }
421 }
422
423 pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
424 let entry = self.visible_entry()?;
425 Some(&entry.panel)
426 }
427
428 pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
429 Some(&self.panel_entries.get(self.active_panel_index)?.panel)
430 }
431
432 fn visible_entry(&self) -> Option<&PanelEntry> {
433 if self.is_open {
434 self.panel_entries.get(self.active_panel_index)
435 } else {
436 None
437 }
438 }
439
440 pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Arc<dyn PanelHandle>> {
441 let entry = self.visible_entry()?;
442 if entry.panel.is_zoomed(cx) {
443 Some(entry.panel.clone())
444 } else {
445 None
446 }
447 }
448
449 pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
450 self.panel_entries
451 .iter()
452 .find(|entry| entry.panel.panel_id() == panel.panel_id())
453 .map(|entry| entry.panel.size(cx))
454 }
455
456 pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
457 if self.is_open {
458 self.panel_entries
459 .get(self.active_panel_index)
460 .map(|entry| entry.panel.size(cx))
461 } else {
462 None
463 }
464 }
465
466 pub fn resize_active_panel(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
467 if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
468 entry.panel.set_size(size, cx);
469 cx.notify();
470 }
471 }
472
473 pub fn toggle_action(&self) -> Box<dyn Action> {
474 match self.position {
475 DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
476 DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
477 DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
478 }
479 }
480}
481
482impl Render for Dock {
483 type Element = Div;
484
485 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
486 if let Some(entry) = self.visible_entry() {
487 let size = entry.panel.size(cx);
488
489 let position = self.position;
490 let mut handle = div()
491 .id("resize-handle")
492 .on_drag(DraggedDock(position), |dock, cx| {
493 cx.stop_propagation();
494 cx.build_view(|_| dock.clone())
495 })
496 .on_click(cx.listener(|v, e: &ClickEvent, cx| {
497 if e.down.button == MouseButton::Left && e.down.click_count == 2 {
498 v.resize_active_panel(None, cx);
499 cx.stop_propagation();
500 }
501 }))
502 .z_index(1);
503
504 const HANDLE_SIZE: Pixels = Pixels(6.);
505
506 match self.position() {
507 DockPosition::Left => {
508 handle = handle
509 .absolute()
510 .right(px(0.))
511 .top(px(0.))
512 .h_full()
513 .w(HANDLE_SIZE)
514 .cursor_col_resize();
515 }
516 DockPosition::Bottom => {
517 handle = handle
518 .absolute()
519 .top(px(0.))
520 .left(px(0.))
521 .w_full()
522 .h(HANDLE_SIZE)
523 .cursor_row_resize();
524 }
525 DockPosition::Right => {
526 handle = handle
527 .absolute()
528 .top(px(0.))
529 .left(px(0.))
530 .h_full()
531 .w(HANDLE_SIZE)
532 .cursor_col_resize();
533 }
534 }
535
536 div()
537 .flex()
538 .border_color(cx.theme().colors().border)
539 .map(|this| match self.position().axis() {
540 Axis::Horizontal => this.w(px(size)).h_full().flex_row(),
541 Axis::Vertical => this.h(px(size)).w_full().flex_col(),
542 })
543 .map(|this| match self.position() {
544 DockPosition::Left => this.border_r(),
545 DockPosition::Right => this.border_l(),
546 DockPosition::Bottom => this.border_t(),
547 })
548 .child(
549 div()
550 .map(|this| match self.position().axis() {
551 Axis::Horizontal => this.min_w(px(size)).h_full(),
552 Axis::Vertical => this.min_h(px(size)).w_full(),
553 })
554 .child(entry.panel.to_any()),
555 )
556 .child(handle)
557 } else {
558 div()
559 }
560 }
561}
562
563impl PanelButtons {
564 pub fn new(dock: View<Dock>, cx: &mut ViewContext<Self>) -> Self {
565 cx.observe(&dock, |_, _, cx| cx.notify()).detach();
566 Self { dock }
567 }
568}
569
570impl Render for PanelButtons {
571 type Element = Div;
572
573 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
574 // todo!()
575 let dock = self.dock.read(cx);
576 let active_index = dock.active_panel_index;
577 let is_open = dock.is_open;
578 let dock_position = dock.position;
579
580 let (menu_anchor, menu_attach) = match dock.position {
581 DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
582 DockPosition::Bottom | DockPosition::Right => {
583 (AnchorCorner::BottomRight, AnchorCorner::TopRight)
584 }
585 };
586
587 let buttons = dock
588 .panel_entries
589 .iter()
590 .enumerate()
591 .filter_map(|(i, entry)| {
592 let icon = entry.panel.icon(cx)?;
593 let name = entry.panel.persistent_name();
594 let panel = entry.panel.clone();
595
596 let is_active_button = i == active_index && is_open;
597
598 let (action, tooltip) = if is_active_button {
599 let action = dock.toggle_action();
600
601 let tooltip: SharedString =
602 format!("Close {} dock", dock.position.to_label()).into();
603
604 (action, tooltip)
605 } else {
606 let action = entry.panel.toggle_action(cx);
607
608 (action, name.into())
609 };
610
611 Some(
612 right_click_menu(name)
613 .menu(move |cx| {
614 const POSITIONS: [DockPosition; 3] = [
615 DockPosition::Left,
616 DockPosition::Right,
617 DockPosition::Bottom,
618 ];
619
620 ContextMenu::build(cx, |mut menu, cx| {
621 for position in POSITIONS {
622 if position != dock_position
623 && panel.position_is_valid(position, cx)
624 {
625 let panel = panel.clone();
626 menu = menu.entry(position.to_label(), move |cx| {
627 panel.set_position(position, cx);
628 })
629 }
630 }
631 menu
632 })
633 })
634 .anchor(menu_anchor)
635 .attach(menu_attach)
636 .trigger(
637 IconButton::new(name, icon)
638 .selected(is_active_button)
639 .on_click({
640 let action = action.boxed_clone();
641 move |_, cx| cx.dispatch_action(action.boxed_clone())
642 })
643 .tooltip(move |cx| {
644 Tooltip::for_action(tooltip.clone(), &*action, cx)
645 }),
646 ),
647 )
648 });
649
650 h_stack().gap_0p5().children(buttons)
651 }
652}
653
654impl StatusItemView for PanelButtons {
655 fn set_active_pane_item(
656 &mut self,
657 _active_pane_item: Option<&dyn crate::ItemHandle>,
658 _cx: &mut ViewContext<Self>,
659 ) {
660 // Nothing to do, panel buttons don't depend on the active center item
661 }
662}
663
664#[cfg(any(test, feature = "test-support"))]
665pub mod test {
666 use super::*;
667 use gpui::{actions, div, Div, ViewContext, WindowContext};
668
669 pub struct TestPanel {
670 pub position: DockPosition,
671 pub zoomed: bool,
672 pub active: bool,
673 pub focus_handle: FocusHandle,
674 pub size: f32,
675 }
676 actions!(test, [ToggleTestPanel]);
677
678 impl EventEmitter<PanelEvent> for TestPanel {}
679
680 impl TestPanel {
681 pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self {
682 Self {
683 position,
684 zoomed: false,
685 active: false,
686 focus_handle: cx.focus_handle(),
687 size: 300.,
688 }
689 }
690 }
691
692 impl Render for TestPanel {
693 type Element = Div;
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() -> &'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(&self, _: &WindowContext) -> Option<ui::Icon> {
727 None
728 }
729
730 fn toggle_action(&self) -> Box<dyn Action> {
731 ToggleTestPanel.boxed_clone()
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
747 impl FocusableView for TestPanel {
748 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
749 self.focus_handle.clone()
750 }
751 }
752}