diff --git a/Cargo.lock b/Cargo.lock index b6d8e621adb5b3255b5a943a2ea26fab85ac8c5c..6c87bb055f7fcbbbffaa3cbd0f5daa818d32f038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6934,6 +6934,7 @@ dependencies = [ "client", "clock", "collections", + "context_menu", "futures", "gpui", "language", diff --git a/assets/icons/split.svg b/assets/icons/split.svg new file mode 100644 index 0000000000000000000000000000000000000000..298de950926571bfd64319442c0646370ef150bf --- /dev/null +++ b/assets/icons/split.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 2123aadf4c34e97a8f0348ead2e8f140c4ec7dc0..85a6cd1e19cbc851c1e4ad71d1216460c5b334e9 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,18 +1,23 @@ -use std::{any::TypeId, time::Duration}; - use gpui::{ - elements::*, geometry::vector::Vector2F, keymap, platform::CursorStyle, Action, AppContext, - Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, Subscription, View, - ViewContext, + elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle, + Action, AppContext, Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, + Subscription, View, ViewContext, }; use menu::*; use settings::Settings; +use std::{any::TypeId, time::Duration}; + +#[derive(Copy, Clone, PartialEq)] +struct Clicked; + +impl_internal_actions!(context_menu, [Clicked]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContextMenu::select_first); cx.add_action(ContextMenu::select_last); cx.add_action(ContextMenu::select_next); cx.add_action(ContextMenu::select_prev); + cx.add_action(ContextMenu::clicked); cx.add_action(ContextMenu::confirm); cx.add_action(ContextMenu::cancel); } @@ -56,6 +61,7 @@ pub struct ContextMenu { selected_index: Option, visible: bool, previously_focused_view_id: Option, + clicked: bool, _actions_observation: Subscription, } @@ -113,6 +119,7 @@ impl ContextMenu { selected_index: Default::default(), visible: Default::default(), previously_focused_view_id: Default::default(), + clicked: false, _actions_observation: cx.observe_actions(Self::action_dispatched), } } @@ -123,22 +130,31 @@ impl ContextMenu { .iter() .position(|item| item.action_id() == Some(action_id)) { - self.selected_index = Some(ix); - cx.notify(); - cx.spawn(|this, mut cx| async move { - cx.background().timer(Duration::from_millis(100)).await; - this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx)); - }) - .detach(); + if self.clicked { + self.cancel(&Default::default(), cx); + } else { + self.selected_index = Some(ix); + cx.notify(); + cx.spawn(|this, mut cx| async move { + cx.background().timer(Duration::from_millis(50)).await; + this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx)); + }) + .detach(); + } } } + fn clicked(&mut self, _: &Clicked, _: &mut ViewContext) { + self.clicked = true; + } + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(ix) = self.selected_index { if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) { let window_id = cx.window_id(); let view_id = cx.view_id(); cx.dispatch_action_at(window_id, view_id, action.as_ref()); + self.reset(cx); } } } @@ -158,6 +174,7 @@ impl ContextMenu { self.items.clear(); self.visible = false; self.selected_index.take(); + self.clicked = false; cx.notify(); } @@ -277,6 +294,8 @@ impl ContextMenu { .boxed(), } })) + .contained() + .with_margin_left(style.keystroke_margin) .boxed(), ) .contained() @@ -315,8 +334,8 @@ impl ContextMenu { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(move |_, _, cx| { + cx.dispatch_action(Clicked); cx.dispatch_any_action(action.boxed_clone()); - cx.dispatch_action(Cancel); }) .boxed() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 32ddb9c8256e7eb1c8c449e3ff4f9149a7bd6253..7936a9b6bb864f2f6cdc949fe2672af2069bde50 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -40,6 +40,7 @@ pub struct Workspace { pub titlebar: Titlebar, pub tab: Tab, pub active_tab: Tab, + pub pane_button: Interactive, pub pane_divider: Border, pub leader_border_opacity: f32, pub leader_border_width: f32, @@ -241,6 +242,7 @@ pub struct ContextMenu { #[serde(flatten)] pub container: ContainerStyle, pub item: Interactive, + pub keystroke_margin: f32, pub separator: ContainerStyle, } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 81dcad6a128da05357e2d673cae2626f65502422..3534e293f8d86f1ec1efface052e246e76bfc0f6 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -14,6 +14,7 @@ test-support = ["client/test-support", "project/test-support", "settings/test-su client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } +context_menu = { path = "../context_menu" } gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 391ddfc1f7a6d0c9e6a727622d925b3d40ead78b..87b6ea7547d3223dd2669d17d298eb765235a7c1 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,11 +2,15 @@ use super::{ItemHandle, SplitDirection}; use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace}; use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; +use context_menu::{ContextMenu, ContextMenuItem}; use futures::StreamExt; use gpui::{ actions, elements::*, - geometry::{rect::RectF, vector::vec2f}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, impl_actions, impl_internal_actions, platform::{CursorStyle, NavigationDirection}, AppContext, AsyncAppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad, @@ -55,8 +59,13 @@ pub struct GoForward { pub pane: Option>, } +#[derive(Clone, PartialEq)] +pub struct DeploySplitMenu { + position: Vector2F, +} + impl_actions!(pane, [GoBack, GoForward, ActivateItem]); -impl_internal_actions!(pane, [CloseItem]); +impl_internal_actions!(pane, [CloseItem, DeploySplitMenu]); const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; @@ -87,6 +96,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)); cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)); cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)); + cx.add_action(Pane::deploy_split_menu); cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { Pane::reopen_closed_item(workspace, cx).detach(); }); @@ -129,6 +139,7 @@ pub struct Pane { autoscroll: bool, nav_history: Rc>, toolbar: ViewHandle, + split_menu: ViewHandle, } pub struct ItemNavHistory { @@ -169,6 +180,7 @@ pub struct NavigationEntry { impl Pane { pub fn new(cx: &mut ViewContext) -> Self { let handle = cx.weak_handle(); + let split_menu = cx.add_view(|cx| ContextMenu::new(cx)); Self { items: Vec::new(), active_item_index: 0, @@ -182,6 +194,7 @@ impl Pane { pane: handle.clone(), })), toolbar: cx.add_view(|_| Toolbar::new(handle)), + split_menu, } } @@ -786,6 +799,21 @@ impl Pane { cx.emit(Event::Split(direction)); } + fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext) { + self.split_menu.update(cx, |menu, cx| { + menu.show( + action.position, + vec![ + ContextMenuItem::item("Split Right", SplitRight), + ContextMenuItem::item("Split Left", SplitLeft), + ContextMenuItem::item("Split Up", SplitUp), + ContextMenuItem::item("Split Down", SplitDown), + ], + cx, + ); + }); + } + pub fn toolbar(&self) -> &ViewHandle { &self.toolbar } @@ -800,13 +828,13 @@ impl Pane { }); } - fn render_tabs(&mut self, cx: &mut RenderContext) -> ElementBox { + fn render_tabs(&mut self, cx: &mut RenderContext) -> impl Element { let theme = cx.global::().theme.clone(); enum Tabs {} enum Tab {} let pane = cx.handle(); - let tabs = MouseEventHandler::new::(0, cx, |mouse_state, cx| { + MouseEventHandler::new::(0, cx, |mouse_state, cx| { let autoscroll = if mem::take(&mut self.autoscroll) { Some(self.active_item_index) } else { @@ -941,11 +969,7 @@ impl Pane { ); row.boxed() - }); - - ConstrainedBox::new(tabs.boxed()) - .with_height(theme.workspace.tab.height) - .named("tabs") + }) } } @@ -959,27 +983,72 @@ impl View for Pane { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + enum SplitIcon {} + let this = cx.handle(); - EventHandler::new(if let Some(active_item) = self.active_item() { - Flex::column() - .with_child(self.render_tabs(cx)) - .with_child(ChildView::new(&self.toolbar).boxed()) - .with_child(ChildView::new(active_item).flex(1., true).boxed()) - .boxed() - } else { - Empty::new().boxed() - }) - .on_navigate_mouse_down(move |direction, cx| { - let this = this.clone(); - match direction { - NavigationDirection::Back => cx.dispatch_action(GoBack { pane: Some(this) }), - NavigationDirection::Forward => cx.dispatch_action(GoForward { pane: Some(this) }), - } + Stack::new() + .with_child( + EventHandler::new(if let Some(active_item) = self.active_item() { + Flex::column() + .with_child( + Flex::row() + .with_child(self.render_tabs(cx).flex(1., true).named("tabs")) + .with_child( + MouseEventHandler::new::( + 0, + cx, + |mouse_state, cx| { + let theme = &cx.global::().theme.workspace; + let style = + theme.pane_button.style_for(mouse_state, false); + Svg::new("icons/split.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .aligned() + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_mouse_down(|position, cx| { + cx.dispatch_action(DeploySplitMenu { position }); + }) + .boxed(), + ) + .constrained() + .with_height(cx.global::().theme.workspace.tab.height) + .boxed(), + ) + .with_child(ChildView::new(&self.toolbar).boxed()) + .with_child(ChildView::new(active_item).flex(1., true).boxed()) + .boxed() + } else { + Empty::new().boxed() + }) + .on_navigate_mouse_down(move |direction, cx| { + let this = this.clone(); + match direction { + NavigationDirection::Back => { + cx.dispatch_action(GoBack { pane: Some(this) }) + } + NavigationDirection::Forward => { + cx.dispatch_action(GoForward { pane: Some(this) }) + } + } - true - }) - .named("pane") + true + }) + .boxed(), + ) + .with_child(ChildView::new(&self.split_menu).boxed()) + .named("pane") } fn on_focus(&mut self, cx: &mut ViewContext) { diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts index f6197930d49530e4dd463225f3c07a094a85368a..43b3a200b12c327f015b1c13aee70cffdc8da3fc 100644 --- a/styles/src/styleTree/contextMenu.ts +++ b/styles/src/styleTree/contextMenu.ts @@ -8,13 +8,14 @@ export default function contextMenu(theme: Theme) { padding: 6, shadow: popoverShadow(theme), border: border(theme, "primary"), + keystrokeMargin: 30, item: { padding: { left: 4, right: 4, top: 2, bottom: 2 }, cornerRadius: 6, label: text(theme, "sans", "secondary", { size: "sm" }), keystroke: { - margin: { left: 60 }, - ...text(theme, "sans", "muted", { size: "sm", weight: "bold" }) + ...text(theme, "sans", "muted", { size: "sm", weight: "bold" }), + padding: { left: 3, right: 3 } }, hover: { background: backgroundColor(theme, 300, "hovered"), diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index fbd7b05a224f8272126631ad5107e1a36215c623..36d47bed9215680b2fd63367dd7127d28a9d9310 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -55,6 +55,18 @@ export default function workspace(theme: Theme) { leaderBorderWidth: 2.0, tab, activeTab, + paneButton: { + color: iconColor(theme, "secondary"), + border: { + ...tab.border + }, + iconWidth: 14, + buttonWidth: tab.height, + hover: { + color: iconColor(theme, "active"), + background: backgroundColor(theme, 300), + } + }, modal: { margin: { bottom: 52,