Add new drag API

Mikayla created

Change summary

crates/gpui2/src/app.rs                  |  4 +
crates/gpui2/src/element.rs              | 73 +++++++++----------------
crates/gpui2/src/elements/canvas.rs      |  6 +-
crates/gpui2/src/elements/div.rs         | 37 ++++++++++++
crates/gpui2_macros/src/derive_render.rs | 23 ++++++++
crates/gpui2_macros/src/gpui2_macros.rs  |  6 ++
crates/workspace2/src/dock.rs            | 25 +-------
crates/workspace2/src/workspace2.rs      | 51 +++++++----------
8 files changed, 124 insertions(+), 101 deletions(-)

Detailed changes

crates/gpui2/src/app.rs 🔗

@@ -1138,6 +1138,10 @@ impl AppContext {
     pub fn has_active_drag(&self) -> bool {
         self.active_drag.is_some()
     }
+
+    pub fn active_drag(&self) -> Option<AnyView> {
+        self.active_drag.as_ref().map(|drag| drag.view.clone())
+    }
 }
 
 impl Context for AppContext {

crates/gpui2/src/element.rs 🔗

@@ -482,48 +482,31 @@ impl IntoElement for AnyElement {
     }
 }
 
-// impl<V, E, F> Element for Option<F>
-// where
-//     V: 'static,
-//     E: Element,
-//     F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
-// {
-//     type State = Option<AnyElement>;
-
-//     fn element_id(&self) -> Option<ElementId> {
-//         None
-//     }
-
-//     fn layout(
-//         &mut self,
-//         _: Option<Self::State>,
-//         cx: &mut WindowContext,
-//     ) -> (LayoutId, Self::State) {
-//         let render = self.take().unwrap();
-//         let mut element = (render)(view_state, cx).into_any();
-//         let layout_id = element.layout(view_state, cx);
-//         (layout_id, Some(element))
-//     }
-
-//     fn paint(
-//         self,
-//         _bounds: Bounds<Pixels>,
-//         rendered_element: &mut Self::State,
-//         cx: &mut WindowContext,
-//     ) {
-//         rendered_element.take().unwrap().paint(view_state, cx);
-//     }
-// }
-
-// impl<V, E, F> RenderOnce for Option<F>
-// where
-//     V: 'static,
-//     E: Element,
-//     F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
-// {
-//     type Element = Self;
-
-//     fn render(self) -> Self::Element {
-//         self
-//     }
-// }
+/// The empty element, which renders nothing.
+pub type Empty = ();
+
+impl IntoElement for () {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<ElementId> {
+        None
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+impl Element for () {
+    type State = ();
+
+    fn layout(
+        &mut self,
+        _state: Option<Self::State>,
+        cx: &mut WindowContext,
+    ) -> (LayoutId, Self::State) {
+        (cx.request_layout(&crate::Style::default(), None), ())
+    }
+
+    fn paint(self, _bounds: Bounds<Pixels>, _state: &mut Self::State, _cx: &mut WindowContext) {}
+}

crates/gpui2/src/elements/canvas.rs 🔗

@@ -2,7 +2,7 @@ use refineable::Refineable as _;
 
 use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
 
-pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
+pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
     Canvas {
         paint_callback: Box::new(callback),
         style: StyleRefinement::default(),
@@ -10,7 +10,7 @@ pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext
 }
 
 pub struct Canvas {
-    paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>,
+    paint_callback: Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>,
     style: StyleRefinement,
 }
 
@@ -41,7 +41,7 @@ impl Element for Canvas {
     }
 
     fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
-        (self.paint_callback)(bounds, cx)
+        (self.paint_callback)(&bounds, cx)
     }
 }
 

crates/gpui2/src/elements/div.rs 🔗

@@ -29,6 +29,11 @@ pub struct GroupStyle {
     pub style: Box<StyleRefinement>,
 }
 
+pub struct DragMoveEvent<W: Render> {
+    pub event: MouseMoveEvent,
+    pub drag: View<W>,
+}
+
 pub trait InteractiveElement: Sized {
     fn interactivity(&mut self) -> &mut Interactivity;
 
@@ -192,6 +197,34 @@ pub trait InteractiveElement: Sized {
         self
     }
 
+    fn on_drag_move<W>(
+        mut self,
+        listener: impl Fn(&DragMoveEvent<W>, &mut WindowContext) + 'static,
+    ) -> Self
+    where
+        W: Render,
+    {
+        self.interactivity().mouse_move_listeners.push(Box::new(
+            move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture
+                    && bounds.drag_target_contains(&event.position, cx)
+                {
+                    if let Some(view) = cx.active_drag().and_then(|view| view.downcast::<W>().ok())
+                    {
+                        (listener)(
+                            &DragMoveEvent {
+                                event: event.clone(),
+                                drag: view,
+                            },
+                            cx,
+                        );
+                    }
+                }
+            },
+        ));
+        self
+    }
+
     fn on_scroll_wheel(
         mut self,
         listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static,
@@ -403,7 +436,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
         self
     }
 
-    fn on_drag<W>(mut self, listener: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self
+    fn on_drag<W>(mut self, constructor: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self
     where
         Self: Sized,
         W: 'static + Render,
@@ -413,7 +446,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
             "calling on_drag more than once on the same element is not supported"
         );
         self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag {
-            view: listener(cx).into(),
+            view: constructor(cx).into(),
             cursor_offset,
         }));
         self

crates/gpui2_macros/src/derive_render.rs 🔗

@@ -0,0 +1,23 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput};
+
+pub fn derive_render(input: TokenStream) -> TokenStream {
+    let ast = parse_macro_input!(input as DeriveInput);
+    let type_name = &ast.ident;
+    let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
+
+    let gen = quote! {
+        impl #impl_generics gpui::Render for #type_name #type_generics
+        #where_clause
+        {
+            type Element = ();
+
+            fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+                ()
+            }
+        }
+    };
+
+    gen.into()
+}

crates/gpui2_macros/src/gpui2_macros.rs 🔗

@@ -1,4 +1,5 @@
 mod derive_into_element;
+mod derive_render;
 mod register_action;
 mod style_helpers;
 mod test;
@@ -15,6 +16,11 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream {
     derive_into_element::derive_into_element(input)
 }
 
+#[proc_macro_derive(Render)]
+pub fn derive_render(input: TokenStream) -> TokenStream {
+    derive_render::derive_render(input)
+}
+
 #[proc_macro]
 pub fn style_helpers(input: TokenStream) -> TokenStream {
     style_helpers::style_helpers(input)

crates/workspace2/src/dock.rs 🔗

@@ -1,5 +1,5 @@
+use crate::DraggedDock;
 use crate::{status_bar::StatusItemView, Workspace};
-use crate::{DockClickReset, DockDragState};
 use gpui::{
     div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
     EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
@@ -493,27 +493,10 @@ impl Render for Dock {
             let handler = div()
                 .id("resize-handle")
                 .bg(cx.theme().colors().border)
-                .on_mouse_down(gpui::MouseButton::Left, move |_, cx| {
-                    cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(position))
-                })
+                .on_drag(move |cx| cx.build_view(|_| DraggedDock(position)))
                 .on_click(cx.listener(|v, e: &ClickEvent, cx| {
-                    if e.down.button == MouseButton::Left {
-                        cx.update_global(|state: &mut DockClickReset, cx| {
-                            if state.0.is_some() {
-                                state.0 = None;
-                                v.resize_active_panel(None, cx)
-                            } else {
-                                let double_click = cx.double_click_interval();
-                                let timer = cx.background_executor().timer(double_click);
-                                state.0 = Some(cx.spawn(|_, mut cx| async move {
-                                    timer.await;
-                                    cx.update_global(|state: &mut DockClickReset, cx| {
-                                        state.0 = None;
-                                    })
-                                    .ok();
-                                }));
-                            }
-                        })
+                    if e.down.button == MouseButton::Left && e.down.click_count == 2 {
+                        v.resize_active_panel(None, cx)
                     }
                 }));
 

crates/workspace2/src/workspace2.rs 🔗

@@ -30,11 +30,11 @@ use futures::{
 };
 use gpui::{
     actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
-    AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity,
-    EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement,
-    KeyContext, ManagedView, Model, ModelContext, MouseMoveEvent, ParentElement, PathPromptOptions,
-    Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext,
-    VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
+    DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels,
+    InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement,
+    PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View,
+    ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -227,9 +227,6 @@ pub fn init_settings(cx: &mut AppContext) {
 }
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
-    cx.default_global::<DockDragState>();
-    cx.default_global::<DockClickReset>();
-
     init_settings(cx);
     notifications::init(cx);
 
@@ -466,6 +463,7 @@ pub struct Workspace {
     _observe_current_user: Task<Result<()>>,
     _schedule_serialize: Option<Task<()>>,
     pane_history_timestamp: Arc<AtomicUsize>,
+    bounds: Bounds<Pixels>,
 }
 
 impl EventEmitter<Event> for Workspace {}
@@ -708,6 +706,8 @@ impl Workspace {
             subscriptions,
             pane_history_timestamp,
             workspace_actions: Default::default(),
+            // This data will be incorrect, but it will be overwritten by the time it needs to be used.
+            bounds: Default::default(),
         }
     }
 
@@ -3580,13 +3580,8 @@ impl FocusableView for Workspace {
 
 struct WorkspaceBounds(Bounds<Pixels>);
 
-//todo!("remove this when better drag APIs are in GPUI2")
-#[derive(Default)]
-struct DockDragState(Option<DockPosition>);
-
-//todo!("remove this when better double APIs are in GPUI2")
-#[derive(Default)]
-struct DockClickReset(Option<Task<()>>);
+#[derive(Render)]
+struct DraggedDock(DockPosition);
 
 impl Render for Workspace {
     type Element = Div;
@@ -3632,37 +3627,33 @@ impl Render for Workspace {
                     .border_t()
                     .border_b()
                     .border_color(cx.theme().colors().border)
-                    .on_mouse_up(gpui::MouseButton::Left, |_, cx| {
-                        cx.update_global(|drag: &mut DockDragState, cx| {
-                            drag.0 = None;
-                        })
-                    })
-                    .on_mouse_move(cx.listener(|workspace, e: &MouseMoveEvent, cx| {
-                        if let Some(types) = &cx.global::<DockDragState>().0 {
-                            let workspace_bounds = cx.global::<WorkspaceBounds>().0;
-                            match types {
+                    .child(canvas(
+                        cx.listener(|workspace, bounds, cx| workspace.bounds = *bounds),
+                    ))
+                    .on_drag_move(
+                        cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
+                            match e.drag.read(cx).0 {
                                 DockPosition::Left => {
-                                    let size = e.position.x;
+                                    let size = e.event.position.x;
                                     workspace.left_dock.update(cx, |left_dock, cx| {
                                         left_dock.resize_active_panel(Some(size.0), cx);
                                     });
                                 }
                                 DockPosition::Right => {
-                                    let size = workspace_bounds.size.width - e.position.x;
+                                    let size = workspace.bounds.size.width - e.event.position.x;
                                     workspace.right_dock.update(cx, |right_dock, cx| {
                                         right_dock.resize_active_panel(Some(size.0), cx);
                                     });
                                 }
                                 DockPosition::Bottom => {
-                                    let size = workspace_bounds.size.height - e.position.y;
+                                    let size = workspace.bounds.size.height - e.event.position.y;
                                     workspace.bottom_dock.update(cx, |bottom_dock, cx| {
                                         bottom_dock.resize_active_panel(Some(size.0), cx);
                                     });
                                 }
                             }
-                        }
-                    }))
-                    .child(canvas(|bounds, cx| cx.set_global(WorkspaceBounds(bounds))))
+                        }),
+                    )
                     .child(self.modal_layer.clone())
                     .child(
                         div()