diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 85694ec629..4c75035c83 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/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) -> ViewHandle { let project = workspace.project().clone(); let project_panel = cx.add_view(|cx: &mut ViewContext| { - // Update the dock position when the setting changes. - let mut old_dock_position = cx.global::().project_panel_overrides.dock; - cx.observe_global::(move |_, cx| { - let new_dock_position = cx.global::().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::(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::() + let settings = cx.global::(); + 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) { + SettingsFile::update(cx, move |settings| { + settings.project_panel.dock = Some(position.into()) + }) + } + fn icon_path(&self) -> &'static str { "icons/folder_tree_16.svg" } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index a49d84c6a8..3bd3786071 100644 --- a/crates/settings/src/settings.rs +++ b/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, pub working_directory: Option, @@ -265,26 +265,7 @@ pub struct TerminalSettings { pub alternate_scroll: Option, pub option_as_meta: Option, pub copy_on_select: Option, - 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, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] @@ -398,7 +379,8 @@ pub struct SettingsFileContent { pub autosave: Option, #[serde(flatten)] pub editor: EditorSettings, - pub project_panel: Option, + #[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; diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 58deda9236..4e90a8fec4 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/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::().terminal_overrides.dock.into() + let settings = cx.global::(); + 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) { + SettingsFile::update(cx, move |settings| { + settings.terminal.dock = Some(position.into()); + }); + } + fn icon_path(&self) -> &'static str { "icons/terminal_12.svg" } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 3d5c4b6c38..6f7f6b1c12 100644 --- a/crates/workspace/src/dock.rs +++ b/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); fn icon_path(&self) -> &'static str; fn icon_tooltip(&self) -> String; fn icon_label(&self, _: &AppContext) -> Option { @@ -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; @@ -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 for DockPosition { } } +impl From 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, + context_menu: ViewHandle, _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::>(); Flex::row() - .with_children(items.into_iter().enumerate().map(|(ix, view)| { - let action = TogglePanel { - dock_position, - item_index: ix, - }; - MouseEventHandler::::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::::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::( + 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::( - 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() diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index d2b9251a62..dfad53c3a3 100644 --- a/crates/workspace/src/item.rs +++ b/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, + ) { + unimplemented!() + } + fn icon_path(&self) -> &'static str { unimplemented!() }