Merge pull request #1342 from zed-industries/mouse-based-splitting
Introduce mouse-based pane splitting
This commit is contained in:
Generated
+1
@@ -6934,6 +6934,7 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"context_menu",
|
||||
"futures",
|
||||
"gpui",
|
||||
"language",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.8 0.800476C11.4619 0.800476 12 1.33766 12 2.00048V8.00048C12 8.66235 11.4619 9.20048 10.8 9.20048H1.2C0.537188 9.20048 0 8.66235 0 8.00048V2.00048C0 1.33766 0.537188 0.800476 1.2 0.800476H10.8ZM3.6 2.00048H1.2V8.00048H3.6V2.00048ZM4.8 8.00048H7.2V2.00048H4.8V8.00048ZM10.8 2.00048H8.4V8.00048H10.8V2.00048Z" fill="#8B8792"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 441 B |
@@ -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<usize>,
|
||||
visible: bool,
|
||||
previously_focused_view_id: Option<usize>,
|
||||
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>) {
|
||||
self.clicked = true;
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ pub struct Workspace {
|
||||
pub titlebar: Titlebar,
|
||||
pub tab: Tab,
|
||||
pub active_tab: Tab,
|
||||
pub pane_button: Interactive<IconButton>,
|
||||
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<ContextMenuItem>,
|
||||
pub keystroke_margin: f32,
|
||||
pub separator: ContainerStyle,
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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<WeakViewHandle<Pane>>,
|
||||
}
|
||||
|
||||
#[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<RefCell<NavHistory>>,
|
||||
toolbar: ViewHandle<Toolbar>,
|
||||
split_menu: ViewHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
pub struct ItemNavHistory {
|
||||
@@ -169,6 +180,7 @@ pub struct NavigationEntry {
|
||||
impl Pane {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> 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>) {
|
||||
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<Toolbar> {
|
||||
&self.toolbar
|
||||
}
|
||||
@@ -800,13 +828,13 @@ impl Pane {
|
||||
});
|
||||
}
|
||||
|
||||
fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
enum Tabs {}
|
||||
enum Tab {}
|
||||
let pane = cx.handle();
|
||||
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
|
||||
MouseEventHandler::new::<Tabs, _, _>(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<Self>) -> 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::<SplitIcon, _, _>(
|
||||
0,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
let theme = &cx.global::<Settings>().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::<Settings>().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<Self>) {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user