wip tab drag and drop

K Simmons created

Change summary

Cargo.lock                                      |   9 
crates/contacts_panel/src/contacts_panel.rs     |   2 
crates/drag_and_drop/Cargo.toml                 |  15 
crates/drag_and_drop/src/drag_and_drop.rs       | 151 +++
crates/gpui/src/elements/container.rs           |   5 
crates/gpui/src/elements/mouse_event_handler.rs |  35 
crates/gpui/src/elements/tooltip.rs             |   9 
crates/gpui/src/presenter.rs                    | 207 +++-
crates/gpui/src/scene.rs                        |   2 
crates/gpui/src/scene/mouse_region.rs           | 199 +---
crates/gpui/src/scene/mouse_region_event.rs     | 231 +++++
crates/project_panel/src/project_panel.rs       |  58 
crates/terminal/src/connected_el.rs             |   4 
crates/terminal/src/terminal_element.rs         | 793 +++++++++++++++++++
crates/theme/src/theme.rs                       |  16 
crates/workspace/Cargo.toml                     |   1 
crates/workspace/src/drag_and_drop.rs           |   0 
crates/workspace/src/pane.rs                    | 283 +++---
crates/workspace/src/sidebar.rs                 |  32 
crates/workspace/src/workspace.rs               |   5 
20 files changed, 1,643 insertions(+), 414 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1577,6 +1577,14 @@ version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
 
+[[package]]
+name = "drag_and_drop"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "gpui",
+]
+
 [[package]]
 name = "dwrote"
 version = "0.11.0"
@@ -6941,6 +6949,7 @@ dependencies = [
  "clock",
  "collections",
  "context_menu",
+ "drag_and_drop",
  "futures",
  "gpui",
  "language",

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -566,7 +566,7 @@ impl ContactsPanel {
                         button
                             .with_cursor_style(CursorStyle::PointingHand)
                             .on_click(MouseButton::Left, move |_, cx| {
-                                let project = project_handle.upgrade(cx.deref_mut());
+                                let project = project_handle.upgrade(cx.app);
                                 cx.dispatch_action(ToggleProjectOnline { project })
                             })
                             .with_tooltip::<ToggleOnline, _>(

crates/drag_and_drop/Cargo.toml 🔗

@@ -0,0 +1,15 @@
+[package]
+name = "drag_and_drop"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/drag_and_drop.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+gpui = { path = "../gpui" }
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -0,0 +1,151 @@
+use std::{any::Any, sync::Arc};
+
+use gpui::{
+    elements::{Container, MouseEventHandler},
+    geometry::{rect::RectF, vector::Vector2F},
+    Element, ElementBox, EventContext, MouseButton, RenderContext, View, ViewContext,
+    WeakViewHandle,
+};
+
+struct State<V: View> {
+    position: Vector2F,
+    region_offset: Vector2F,
+    payload: Arc<dyn Any>,
+    render: Arc<dyn Fn(Arc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
+}
+
+impl<V: View> Clone for State<V> {
+    fn clone(&self) -> Self {
+        Self {
+            position: self.position.clone(),
+            region_offset: self.region_offset.clone(),
+            payload: self.payload.clone(),
+            render: self.render.clone(),
+        }
+    }
+}
+
+pub struct DragAndDrop<V: View> {
+    parent: WeakViewHandle<V>,
+    currently_dragged: Option<State<V>>,
+}
+
+impl<V: View> DragAndDrop<V> {
+    pub fn new(parent: WeakViewHandle<V>, cx: &mut ViewContext<V>) -> Self {
+        // TODO: Figure out if detaching here would result in a memory leak
+        cx.observe_global::<Self, _>(|cx| {
+            if let Some(parent) = cx.global::<Self>().parent.upgrade(cx) {
+                parent.update(cx, |_, cx| cx.notify())
+            }
+        })
+        .detach();
+
+        Self {
+            parent,
+            currently_dragged: None,
+        }
+    }
+
+    pub fn currently_dragged<T: Any>(&self) -> Option<(Vector2F, &T)> {
+        self.currently_dragged.as_ref().and_then(
+            |State {
+                 position, payload, ..
+             }| {
+                payload
+                    .downcast_ref::<T>()
+                    .map(|payload| (position.clone(), payload))
+            },
+        )
+    }
+
+    pub fn dragging<T: Any>(
+        relative_to: Option<RectF>,
+        position: Vector2F,
+        payload: Arc<T>,
+        cx: &mut EventContext,
+        render: Arc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
+    ) {
+        cx.update_global::<Self, _, _>(|this, cx| {
+            let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() {
+                previous_state.region_offset
+            } else {
+                if let Some(relative_to) = relative_to {
+                    relative_to.origin() - position
+                } else {
+                    Vector2F::zero()
+                }
+            };
+
+            this.currently_dragged = Some(State {
+                region_offset,
+                position,
+                payload,
+                render: Arc::new(move |payload, cx| {
+                    render(payload.downcast_ref::<T>().unwrap(), cx)
+                }),
+            });
+
+            if let Some(parent) = this.parent.upgrade(cx) {
+                parent.update(cx, |_, cx| cx.notify())
+            }
+        });
+    }
+
+    pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
+        let currently_dragged = cx.global::<Self>().currently_dragged.clone();
+
+        currently_dragged.map(
+            |State {
+                 region_offset,
+                 position,
+                 payload,
+                 render,
+             }| {
+                let position = position + region_offset;
+
+                MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
+                    Container::new(render(payload, cx))
+                        .with_margin_left(position.x())
+                        .with_margin_top(position.y())
+                        .boxed()
+                })
+                .on_up(MouseButton::Left, |_, cx| {
+                    cx.defer(|cx| {
+                        cx.update_global::<Self, _, _>(|this, _| this.currently_dragged.take());
+                    });
+                    cx.propogate_event();
+                })
+                .boxed()
+            },
+        )
+    }
+}
+
+pub trait Draggable {
+    fn as_draggable<V: View, P: Any>(
+        self,
+        payload: P,
+        render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
+    ) -> Self
+    where
+        Self: Sized;
+}
+
+impl Draggable for MouseEventHandler {
+    fn as_draggable<V: View, P: Any>(
+        self,
+        payload: P,
+        render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        let payload = Arc::new(payload);
+        let render = Arc::new(render);
+        self.on_drag(MouseButton::Left, move |e, cx| {
+            let payload = payload.clone();
+            let render = render.clone();
+            DragAndDrop::<V>::dragging(Some(e.region), e.position, payload, cx, render)
+        })
+    }
+}

crates/gpui/src/elements/container.rs 🔗

@@ -104,6 +104,11 @@ impl Container {
         self
     }
 
+    pub fn with_padding_top(mut self, padding: f32) -> Self {
+        self.style.padding.top = padding;
+        self
+    }
+
     pub fn with_padding_bottom(mut self, padding: f32) -> Self {
         self.style.padding.bottom = padding;
         self

crates/gpui/src/elements/mouse_event_handler.rs 🔗

@@ -5,10 +5,13 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     platform::CursorStyle,
-    scene::{CursorRegion, HandlerSet},
+    scene::{
+        ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragOverRegionEvent,
+        DragRegionEvent, HandlerSet, HoverRegionEvent, MoveRegionEvent, UpOutRegionEvent,
+        UpRegionEvent,
+    },
     DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
-    MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseState, PaintContext,
-    RenderContext, SizeConstraint, View,
+    MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
 };
 use serde_json::json;
 use std::{any::TypeId, ops::Range};
@@ -42,10 +45,18 @@ impl MouseEventHandler {
         self
     }
 
+    pub fn on_move(
+        mut self,
+        handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_move(handler);
+        self
+    }
+
     pub fn on_down(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_down(button, handler);
         self
@@ -54,7 +65,7 @@ impl MouseEventHandler {
     pub fn on_up(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_up(button, handler);
         self
@@ -63,7 +74,7 @@ impl MouseEventHandler {
     pub fn on_click(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_click(button, handler);
         self
@@ -72,7 +83,7 @@ impl MouseEventHandler {
     pub fn on_down_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_down_out(button, handler);
         self
@@ -81,16 +92,16 @@ impl MouseEventHandler {
     pub fn on_up_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
-        self.handlers = self.handlers.on_up(button, handler);
+        self.handlers = self.handlers.on_up_out(button, handler);
         self
     }
 
     pub fn on_drag(
         mut self,
         button: MouseButton,
-        handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_drag(button, handler);
         self
@@ -99,7 +110,7 @@ impl MouseEventHandler {
     pub fn on_drag_over(
         mut self,
         button: MouseButton,
-        handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DragOverRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_drag_over(button, handler);
         self
@@ -107,7 +118,7 @@ impl MouseEventHandler {
 
     pub fn on_hover(
         mut self,
-        handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_hover(handler);
         self

crates/gpui/src/elements/tooltip.rs 🔗

@@ -7,8 +7,8 @@ use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
     presenter::MeasurementContext,
-    Action, Axis, ElementStateHandle, LayoutContext, MouseMovedEvent, PaintContext, RenderContext,
-    SizeConstraint, Task, View,
+    Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
+    Task, View,
 };
 use serde::Deserialize;
 use std::{
@@ -93,10 +93,11 @@ impl Tooltip {
         };
         let child =
             MouseEventHandler::new::<MouseEventHandlerState<Tag>, _, _>(id, cx, |_, _| child)
-                .on_hover(move |hover, MouseMovedEvent { position, .. }, cx| {
+                .on_hover(move |e, cx| {
+                    let position = e.position;
                     let window_id = cx.window_id();
                     if let Some(view_id) = cx.view_id() {
-                        if hover {
+                        if e.started {
                             if !state.visible.get() {
                                 state.position.set(position);
 

crates/gpui/src/presenter.rs 🔗

@@ -6,7 +6,11 @@ use crate::{
     json::{self, ToJson},
     keymap::Keystroke,
     platform::{CursorStyle, Event},
-    scene::{CursorRegion, MouseRegionEvent},
+    scene::{
+        ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragOverRegionEvent,
+        DragRegionEvent, HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent,
+        UpRegionEvent,
+    },
     text_layout::TextLayoutCache,
     Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
     FontSystem, ModelHandle, MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseRegionId,
@@ -140,8 +144,7 @@ impl Presenter {
 
             if cx.window_is_active(self.window_id) {
                 if let Some(event) = self.last_mouse_moved_event.clone() {
-                    let mut invalidated_views = Vec::new();
-                    self.handle_hover_events(&event, &mut invalidated_views, cx);
+                    let invalidated_views = self.handle_hover_events(&event, cx).invalidated_views;
 
                     for view_id in invalidated_views {
                         cx.notify_view(self.window_id, view_id);
@@ -216,48 +219,60 @@ impl Presenter {
 
     pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) -> bool {
         if let Some(root_view_id) = cx.root_view_id(self.window_id) {
-            let mut invalidated_views = Vec::new();
             let mut events_to_send = Vec::new();
-
             match &event {
                 Event::MouseDown(e @ MouseButtonEvent { position, .. }) => {
-                    let mut hit = false;
                     for (region, _) in self.mouse_regions.iter().rev() {
                         if region.bounds.contains_point(*position) {
-                            if !hit {
-                                hit = true;
-                                invalidated_views.push(region.view_id);
-                                events_to_send
-                                    .push((region.clone(), MouseRegionEvent::Down(e.clone())));
-                                self.clicked_region = Some(region.clone());
-                                self.prev_drag_position = Some(*position);
-                            }
+                            events_to_send.push((
+                                region.clone(),
+                                MouseRegionEvent::Down(DownRegionEvent {
+                                    region: region.bounds,
+                                    platform_event: e.clone(),
+                                }),
+                            ));
                         } else {
-                            events_to_send
-                                .push((region.clone(), MouseRegionEvent::DownOut(e.clone())));
+                            events_to_send.push((
+                                region.clone(),
+                                MouseRegionEvent::DownOut(DownOutRegionEvent {
+                                    region: region.bounds,
+                                    platform_event: e.clone(),
+                                }),
+                            ));
                         }
                     }
                 }
                 Event::MouseUp(e @ MouseButtonEvent { position, .. }) => {
-                    let mut hit = false;
                     for (region, _) in self.mouse_regions.iter().rev() {
                         if region.bounds.contains_point(*position) {
-                            if !hit {
-                                hit = true;
-                                invalidated_views.push(region.view_id);
-                                events_to_send
-                                    .push((region.clone(), MouseRegionEvent::Up(e.clone())));
-                            }
+                            events_to_send.push((
+                                region.clone(),
+                                MouseRegionEvent::Up(UpRegionEvent {
+                                    region: region.bounds,
+                                    platform_event: e.clone(),
+                                }),
+                            ));
                         } else {
-                            events_to_send
-                                .push((region.clone(), MouseRegionEvent::UpOut(e.clone())));
+                            events_to_send.push((
+                                region.clone(),
+                                MouseRegionEvent::UpOut(UpOutRegionEvent {
+                                    region: region.bounds,
+                                    platform_event: e.clone(),
+                                }),
+                            ));
                         }
                     }
                     self.prev_drag_position.take();
                     if let Some(region) = self.clicked_region.take() {
-                        invalidated_views.push(region.view_id);
                         if region.bounds.contains_point(*position) {
-                            events_to_send.push((region, MouseRegionEvent::Click(e.clone())));
+                            let bounds = region.bounds.clone();
+                            events_to_send.push((
+                                region,
+                                MouseRegionEvent::Click(ClickRegionEvent {
+                                    region: bounds,
+                                    platform_event: e.clone(),
+                                }),
+                            ));
                         }
                     }
                 }
@@ -269,9 +284,28 @@ impl Presenter {
                     {
                         events_to_send.push((
                             clicked_region.clone(),
+<<<<<<< HEAD
                             MouseRegionEvent::Drag(*prev_drag_position, *e),
+=======
+                            MouseRegionEvent::Drag(DragRegionEvent {
+                                region: clicked_region.bounds,
+                                prev_drag_position: *prev_drag_position,
+                                platform_event: e.clone(),
+                            }),
+>>>>>>> 4bd8a4b0 (wip tab drag and drop)
                         ));
-                        *prev_drag_position = *position;
+                    }
+
+                    for (region, _) in self.mouse_regions.iter().rev() {
+                        if region.bounds.contains_point(*position) {
+                            events_to_send.push((
+                                region.clone(),
+                                MouseRegionEvent::Move(MoveRegionEvent {
+                                    region: region.bounds,
+                                    platform_event: e.clone(),
+                                }),
+                            ));
+                        }
                     }
 
                     self.last_mouse_moved_event = Some(event.clone());
@@ -279,12 +313,12 @@ impl Presenter {
                 _ => {}
             }
 
-            let (mut handled, mut event_cx) =
-                self.handle_hover_events(&event, &mut invalidated_views, cx);
+            let (invalidated_views, dispatch_directives, handled) = {
+                let mut event_cx = self.handle_hover_events(&event, cx);
+                event_cx.process_region_events(events_to_send);
 
-            for (region, event) in events_to_send {
-                if event.is_local() {
-                    handled = true;
+                if !event_cx.handled {
+                    event_cx.handled = event_cx.dispatch_event(root_view_id, &event);
                 }
 
                 if let Some(callback) = region.handlers.get(&event.handler_key()) {
@@ -292,7 +326,13 @@ impl Presenter {
                         callback(event, event_cx);
                     })
                 }
-            }
+                
+                (
+                    event_cx.invalidated_views,
+                    event_cx.dispatched_actions,
+                    event_cx.handled,
+                )
+            };
 
             if !handled {
                 handled = event_cx.dispatch_event(root_view_id, &event);
@@ -313,9 +353,8 @@ impl Presenter {
     fn handle_hover_events<'a>(
         &'a mut self,
         event: &Event,
-        invalidated_views: &mut Vec<usize>,
         cx: &'a mut MutableAppContext,
-    ) -> (bool, EventContext<'a>) {
+    ) -> EventContext<'a> {
         let mut events_to_send = Vec::new();
 
         if let Event::MouseMoved(
@@ -343,46 +382,48 @@ impl Presenter {
                     hover_depth = Some(*depth);
                     if let Some(region_id) = region.id() {
                         if !self.hovered_region_ids.contains(&region_id) {
-                            invalidated_views.push(region.view_id);
                             let region_event = if pressed_button.is_some() {
-                                MouseRegionEvent::DragOver(true, e.clone())
+                                MouseRegionEvent::DragOver(DragOverRegionEvent {
+                                    region: region.bounds,
+                                    started: true,
+                                    platform_event: e.clone(),
+                                })
                             } else {
-                                MouseRegionEvent::Hover(true, e.clone())
+                                MouseRegionEvent::Hover(HoverRegionEvent {
+                                    region: region.bounds,
+                                    started: true,
+                                    platform_event: e.clone(),
+                                })
                             };
                             events_to_send.push((region.clone(), region_event));
                             self.hovered_region_ids.insert(region_id);
                         }
                     }
                 } else if let Some(region_id) = region.id() {
-                    if self.hovered_region_ids.contains(&region_id) {
-                        invalidated_views.push(region.view_id);
-                        let region_event = if pressed_button.is_some() {
-                            MouseRegionEvent::DragOver(false, e.clone())
-                        } else {
-                            MouseRegionEvent::Hover(false, e.clone())
-                        };
-                        events_to_send.push((region.clone(), region_event));
-                        self.hovered_region_ids.remove(&region_id);
-                    }
+                        if self.hovered_region_ids.contains(&region_id) {
+                            let region_event = if pressed_button.is_some() {
+                                MouseRegionEvent::DragOver(DragOverRegionEvent {
+                                    region: region.bounds,
+                                    started: false,
+                                    platform_event: e.clone(),
+                                })
+                            } else {
+                                MouseRegionEvent::Hover(HoverRegionEvent {
+                                    region: region.bounds,
+                                    started: false,
+                                    platform_event: e.clone(),
+                                })
+                            };
+                            events_to_send.push((region.clone(), region_event));
+                            self.hovered_region_ids.remove(&region_id);
+                        }
                 }
             }
         }
 
         let mut event_cx = self.build_event_context(cx);
-        let mut handled = false;
-
-        for (region, event) in events_to_send {
-            if event.is_local() {
-                handled = true;
-            }
-            if let Some(callback) = region.handlers.get(&event.handler_key()) {
-                event_cx.with_current_view(region.view_id, |event_cx| {
-                    callback(event, event_cx);
-                })
-            }
-        }
-
-        (handled, event_cx)
+        event_cx.process_region_events(events_to_send);
+        event_cx
     }
 
     pub fn build_event_context<'a>(
@@ -396,6 +437,9 @@ impl Presenter {
             view_stack: Default::default(),
             invalidated_views: Default::default(),
             notify_count: 0,
+            clicked_region: &mut self.clicked_region,
+            prev_drag_position: &mut self.prev_drag_position,
+            handled: false,
             window_id: self.window_id,
             app: cx,
         }
@@ -615,6 +659,9 @@ pub struct EventContext<'a> {
     pub window_id: usize,
     pub notify_count: usize,
     view_stack: Vec<usize>,
+    clicked_region: &'a mut Option<MouseRegion>,
+    prev_drag_position: &'a mut Option<Vector2F>,
+    handled: bool,
     invalidated_views: HashSet<usize>,
 }
 
@@ -630,6 +677,36 @@ impl<'a> EventContext<'a> {
         }
     }
 
+    fn process_region_events(&mut self, events: Vec<(MouseRegion, MouseRegionEvent)>) {
+        for (region, event) in events {
+            if event.is_local() {
+                if self.handled {
+                    continue;
+                }
+
+                match &event {
+                    MouseRegionEvent::Down(e) => {
+                        *self.clicked_region = Some(region.clone());
+                        *self.prev_drag_position = Some(e.position);
+                    }
+                    MouseRegionEvent::Drag(e) => {
+                        *self.prev_drag_position = Some(e.position);
+                    }
+                    _ => {}
+                }
+
+                self.invalidated_views.insert(region.view_id);
+            }
+
+            if let Some(callback) = region.handlers.get(&event.handler_key()) {
+                self.handled = true;
+                self.with_current_view(region.view_id, |event_cx| {
+                    callback(event, event_cx);
+                })
+            }
+        }
+    }
+
     fn with_current_view<F, T>(&mut self, view_id: usize, f: F) -> T
     where
         F: FnOnce(&mut Self) -> T,
@@ -681,6 +758,10 @@ impl<'a> EventContext<'a> {
     pub fn notify_count(&self) -> usize {
         self.notify_count
     }
+
+    pub fn propogate_event(&mut self) {
+        self.handled = false;
+    }
 }
 
 impl<'a> Deref for EventContext<'a> {

crates/gpui/src/scene.rs 🔗

@@ -1,4 +1,5 @@
 mod mouse_region;
+mod mouse_region_event;
 
 use serde::Deserialize;
 use serde_json::json;
@@ -13,6 +14,7 @@ use crate::{
     ImageData,
 };
 pub use mouse_region::*;
+pub use mouse_region_event::*;
 
 pub struct Scene {
     scale_factor: f32,

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -1,13 +1,14 @@
-use std::{
-    any::TypeId,
-    mem::{discriminant, Discriminant},
-    rc::Rc,
-};
+use std::{any::TypeId, mem::Discriminant, rc::Rc};
 
 use collections::HashMap;
-use pathfinder_geometry::{rect::RectF, vector::Vector2F};
+use pathfinder_geometry::rect::RectF;
+
+use crate::{EventContext, MouseButton};
 
-use crate::{EventContext, MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
+use super::mouse_region_event::{
+    ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragOverRegionEvent, DragRegionEvent,
+    HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+};
 
 #[derive(Clone, Default)]
 pub struct MouseRegion {
@@ -52,7 +53,7 @@ impl MouseRegion {
     pub fn on_down(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_down(button, handler);
         self
@@ -61,7 +62,7 @@ impl MouseRegion {
     pub fn on_up(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_up(button, handler);
         self
@@ -70,7 +71,7 @@ impl MouseRegion {
     pub fn on_click(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_click(button, handler);
         self
@@ -79,7 +80,7 @@ impl MouseRegion {
     pub fn on_down_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_down_out(button, handler);
         self
@@ -88,7 +89,7 @@ impl MouseRegion {
     pub fn on_up_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_up_out(button, handler);
         self
@@ -97,7 +98,7 @@ impl MouseRegion {
     pub fn on_drag(
         mut self,
         button: MouseButton,
-        handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_drag(button, handler);
         self
@@ -106,7 +107,7 @@ impl MouseRegion {
     pub fn on_drag_over(
         mut self,
         button: MouseButton,
-        handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DragOverRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_drag_over(button, handler);
         self
@@ -114,7 +115,7 @@ impl MouseRegion {
 
     pub fn on_hover(
         mut self,
-        handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_hover(handler);
         self
@@ -191,15 +192,32 @@ impl HandlerSet {
         self.set.get(key).cloned()
     }
 
+    pub fn on_move(
+        mut self,
+        handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.set.insert((MouseRegionEvent::move_disc(), None),
+            Rc::new(move |region_event, cx| {
+                if let MouseRegionEvent::Move(e) = region_event {
+                    handler(e, cx);
+                } else {
+                    panic!(
+                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Move, found {:?}", 
+                        region_event);
+                }
+            }));
+        self
+    }
+
     pub fn on_down(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::down_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Down(mouse_button_event) = region_event {
-                    handler(mouse_button_event, cx);
+                if let MouseRegionEvent::Down(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Down, found {:?}", 
@@ -212,12 +230,12 @@ impl HandlerSet {
     pub fn on_up(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::up_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Up(mouse_button_event) = region_event {
-                    handler(mouse_button_event, cx);
+                if let MouseRegionEvent::Up(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Up, found {:?}", 
@@ -230,12 +248,12 @@ impl HandlerSet {
     pub fn on_click(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::click_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Click(mouse_button_event) = region_event {
-                    handler(mouse_button_event, cx);
+                if let MouseRegionEvent::Click(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Click, found {:?}", 
@@ -248,12 +266,12 @@ impl HandlerSet {
     pub fn on_down_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::down_out_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::DownOut(mouse_button_event) = region_event {
-                    handler(mouse_button_event, cx);
+                if let MouseRegionEvent::DownOut(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::DownOut, found {:?}", 
@@ -266,12 +284,12 @@ impl HandlerSet {
     pub fn on_up_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::up_out_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::UpOut(mouse_button_event) = region_event {
-                    handler(mouse_button_event, cx);
+                if let MouseRegionEvent::UpOut(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::UpOut, found {:?}", 
@@ -284,12 +302,12 @@ impl HandlerSet {
     pub fn on_drag(
         mut self,
         button: MouseButton,
-        handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::drag_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Drag(prev_drag_position, mouse_moved_event) = region_event {
-                    handler(prev_drag_position, mouse_moved_event, cx);
+                if let MouseRegionEvent::Drag(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Drag, found {:?}", 
@@ -302,12 +320,12 @@ impl HandlerSet {
     pub fn on_drag_over(
         mut self,
         button: MouseButton,
-        handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DragOverRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::drag_over_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::DragOver(started, mouse_moved_event) = region_event {
-                    handler(started, mouse_moved_event, cx);
+                if let MouseRegionEvent::DragOver(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::DragOver, found {:?}", 
@@ -319,12 +337,12 @@ impl HandlerSet {
 
     pub fn on_hover(
         mut self,
-        handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::hover_disc(), None),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Hover(started, mouse_moved_event) = region_event {
-                    handler(started, mouse_moved_event, cx);
+                if let MouseRegionEvent::Hover(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Hover, found {:?}", 
@@ -334,106 +352,3 @@ impl HandlerSet {
         self
     }
 }
-
-#[derive(Debug)]
-pub enum MouseRegionEvent {
-    Move(MouseMovedEvent),
-    Drag(Vector2F, MouseMovedEvent),
-    DragOver(bool, MouseMovedEvent),
-    Hover(bool, MouseMovedEvent),
-    Down(MouseButtonEvent),
-    Up(MouseButtonEvent),
-    Click(MouseButtonEvent),
-    UpOut(MouseButtonEvent),
-    DownOut(MouseButtonEvent),
-    ScrollWheel(ScrollWheelEvent),
-}
-
-impl MouseRegionEvent {
-    pub fn move_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::Move(Default::default()))
-    }
-
-    pub fn drag_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::Drag(
-            Default::default(),
-            Default::default(),
-        ))
-    }
-
-    pub fn drag_over_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::DragOver(
-            Default::default(),
-            Default::default(),
-        ))
-    }
-
-    pub fn hover_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::Hover(
-            Default::default(),
-            Default::default(),
-        ))
-    }
-
-    pub fn down_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::Down(Default::default()))
-    }
-
-    pub fn up_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::Up(Default::default()))
-    }
-
-    pub fn up_out_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::UpOut(Default::default()))
-    }
-
-    pub fn click_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::Click(Default::default()))
-    }
-
-    pub fn down_out_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::DownOut(Default::default()))
-    }
-
-    pub fn scroll_wheel_disc() -> Discriminant<MouseRegionEvent> {
-        discriminant(&MouseRegionEvent::ScrollWheel(Default::default()))
-    }
-
-    pub fn is_local(&self) -> bool {
-        match self {
-            MouseRegionEvent::DownOut(_)
-            | MouseRegionEvent::UpOut(_)
-            | MouseRegionEvent::DragOver(_, _) => false,
-            _ => true,
-        }
-    }
-
-    pub fn handler_key(&self) -> (Discriminant<MouseRegionEvent>, Option<MouseButton>) {
-        match self {
-            MouseRegionEvent::Move(_) => (Self::move_disc(), None),
-            MouseRegionEvent::Drag(_, MouseMovedEvent { pressed_button, .. }) => {
-                (Self::drag_disc(), *pressed_button)
-            }
-            MouseRegionEvent::DragOver(_, MouseMovedEvent { pressed_button, .. }) => {
-                (Self::drag_over_disc(), *pressed_button)
-            }
-            MouseRegionEvent::Hover(_, _) => (Self::hover_disc(), None),
-            MouseRegionEvent::Down(MouseButtonEvent { button, .. }) => {
-                (Self::down_disc(), Some(*button))
-            }
-            MouseRegionEvent::Up(MouseButtonEvent { button, .. }) => {
-                (Self::up_disc(), Some(*button))
-            }
-            MouseRegionEvent::Click(MouseButtonEvent { button, .. }) => {
-                (Self::click_disc(), Some(*button))
-            }
-            MouseRegionEvent::UpOut(MouseButtonEvent { button, .. }) => {
-                (Self::up_out_disc(), Some(*button))
-            }
-            MouseRegionEvent::DownOut(MouseButtonEvent { button, .. }) => {
-                (Self::down_out_disc(), Some(*button))
-            }
-            MouseRegionEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None),
-        }
-    }
-}

crates/gpui/src/scene/mouse_region_event.rs 🔗

@@ -0,0 +1,231 @@
+use std::{
+    mem::{discriminant, Discriminant},
+    ops::Deref,
+};
+
+use pathfinder_geometry::{rect::RectF, vector::Vector2F};
+
+use crate::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
+
+#[derive(Debug, Default)]
+pub struct MoveRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseMovedEvent,
+}
+
+impl Deref for MoveRegionEvent {
+    type Target = MouseMovedEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct DragRegionEvent {
+    pub region: RectF,
+    pub prev_drag_position: Vector2F,
+    pub platform_event: MouseMovedEvent,
+}
+
+impl Deref for DragRegionEvent {
+    type Target = MouseMovedEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct DragOverRegionEvent {
+    pub region: RectF,
+    pub started: bool,
+    pub platform_event: MouseMovedEvent,
+}
+
+impl Deref for DragOverRegionEvent {
+    type Target = MouseMovedEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct HoverRegionEvent {
+    pub region: RectF,
+    pub started: bool,
+    pub platform_event: MouseMovedEvent,
+}
+
+impl Deref for HoverRegionEvent {
+    type Target = MouseMovedEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct DownRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for DownRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct UpRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for UpRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct ClickRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for ClickRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct DownOutRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for DownOutRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct UpOutRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for UpOutRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct ScrollWheelRegionEvent {
+    pub region: RectF,
+    pub platform_event: ScrollWheelEvent,
+}
+
+impl Deref for ScrollWheelRegionEvent {
+    type Target = ScrollWheelEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug)]
+pub enum MouseRegionEvent {
+    Move(MoveRegionEvent),
+    Drag(DragRegionEvent),
+    DragOver(DragOverRegionEvent),
+    Hover(HoverRegionEvent),
+    Down(DownRegionEvent),
+    Up(UpRegionEvent),
+    Click(ClickRegionEvent),
+    DownOut(DownOutRegionEvent),
+    UpOut(UpOutRegionEvent),
+    ScrollWheel(ScrollWheelRegionEvent),
+}
+
+impl MouseRegionEvent {
+    pub fn move_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Move(Default::default()))
+    }
+
+    pub fn drag_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Drag(Default::default()))
+    }
+
+    pub fn drag_over_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::DragOver(Default::default()))
+    }
+
+    pub fn hover_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Hover(Default::default()))
+    }
+
+    pub fn down_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Down(Default::default()))
+    }
+
+    pub fn up_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Up(Default::default()))
+    }
+
+    pub fn up_out_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::UpOut(Default::default()))
+    }
+
+    pub fn click_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Click(Default::default()))
+    }
+
+    pub fn down_out_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::DownOut(Default::default()))
+    }
+
+    pub fn scroll_wheel_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::ScrollWheel(Default::default()))
+    }
+
+    pub fn is_local(&self) -> bool {
+        match self {
+            MouseRegionEvent::DownOut(_)
+            | MouseRegionEvent::UpOut(_)
+            | MouseRegionEvent::DragOver(_) => false,
+            _ => true,
+        }
+    }
+
+    pub fn handler_key(&self) -> (Discriminant<MouseRegionEvent>, Option<MouseButton>) {
+        match self {
+            MouseRegionEvent::Move(_) => (Self::move_disc(), None),
+            MouseRegionEvent::Drag(e) => (Self::drag_disc(), e.pressed_button),
+            MouseRegionEvent::DragOver(e) => (Self::drag_over_disc(), e.pressed_button),
+            MouseRegionEvent::Hover(_) => (Self::hover_disc(), None),
+            MouseRegionEvent::Down(e) => (Self::down_disc(), Some(e.button)),
+            MouseRegionEvent::Up(e) => (Self::up_disc(), Some(e.button)),
+            MouseRegionEvent::Click(e) => (Self::click_disc(), Some(e.button)),
+            MouseRegionEvent::UpOut(e) => (Self::up_out_disc(), Some(e.button)),
+            MouseRegionEvent::DownOut(e) => (Self::down_out_disc(), Some(e.button)),
+            MouseRegionEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None),
+        }
+    }
+}

crates/project_panel/src/project_panel.rs 🔗

@@ -12,8 +12,7 @@ use gpui::{
     impl_internal_actions, keymap,
     platform::CursorStyle,
     AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
-    MouseButtonEvent, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext,
-    ViewHandle,
+    MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
@@ -1064,25 +1063,22 @@ impl ProjectPanel {
                 .with_padding_left(padding)
                 .boxed()
         })
-        .on_click(
-            MouseButton::Left,
-            move |MouseButtonEvent { click_count, .. }, cx| {
-                if kind == EntryKind::Dir {
-                    cx.dispatch_action(ToggleExpanded(entry_id))
-                } else {
-                    cx.dispatch_action(Open {
-                        entry_id,
-                        change_focus: click_count > 1,
-                    })
-                }
-            },
-        )
-        .on_down(
-            MouseButton::Right,
-            move |MouseButtonEvent { position, .. }, cx| {
-                cx.dispatch_action(DeployContextMenu { entry_id, position })
-            },
-        )
+        .on_click(MouseButton::Left, move |e, cx| {
+            if kind == EntryKind::Dir {
+                cx.dispatch_action(ToggleExpanded(entry_id))
+            } else {
+                cx.dispatch_action(Open {
+                    entry_id,
+                    change_focus: e.click_count > 1,
+                })
+            }
+        })
+        .on_down(MouseButton::Right, move |e, cx| {
+            cx.dispatch_action(DeployContextMenu {
+                entry_id,
+                position: e.position,
+            })
+        })
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }
@@ -1129,16 +1125,16 @@ impl View for ProjectPanel {
                     .expanded()
                     .boxed()
                 })
-                .on_down(
-                    MouseButton::Right,
-                    move |MouseButtonEvent { position, .. }, cx| {
-                        // When deploying the context menu anywhere below the last project entry,
-                        // act as if the user clicked the root of the last worktree.
-                        if let Some(entry_id) = last_worktree_root_id {
-                            cx.dispatch_action(DeployContextMenu { entry_id, position })
-                        }
-                    },
-                )
+                .on_down(MouseButton::Right, move |e, cx| {
+                    // When deploying the context menu anywhere below the last project entry,
+                    // act as if the user clicked the root of the last worktree.
+                    if let Some(entry_id) = last_worktree_root_id {
+                        cx.dispatch_action(DeployContextMenu {
+                            entry_id,
+                            position: e.position,
+                        })
+                    }
+                })
                 .boxed(),
             )
             .with_child(ChildView::new(&self.context_menu).boxed())

crates/terminal/src/connected_el.rs 🔗

@@ -16,8 +16,8 @@ use gpui::{
     },
     json::json,
     text_layout::{Line, RunStyle},
-    Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion,
-    PaintContext, Quad, ScrollWheelEvent, TextLayoutCache, WeakModelHandle, WeakViewHandle,
+    Event, FontCache, KeyDownEvent, MouseButton, MouseRegion, PaintContext, Quad, ScrollWheelEvent,
+    TextLayoutCache, WeakModelHandle, WeakViewHandle,
 };
 use itertools::Itertools;
 use ordered_float::OrderedFloat;

crates/terminal/src/terminal_element.rs 🔗

@@ -0,0 +1,793 @@
+use alacritty_terminal::{
+    grid::{Dimensions, GridIterator, Indexed, Scroll},
+    index::{Column as GridCol, Line as GridLine, Point, Side},
+    selection::{Selection, SelectionRange, SelectionType},
+    sync::FairMutex,
+    term::{
+        cell::{Cell, Flags},
+        SizeInfo,
+    },
+    Term,
+};
+use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
+use gpui::{
+    color::Color,
+    elements::*,
+    fonts::{TextStyle, Underline},
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    json::json,
+    text_layout::{Line, RunStyle},
+    Event, FontCache, KeyDownEvent, MouseButton, MouseRegion, PaintContext, Quad, ScrollWheelEvent,
+    SizeConstraint, TextLayoutCache, WeakModelHandle,
+};
+use itertools::Itertools;
+use ordered_float::OrderedFloat;
+use settings::Settings;
+use theme::TerminalStyle;
+use util::ResultExt;
+
+use std::{cmp::min, ops::Range, sync::Arc};
+use std::{fmt::Debug, ops::Sub};
+
+use crate::{color_translation::convert_color, connection::TerminalConnection, ZedListener};
+
+///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
+///Scroll multiplier that is set to 3 by default. This will be removed when I
+///Implement scroll bars.
+const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
+
+///Used to display the grid as passed to Alacritty and the TTY.
+///Useful for debugging inconsistencies between behavior and display
+#[cfg(debug_assertions)]
+const DEBUG_GRID: bool = false;
+
+///The GPUI element that paints the terminal.
+///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
+pub struct TerminalEl {
+    connection: WeakModelHandle<TerminalConnection>,
+    view_id: usize,
+    modal: bool,
+}
+
+///New type pattern so I don't mix these two up
+struct CellWidth(f32);
+struct LineHeight(f32);
+
+struct LayoutLine {
+    cells: Vec<LayoutCell>,
+    highlighted_range: Option<Range<usize>>,
+}
+
+///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than
+struct PaneRelativePos(Vector2F);
+
+///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position
+fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos {
+    PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating
+}
+
+#[derive(Clone, Debug, Default)]
+struct LayoutCell {
+    point: Point<i32, i32>,
+    text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN!
+    background_color: Color,
+}
+
+impl LayoutCell {
+    fn new(point: Point<i32, i32>, text: Line, background_color: Color) -> LayoutCell {
+        LayoutCell {
+            point,
+            text,
+            background_color,
+        }
+    }
+}
+
+///The information generated during layout that is nescessary for painting
+pub struct LayoutState {
+    layout_lines: Vec<LayoutLine>,
+    line_height: LineHeight,
+    em_width: CellWidth,
+    cursor: Option<Cursor>,
+    background_color: Color,
+    cur_size: SizeInfo,
+    terminal: Arc<FairMutex<Term<ZedListener>>>,
+    selection_color: Color,
+}
+
+impl TerminalEl {
+    pub fn new(
+        view_id: usize,
+        connection: WeakModelHandle<TerminalConnection>,
+        modal: bool,
+    ) -> TerminalEl {
+        TerminalEl {
+            view_id,
+            connection,
+            modal,
+        }
+    }
+}
+
+impl Element for TerminalEl {
+    type LayoutState = LayoutState;
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        cx: &mut gpui::LayoutContext,
+    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+        //Settings immutably borrows cx here for the settings and font cache
+        //and we need to modify the cx to resize the terminal. So instead of
+        //storing Settings or the font_cache(), we toss them ASAP and then reborrow later
+        let text_style = make_text_style(cx.font_cache(), cx.global::<Settings>());
+        let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size));
+        let cell_width = CellWidth(
+            cx.font_cache()
+                .em_advance(text_style.font_id, text_style.font_size),
+        );
+        let connection_handle = self.connection.upgrade(cx).unwrap();
+
+        //Tell the view our new size. Requires a mutable borrow of cx and the view
+        let cur_size = make_new_size(constraint, &cell_width, &line_height);
+        //Note that set_size locks and mutates the terminal.
+        connection_handle.update(cx.app, |connection, _| connection.set_size(cur_size));
+
+        let (selection_color, terminal_theme) = {
+            let theme = &(cx.global::<Settings>()).theme;
+            (theme.editor.selection.selection, &theme.terminal)
+        };
+
+        let terminal_mutex = connection_handle.read(cx).term.clone();
+        let term = terminal_mutex.lock();
+        let grid = term.grid();
+        let cursor_point = grid.cursor.point;
+        let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
+
+        let content = term.renderable_content();
+
+        let layout_lines = layout_lines(
+            content.display_iter,
+            &text_style,
+            terminal_theme,
+            cx.text_layout_cache,
+            self.modal,
+            content.selection,
+        );
+
+        let block_text = cx.text_layout_cache.layout_str(
+            &cursor_text,
+            text_style.font_size,
+            &[(
+                cursor_text.len(),
+                RunStyle {
+                    font_id: text_style.font_id,
+                    color: terminal_theme.colors.background,
+                    underline: Default::default(),
+                },
+            )],
+        );
+
+        let cursor = get_cursor_shape(
+            content.cursor.point.line.0 as usize,
+            content.cursor.point.column.0 as usize,
+            content.display_offset,
+            &line_height,
+            &cell_width,
+            cur_size.total_lines(),
+            &block_text,
+        )
+        .map(move |(cursor_position, block_width)| {
+            let block_width = if block_width != 0.0 {
+                block_width
+            } else {
+                cell_width.0
+            };
+
+            Cursor::new(
+                cursor_position,
+                block_width,
+                line_height.0,
+                terminal_theme.colors.cursor,
+                CursorShape::Block,
+                Some(block_text.clone()),
+            )
+        });
+        drop(term);
+
+        let background_color = if self.modal {
+            terminal_theme.colors.modal_background
+        } else {
+            terminal_theme.colors.background
+        };
+
+        (
+            constraint.max,
+            LayoutState {
+                layout_lines,
+                line_height,
+                em_width: cell_width,
+                cursor,
+                cur_size,
+                background_color,
+                terminal: terminal_mutex,
+                selection_color,
+            },
+        )
+    }
+
+    fn paint(
+        &mut self,
+        bounds: gpui::geometry::rect::RectF,
+        visible_bounds: gpui::geometry::rect::RectF,
+        layout: &mut Self::LayoutState,
+        cx: &mut gpui::PaintContext,
+    ) -> Self::PaintState {
+        //Setup element stuff
+        let clip_bounds = Some(visible_bounds);
+
+        cx.paint_layer(clip_bounds, |cx| {
+            let cur_size = layout.cur_size.clone();
+            let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
+
+            //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
+            attach_mouse_handlers(
+                origin,
+                cur_size,
+                self.view_id,
+                &layout.terminal,
+                visible_bounds,
+                cx,
+            );
+
+            cx.paint_layer(clip_bounds, |cx| {
+                //Start with a background color
+                cx.scene.push_quad(Quad {
+                    bounds: RectF::new(bounds.origin(), bounds.size()),
+                    background: Some(layout.background_color),
+                    border: Default::default(),
+                    corner_radius: 0.,
+                });
+
+                //Draw cell backgrounds
+                for layout_line in &layout.layout_lines {
+                    for layout_cell in &layout_line.cells {
+                        let position = vec2f(
+                            (origin.x() + layout_cell.point.column as f32 * layout.em_width.0)
+                                .floor(),
+                            origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
+                        );
+                        let size = vec2f(layout.em_width.0.ceil(), layout.line_height.0);
+
+                        cx.scene.push_quad(Quad {
+                            bounds: RectF::new(position, size),
+                            background: Some(layout_cell.background_color),
+                            border: Default::default(),
+                            corner_radius: 0.,
+                        })
+                    }
+                }
+            });
+
+            //Draw Selection
+            cx.paint_layer(clip_bounds, |cx| {
+                let mut highlight_y = None;
+                let highlight_lines = layout
+                    .layout_lines
+                    .iter()
+                    .filter_map(|line| {
+                        if let Some(range) = &line.highlighted_range {
+                            if let None = highlight_y {
+                                highlight_y = Some(
+                                    origin.y()
+                                        + line.cells[0].point.line as f32 * layout.line_height.0,
+                                );
+                            }
+                            let start_x = origin.x()
+                                + line.cells[range.start].point.column as f32 * layout.em_width.0;
+                            let end_x = origin.x()
+                                + line.cells[range.end].point.column as f32 * layout.em_width.0
+                                + layout.em_width.0;
+
+                            return Some(HighlightedRangeLine { start_x, end_x });
+                        } else {
+                            return None;
+                        }
+                    })
+                    .collect::<Vec<HighlightedRangeLine>>();
+
+                if let Some(y) = highlight_y {
+                    let hr = HighlightedRange {
+                        start_y: y, //Need to change this
+                        line_height: layout.line_height.0,
+                        lines: highlight_lines,
+                        color: layout.selection_color,
+                        //Copied from editor. TODO: move to theme or something
+                        corner_radius: 0.15 * layout.line_height.0,
+                    };
+                    hr.paint(bounds, cx.scene);
+                }
+            });
+
+            cx.paint_layer(clip_bounds, |cx| {
+                for layout_line in &layout.layout_lines {
+                    for layout_cell in &layout_line.cells {
+                        let point = layout_cell.point;
+
+                        //Don't actually know the start_x for a line, until here:
+                        let cell_origin = vec2f(
+                            (origin.x() + point.column as f32 * layout.em_width.0).floor(),
+                            origin.y() + point.line as f32 * layout.line_height.0,
+                        );
+
+                        layout_cell.text.paint(
+                            cell_origin,
+                            visible_bounds,
+                            layout.line_height.0,
+                            cx,
+                        );
+                    }
+                }
+            });
+
+            //Draw cursor
+            if let Some(cursor) = &layout.cursor {
+                cx.paint_layer(clip_bounds, |cx| {
+                    cursor.paint(origin, cx);
+                })
+            }
+
+            #[cfg(debug_assertions)]
+            if DEBUG_GRID {
+                cx.paint_layer(clip_bounds, |cx| {
+                    draw_debug_grid(bounds, layout, cx);
+                })
+            }
+        });
+    }
+
+    fn dispatch_event(
+        &mut self,
+        event: &gpui::Event,
+        _bounds: gpui::geometry::rect::RectF,
+        visible_bounds: gpui::geometry::rect::RectF,
+        layout: &mut Self::LayoutState,
+        _paint: &mut Self::PaintState,
+        cx: &mut gpui::EventContext,
+    ) -> bool {
+        match event {
+            Event::ScrollWheel(ScrollWheelEvent {
+                delta, position, ..
+            }) => visible_bounds
+                .contains_point(*position)
+                .then(|| {
+                    let vertical_scroll =
+                        (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
+
+                    if let Some(connection) = self.connection.upgrade(cx.app) {
+                        connection.update(cx.app, |connection, _| {
+                            connection
+                                .term
+                                .lock()
+                                .scroll_display(Scroll::Delta(vertical_scroll.round() as i32));
+                        })
+                    }
+                })
+                .is_some(),
+            Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
+                if !cx.is_parent_view_focused() {
+                    return false;
+                }
+
+                self.connection
+                    .upgrade(cx.app)
+                    .map(|connection| {
+                        connection
+                            .update(cx.app, |connection, _| connection.try_keystroke(keystroke))
+                    })
+                    .unwrap_or(false)
+            }
+            _ => false,
+        }
+    }
+
+    fn debug(
+        &self,
+        _bounds: gpui::geometry::rect::RectF,
+        _layout: &Self::LayoutState,
+        _paint: &Self::PaintState,
+        _cx: &gpui::DebugContext,
+    ) -> gpui::serde_json::Value {
+        json!({
+            "type": "TerminalElement",
+        })
+    }
+}
+
+pub fn mouse_to_cell_data(
+    pos: Vector2F,
+    origin: Vector2F,
+    cur_size: SizeInfo,
+    display_offset: usize,
+) -> (Point, alacritty_terminal::index::Direction) {
+    let relative_pos = relative_pos(pos, origin);
+    let point = grid_cell(&relative_pos, cur_size, display_offset);
+    let side = cell_side(&relative_pos, cur_size);
+    (point, side)
+}
+
+///Configures a text style from the current settings.
+fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
+    // Pull the font family from settings properly overriding
+    let family_id = settings
+        .terminal_overrides
+        .font_family
+        .as_ref()
+        .and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
+        .or_else(|| {
+            settings
+                .terminal_defaults
+                .font_family
+                .as_ref()
+                .and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
+        })
+        .unwrap_or(settings.buffer_font_family);
+
+    TextStyle {
+        color: settings.theme.editor.text_color,
+        font_family_id: family_id,
+        font_family_name: font_cache.family_name(family_id).unwrap(),
+        font_id: font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap(),
+        font_size: settings
+            .terminal_overrides
+            .font_size
+            .or(settings.terminal_defaults.font_size)
+            .unwrap_or(settings.buffer_font_size),
+        font_properties: Default::default(),
+        underline: Default::default(),
+    }
+}
+
+///Configures a size info object from the given information.
+fn make_new_size(
+    constraint: SizeConstraint,
+    cell_width: &CellWidth,
+    line_height: &LineHeight,
+) -> SizeInfo {
+    SizeInfo::new(
+        constraint.max.x() - cell_width.0,
+        constraint.max.y(),
+        cell_width.0,
+        line_height.0,
+        0.,
+        0.,
+        false,
+    )
+}
+
+fn layout_lines(
+    grid: GridIterator<Cell>,
+    text_style: &TextStyle,
+    terminal_theme: &TerminalStyle,
+    text_layout_cache: &TextLayoutCache,
+    modal: bool,
+    selection_range: Option<SelectionRange>,
+) -> Vec<LayoutLine> {
+    let lines = grid.group_by(|i| i.point.line);
+    lines
+        .into_iter()
+        .enumerate()
+        .map(|(line_index, (_, line))| {
+            let mut highlighted_range = None;
+            let cells = line
+                .enumerate()
+                .map(|(x_index, indexed_cell)| {
+                    if selection_range
+                        .map(|range| range.contains(indexed_cell.point))
+                        .unwrap_or(false)
+                    {
+                        let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
+                        range.end = range.end.max(x_index);
+                        highlighted_range = Some(range);
+                    }
+
+                    let cell_text = &indexed_cell.c.to_string();
+
+                    let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal);
+
+                    //This is where we might be able to get better performance
+                    let layout_cell = text_layout_cache.layout_str(
+                        cell_text,
+                        text_style.font_size,
+                        &[(cell_text.len(), cell_style)],
+                    );
+
+                    LayoutCell::new(
+                        Point::new(line_index as i32, indexed_cell.point.column.0 as i32),
+                        layout_cell,
+                        convert_color(&indexed_cell.bg, &terminal_theme.colors, modal),
+                    )
+                })
+                .collect::<Vec<LayoutCell>>();
+
+            LayoutLine {
+                cells,
+                highlighted_range,
+            }
+        })
+        .collect::<Vec<LayoutLine>>()
+}
+
+// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
+// the same position for sequential indexes. Use em_width instead
+//TODO: This function is messy, too many arguments and too many ifs. Simplify.
+fn get_cursor_shape(
+    line: usize,
+    line_index: usize,
+    display_offset: usize,
+    line_height: &LineHeight,
+    cell_width: &CellWidth,
+    total_lines: usize,
+    text_fragment: &Line,
+) -> Option<(Vector2F, f32)> {
+    let cursor_line = line + display_offset;
+    if cursor_line <= total_lines {
+        let cursor_width = if text_fragment.width() == 0. {
+            cell_width.0
+        } else {
+            text_fragment.width()
+        };
+
+        Some((
+            vec2f(
+                line_index as f32 * cell_width.0,
+                cursor_line as f32 * line_height.0,
+            ),
+            cursor_width,
+        ))
+    } else {
+        None
+    }
+}
+
+///Convert the Alacritty cell styles to GPUI text styles and background color
+fn cell_style(
+    indexed: &Indexed<&Cell>,
+    style: &TerminalStyle,
+    text_style: &TextStyle,
+    modal: bool,
+) -> RunStyle {
+    let flags = indexed.cell.flags;
+    let fg = convert_color(&indexed.cell.fg, &style.colors, modal);
+
+    let underline = flags
+        .contains(Flags::UNDERLINE)
+        .then(|| Underline {
+            color: Some(fg),
+            squiggly: false,
+            thickness: OrderedFloat(1.),
+        })
+        .unwrap_or_default();
+
+    RunStyle {
+        color: fg,
+        font_id: text_style.font_id,
+        underline,
+    }
+}
+
+fn attach_mouse_handlers(
+    origin: Vector2F,
+    cur_size: SizeInfo,
+    view_id: usize,
+    terminal_mutex: &Arc<FairMutex<Term<ZedListener>>>,
+    visible_bounds: RectF,
+    cx: &mut PaintContext,
+) {
+    let click_mutex = terminal_mutex.clone();
+    let drag_mutex = terminal_mutex.clone();
+    let mouse_down_mutex = terminal_mutex.clone();
+
+    cx.scene.push_mouse_region(
+        MouseRegion::new(view_id, None, visible_bounds)
+            .on_down(MouseButton::Left, move |e, _| {
+                let mut term = mouse_down_mutex.lock();
+
+                let (point, side) = mouse_to_cell_data(
+                    e.position,
+                    origin,
+                    cur_size,
+                    term.renderable_content().display_offset,
+                );
+                term.selection = Some(Selection::new(SelectionType::Simple, point, side))
+            })
+            .on_click(MouseButton::Left, move |e, cx| {
+                let mut term = click_mutex.lock();
+
+                let (point, side) = mouse_to_cell_data(
+                    e.position,
+                    origin,
+                    cur_size,
+                    term.renderable_content().display_offset,
+                );
+
+                let selection_type = match e.click_count {
+                    0 => return, //This is a release
+                    1 => Some(SelectionType::Simple),
+                    2 => Some(SelectionType::Semantic),
+                    3 => Some(SelectionType::Lines),
+                    _ => None,
+                };
+
+                let selection = selection_type
+                    .map(|selection_type| Selection::new(selection_type, point, side));
+
+                term.selection = selection;
+                cx.focus_parent_view();
+                cx.notify();
+            })
+            .on_drag(MouseButton::Left, move |e, cx| {
+                let mut term = drag_mutex.lock();
+
+                let (point, side) = mouse_to_cell_data(
+                    e.position,
+                    origin,
+                    cur_size,
+                    term.renderable_content().display_offset,
+                );
+
+                if let Some(mut selection) = term.selection.take() {
+                    selection.update(point, side);
+                    term.selection = Some(selection);
+                }
+
+                cx.notify();
+            }),
+    );
+}
+
+///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
+fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side {
+    let x = pos.0.x() as usize;
+    let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize;
+    let half_cell_width = (cur_size.cell_width() / 2.0) as usize;
+
+    let additional_padding =
+        (cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width();
+    let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding;
+
+    if cell_x > half_cell_width
+            // Edge case when mouse leaves the window.
+            || x as f32 >= end_of_grid
+    {
+        Side::Right
+    } else {
+        Side::Left
+    }
+}
+
+///Copied (with modifications) from alacritty/src/event.rs > Mouse::point()
+///Position is a pane-relative position. That means the top left corner of the mouse
+///Region should be (0,0)
+fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point {
+    let pos = pos.0;
+    let col = pos.x() / cur_size.cell_width(); //TODO: underflow...
+    let col = min(GridCol(col as usize), cur_size.last_column());
+
+    let line = pos.y() / cur_size.cell_height();
+    let line = min(line as i32, cur_size.bottommost_line().0);
+
+    //when clicking, need to ADD to get to the top left cell
+    //e.g. total_lines - viewport_height, THEN subtract display offset
+    //0 -> total_lines - viewport_height - display_offset + mouse_line
+
+    Point::new(GridLine(line - display_offset as i32), col)
+}
+
+///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
+///Display and conceptual grid.
+#[cfg(debug_assertions)]
+fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
+    let width = layout.cur_size.width();
+    let height = layout.cur_size.height();
+    //Alacritty uses 'as usize', so shall we.
+    for col in 0..(width / layout.em_width.0).round() as usize {
+        cx.scene.push_quad(Quad {
+            bounds: RectF::new(
+                bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
+                vec2f(1., height),
+            ),
+            background: Some(Color::green()),
+            border: Default::default(),
+            corner_radius: 0.,
+        });
+    }
+    for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
+        cx.scene.push_quad(Quad {
+            bounds: RectF::new(
+                bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
+                vec2f(width, 1.),
+            ),
+            background: Some(Color::green()),
+            border: Default::default(),
+            corner_radius: 0.,
+        });
+    }
+}
+
+mod test {
+
+    #[test]
+    fn test_mouse_to_selection() {
+        let term_width = 100.;
+        let term_height = 200.;
+        let cell_width = 10.;
+        let line_height = 20.;
+        let mouse_pos_x = 100.; //Window relative
+        let mouse_pos_y = 100.; //Window relative
+        let origin_x = 10.;
+        let origin_y = 20.;
+
+        let cur_size = alacritty_terminal::term::SizeInfo::new(
+            term_width,
+            term_height,
+            cell_width,
+            line_height,
+            0.,
+            0.,
+            false,
+        );
+
+        let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
+        let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
+        let (point, _) =
+            crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
+        assert_eq!(
+            point,
+            alacritty_terminal::index::Point::new(
+                alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
+                alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
+            )
+        );
+    }
+
+    #[test]
+    fn test_mouse_to_selection_off_edge() {
+        let term_width = 100.;
+        let term_height = 200.;
+        let cell_width = 10.;
+        let line_height = 20.;
+        let mouse_pos_x = 100.; //Window relative
+        let mouse_pos_y = 100.; //Window relative
+        let origin_x = 10.;
+        let origin_y = 20.;
+
+        let cur_size = alacritty_terminal::term::SizeInfo::new(
+            term_width,
+            term_height,
+            cell_width,
+            line_height,
+            0.,
+            0.,
+            false,
+        );
+
+        let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
+        let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
+        let (point, _) =
+            crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
+        assert_eq!(
+            point,
+            alacritty_terminal::index::Point::new(
+                alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
+                alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
+            )
+        );
+    }
+}

crates/theme/src/theme.rs 🔗

@@ -78,6 +78,22 @@ pub struct TabBar {
     pub height: f32,
 }
 
+impl TabBar {
+    pub fn tab_style(&self, pane_active: bool, tab_active: bool) -> &Tab {
+        let tabs = if pane_active {
+            &self.active_pane
+        } else {
+            &self.inactive_pane
+        };
+
+        if tab_active {
+            &tabs.active_tab
+        } else {
+            &tabs.inactive_tab
+        }
+    }
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct TabStyles {
     pub active_tab: Tab,

crates/workspace/Cargo.toml 🔗

@@ -15,6 +15,7 @@ client = { path = "../client" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
+drag_and_drop = { path = "../drag_and_drop" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 menu = { path = "../menu" }

crates/workspace/src/pane.rs 🔗

@@ -876,7 +876,7 @@ impl Pane {
         });
     }
 
-    fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
+    fn render_tab_bar(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
         let theme = cx.global::<Settings>().theme.clone();
 
         enum Tabs {}
@@ -889,131 +889,38 @@ impl Pane {
                 None
             };
 
-            let is_pane_active = self.is_active;
-
-            let tab_styles = match is_pane_active {
-                true => theme.workspace.tab_bar.active_pane.clone(),
-                false => theme.workspace.tab_bar.inactive_pane.clone(),
-            };
-            let filler_style = tab_styles.inactive_tab.clone();
+            let pane_active = self.is_active;
 
             let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
-            for (ix, (item, detail)) in self.items.iter().zip(self.tab_details(cx)).enumerate() {
-                let item_id = item.id();
+            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 is_tab_active = ix == self.active_item_index;
-
-                let close_tab_callback = {
-                    let pane = pane.clone();
-                    move |_, cx: &mut EventContext| {
-                        cx.dispatch_action(CloseItem {
-                            item_id,
-                            pane: pane.clone(),
-                        })
-                    }
-                };
+                let tab_active = ix == self.active_item_index;
 
                 row.add_child({
-                    let mut tab_style = match is_tab_active {
-                        true => tab_styles.active_tab.clone(),
-                        false => tab_styles.inactive_tab.clone(),
-                    };
-
-                    let title = item.tab_content(detail, &tab_style, cx);
-
-                    if ix == 0 {
-                        tab_style.container.border.left = false;
-                    }
-
-                    MouseEventHandler::new::<Tab, _, _>(ix, cx, |_, cx| {
-                        Container::new(
-                            Flex::row()
-                                .with_child(
-                                    Align::new({
-                                        let diameter = 7.0;
-                                        let icon_color = if item.has_conflict(cx) {
-                                            Some(tab_style.icon_conflict)
-                                        } else if item.is_dirty(cx) {
-                                            Some(tab_style.icon_dirty)
-                                        } else {
-                                            None
-                                        };
-
-                                        ConstrainedBox::new(
-                                            Canvas::new(move |bounds, _, cx| {
-                                                if let Some(color) = icon_color {
-                                                    let square = RectF::new(
-                                                        bounds.origin(),
-                                                        vec2f(diameter, diameter),
-                                                    );
-                                                    cx.scene.push_quad(Quad {
-                                                        bounds: square,
-                                                        background: Some(color),
-                                                        border: Default::default(),
-                                                        corner_radius: diameter / 2.,
-                                                    });
-                                                }
-                                            })
-                                            .boxed(),
-                                        )
-                                        .with_width(diameter)
-                                        .with_height(diameter)
-                                        .boxed()
-                                    })
-                                    .boxed(),
-                                )
-                                .with_child(
-                                    Container::new(Align::new(title).boxed())
-                                        .with_style(ContainerStyle {
-                                            margin: Margin {
-                                                left: tab_style.spacing,
-                                                right: tab_style.spacing,
-                                                ..Default::default()
-                                            },
-                                            ..Default::default()
-                                        })
-                                        .boxed(),
-                                )
-                                .with_child(
-                                    Align::new(
-                                        ConstrainedBox::new(if mouse_state.hovered {
-                                            enum TabCloseButton {}
-                                            let icon = Svg::new("icons/x_mark_thin_8.svg");
-                                            MouseEventHandler::new::<TabCloseButton, _, _>(
-                                                item_id,
-                                                cx,
-                                                |mouse_state, _| {
-                                                    if mouse_state.hovered {
-                                                        icon.with_color(tab_style.icon_close_active)
-                                                            .boxed()
-                                                    } else {
-                                                        icon.with_color(tab_style.icon_close)
-                                                            .boxed()
-                                                    }
-                                                },
-                                            )
-                                            .with_padding(Padding::uniform(4.))
-                                            .with_cursor_style(CursorStyle::PointingHand)
-                                            .on_click(MouseButton::Left, close_tab_callback.clone())
-                                            .on_click(
-                                                MouseButton::Middle,
-                                                close_tab_callback.clone(),
-                                            )
-                                            .named("close-tab-icon")
-                                        } else {
-                                            Empty::new().boxed()
-                                        })
-                                        .with_width(tab_style.icon_width)
-                                        .boxed(),
-                                    )
-                                    .boxed(),
-                                )
-                                .boxed(),
-                        )
-                        .with_style(tab_style.container)
-                        .boxed()
+                    MouseEventHandler::new::<Tab, _, _>(ix, cx, {
+                        let item = item.clone();
+                        let pane = pane.clone();
+                        let hovered = mouse_state.hovered;
+
+                        move |_, cx| {
+                            Self::render_tab(
+                                &item,
+                                pane,
+                                detail,
+                                hovered,
+                                pane_active,
+                                tab_active,
+                                cx,
+                            )
+                        }
                     })
-                    .with_cursor_style(if is_tab_active && is_pane_active {
+                    .with_cursor_style(if pane_active && tab_active {
                         CursorStyle::Arrow
                     } else {
                         CursorStyle::PointingHand
@@ -1021,24 +928,20 @@ impl Pane {
                     .on_down(MouseButton::Left, move |_, cx| {
                         cx.dispatch_action(ActivateItem(ix));
                     })
-                    .on_click(MouseButton::Middle, close_tab_callback)
-                    .on_drag(MouseButton::Left, |_, cx| {
-                        cx.global::<DragAndDrop>().dragging(some view handle)
-                    })
-                    .on_up_out(MouseButton::Left, |_, cx| {
-                        cx.global::<DragAndDrop>().stopped_dragging(some view handle)
-                    })
-                    .on_drag_over(MouseButton::Left, |started, _, _, cx| {
-                        if started {
-                            if let Some(tab) = cx.global::<DragAndDrop>().current_dragged::<Tab>() {
-                                cx.dispatch_action(ReceivingTab);
-                            }
+                    .on_click(MouseButton::Middle, {
+                        let pane = pane.clone();
+                        move |_, cx: &mut EventContext| {
+                            cx.dispatch_action(CloseItem {
+                                item_id: item.id(),
+                                pane: pane.clone(),
+                            })
                         }
                     })
                     .boxed()
                 })
             }
 
+            let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
             row.add_child(
                 Empty::new()
                     .contained()
@@ -1088,6 +991,109 @@ impl Pane {
 
         tab_details
     }
+
+    fn render_tab<V: View>(
+        item: &Box<dyn ItemHandle>,
+        pane: WeakViewHandle<Pane>,
+        detail: Option<usize>,
+        hovered: bool,
+        pane_active: bool,
+        tab_active: bool,
+        cx: &mut RenderContext<V>,
+    ) -> ElementBox {
+        let theme = cx.global::<Settings>().theme.clone();
+        let tab_style = theme.workspace.tab_bar.tab_style(pane_active, tab_active);
+        let title = item.tab_content(detail, tab_style, cx);
+
+        Container::new(
+            Flex::row()
+                .with_child(
+                    Align::new({
+                        let diameter = 7.0;
+                        let icon_color = if item.has_conflict(cx) {
+                            Some(tab_style.icon_conflict)
+                        } else if item.is_dirty(cx) {
+                            Some(tab_style.icon_dirty)
+                        } else {
+                            None
+                        };
+
+                        ConstrainedBox::new(
+                            Canvas::new(move |bounds, _, cx| {
+                                if let Some(color) = icon_color {
+                                    let square =
+                                        RectF::new(bounds.origin(), vec2f(diameter, diameter));
+                                    cx.scene.push_quad(Quad {
+                                        bounds: square,
+                                        background: Some(color),
+                                        border: Default::default(),
+                                        corner_radius: diameter / 2.,
+                                    });
+                                }
+                            })
+                            .boxed(),
+                        )
+                        .with_width(diameter)
+                        .with_height(diameter)
+                        .boxed()
+                    })
+                    .boxed(),
+                )
+                .with_child(
+                    Container::new(Align::new(title).boxed())
+                        .with_style(ContainerStyle {
+                            margin: Margin {
+                                left: tab_style.spacing,
+                                right: tab_style.spacing,
+                                ..Default::default()
+                            },
+                            ..Default::default()
+                        })
+                        .boxed(),
+                )
+                .with_child(
+                    Align::new(
+                        ConstrainedBox::new(if hovered {
+                            let item_id = item.id();
+                            enum TabCloseButton {}
+                            let icon = Svg::new("icons/x_mark_thin_8.svg");
+                            MouseEventHandler::new::<TabCloseButton, _, _>(
+                                item_id,
+                                cx,
+                                |mouse_state, _| {
+                                    if mouse_state.hovered {
+                                        icon.with_color(tab_style.icon_close_active).boxed()
+                                    } else {
+                                        icon.with_color(tab_style.icon_close).boxed()
+                                    }
+                                },
+                            )
+                            .with_padding(Padding::uniform(4.))
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .on_click(MouseButton::Left, {
+                                let pane = pane.clone();
+                                move |_, cx| {
+                                    cx.dispatch_action(CloseItem {
+                                        item_id,
+                                        pane: pane.clone(),
+                                    })
+                                }
+                            })
+                            .on_click(MouseButton::Middle, |_, cx| cx.propogate_event())
+                            .named("close-tab-icon")
+                        } else {
+                            Empty::new().boxed()
+                        })
+                        .with_width(tab_style.icon_width)
+                        .boxed(),
+                    )
+                    .boxed(),
+                )
+                .boxed(),
+        )
+        .with_style(tab_style.container)
+        .boxed()
+    }
 }
 
 impl Entity for Pane {
@@ -1110,7 +1116,7 @@ impl View for Pane {
                     Flex::column()
                         .with_child({
                             let mut tab_row = Flex::row()
-                                .with_child(self.render_tabs(cx).flex(1., true).named("tabs"));
+                                .with_child(self.render_tab_bar(cx).flex(1., true).named("tabs"));
 
                             if self.is_active {
                                 tab_row.add_children([
@@ -1167,12 +1173,11 @@ impl View for Pane {
                                         },
                                     )
                                     .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_down(
-                                        MouseButton::Left,
-                                        |MouseButtonEvent { position, .. }, cx| {
-                                            cx.dispatch_action(DeploySplitMenu { position });
-                                        },
-                                    )
+                                    .on_down(MouseButton::Left, |e, cx| {
+                                        cx.dispatch_action(DeploySplitMenu {
+                                            position: e.position,
+                                        });
+                                    })
                                     .boxed(),
                                 ])
                             }

crates/workspace/src/sidebar.rs 🔗

@@ -1,7 +1,7 @@
 use crate::StatusItemView;
 use gpui::{
     elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity,
-    MouseButton, MouseMovedEvent, RenderContext, Subscription, View, ViewContext, ViewHandle,
+    MouseButton, RenderContext, Subscription, View, ViewContext, ViewHandle,
 };
 use serde::Deserialize;
 use settings::Settings;
@@ -189,26 +189,18 @@ impl Sidebar {
         })
         .with_cursor_style(CursorStyle::ResizeLeftRight)
         .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
-        .on_drag(
-            MouseButton::Left,
-            move |old_position,
-                  MouseMovedEvent {
-                      position: new_position,
-                      ..
-                  },
-                  cx| {
-                let delta = new_position.x() - old_position.x();
-                let prev_width = *actual_width.borrow();
-                *custom_width.borrow_mut() = 0f32
-                    .max(match side {
-                        Side::Left => prev_width + delta,
-                        Side::Right => prev_width - delta,
-                    })
-                    .round();
+        .on_drag(MouseButton::Left, move |e, cx| {
+            let delta = e.prev_drag_position.x() - e.position.x();
+            let prev_width = *actual_width.borrow();
+            *custom_width.borrow_mut() = 0f32
+                .max(match side {
+                    Side::Left => prev_width + delta,
+                    Side::Right => prev_width - delta,
+                })
+                .round();
 
-                cx.notify();
-            },
-        )
+            cx.notify();
+        })
         .boxed()
     }
 }

crates/workspace/src/workspace.rs 🔗

@@ -16,6 +16,7 @@ use client::{
 };
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
+use drag_and_drop::DragAndDrop;
 use futures::{channel::oneshot, FutureExt};
 use gpui::{
     actions,
@@ -895,6 +896,9 @@ impl Workspace {
             status_bar
         });
 
+        let drag_and_drop = DragAndDrop::new(cx.weak_handle(), cx);
+        cx.set_global(drag_and_drop);
+
         let mut this = Workspace {
             modal: None,
             weak_self,
@@ -2471,6 +2475,7 @@ impl View for Workspace {
                     .with_background_color(theme.workspace.background)
                     .boxed(),
             )
+            .with_children(DragAndDrop::render(cx))
             .with_children(self.render_disconnected_overlay(cx))
             .named("workspace")
     }