diff --git a/Cargo.lock b/Cargo.lock index c3c02aeb96..96fc7b0055 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1736,6 +1736,7 @@ dependencies = [ "collections", "context_menu", "ctor", + "drag_and_drop", "env_logger", "futures 0.3.24", "fuzzy", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 40d486c161..b4ce8c1b92 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -431,6 +431,12 @@ "shift-escape": "dock::HideDock" } }, + { + "context": "Pane", + "bindings": { + "cmd-escape": "dock::MoveActiveItemToDock" + } + }, { "context": "ProjectPanel", "bindings": { diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index 31265a1697..bb660c750f 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -125,7 +125,7 @@ impl DragAndDrop { cx.defer(|cx| { cx.update_global::(|this, cx| this.stop_dragging(cx)); }); - cx.propogate_event(); + cx.propagate_event(); }) .on_up_out(MouseButton::Left, |_, cx| { cx.defer(|cx| { diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index db634376d0..f56ed36f75 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -20,6 +20,7 @@ test-support = [ ] [dependencies] +drag_and_drop = { path = "../drag_and_drop" } text = { path = "../text" } clock = { path = "../clock" } collections = { path = "../collections" } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 58978c51f0..839f804ec3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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::::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); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d8f5c83daf..5628b886fe 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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() } } }), diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 95477c7560..129d36dadd 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -277,7 +277,7 @@ impl Element for Flex { cx.notify(); } else { - cx.propogate_event(); + cx.propagate_event(); } } }) diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index c8ba330e70..132a915f0b 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -23,10 +23,13 @@ pub struct MouseEventHandler { hoverable: bool, notify_on_hover: bool, notify_on_click: bool, + above: bool, padding: Padding, _tag: PhantomData, } +/// 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 MouseEventHandler { pub fn new(region_id: usize, cx: &mut RenderContext, render_child: F) -> Self where @@ -45,11 +48,25 @@ impl MouseEventHandler { 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(region_id: usize, cx: &mut RenderContext, render_child: F) -> Self + where + V: View, + F: FnOnce(&mut MouseState, &mut RenderContext) -> 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 MouseEventHandler { ) .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::( + 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 Element for MouseEventHandler { @@ -170,28 +210,16 @@ impl Element for MouseEventHandler { _: &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::( - 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( diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 253c88f703..15d3d764f2 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -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::( + 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::( - 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( diff --git a/crates/gpui/src/elements/stack.rs b/crates/gpui/src/elements/stack.rs index f08ce04649..3b5c19505d 100644 --- a/crates/gpui/src/elements/stack.rs +++ b/crates/gpui/src/elements/stack.rs @@ -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, @@ -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); + }); } } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index ca3601491a..0eab7f0bc9 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -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(); } } }), diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 74c2dddd66..71803a4cf0 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -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(&mut self, clip_bounds: Option, 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(&mut self, clip_bounds: Option, 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 { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index db6609fa82..eaf0972367 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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 { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index cf6c8e287b..b17a7ea22e 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -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| { + 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::::new(0, cx, |_state, cx| { + Stack::new() + .with_child( + // Render wash under the dock which when clicked hides it + MouseEventHandler::::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::::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::().theme.clone(); + let button = MouseEventHandler::::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 diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 882b501d2e..651afce1c6 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -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> { self.items.iter() } @@ -943,11 +949,11 @@ impl Pane { } } - fn move_item( + pub fn move_item( workspace: &mut Workspace, from: ViewHandle, to: ViewHandle, - item_to_move: usize, + item_id_to_move: usize, destination_index: usize, cx: &mut ViewContext, ) { @@ -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) -> impl Element { let theme = cx.global::().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::::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::(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::(ix, ix, true, None, cx, { + let item = item.clone(); + let pane = pane.clone(); + let detail = detail.clone(); - let mut row = Flex::row().scrollable::(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::::new(ix, cx, { - let item = item.clone(); - let pane = pane.clone(); - let detail = detail.clone(); + let theme = cx.global::().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::().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| { + 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::().theme.clone(); + }, + ) + .boxed() + }) + } - let detail = detail.clone(); - move |dragged_item, cx: &mut RenderContext| { - 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::(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::::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 { @@ -1223,7 +1203,6 @@ impl Pane { first: bool, detail: Option, hovered: bool, - overlay: Option, tab_style: &theme::Tab, cx: &mut RenderContext, ) -> 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, index: usize, cx: &mut EventContext) { - if let Some((_, dragged_item)) = cx - .global::>() - .currently_dragged::(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, - ) -> Option { - if hovered - && cx - .global::>() - .currently_dragged::(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::().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::( + 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::().theme.clone(); - MouseEventHandler::::new(0, cx, |_, _| { + dragged_item_receiver::(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() } }) diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs new file mode 100644 index 0000000000..a3f1285e51 --- /dev/null +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -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( + region_id: usize, + drop_index: usize, + allow_same_pane: bool, + split_margin: Option, + cx: &mut RenderContext, + render_child: F, +) -> MouseEventHandler +where + Tag: 'static, + F: FnOnce(&mut MouseState, &mut RenderContext) -> ElementBox, +{ + MouseEventHandler::::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::>() + .currently_dragged::(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::>() + .currently_dragged::(cx.window_id()) + .is_some() + { + cx.notify(); + } + }) +} + +pub fn handle_dropped_item( + event: MouseUp, + pane: &WeakViewHandle, + index: usize, + allow_same_pane: bool, + split_margin: Option, + cx: &mut EventContext, +) { + if let Some((_, dragged_item)) = cx + .global::>() + .currently_dragged::(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 { + 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::() + .theme + .workspace + .drop_target_overlay_color +} diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 6c379ffd2a..45b9af6f38 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -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, 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(()) - // } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e7752219c5..349217985c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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_to_split: WeakViewHandle, + 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, 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_to_split: WeakViewHandle, + item_id_to_move: usize, + split_direction: SplitDirection, + cx: &mut ViewContext, + ) { + 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, cx: &mut ViewContext) { 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); }); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 3319aebd09..ef504026d5 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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. diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts index bd6070e0ed..2824c43483 100644 --- a/styles/src/styleTree/tabBar.ts +++ b/styles/src/styleTree/tabBar.ts @@ -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, diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 3edb746224..50ee0c26da 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -227,5 +227,9 @@ export default function workspace(colorScheme: ColorScheme) { shadow: colorScheme.modalShadow, }, }, + dropTargetOverlayColor: withOpacity( + foreground(layer, "variant"), + 0.5 + ), }; }