Add pane splitting by dragged item. Works, but the overlay doesn't clear quite right

This commit is contained in:
K Simmons 2022-10-21 19:14:24 -07:00
parent 70e2951e35
commit cfde3e348c
12 changed files with 308 additions and 280 deletions

View file

@ -397,10 +397,10 @@ impl View for ToggleDockButton {
}
})
.with_cursor_style(CursorStyle::PointingHand)
.on_up(MouseButton::Left, move |_, cx| {
.on_up(MouseButton::Left, move |event, cx| {
let dock_pane = workspace.read(cx.app).dock_pane();
let drop_index = dock_pane.read(cx.app).items_len() + 1;
Pane::handle_dropped_item(&dock_pane.downgrade(), drop_index, false, cx);
Pane::handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
});
if dock_position.is_visible() {

View file

@ -2,7 +2,7 @@ use super::{ItemHandle, SplitDirection};
use crate::{
dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
toolbar::Toolbar,
Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
Item, NewFile, NewSearch, NewTerminal, SplitWithItem, WeakItemHandle, Workspace,
};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
@ -19,6 +19,7 @@ use gpui::{
},
impl_actions, impl_internal_actions,
platform::{CursorStyle, NavigationDirection},
scene::MouseUp,
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
@ -98,7 +99,7 @@ impl_internal_actions!(
DeploySplitMenu,
DeployNewMenu,
DeployDockMenu,
MoveItem
MoveItem,
]
);
@ -1097,7 +1098,7 @@ impl Pane {
ix == 0,
detail,
hovered,
Self::tab_overlay_color(hovered, theme.as_ref(), cx),
Self::tab_overlay_color(hovered, cx),
tab_style,
cx,
)
@ -1124,7 +1125,7 @@ impl Pane {
})
.on_up(MouseButton::Left, {
let pane = pane.clone();
move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, ix, true, cx)
move |event, cx| Pane::handle_dropped_item(event, &pane, ix, true, None, cx)
})
.as_draggable(
DraggedItem {
@ -1164,14 +1165,14 @@ impl Pane {
.with_style(filler_style.container)
.with_border(filler_style.container.border);
if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered(), &theme, cx) {
if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered(), cx) {
filler = filler.with_overlay_color(overlay);
}
filler.boxed()
})
.on_up(MouseButton::Left, move |_, cx| {
Pane::handle_dropped_item(&pane, filler_index, true, cx)
.on_up(MouseButton::Left, move |event, cx| {
Pane::handle_dropped_item(event, &pane, filler_index, true, None, cx)
})
.flex(1., true)
.named("filler"),
@ -1320,17 +1321,64 @@ impl Pane {
tab.constrained().with_height(tab_style.height).boxed()
}
fn render_tab_bar_buttons(
&mut self,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
Flex::row()
// New menu
.with_child(tab_bar_button(0, "icons/plus_12.svg", cx, |position| {
DeployNewMenu { position }
}))
.with_child(
self.docked
.map(|anchor| {
// Add the dock menu button if this pane is a dock
let dock_icon = icon_for_dock_anchor(anchor);
tab_bar_button(1, dock_icon, cx, |position| DeployDockMenu { position })
})
.unwrap_or_else(|| {
// Add the split menu if this pane is not a dock
tab_bar_button(2, "icons/split_12.svg", cx, |position| DeploySplitMenu {
position,
})
}),
)
// Add the close dock button if this pane is a dock
.with_children(
self.docked
.map(|_| tab_bar_button(3, "icons/x_mark_thin_8.svg", cx, |_| HideDock)),
)
.contained()
.with_style(theme.workspace.tab_bar.pane_button_container)
.flex(1., false)
.boxed()
}
pub fn handle_dropped_item(
event: MouseUp,
pane: &WeakViewHandle<Pane>,
index: usize,
allow_same_pane: bool,
split_margin: Option<f32>,
cx: &mut EventContext,
) {
if let Some((_, dragged_item)) = cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedItem>(cx.window_id)
{
if pane != &dragged_item.pane || allow_same_pane {
if let Some(split_direction) = split_margin
.and_then(|margin| Self::drop_split_direction(event.position, event.region, margin))
{
cx.dispatch_action(SplitWithItem {
item_id_to_move: dragged_item.item.id(),
pane_to_split: pane.clone(),
split_direction,
});
} else if pane != &dragged_item.pane || allow_same_pane {
// If no split margin or not close enough to the edge, just move the item
cx.dispatch_action(MoveItem {
item_id: dragged_item.item.id(),
from: dragged_item.pane.clone(),
@ -1343,18 +1391,39 @@ impl Pane {
}
}
fn tab_overlay_color(
hovered: bool,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Option<Color> {
fn drop_split_direction(
position: Vector2F,
region: RectF,
split_margin: f32,
) -> Option<SplitDirection> {
let mut min_direction = None;
let mut min_distance = split_margin;
for direction in SplitDirection::all() {
let edge_distance =
(direction.edge(region) - direction.axis().component(position)).abs();
if edge_distance < min_distance {
min_direction = Some(direction);
min_distance = edge_distance;
}
}
min_direction
}
fn tab_overlay_color(hovered: bool, cx: &mut RenderContext<Self>) -> Option<Color> {
if hovered
&& cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedItem>(cx.window_id())
.is_some()
{
Some(theme.workspace.tab_bar.drop_target_overlay_color)
Some(
cx.global::<Settings>()
.theme
.workspace
.drop_target_overlay_color,
)
} else {
None
}
@ -1389,55 +1458,7 @@ impl View for Pane {
// Render pane buttons
let theme = cx.global::<Settings>().theme.clone();
if self.is_active {
tab_row.add_child(
Flex::row()
// New menu
.with_child(tab_bar_button(
0,
"icons/plus_12.svg",
cx,
|position| DeployNewMenu { position },
))
.with_child(
self.docked
.map(|anchor| {
// Add the dock menu button if this pane is a dock
let dock_icon =
icon_for_dock_anchor(anchor);
tab_bar_button(
1,
dock_icon,
cx,
|position| DeployDockMenu { position },
)
})
.unwrap_or_else(|| {
// Add the split menu if this pane is not a dock
tab_bar_button(
2,
"icons/split_12.svg",
cx,
|position| DeploySplitMenu { position },
)
}),
)
// Add the close dock button if this pane is a dock
.with_children(self.docked.map(|_| {
tab_bar_button(
3,
"icons/x_mark_thin_8.svg",
cx,
|_| HideDock,
)
}))
.contained()
.with_style(
theme.workspace.tab_bar.pane_button_container,
)
.flex(1., false)
.boxed(),
)
tab_row.add_child(self.render_tab_bar_buttons(&theme, cx))
}
tab_row
@ -1453,25 +1474,66 @@ impl View for Pane {
MouseEventHandler::<PaneContentTabDropTarget>::above(
0,
cx,
|_, cx| {
Flex::column()
|state, cx| {
let overlay_color = Self::tab_overlay_color(true, cx);
let drag_position = cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedItem>(cx.window_id())
.map(|_| state.mouse_position());
Stack::new()
.with_child(
ChildView::new(&self.toolbar, cx)
.expanded()
.boxed(),
)
.with_child(
ChildView::new(active_item, cx)
.flex(1., true)
Flex::column()
.with_child(
ChildView::new(&self.toolbar, cx)
.expanded()
.boxed(),
)
.with_child(
ChildView::new(active_item, cx)
.flex(1., true)
.boxed(),
)
.boxed(),
)
.with_children(drag_position.map(|drag_position| {
Canvas::new(move |region, _, cx| {
let overlay_region =
if let Some(split_direction) =
Self::drop_split_direction(
drag_position,
region,
100., /* Replace with theme value */
)
{
split_direction.along_edge(region, 100.)
} else {
region
};
cx.scene.push_quad(Quad {
bounds: overlay_region,
background: overlay_color,
border: Default::default(),
corner_radius: 0.,
});
})
.boxed()
}))
.boxed()
},
)
.on_up(MouseButton::Left, {
let pane = cx.handle();
move |_, cx: &mut EventContext| {
Pane::handle_dropped_item(&pane, drop_index, false, cx)
move |event, cx| {
Pane::handle_dropped_item(
event,
&pane,
drop_index,
false,
Some(100.), /* Use theme value */
cx,
)
}
})
.flex(1., true)
@ -1493,8 +1555,8 @@ impl View for Pane {
})
.on_up(MouseButton::Left, {
let pane = this.clone();
move |_, cx: &mut EventContext| {
Pane::handle_dropped_item(&pane, 0, true, cx)
move |event, cx| {
Pane::handle_dropped_item(event, &pane, 0, true, None, cx)
}
})
.boxed()

View file

@ -2,7 +2,9 @@ use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
use gpui::{
elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
elements::*,
geometry::{rect::RectF, vector::Vector2F},
Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
};
use project::Project;
use serde::Deserialize;
@ -263,9 +265,7 @@ impl PaneAxis {
new_pane: &ViewHandle<Pane>,
direction: SplitDirection,
) -> Result<()> {
use SplitDirection::*;
for (idx, member) in self.members.iter_mut().enumerate() {
for (mut idx, member) in self.members.iter_mut().enumerate() {
match member {
Member::Axis(axis) => {
if axis.split(old_pane, new_pane, direction).is_ok() {
@ -274,15 +274,12 @@ impl PaneAxis {
}
Member::Pane(pane) => {
if pane == old_pane {
if direction.matches_axis(self.axis) {
match direction {
Up | Left => {
self.members.insert(idx, Member::Pane(new_pane.clone()));
}
Down | Right => {
self.members.insert(idx + 1, Member::Pane(new_pane.clone()));
}
if direction.axis() == self.axis {
if direction.increasing() {
idx += 1;
}
self.members.insert(idx, Member::Pane(new_pane.clone()));
} else {
*member =
Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
@ -374,187 +371,46 @@ pub enum SplitDirection {
}
impl SplitDirection {
fn matches_axis(self, orientation: Axis) -> bool {
use Axis::*;
use SplitDirection::*;
pub fn all() -> [Self; 4] {
[Self::Up, Self::Down, Self::Left, Self::Right]
}
pub fn edge(&self, rect: RectF) -> f32 {
match self {
Up | Down => match orientation {
Vertical => true,
Horizontal => false,
},
Left | Right => match orientation {
Vertical => false,
Horizontal => true,
},
Self::Up => rect.min_y(),
Self::Down => rect.max_y(),
Self::Left => rect.min_x(),
Self::Right => rect.max_x(),
}
}
// Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection
pub fn along_edge(&self, rect: RectF, size: f32) -> RectF {
match self {
Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)),
Self::Down => RectF::new(
rect.lower_left() - Vector2F::new(0., size),
Vector2F::new(rect.width(), size),
),
Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())),
Self::Right => RectF::new(
rect.upper_right() - Vector2F::new(size, 0.),
Vector2F::new(size, rect.height()),
),
}
}
pub fn axis(&self) -> Axis {
match self {
Self::Up | Self::Down => Axis::Vertical,
Self::Left | Self::Right => Axis::Horizontal,
}
}
pub fn increasing(&self) -> bool {
match self {
Self::Left | Self::Up => false,
Self::Down | Self::Right => true,
}
}
}
#[cfg(test)]
mod tests {
// use super::*;
// use serde_json::json;
// #[test]
// fn test_split_and_remove() -> Result<()> {
// let mut group = PaneGroup::new(1);
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "pane",
// "paneId": 1,
// })
// );
// group.split(1, 2, SplitDirection::Right)?;
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "axis",
// "orientation": "horizontal",
// "members": [
// {"type": "pane", "paneId": 1},
// {"type": "pane", "paneId": 2},
// ]
// })
// );
// group.split(2, 3, SplitDirection::Up)?;
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "axis",
// "orientation": "horizontal",
// "members": [
// {"type": "pane", "paneId": 1},
// {
// "type": "axis",
// "orientation": "vertical",
// "members": [
// {"type": "pane", "paneId": 3},
// {"type": "pane", "paneId": 2},
// ]
// },
// ]
// })
// );
// group.split(1, 4, SplitDirection::Right)?;
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "axis",
// "orientation": "horizontal",
// "members": [
// {"type": "pane", "paneId": 1},
// {"type": "pane", "paneId": 4},
// {
// "type": "axis",
// "orientation": "vertical",
// "members": [
// {"type": "pane", "paneId": 3},
// {"type": "pane", "paneId": 2},
// ]
// },
// ]
// })
// );
// group.split(2, 5, SplitDirection::Up)?;
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "axis",
// "orientation": "horizontal",
// "members": [
// {"type": "pane", "paneId": 1},
// {"type": "pane", "paneId": 4},
// {
// "type": "axis",
// "orientation": "vertical",
// "members": [
// {"type": "pane", "paneId": 3},
// {"type": "pane", "paneId": 5},
// {"type": "pane", "paneId": 2},
// ]
// },
// ]
// })
// );
// assert_eq!(true, group.remove(5)?);
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "axis",
// "orientation": "horizontal",
// "members": [
// {"type": "pane", "paneId": 1},
// {"type": "pane", "paneId": 4},
// {
// "type": "axis",
// "orientation": "vertical",
// "members": [
// {"type": "pane", "paneId": 3},
// {"type": "pane", "paneId": 2},
// ]
// },
// ]
// })
// );
// assert_eq!(true, group.remove(4)?);
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "axis",
// "orientation": "horizontal",
// "members": [
// {"type": "pane", "paneId": 1},
// {
// "type": "axis",
// "orientation": "vertical",
// "members": [
// {"type": "pane", "paneId": 3},
// {"type": "pane", "paneId": 2},
// ]
// },
// ]
// })
// );
// assert_eq!(true, group.remove(3)?);
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "axis",
// "orientation": "horizontal",
// "members": [
// {"type": "pane", "paneId": 1},
// {"type": "pane", "paneId": 2},
// ]
// })
// );
// assert_eq!(true, group.remove(2)?);
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "pane",
// "paneId": 1,
// })
// );
// assert_eq!(false, group.remove(1)?);
// assert_eq!(
// serde_json::to_value(&group)?,
// json!({
// "type": "pane",
// "paneId": 1,
// })
// );
// Ok(())
// }
}

View file

@ -100,7 +100,7 @@ actions!(
ToggleLeftSidebar,
ToggleRightSidebar,
NewTerminal,
NewSearch
NewSearch,
]
);
@ -126,6 +126,12 @@ pub struct OpenSharedScreen {
pub peer_id: PeerId,
}
pub struct SplitWithItem {
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
item_id_to_move: usize,
}
impl_internal_actions!(
workspace,
[
@ -133,7 +139,8 @@ impl_internal_actions!(
ToggleFollow,
JoinProject,
OpenSharedScreen,
RemoveWorktreeFromProject
RemoveWorktreeFromProject,
SplitWithItem,
]
);
impl_actions!(workspace, [ActivatePane]);
@ -206,6 +213,22 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
workspace.toggle_sidebar(SidebarSide::Right, cx);
});
cx.add_action(Workspace::activate_pane_at_index);
cx.add_action(
|workspace: &mut Workspace,
SplitWithItem {
pane_to_split,
item_id_to_move,
split_direction,
}: &_,
cx| {
workspace.split_pane_with_item(
pane_to_split.clone(),
*item_id_to_move,
*split_direction,
cx,
)
},
);
let client = &app_state.client;
client.add_view_request_handler(Workspace::handle_follow);
@ -1950,6 +1973,35 @@ impl Workspace {
})
}
pub fn split_pane_with_item(
&mut self,
pane_to_split: WeakViewHandle<Pane>,
item_id_to_move: usize,
split_direction: SplitDirection,
cx: &mut ViewContext<Self>,
) {
if let Some(pane_to_split) = pane_to_split.upgrade(cx) {
if &pane_to_split == self.dock_pane() {
warn!("Can't split dock pane.");
return;
}
let new_pane = self.add_pane(cx);
Pane::move_item(
self,
pane_to_split.clone(),
new_pane.clone(),
item_id_to_move,
0,
cx,
);
self.center
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
cx.notify();
}
}
fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
if self.center.remove(&pane).unwrap() {
self.panes.retain(|p| p != &pane);