Add context menu to change panel position

Antonio Scandurra created

Change summary

crates/project_panel/src/project_panel.rs  |  44 +++--
crates/settings/src/settings.rs            |  30 ---
crates/terminal_view/src/terminal_panel.rs |  24 ++
crates/workspace/src/dock.rs               | 181 ++++++++++++++++-------
crates/workspace/src/item.rs               |   8 +
5 files changed, 190 insertions(+), 97 deletions(-)

Detailed changes

crates/project_panel/src/project_panel.rs 🔗

@@ -17,7 +17,7 @@ use gpui::{
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
-use settings::Settings;
+use settings::{settings_file::SettingsFile, Settings};
 use std::{
     cmp::Ordering,
     collections::{hash_map, HashMap},
@@ -28,7 +28,10 @@ use std::{
 };
 use theme::ProjectPanelEntry;
 use unicase::UniCase;
-use workspace::{dock::DockPosition, Workspace};
+use workspace::{
+    dock::{DockPosition, Panel},
+    Workspace,
+};
 
 const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
 
@@ -142,17 +145,6 @@ impl ProjectPanel {
     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
         let project = workspace.project().clone();
         let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
-            // Update the dock position when the setting changes.
-            let mut old_dock_position = cx.global::<Settings>().project_panel_overrides.dock;
-            cx.observe_global::<Settings, _>(move |_, cx| {
-                let new_dock_position = cx.global::<Settings>().project_panel_overrides.dock;
-                if new_dock_position != old_dock_position {
-                    old_dock_position = new_dock_position;
-                    cx.emit(Event::DockPositionChanged);
-                }
-            })
-            .detach();
-
             cx.observe(&project, |this, _, cx| {
                 this.update_visible_entries(None, cx);
                 cx.notify();
@@ -224,6 +216,18 @@ impl ProjectPanel {
                 workspace: workspace.weak_handle(),
             };
             this.update_visible_entries(None, cx);
+
+            // Update the dock position when the setting changes.
+            let mut old_dock_position = this.position(cx);
+            cx.observe_global::<Settings, _>(move |this, cx| {
+                let new_dock_position = this.position(cx);
+                if new_dock_position != old_dock_position {
+                    old_dock_position = new_dock_position;
+                    cx.emit(Event::DockPositionChanged);
+                }
+            })
+            .detach();
+
             this
         });
 
@@ -1342,17 +1346,25 @@ impl Entity for ProjectPanel {
 
 impl workspace::dock::Panel for ProjectPanel {
     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        cx.global::<Settings>()
+        let settings = cx.global::<Settings>();
+        settings
             .project_panel_overrides
             .dock
-            .map(Into::into)
-            .unwrap_or(DockPosition::Left)
+            .or(settings.project_panel_defaults.dock)
+            .unwrap()
+            .into()
     }
 
     fn position_is_valid(&self, position: DockPosition) -> bool {
         matches!(position, DockPosition::Left | DockPosition::Right)
     }
 
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        SettingsFile::update(cx, move |settings| {
+            settings.project_panel.dock = Some(position.into())
+        })
+    }
+
     fn icon_path(&self) -> &'static str {
         "icons/folder_tree_16.svg"
     }

crates/settings/src/settings.rs 🔗

@@ -252,7 +252,7 @@ impl Default for HourFormat {
     }
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct TerminalSettings {
     pub shell: Option<Shell>,
     pub working_directory: Option<WorkingDirectory>,
@@ -265,26 +265,7 @@ pub struct TerminalSettings {
     pub alternate_scroll: Option<AlternateScroll>,
     pub option_as_meta: Option<bool>,
     pub copy_on_select: Option<bool>,
-    pub dock: DockPosition,
-}
-
-impl Default for TerminalSettings {
-    fn default() -> Self {
-        Self {
-            shell: Default::default(),
-            working_directory: Default::default(),
-            font_size: Default::default(),
-            font_family: Default::default(),
-            line_height: Default::default(),
-            font_features: Default::default(),
-            env: Default::default(),
-            blinking: Default::default(),
-            alternate_scroll: Default::default(),
-            option_as_meta: Default::default(),
-            copy_on_select: Default::default(),
-            dock: DockPosition::Bottom,
-        }
-    }
+    pub dock: Option<DockPosition>,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
@@ -398,7 +379,8 @@ pub struct SettingsFileContent {
     pub autosave: Option<Autosave>,
     #[serde(flatten)]
     pub editor: EditorSettings,
-    pub project_panel: Option<ProjectPanelSettings>,
+    #[serde(default)]
+    pub project_panel: ProjectPanelSettings,
     #[serde(default)]
     pub journal: JournalSettings,
     #[serde(default)]
@@ -493,7 +475,7 @@ impl Settings {
             show_call_status_icon: defaults.show_call_status_icon.unwrap(),
             vim_mode: defaults.vim_mode.unwrap(),
             autosave: defaults.autosave.unwrap(),
-            project_panel_defaults: defaults.project_panel.unwrap(),
+            project_panel_defaults: defaults.project_panel,
             project_panel_overrides: Default::default(),
             editor_defaults: EditorSettings {
                 tab_size: required(defaults.editor.tab_size),
@@ -601,7 +583,7 @@ impl Settings {
             }
         }
         self.editor_overrides = data.editor;
-        self.project_panel_overrides = data.project_panel.unwrap_or_default();
+        self.project_panel_overrides = data.project_panel;
         self.git_overrides = data.git.unwrap_or_default();
         self.journal_overrides = data.journal;
         self.terminal_defaults.font_size = data.terminal.font_size;

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -4,9 +4,12 @@ use gpui::{
     WeakViewHandle,
 };
 use project::Project;
-use settings::{Settings, WorkingDirectory};
+use settings::{settings_file::SettingsFile, Settings, WorkingDirectory};
 use util::ResultExt;
-use workspace::{dock::{Panel, DockPosition}, pane, DraggedItem, Pane, Workspace};
+use workspace::{
+    dock::{DockPosition, Panel},
+    pane, DraggedItem, Pane, Workspace,
+};
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(TerminalPanel::add_terminal);
@@ -33,7 +36,8 @@ impl TerminalPanel {
                 old_dock_position = new_dock_position;
                 cx.emit(Event::DockPositionChanged);
             }
-        }).detach();
+        })
+        .detach();
 
         let this = cx.weak_handle();
         let pane = cx.add_view(|cx| {
@@ -146,13 +150,25 @@ impl View for TerminalPanel {
 
 impl Panel for TerminalPanel {
     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        cx.global::<Settings>().terminal_overrides.dock.into()
+        let settings = cx.global::<Settings>();
+        settings
+            .terminal_overrides
+            .dock
+            .or(settings.terminal_defaults.dock)
+            .unwrap()
+            .into()
     }
 
     fn position_is_valid(&self, _: DockPosition) -> bool {
         true
     }
 
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        SettingsFile::update(cx, move |settings| {
+            settings.terminal.dock = Some(position.into());
+        });
+    }
+
     fn icon_path(&self) -> &'static str {
         "icons/terminal_12.svg"
     }

crates/workspace/src/dock.rs 🔗

@@ -1,4 +1,5 @@
 use crate::{StatusItemView, Workspace};
+use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle,
     AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
@@ -10,6 +11,7 @@ use std::rc::Rc;
 pub trait Panel: View {
     fn position(&self, cx: &WindowContext) -> DockPosition;
     fn position_is_valid(&self, position: DockPosition) -> bool;
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
     fn icon_path(&self) -> &'static str;
     fn icon_tooltip(&self) -> String;
     fn icon_label(&self, _: &AppContext) -> Option<String> {
@@ -24,6 +26,7 @@ pub trait PanelHandle {
     fn id(&self) -> usize;
     fn position(&self, cx: &WindowContext) -> DockPosition;
     fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
+    fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
     fn icon_path(&self, cx: &WindowContext) -> &'static str;
     fn icon_tooltip(&self, cx: &WindowContext) -> String;
     fn icon_label(&self, cx: &WindowContext) -> Option<String>;
@@ -47,6 +50,10 @@ where
         self.read(cx).position_is_valid(position)
     }
 
+    fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
+        self.update(cx, |this, cx| this.set_position(position, cx))
+    }
+
     fn icon_path(&self, cx: &WindowContext) -> &'static str {
         self.read(cx).icon_path()
     }
@@ -102,7 +109,25 @@ impl From<settings::DockPosition> for DockPosition {
     }
 }
 
+impl From<DockPosition> for settings::DockPosition {
+    fn from(value: DockPosition) -> settings::DockPosition {
+        match value {
+            DockPosition::Left => settings::DockPosition::Left,
+            DockPosition::Bottom => settings::DockPosition::Bottom,
+            DockPosition::Right => settings::DockPosition::Right,
+        }
+    }
+}
+
 impl DockPosition {
+    fn to_label(&self) -> &'static str {
+        match self {
+            Self::Left => "left",
+            Self::Bottom => "bottom",
+            Self::Right => "right",
+        }
+    }
+
     fn to_resizable_side(self) -> Side {
         match self {
             Self::Left => Side::Right,
@@ -114,6 +139,7 @@ impl DockPosition {
 
 struct PanelEntry {
     panel: Rc<dyn PanelHandle>,
+    context_menu: ViewHandle<ContextMenu>,
     _subscriptions: [Subscription; 2],
 }
 
@@ -179,8 +205,14 @@ impl Dock {
             }),
         ];
 
+        let dock_view_id = cx.view_id();
         self.panels.push(PanelEntry {
             panel: Rc::new(panel),
+            context_menu: cx.add_view(|cx| {
+                let mut menu = ContextMenu::new(dock_view_id, cx);
+                menu.set_position_mode(OverlayPositionMode::Local);
+                menu
+            }),
             _subscriptions: subscriptions,
         });
         cx.notify()
@@ -292,66 +324,109 @@ impl View for PanelButtons {
             DockPosition::Bottom => theme.group_bottom,
             DockPosition::Right => theme.group_right,
         };
+        let menu_corner = match dock_position {
+            DockPosition::Left => AnchorCorner::BottomLeft,
+            DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
+        };
 
         let items = dock
             .panels
             .iter()
-            .map(|item| item.panel.clone())
+            .map(|item| (item.panel.clone(), item.context_menu.clone()))
             .collect::<Vec<_>>();
         Flex::row()
-            .with_children(items.into_iter().enumerate().map(|(ix, view)| {
-                let action = TogglePanel {
-                    dock_position,
-                    item_index: ix,
-                };
-                MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
-                    let is_active = is_open && ix == active_ix;
-                    let style = item_style.style_for(state, is_active);
-                    Flex::row()
-                        .with_child(
-                            Svg::new(view.icon_path(cx))
-                                .with_color(style.icon_color)
-                                .constrained()
-                                .with_width(style.icon_size)
-                                .aligned(),
-                        )
-                        .with_children(if let Some(label) = view.icon_label(cx) {
-                            Some(
-                                Label::new(label, style.label.text.clone())
-                                    .contained()
-                                    .with_style(style.label.container)
-                                    .aligned(),
+            .with_children(
+                items
+                    .into_iter()
+                    .enumerate()
+                    .map(|(ix, (view, context_menu))| {
+                        let action = TogglePanel {
+                            dock_position,
+                            item_index: ix,
+                        };
+
+                        Stack::new()
+                            .with_child(
+                                MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
+                                    let is_active = is_open && ix == active_ix;
+                                    let style = item_style.style_for(state, is_active);
+                                    Flex::row()
+                                        .with_child(
+                                            Svg::new(view.icon_path(cx))
+                                                .with_color(style.icon_color)
+                                                .constrained()
+                                                .with_width(style.icon_size)
+                                                .aligned(),
+                                        )
+                                        .with_children(if let Some(label) = view.icon_label(cx) {
+                                            Some(
+                                                Label::new(label, style.label.text.clone())
+                                                    .contained()
+                                                    .with_style(style.label.container)
+                                                    .aligned(),
+                                            )
+                                        } else {
+                                            None
+                                        })
+                                        .constrained()
+                                        .with_height(style.icon_size)
+                                        .contained()
+                                        .with_style(style.container)
+                                })
+                                .with_cursor_style(CursorStyle::PointingHand)
+                                .on_click(MouseButton::Left, {
+                                    let action = action.clone();
+                                    move |_, this, cx| {
+                                        if let Some(workspace) = this.workspace.upgrade(cx) {
+                                            let action = action.clone();
+                                            cx.window_context().defer(move |cx| {
+                                                workspace.update(cx, |workspace, cx| {
+                                                    workspace.toggle_panel(&action, cx)
+                                                });
+                                            });
+                                        }
+                                    }
+                                })
+                                .on_click(MouseButton::Right, {
+                                    let view = view.clone();
+                                    let menu = context_menu.clone();
+                                    move |_, _, cx| {
+                                        const POSITIONS: [DockPosition; 3] = [
+                                            DockPosition::Left,
+                                            DockPosition::Right,
+                                            DockPosition::Bottom,
+                                        ];
+
+                                        menu.update(cx, |menu, cx| {
+                                            let items = POSITIONS
+                                                .into_iter()
+                                                .filter(|position| {
+                                                    *position != dock_position
+                                                        && view.position_is_valid(*position, cx)
+                                                })
+                                                .map(|position| {
+                                                    let view = view.clone();
+                                                    ContextMenuItem::handler(
+                                                        format!("Dock {}", position.to_label()),
+                                                        move |cx| view.set_position(position, cx),
+                                                    )
+                                                })
+                                                .collect();
+                                            menu.show(Default::default(), menu_corner, items, cx);
+                                        })
+                                    }
+                                })
+                                .with_tooltip::<Self>(
+                                    ix,
+                                    view.icon_tooltip(cx),
+                                    Some(Box::new(action)),
+                                    tooltip_style.clone(),
+                                    cx,
+                                ),
                             )
-                        } else {
-                            None
-                        })
-                        .constrained()
-                        .with_height(style.icon_size)
-                        .contained()
-                        .with_style(style.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, {
-                    let action = action.clone();
-                    move |_, this, cx| {
-                        if let Some(workspace) = this.workspace.upgrade(cx) {
-                            let action = action.clone();
-                            cx.window_context().defer(move |cx| {
-                                workspace.update(cx, |workspace, cx| {
-                                    workspace.toggle_panel(&action, cx)
-                                });
-                            });
-                        }
-                    }
-                })
-                .with_tooltip::<Self>(
-                    ix,
-                    view.icon_tooltip(cx),
-                    Some(Box::new(action)),
-                    tooltip_style.clone(),
-                    cx,
-                )
-            }))
+                            .with_child(ChildView::new(&context_menu, cx))
+                    }),
+            )
             .contained()
             .with_style(group_style)
             .into_any()

crates/workspace/src/item.rs 🔗

@@ -1069,6 +1069,14 @@ pub(crate) mod test {
             unimplemented!()
         }
 
+        fn set_position(
+            &mut self,
+            _position: crate::dock::DockPosition,
+            _cx: &mut ViewContext<Self>,
+        ) {
+            unimplemented!()
+        }
+
         fn icon_path(&self) -> &'static str {
             unimplemented!()
         }