Add double click handling

Mikayla created

Change summary

crates/gpui2/src/app.rs                    |  4 +
crates/gpui2/src/platform.rs               |  1 
crates/gpui2/src/platform/mac/platform.rs  |  8 +++
crates/gpui2/src/platform/test/platform.rs |  5 ++
crates/workspace2/src/dock.rs              | 53 +++++++++++++++++++++-
crates/workspace2/src/workspace2.rs        | 54 +++++++++++++++++++++--
6 files changed, 115 insertions(+), 10 deletions(-)

Detailed changes

crates/gpui2/src/app.rs 🔗

@@ -514,6 +514,10 @@ impl AppContext {
         self.platform.path_for_auxiliary_executable(name)
     }
 
+    pub fn double_click_interval(&self) -> Duration {
+        self.platform.double_click_interval()
+    }
+
     pub fn prompt_for_paths(
         &self,
         options: PathPromptOptions,

crates/gpui2/src/platform.rs 🔗

@@ -102,6 +102,7 @@ pub(crate) trait Platform: 'static {
     fn app_version(&self) -> Result<SemanticVersion>;
     fn app_path(&self) -> Result<PathBuf>;
     fn local_timezone(&self) -> UtcOffset;
+    fn double_click_interval(&self) -> Duration;
     fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
 
     fn set_cursor_style(&self, style: CursorStyle);

crates/gpui2/src/platform/mac/platform.rs 🔗

@@ -48,6 +48,7 @@ use std::{
     rc::Rc,
     slice, str,
     sync::Arc,
+    time::Duration,
 };
 use time::UtcOffset;
 
@@ -650,6 +651,13 @@ impl Platform for MacPlatform {
         "macOS"
     }
 
+    fn double_click_interval(&self) -> Duration {
+        unsafe {
+            let double_click_interval: f64 = msg_send![class!(NSEvent), doubleClickInterval];
+            Duration::from_secs_f64(double_click_interval)
+        }
+    }
+
     fn os_version(&self) -> Result<SemanticVersion> {
         unsafe {
             let process_info = NSProcessInfo::processInfo(nil);

crates/gpui2/src/platform/test/platform.rs 🔗

@@ -11,6 +11,7 @@ use std::{
     path::PathBuf,
     rc::{Rc, Weak},
     sync::Arc,
+    time::Duration,
 };
 
 pub struct TestPlatform {
@@ -272,4 +273,8 @@ impl Platform for TestPlatform {
     fn delete_credentials(&self, _url: &str) -> Result<()> {
         Ok(())
     }
+
+    fn double_click_interval(&self) -> std::time::Duration {
+        Duration::from_millis(500)
+    }
 }

crates/workspace2/src/dock.rs 🔗

@@ -1,8 +1,9 @@
 use crate::{status_bar::StatusItemView, Workspace};
+use crate::{DockClickReset, DockDragState};
 use gpui::{
-    div, px, Action, AnchorCorner, AnyView, AppContext, Axis, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, FocusableView, IntoElement, ParentElement, Render, SharedString, Styled,
-    Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
+    div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
+    EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
+    SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -364,7 +365,7 @@ impl Dock {
                         this.set_open(false, cx);
                     }
                 }
-                PanelEvent::Focus => todo!(),
+                PanelEvent::Focus => {}
             }),
         ];
 
@@ -485,6 +486,48 @@ impl Render for Dock {
         if let Some(entry) = self.visible_entry() {
             let size = entry.panel.size(cx);
 
+            let mut pre_resize_handle = None;
+            let mut post_resize_handle = None;
+            let position = self.position;
+            let handler = div()
+                .id("resize-handle")
+                .bg(gpui::red())
+                .on_mouse_down(gpui::MouseButton::Left, move |_, cx| {
+                    cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(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();
+                                }));
+                            }
+                        })
+                    }
+                }));
+
+            match self.position() {
+                DockPosition::Left => {
+                    post_resize_handle = Some(handler.w_2().h_full().cursor_col_resize())
+                }
+                DockPosition::Bottom => {
+                    pre_resize_handle = Some(handler.w_full().h_2().cursor_row_resize())
+                }
+                DockPosition::Right => {
+                    pre_resize_handle = Some(handler.w_full().h_1().cursor_col_resize())
+                }
+            }
+
             div()
                 .border_color(cx.theme().colors().border)
                 .map(|this| match self.position().axis() {
@@ -496,7 +539,9 @@ impl Render for Dock {
                     DockPosition::Right => this.border_l(),
                     DockPosition::Bottom => this.border_t(),
                 })
+                .children(pre_resize_handle)
                 .child(entry.panel.to_any())
+                .children(post_resize_handle)
         } else {
             div()
         }

crates/workspace2/src/workspace2.rs 🔗

@@ -29,12 +29,12 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
+    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, ParentElement, PathPromptOptions, Point,
-    PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext,
-    WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    KeyContext, ManagedView, Model, ModelContext, MouseMoveEvent, 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,6 +227,9 @@ 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);
 
@@ -480,8 +483,6 @@ struct FollowerState {
     items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
 }
 
-enum WorkspaceBounds {}
-
 impl Workspace {
     pub fn new(
         workspace_id: WorkspaceId,
@@ -3571,6 +3572,16 @@ 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<()>>);
+
 impl Render for Workspace {
     type Element = Div;
 
@@ -3614,6 +3625,37 @@ 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 {
+                                DockPosition::Left => {
+                                    let size = e.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;
+                                    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;
+                                    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()