From cfde3e348c2fa5b6ee16d0397973cc9a74d1d039 Mon Sep 17 00:00:00 2001 From: K Simmons Date: Fri, 21 Oct 2022 19:14:24 -0700 Subject: [PATCH] Add pane splitting by dragged item. Works, but the overlay doesn't clear quite right --- crates/gpui/src/app.rs | 18 ++ crates/gpui/src/app/test_app_context.rs | 1 + .../gpui/src/elements/mouse_event_handler.rs | 12 +- crates/gpui/src/presenter.rs | 28 ++- crates/gpui/src/scene/mouse_region.rs | 7 + crates/theme/src/theme.rs | 2 +- crates/workspace/src/dock.rs | 4 +- crates/workspace/src/pane.rs | 214 ++++++++++------ crates/workspace/src/pane_group.rs | 238 ++++-------------- crates/workspace/src/workspace.rs | 56 ++++- styles/src/styleTree/tabBar.ts | 4 - styles/src/styleTree/workspace.ts | 4 + 12 files changed, 308 insertions(+), 280 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5cbc786b72..e7ce384f16 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -21,6 +21,7 @@ use std::{ use anyhow::{anyhow, Context, Result}; use lazy_static::lazy_static; use parking_lot::Mutex; +use pathfinder_geometry::vector::Vector2F; use postage::oneshot; use smallvec::SmallVec; use smol::prelude::*; @@ -939,6 +940,7 @@ impl MutableAppContext { window_id, view_id, titlebar_height, + mouse_position: Default::default(), hovered_region_ids: Default::default(), clicked_region_ids: None, refreshing: false, @@ -3895,6 +3897,7 @@ pub struct RenderParams { pub window_id: usize, pub view_id: usize, pub titlebar_height: f32, + pub mouse_position: Vector2F, pub hovered_region_ids: HashSet, pub clicked_region_ids: Option<(HashSet, MouseButton)>, pub refreshing: bool, @@ -3905,6 +3908,7 @@ pub struct RenderContext<'a, T: View> { pub(crate) window_id: usize, pub(crate) view_id: usize, pub(crate) view_type: PhantomData, + pub(crate) mouse_position: Vector2F, pub(crate) hovered_region_ids: HashSet, pub(crate) clicked_region_ids: Option<(HashSet, MouseButton)>, pub app: &'a mut MutableAppContext, @@ -3916,12 +3920,19 @@ pub struct RenderContext<'a, T: View> { #[derive(Clone, Default)] pub struct MouseState { hovered: bool, + mouse_position: Vector2F, clicked: Option, + accessed_mouse_position: bool, accessed_hovered: bool, accessed_clicked: bool, } impl MouseState { + pub fn mouse_position(&mut self) -> Vector2F { + self.accessed_mouse_position = true; + self.mouse_position + } + pub fn hovered(&mut self) -> bool { self.accessed_hovered = true; self.hovered @@ -3932,6 +3943,10 @@ impl MouseState { self.clicked } + pub fn accessed_mouse_position(&self) -> bool { + self.accessed_mouse_position + } + pub fn accessed_hovered(&self) -> bool { self.accessed_hovered } @@ -3949,6 +3964,7 @@ impl<'a, V: View> RenderContext<'a, V> { view_id: params.view_id, view_type: PhantomData, titlebar_height: params.titlebar_height, + mouse_position: params.mouse_position, hovered_region_ids: params.hovered_region_ids.clone(), clicked_region_ids: params.clicked_region_ids.clone(), refreshing: params.refreshing, @@ -3971,6 +3987,7 @@ impl<'a, V: View> RenderContext<'a, V> { pub fn mouse_state(&self, region_id: usize) -> MouseState { let region_id = MouseRegionId::new::(self.view_id, region_id); MouseState { + mouse_position: self.mouse_position.clone(), hovered: self.hovered_region_ids.contains(®ion_id), clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| { if ids.contains(®ion_id) { @@ -3979,6 +3996,7 @@ impl<'a, V: View> RenderContext<'a, V> { None } }), + accessed_mouse_position: false, accessed_hovered: false, accessed_clicked: false, } diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 72f1f546fb..0eebc6485d 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -186,6 +186,7 @@ impl TestAppContext { view_id: handle.id(), view_type: PhantomData, titlebar_height: 0., + mouse_position: Default::default(), hovered_region_ids: Default::default(), clicked_region_ids: None, refreshing: false, diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 01779fb362..3548b1a38c 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -21,6 +21,7 @@ pub struct MouseEventHandler { cursor_style: Option, handlers: HandlerSet, hoverable: bool, + notify_on_move: bool, notify_on_hover: bool, notify_on_click: bool, above: bool, @@ -28,9 +29,8 @@ pub struct MouseEventHandler { _tag: PhantomData, } -// MouseEventHandler::new -// MouseEventHandler::above - +/// 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 @@ -39,6 +39,7 @@ impl MouseEventHandler { { let mut mouse_state = cx.mouse_state::(region_id); let child = render_child(&mut mouse_state, cx); + let notify_on_move = mouse_state.accessed_mouse_position(); let notify_on_hover = mouse_state.accessed_hovered(); let notify_on_click = mouse_state.accessed_clicked(); Self { @@ -46,6 +47,7 @@ impl MouseEventHandler { region_id, cursor_style: None, handlers: Default::default(), + notify_on_move, notify_on_hover, notify_on_click, hoverable: true, @@ -55,6 +57,9 @@ impl MouseEventHandler { } } + /// 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, @@ -183,6 +188,7 @@ impl MouseEventHandler { self.handlers.clone(), ) .with_hoverable(self.hoverable) + .with_notify_on_move(self.notify_on_move) .with_notify_on_hover(self.notify_on_hover) .with_notify_on_click(self.notify_on_click), ); diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 453ee0d60d..16ea5da642 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -90,6 +90,7 @@ impl Presenter { window_id: self.window_id, view_id: *view_id, titlebar_height: self.titlebar_height, + mouse_position: self.mouse_position.clone(), hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_ids: self .clicked_button @@ -116,6 +117,7 @@ impl Presenter { window_id: self.window_id, view_id: *view_id, titlebar_height: self.titlebar_height, + mouse_position: self.mouse_position.clone(), hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_ids: self .clicked_button @@ -183,6 +185,7 @@ impl Presenter { asset_cache: &self.asset_cache, view_stack: Vec::new(), refreshing, + mouse_position: self.mouse_position.clone(), hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_ids: self .clicked_button @@ -231,6 +234,10 @@ impl Presenter { let mut mouse_events = SmallVec::<[_; 2]>::new(); let mut notified_views: HashSet = Default::default(); + if let Some(mouse_position) = event.position() { + self.mouse_position = mouse_position; + } + // 1. Handle platform event. Keyboard events get dispatched immediately, while mouse events // get mapped into the mouse-specific MouseEvent type. // -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?] @@ -402,10 +409,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()); } } } @@ -447,6 +454,16 @@ impl Presenter { } } } + MouseEvent::Move(_) => { + for (mouse_region, _) in self.mouse_regions.iter().rev() { + if mouse_region.bounds.contains_point(self.mouse_position) { + valid_regions.push(mouse_region.clone()); + if mouse_region.notify_on_move { + notified_views.insert(mouse_region.id().view_id()); + } + } + } + } _ => { for (mouse_region, _) in self.mouse_regions.iter().rev() { // Contains @@ -551,6 +568,7 @@ pub struct LayoutContext<'a> { pub window_size: Vector2F, titlebar_height: f32, appearance: Appearance, + mouse_position: Vector2F, hovered_region_ids: HashSet, clicked_region_ids: Option<(HashSet, MouseButton)>, } @@ -622,6 +640,7 @@ impl<'a> LayoutContext<'a> { view_id: handle.id(), view_type: PhantomData, titlebar_height: self.titlebar_height, + mouse_position: self.mouse_position.clone(), hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_ids: self.clicked_region_ids.clone(), refreshing: self.refreshing, @@ -861,6 +880,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/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index 4b5217cc2d..f567fb7fe0 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -20,6 +20,7 @@ pub struct MouseRegion { pub bounds: RectF, pub handlers: HandlerSet, pub hoverable: bool, + pub notify_on_move: bool, pub notify_on_hover: bool, pub notify_on_click: bool, } @@ -54,6 +55,7 @@ impl MouseRegion { bounds, handlers, hoverable: true, + notify_on_move: false, notify_on_hover: false, notify_on_click: false, } @@ -136,6 +138,11 @@ impl MouseRegion { self } + pub fn with_notify_on_move(mut self, notify: bool) -> Self { + self.notify_on_move = notify; + self + } + pub fn with_notify_on_hover(mut self, notify: bool) -> Self { self.notify_on_hover = notify; self 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 9b1dec8d2d..283b50f4c3 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -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() { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e3ac7a2947..9f103946bf 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -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, + ) -> 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, 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 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, - ) -> Option { + 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 tab_overlay_color(hovered: bool, cx: &mut RenderContext) -> Option { if hovered && cx .global::>() .currently_dragged::(cx.window_id()) .is_some() { - Some(theme.workspace.tab_bar.drop_target_overlay_color) + Some( + cx.global::() + .theme + .workspace + .drop_target_overlay_color, + ) } else { None } @@ -1389,55 +1458,7 @@ impl View for Pane { // 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 @@ -1453,25 +1474,66 @@ impl View for Pane { MouseEventHandler::::above( 0, cx, - |_, cx| { - Flex::column() + |state, cx| { + let overlay_color = Self::tab_overlay_color(true, cx); + let drag_position = cx + .global::>() + .currently_dragged::(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() 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 714325a699..512ca23950 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,12 @@ pub struct OpenSharedScreen { pub peer_id: PeerId, } +pub struct SplitWithItem { + pane_to_split: WeakViewHandle, + 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, 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, + item_id_to_move: usize, + split_direction: SplitDirection, + cx: &mut ViewContext, + ) { + 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, cx: &mut ViewContext) { if self.center.remove(&pane).unwrap() { self.panes.retain(|p| p != &pane); 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..fea7df4313 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), + 0.6 + ), }; }