Merge pull request #1791 from zed-industries/drag-tabs-more-places

Drag tabs more places
This commit is contained in:
Kay Simmons 2022-10-25 00:34:50 -07:00 committed by GitHub
commit 8bd9577318
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 599 additions and 496 deletions

1
Cargo.lock generated
View file

@ -1736,6 +1736,7 @@ dependencies = [
"collections",
"context_menu",
"ctor",
"drag_and_drop",
"env_logger",
"futures 0.3.24",
"fuzzy",

View file

@ -431,6 +431,12 @@
"shift-escape": "dock::HideDock"
}
},
{
"context": "Pane",
"bindings": {
"cmd-escape": "dock::MoveActiveItemToDock"
}
},
{
"context": "ProjectPanel",
"bindings": {

View file

@ -125,7 +125,7 @@ impl<V: View> DragAndDrop<V> {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
});
cx.propogate_event();
cx.propagate_event();
})
.on_up_out(MouseButton::Left, |_, cx| {
cx.defer(|cx| {

View file

@ -20,6 +20,7 @@ test-support = [
]
[dependencies]
drag_and_drop = { path = "../drag_and_drop" }
text = { path = "../text" }
clock = { path = "../clock" }
collections = { path = "../collections" }

View file

@ -1,5 +1,6 @@
use std::{cell::RefCell, rc::Rc, time::Instant};
use drag_and_drop::DragAndDrop;
use futures::StreamExt;
use indoc::indoc;
use unindent::Unindent;
@ -472,6 +473,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
#[gpui::test]
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
cx.set_global(DragAndDrop::<Workspace>::default());
use workspace::Item;
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);

View file

@ -137,7 +137,7 @@ impl EditorElement {
gutter_bounds,
cx,
) {
cx.propogate_event();
cx.propagate_event();
}
}
})
@ -150,7 +150,7 @@ impl EditorElement {
text_bounds,
cx,
) {
cx.propogate_event();
cx.propagate_event();
}
}
})
@ -167,7 +167,7 @@ impl EditorElement {
text_bounds,
cx,
) {
cx.propogate_event()
cx.propagate_event()
}
}
})
@ -182,7 +182,7 @@ impl EditorElement {
text_bounds,
cx,
) {
cx.propogate_event()
cx.propagate_event()
}
}
})
@ -190,7 +190,7 @@ impl EditorElement {
let position_map = position_map.clone();
move |e, cx| {
if !Self::mouse_moved(e.platform_event, &position_map, text_bounds, cx) {
cx.propogate_event()
cx.propagate_event()
}
}
})
@ -199,7 +199,7 @@ impl EditorElement {
move |e, cx| {
if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
{
cx.propogate_event()
cx.propagate_event()
}
}
}),

View file

@ -277,7 +277,7 @@ impl Element for Flex {
cx.notify();
} else {
cx.propogate_event();
cx.propagate_event();
}
}
})

View file

@ -23,10 +23,13 @@ pub struct MouseEventHandler<Tag: 'static> {
hoverable: bool,
notify_on_hover: bool,
notify_on_click: bool,
above: bool,
padding: Padding,
_tag: PhantomData<Tag>,
}
/// Element which provides a render_child callback with a MouseState and paints a mouse
/// region under (or above) it for easy mouse event handling.
impl<Tag> MouseEventHandler<Tag> {
pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
where
@ -45,11 +48,25 @@ impl<Tag> MouseEventHandler<Tag> {
notify_on_hover,
notify_on_click,
hoverable: true,
above: false,
padding: Default::default(),
_tag: PhantomData,
}
}
/// Modifies the MouseEventHandler to render the MouseRegion above the child element. Useful
/// for drag and drop handling and similar events which should be captured before the child
/// gets the opportunity
pub fn above<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
where
V: View,
F: FnOnce(&mut MouseState, &mut RenderContext<V>) -> ElementBox,
{
let mut handler = Self::new(region_id, cx, render_child);
handler.above = true;
handler
}
pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
self.cursor_style = Some(cursor);
self
@ -149,6 +166,29 @@ impl<Tag> MouseEventHandler<Tag> {
)
.round_out()
}
fn paint_regions(&self, bounds: RectF, visible_bounds: RectF, cx: &mut PaintContext) {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
let hit_bounds = self.hit_bounds(visible_bounds);
if let Some(style) = self.cursor_style {
cx.scene.push_cursor_region(CursorRegion {
bounds: hit_bounds,
style,
});
}
cx.scene.push_mouse_region(
MouseRegion::from_handlers::<Tag>(
cx.current_view_id(),
self.region_id,
hit_bounds,
self.handlers.clone(),
)
.with_hoverable(self.hoverable)
.with_notify_on_hover(self.notify_on_hover)
.with_notify_on_click(self.notify_on_click),
);
}
}
impl<Tag> Element for MouseEventHandler<Tag> {
@ -170,28 +210,16 @@ impl<Tag> Element for MouseEventHandler<Tag> {
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
let hit_bounds = self.hit_bounds(visible_bounds);
if let Some(style) = self.cursor_style {
cx.scene.push_cursor_region(CursorRegion {
bounds: hit_bounds,
style,
if self.above {
self.child.paint(bounds.origin(), visible_bounds, cx);
cx.paint_layer(None, |cx| {
self.paint_regions(bounds, visible_bounds, cx);
});
} else {
self.paint_regions(bounds, visible_bounds, cx);
self.child.paint(bounds.origin(), visible_bounds, cx);
}
cx.scene.push_mouse_region(
MouseRegion::from_handlers::<Tag>(
cx.current_view_id(),
self.region_id,
hit_bounds,
self.handlers.clone(),
)
.with_hoverable(self.hoverable)
.with_notify_on_hover(self.notify_on_hover)
.with_notify_on_click(self.notify_on_click),
);
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn rect_for_text_range(

View file

@ -204,25 +204,24 @@ impl Element for Overlay {
OverlayFitMode::None => {}
}
cx.scene.push_stacking_context(None);
cx.paint_stacking_context(None, |cx| {
if self.hoverable {
enum OverlayHoverCapture {}
// Block hovers in lower stacking contexts
cx.scene
.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
cx.current_view_id(),
cx.current_view_id(),
bounds,
));
}
if self.hoverable {
enum OverlayHoverCapture {}
// Block hovers in lower stacking contexts
cx.scene
.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
cx.current_view_id(),
cx.current_view_id(),
bounds,
));
}
self.child.paint(
bounds.origin(),
RectF::new(Vector2F::zero(), cx.window_size),
cx,
);
cx.scene.pop_stacking_context();
self.child.paint(
bounds.origin(),
RectF::new(Vector2F::zero(), cx.window_size),
cx,
);
});
}
fn rect_for_text_range(

View file

@ -7,6 +7,8 @@ use crate::{
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
};
/// Element which renders it's children in a stack on top of each other.
/// The first child determines the size of the others.
#[derive(Default)]
pub struct Stack {
children: Vec<ElementBox>,
@ -24,13 +26,20 @@ impl Element for Stack {
fn layout(
&mut self,
constraint: SizeConstraint,
mut constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.min;
for child in &mut self.children {
size = size.max(child.layout(constraint, cx));
let mut children = self.children.iter_mut();
if let Some(bottom_child) = children.next() {
size = bottom_child.layout(constraint, cx);
constraint = SizeConstraint::strict(size);
}
for child in children {
child.layout(constraint, cx);
}
(size, ())
}
@ -42,9 +51,9 @@ impl Element for Stack {
cx: &mut PaintContext,
) -> Self::PaintState {
for child in &mut self.children {
cx.scene.push_layer(None);
child.paint(bounds.origin(), visible_bounds, cx);
cx.scene.pop_layer();
cx.paint_layer(None, |cx| {
child.paint(bounds.origin(), visible_bounds, cx);
});
}
}

View file

@ -304,7 +304,7 @@ impl Element for UniformList {
},
cx| {
if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
cx.propogate_event();
cx.propagate_event();
}
}
}),

View file

@ -402,10 +402,10 @@ impl Presenter {
MouseEvent::Down(_) | MouseEvent::Up(_) => {
for (region, _) in self.mouse_regions.iter().rev() {
if region.bounds.contains_point(self.mouse_position) {
valid_regions.push(region.clone());
if region.notify_on_click {
notified_views.insert(region.id().view_id());
}
valid_regions.push(region.clone());
}
}
}
@ -485,9 +485,7 @@ impl Presenter {
event_cx.handled = true;
event_cx.with_current_view(valid_region.id().view_id(), {
let region_event = mouse_event.clone();
|cx| {
callback(region_event, cx);
}
|cx| callback(region_event, cx)
});
}
@ -707,6 +705,16 @@ impl<'a> PaintContext<'a> {
}
}
#[inline]
pub fn paint_stacking_context<F>(&mut self, clip_bounds: Option<RectF>, f: F)
where
F: FnOnce(&mut Self),
{
self.scene.push_stacking_context(clip_bounds);
f(self);
self.scene.pop_stacking_context();
}
#[inline]
pub fn paint_layer<F>(&mut self, clip_bounds: Option<RectF>, f: F)
where
@ -794,7 +802,7 @@ impl<'a> EventContext<'a> {
self.notify_count
}
pub fn propogate_event(&mut self) {
pub fn propagate_event(&mut self) {
self.handled = false;
}
}
@ -853,6 +861,13 @@ impl Axis {
Self::Vertical => Self::Horizontal,
}
}
pub fn component(&self, point: Vector2F) -> f32 {
match self {
Self::Horizontal => point.x(),
Self::Vertical => point.y(),
}
}
}
impl ToJson for Axis {

View file

@ -62,6 +62,7 @@ pub struct Workspace {
pub joining_project_message: ContainedText,
pub external_location_message: ContainedText,
pub dock: Dock,
pub drop_target_overlay_color: Color,
}
#[derive(Clone, Deserialize, Default)]
@ -150,7 +151,6 @@ pub struct TabBar {
pub inactive_pane: TabStyles,
pub dragged_tab: Tab,
pub height: f32,
pub drop_target_overlay_color: Color,
}
impl TabBar {

View file

@ -1,7 +1,7 @@
use collections::HashMap;
use gpui::{
actions,
elements::{ChildView, Container, Empty, MouseEventHandler, Side, Svg},
elements::{ChildView, Container, Empty, MouseEventHandler, ParentElement, Side, Stack, Svg},
impl_internal_actions, Border, CursorStyle, Element, ElementBox, Entity, MouseButton,
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
};
@ -9,7 +9,9 @@ use serde::Deserialize;
use settings::{DockAnchor, Settings};
use theme::Theme;
use crate::{sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace};
use crate::{
handle_dropped_item, sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace,
};
#[derive(PartialEq, Clone, Deserialize)]
pub struct MoveDock(pub DockAnchor);
@ -24,7 +26,8 @@ actions!(
HideDock,
AnchorDockRight,
AnchorDockBottom,
ExpandDock
ExpandDock,
MoveActiveItemToDock,
]
);
impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
@ -48,6 +51,30 @@ pub fn init(cx: &mut MutableAppContext) {
Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx)
},
);
cx.add_action(
|workspace: &mut Workspace, _: &MoveActiveItemToDock, cx: &mut ViewContext<Workspace>| {
if let Some(active_item) = workspace.active_item(cx) {
let item_id = active_item.id();
let from = workspace.active_pane();
let to = workspace.dock_pane();
if from.id() == to.id() {
return;
}
let destination_index = to.read(cx).items_len() + 1;
Pane::move_item(
workspace,
from.clone(),
to.clone(),
item_id,
destination_index,
cx,
);
}
},
);
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
@ -283,25 +310,34 @@ impl Dock {
DockAnchor::Expanded => {
enum ExpandedDockWash {}
enum ExpandedDockPane {}
Container::new(
MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_state, cx| {
Stack::new()
.with_child(
// Render wash under the dock which when clicked hides it
MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_, _| {
Empty::new()
.contained()
.with_background_color(style.wash_color)
.boxed()
})
.capture_all()
.on_down(MouseButton::Left, |_, cx| {
cx.dispatch_action(HideDock);
})
.with_cursor_style(CursorStyle::Arrow)
.boxed(),
)
.with_child(
MouseEventHandler::<ExpandedDockPane>::new(0, cx, |_state, cx| {
ChildView::new(&self.pane, cx).boxed()
})
// Make sure all events directly under the dock pane
// are captured
.capture_all()
.contained()
.with_style(style.maximized)
.boxed()
})
.capture_all()
.on_down(MouseButton::Left, |_, cx| {
cx.dispatch_action(HideDock);
})
.with_cursor_style(CursorStyle::Arrow)
.boxed(),
)
.with_background_color(style.wash_color)
.boxed()
.boxed(),
)
.boxed()
}
})
}
@ -338,9 +374,11 @@ impl View for ToggleDockButton {
return Empty::new().boxed();
}
let dock_position = workspace.unwrap().read(cx).dock.position;
let workspace = workspace.unwrap();
let dock_position = workspace.read(cx).dock.position;
let theme = cx.global::<Settings>().theme.clone();
let button = MouseEventHandler::<Self>::new(0, cx, {
let theme = theme.clone();
move |state, _| {
@ -361,7 +399,12 @@ impl View for ToggleDockButton {
.boxed()
}
})
.with_cursor_style(CursorStyle::PointingHand);
.with_cursor_style(CursorStyle::PointingHand)
.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;
handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
});
if dock_position.is_visible() {
button

View file

@ -1,3 +1,5 @@
mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection};
use crate::{
dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
@ -7,11 +9,11 @@ use crate::{
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use context_menu::{ContextMenu, ContextMenuItem};
use drag_and_drop::{DragAndDrop, Draggable};
use drag_and_drop::Draggable;
pub use dragged_item_receiver::{dragged_item_receiver, handle_dropped_item};
use futures::StreamExt;
use gpui::{
actions,
color::Color,
elements::*,
geometry::{
rect::RectF,
@ -98,7 +100,7 @@ impl_internal_actions!(
DeploySplitMenu,
DeployNewMenu,
DeployDockMenu,
MoveItem
MoveItem,
]
);
@ -575,6 +577,10 @@ impl Pane {
}
}
pub fn items_len(&self) -> usize {
self.items.len()
}
pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
self.items.iter()
}
@ -943,11 +949,11 @@ impl Pane {
}
}
fn move_item(
pub fn move_item(
workspace: &mut Workspace,
from: ViewHandle<Pane>,
to: ViewHandle<Pane>,
item_to_move: usize,
item_id_to_move: usize,
destination_index: usize,
cx: &mut ViewContext<Workspace>,
) {
@ -955,7 +961,7 @@ impl Pane {
.read(cx)
.items()
.enumerate()
.find(|(_, item_handle)| item_handle.id() == item_to_move);
.find(|(_, item_handle)| item_handle.id() == item_id_to_move);
if item_to_move.is_none() {
log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
@ -1051,133 +1057,107 @@ impl Pane {
fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
let theme = cx.global::<Settings>().theme.clone();
let filler_index = self.items.len();
let pane = cx.handle();
let autoscroll = if mem::take(&mut self.autoscroll) {
Some(self.active_item_index)
} else {
None
};
let pane_active = self.is_active;
enum Tabs {}
enum Tab {}
enum Filler {}
let pane = cx.handle();
MouseEventHandler::<Tabs>::new(0, cx, |_, cx| {
let autoscroll = if mem::take(&mut self.autoscroll) {
Some(self.active_item_index)
} else {
None
};
let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
for (ix, (item, detail)) in self
.items
.iter()
.cloned()
.zip(self.tab_details(cx))
.enumerate()
{
let detail = if detail == 0 { None } else { Some(detail) };
let tab_active = ix == self.active_item_index;
let pane_active = self.is_active;
row.add_child({
enum Tab {}
dragged_item_receiver::<Tab, _>(ix, ix, true, None, cx, {
let item = item.clone();
let pane = pane.clone();
let detail = detail.clone();
let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
for (ix, (item, detail)) in self
.items
.iter()
.cloned()
.zip(self.tab_details(cx))
.enumerate()
{
let detail = if detail == 0 { None } else { Some(detail) };
let tab_active = ix == self.active_item_index;
row.add_child({
MouseEventHandler::<Tab>::new(ix, cx, {
let item = item.clone();
let pane = pane.clone();
let detail = detail.clone();
let theme = cx.global::<Settings>().theme.clone();
move |mouse_state, cx| {
let tab_style = theme.workspace.tab_bar.tab_style(pane_active, tab_active);
let hovered = mouse_state.hovered();
Self::render_tab(&item, pane, ix == 0, detail, hovered, tab_style, cx)
}
})
.with_cursor_style(if pane_active && tab_active {
CursorStyle::Arrow
} else {
CursorStyle::PointingHand
})
.on_down(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ActivateItem(ix));
cx.propagate_event();
})
.on_click(MouseButton::Middle, {
let item = item.clone();
let pane = pane.clone();
move |_, cx: &mut EventContext| {
cx.dispatch_action(CloseItem {
item_id: item.id(),
pane: pane.clone(),
})
}
})
.as_draggable(
DraggedItem {
item,
pane: pane.clone(),
},
{
let theme = cx.global::<Settings>().theme.clone();
move |mouse_state, cx| {
let tab_style =
theme.workspace.tab_bar.tab_style(pane_active, tab_active);
let hovered = mouse_state.hovered();
let detail = detail.clone();
move |dragged_item, cx: &mut RenderContext<Workspace>| {
let tab_style = &theme.workspace.tab_bar.dragged_tab;
Self::render_tab(
&item,
pane,
ix == 0,
&dragged_item.item,
dragged_item.pane.clone(),
false,
detail,
hovered,
Self::tab_overlay_color(hovered, theme.as_ref(), cx),
tab_style,
false,
&tab_style,
cx,
)
}
})
.with_cursor_style(if pane_active && tab_active {
CursorStyle::Arrow
} else {
CursorStyle::PointingHand
})
.on_down(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ActivateItem(ix));
})
.on_click(MouseButton::Middle, {
let item = item.clone();
let pane = pane.clone();
move |_, cx: &mut EventContext| {
cx.dispatch_action(CloseItem {
item_id: item.id(),
pane: pane.clone(),
})
}
})
.on_up(MouseButton::Left, {
let pane = pane.clone();
move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, ix, cx)
})
.as_draggable(
DraggedItem {
item,
pane: pane.clone(),
},
{
let theme = cx.global::<Settings>().theme.clone();
},
)
.boxed()
})
}
let detail = detail.clone();
move |dragged_item, cx: &mut RenderContext<Workspace>| {
let tab_style = &theme.workspace.tab_bar.dragged_tab;
Self::render_tab(
&dragged_item.item,
dragged_item.pane.clone(),
false,
detail,
false,
None,
&tab_style,
cx,
)
}
},
)
// Use the inactive tab style along with the current pane's active status to decide how to render
// the filler
let filler_index = self.items.len();
let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
enum Filler {}
row.add_child(
dragged_item_receiver::<Filler, _>(0, filler_index, true, None, cx, |_, _| {
Empty::new()
.contained()
.with_style(filler_style.container)
.with_border(filler_style.container.border)
.boxed()
})
}
})
.flex(1., true)
.named("filler"),
);
// Use the inactive tab style along with the current pane's active status to decide how to render
// the filler
let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
row.add_child(
MouseEventHandler::<Filler>::new(0, cx, |mouse_state, cx| {
let mut filler = Empty::new()
.contained()
.with_style(filler_style.container)
.with_border(filler_style.container.border);
if let Some(overlay) =
Self::tab_overlay_color(mouse_state.hovered(), &theme, cx)
{
filler = filler.with_overlay_color(overlay);
}
filler.boxed()
})
.flex(1., true)
.named("filler"),
);
row.boxed()
})
.on_up(MouseButton::Left, move |_, cx| {
Pane::handle_dropped_item(&pane, filler_index, cx)
})
row
}
fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
@ -1223,7 +1203,6 @@ impl Pane {
first: bool,
detail: Option<usize>,
hovered: bool,
overlay: Option<Color>,
tab_style: &theme::Tab,
cx: &mut RenderContext<V>,
) -> ElementBox {
@ -1233,7 +1212,7 @@ impl Pane {
container.border.left = false;
}
let mut tab = Flex::row()
Flex::row()
.with_child(
Align::new({
let diameter = 7.0;
@ -1301,7 +1280,6 @@ impl Pane {
})
}
})
.on_click(MouseButton::Middle, |_, cx| cx.propogate_event())
.named("close-tab-icon")
} else {
Empty::new().boxed()
@ -1312,46 +1290,46 @@ impl Pane {
.boxed(),
)
.contained()
.with_style(container);
if let Some(overlay) = overlay {
tab = tab.with_overlay_color(overlay);
}
tab.constrained().with_height(tab_style.height).boxed()
.with_style(container)
.constrained()
.with_height(tab_style.height)
.boxed()
}
fn handle_dropped_item(pane: &WeakViewHandle<Pane>, index: usize, cx: &mut EventContext) {
if let Some((_, dragged_item)) = cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedItem>(cx.window_id)
{
cx.dispatch_action(MoveItem {
item_id: dragged_item.item.id(),
from: dragged_item.pane.clone(),
to: pane.clone(),
destination_index: index,
})
} else {
cx.propogate_event();
}
}
fn tab_overlay_color(
hovered: bool,
fn render_tab_bar_buttons(
&mut self,
theme: &Theme,
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)
} else {
None
}
) -> 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()
}
}
@ -1376,60 +1354,12 @@ impl View for Pane {
Flex::column()
.with_child({
let mut tab_row = Flex::row()
.with_child(self.render_tabs(cx).flex(1.0, true).named("tabs"));
.with_child(self.render_tabs(cx).flex(1., true).named("tabs"));
// 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
@ -1440,14 +1370,39 @@ impl View for Pane {
.flex(1., false)
.named("tab bar")
})
.with_child(ChildView::new(&self.toolbar, cx).expanded().boxed())
.with_child(ChildView::new(active_item, cx).flex(1., true).boxed())
.with_child({
enum PaneContentTabDropTarget {}
dragged_item_receiver::<PaneContentTabDropTarget, _>(
0,
self.active_item_index + 1,
false,
Some(100.),
cx,
{
let toolbar = self.toolbar.clone();
move |_, cx| {
Flex::column()
.with_child(
ChildView::new(&toolbar, cx).expanded().boxed(),
)
.with_child(
ChildView::new(active_item, cx)
.flex(1., true)
.boxed(),
)
.boxed()
}
},
)
.flex(1., true)
.boxed()
})
.boxed()
} else {
enum EmptyPane {}
let theme = cx.global::<Settings>().theme.clone();
MouseEventHandler::<EmptyPane>::new(0, cx, |_, _| {
dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, _| {
Empty::new()
.contained()
.with_background_color(theme.workspace.background)
@ -1456,10 +1411,6 @@ impl View for Pane {
.on_down(MouseButton::Left, |_, cx| {
cx.focus_parent_view();
})
.on_up(MouseButton::Left, {
let pane = this.clone();
move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, 0, cx)
})
.boxed()
}
})

View file

@ -0,0 +1,142 @@
use drag_and_drop::DragAndDrop;
use gpui::{
color::Color,
elements::{Canvas, MouseEventHandler, ParentElement, Stack},
geometry::{rect::RectF, vector::Vector2F},
scene::MouseUp,
AppContext, Element, ElementBox, EventContext, MouseButton, MouseState, Quad, RenderContext,
WeakViewHandle,
};
use settings::Settings;
use crate::{MoveItem, Pane, SplitDirection, SplitWithItem, Workspace};
use super::DraggedItem;
pub fn dragged_item_receiver<Tag, F>(
region_id: usize,
drop_index: usize,
allow_same_pane: bool,
split_margin: Option<f32>,
cx: &mut RenderContext<Pane>,
render_child: F,
) -> MouseEventHandler<Tag>
where
Tag: 'static,
F: FnOnce(&mut MouseState, &mut RenderContext<Pane>) -> ElementBox,
{
MouseEventHandler::<Tag>::above(region_id, cx, |state, cx| {
// Observing hovered will cause a render when the mouse enters regardless
// of if mouse position was accessed before
let hovered = state.hovered();
let drag_position = cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedItem>(cx.window_id())
.filter(|_| hovered)
.map(|(drag_position, _)| drag_position);
Stack::new()
.with_child(render_child(state, cx))
.with_children(drag_position.map(|drag_position| {
Canvas::new(move |bounds, _, cx| {
if bounds.contains_point(drag_position) {
let overlay_region = split_margin
.and_then(|split_margin| {
drop_split_direction(drag_position, bounds, split_margin)
.map(|dir| (dir, split_margin))
})
.map(|(dir, margin)| dir.along_edge(bounds, margin))
.unwrap_or(bounds);
cx.paint_stacking_context(None, |cx| {
cx.scene.push_quad(Quad {
bounds: overlay_region,
background: Some(overlay_color(cx)),
border: Default::default(),
corner_radius: 0.,
});
});
}
})
.boxed()
}))
.boxed()
})
.on_up(MouseButton::Left, {
let pane = cx.handle();
move |event, cx| {
handle_dropped_item(event, &pane, drop_index, allow_same_pane, split_margin, cx);
cx.notify();
}
})
.on_move(|_, cx| {
if cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedItem>(cx.window_id())
.is_some()
{
cx.notify();
}
})
}
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 let Some(split_direction) = split_margin
.and_then(|margin| drop_split_direction(event.position, event.region, margin))
{
cx.dispatch_action(SplitWithItem {
from: dragged_item.pane.clone(),
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(),
to: pane.clone(),
destination_index: index,
})
}
} else {
cx.propagate_event();
}
}
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 overlay_color(cx: &AppContext) -> Color {
cx.global::<Settings>()
.theme
.workspace
.drop_target_overlay_color
}

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,14 @@ pub struct OpenSharedScreen {
pub peer_id: PeerId,
}
#[derive(Clone, PartialEq)]
pub struct SplitWithItem {
from: WeakViewHandle<Pane>,
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
item_id_to_move: usize,
}
impl_internal_actions!(
workspace,
[
@ -133,7 +141,8 @@ impl_internal_actions!(
ToggleFollow,
JoinProject,
OpenSharedScreen,
RemoveWorktreeFromProject
RemoveWorktreeFromProject,
SplitWithItem,
]
);
impl_actions!(workspace, [ActivatePane]);
@ -206,6 +215,24 @@ 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 {
from,
pane_to_split,
item_id_to_move,
split_direction,
}: &_,
cx| {
workspace.split_pane_with_item(
from.clone(),
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 +1977,29 @@ impl Workspace {
})
}
pub fn split_pane_with_item(
&mut self,
from: WeakViewHandle<Pane>,
pane_to_split: WeakViewHandle<Pane>,
item_id_to_move: usize,
split_direction: SplitDirection,
cx: &mut ViewContext<Self>,
) {
if let Some((pane_to_split, from)) = pane_to_split.upgrade(cx).zip(from.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, from.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);
@ -3175,7 +3225,7 @@ mod tests {
cx.foreground().run_until_parked();
pane.read_with(cx, |pane, _| {
assert_eq!(pane.items().count(), 4);
assert_eq!(pane.items_len(), 4);
assert_eq!(pane.active_item().unwrap().id(), item1.id());
});
@ -3185,7 +3235,7 @@ mod tests {
assert_eq!(item1.read(cx).save_count, 1);
assert_eq!(item1.read(cx).save_as_count, 0);
assert_eq!(item1.read(cx).reload_count, 0);
assert_eq!(pane.items().count(), 3);
assert_eq!(pane.items_len(), 3);
assert_eq!(pane.active_item().unwrap().id(), item3.id());
});
@ -3195,7 +3245,7 @@ mod tests {
assert_eq!(item3.read(cx).save_count, 0);
assert_eq!(item3.read(cx).save_as_count, 0);
assert_eq!(item3.read(cx).reload_count, 1);
assert_eq!(pane.items().count(), 2);
assert_eq!(pane.items_len(), 2);
assert_eq!(pane.active_item().unwrap().id(), item4.id());
});
@ -3207,7 +3257,7 @@ mod tests {
assert_eq!(item4.read(cx).save_count, 0);
assert_eq!(item4.read(cx).save_as_count, 1);
assert_eq!(item4.read(cx).reload_count, 0);
assert_eq!(pane.items().count(), 1);
assert_eq!(pane.items_len(), 1);
assert_eq!(pane.active_item().unwrap().id(), item2.id());
});
}
@ -3309,7 +3359,7 @@ mod tests {
cx.foreground().run_until_parked();
close.await.unwrap();
left_pane.read_with(cx, |pane, _| {
assert_eq!(pane.items().count(), 0);
assert_eq!(pane.items_len(), 0);
});
}

View file

@ -811,7 +811,7 @@ mod tests {
pane.active_item().unwrap().project_path(cx),
Some(file1.clone())
);
assert_eq!(pane.items().count(), 1);
assert_eq!(pane.items_len(), 1);
});
// Open the second entry
@ -825,7 +825,7 @@ mod tests {
pane.active_item().unwrap().project_path(cx),
Some(file2.clone())
);
assert_eq!(pane.items().count(), 2);
assert_eq!(pane.items_len(), 2);
});
// Open the first entry again. The existing pane item is activated.
@ -841,7 +841,7 @@ mod tests {
pane.active_item().unwrap().project_path(cx),
Some(file1.clone())
);
assert_eq!(pane.items().count(), 2);
assert_eq!(pane.items_len(), 2);
});
// Split the pane with the first entry, then open the second entry again.

View file

@ -75,10 +75,6 @@ export default function tabBar(colorScheme: ColorScheme) {
return {
height,
background: background(layer),
dropTargetOverlayColor: withOpacity(
foreground(layer),
0.6
),
activePane: {
activeTab: activePaneActiveTab,
inactiveTab: tab,

View file

@ -227,5 +227,9 @@ export default function workspace(colorScheme: ColorScheme) {
shadow: colorScheme.modalShadow,
},
},
dropTargetOverlayColor: withOpacity(
foreground(layer, "variant"),
0.5
),
};
}