Fix scrolling in collab panel (#4105)

Mikayla Maki created

When the `List` element's state is `ListState::reset()`, it eagerly
trashes it's cached element heights in anticipation of a prompt render.
But, due to the recent `display_layer` changes, that re-render is not
always forthcoming. This is a problem for `ListState::scroll()`, which
depends on these cached elements to correctly calculate the new logical
scroll offset.

Solutions we attempted:

- Cache the element heights and continue the scroll calculation 
- This was conceptually incorrect, reset should only be called when the
underlying data has been changed, making any calculation with the old
results meaningless.
- Lazily re-compute the element heights in scroll 
- Beyond being a non-trivial refactor, this would probably also cause us
to double-render the list in a single frame, which is bad.
- Cache the scroll offset and only calculate it in paint 
- This solution felt awkward to implement and meant we can't supply
synchronous list scroll events.
- Delay resetting until paint 
- This means that all of the other APIs that `ListState` supplies would
give temporarily incorrect results, worsening the problem

Given these issues, we settled on the solution with the least
compromises: drop scroll events if the state has been `reset()` between
`paint()` and `scroll()`. This shifts the responsibility for the problem
out of the List element and into consumers of `List`, if you want
perfectly smooth scrolling then you need to use `reset()` judiciously
and prefer `splice()`.

That said, I tested this by aggressively scrolling the Collab panel, and
it seems to work as well as it did before.

This PR also includes some initial testing infrastructure for working
with input from the platform and rendered elements.

Release Notes:

- N/A

Change summary

crates/collab/src/tests/editor_tests.rs   |  78 ++++------
crates/gpui/src/app/test_context.rs       |  64 ++++++--
crates/gpui/src/element.rs                |   6 
crates/gpui/src/elements/list.rs          |  69 +++++++++
crates/gpui/src/interactive.rs            | 175 ++++++++++++++++--------
crates/gpui/src/keymap/matcher.rs         |   1 
crates/gpui/src/platform.rs               |   6 
crates/gpui/src/platform/mac/events.rs    |   8 
crates/gpui/src/platform/mac/platform.rs  |  10 
crates/gpui/src/platform/mac/window.rs    |  56 ++++---
crates/gpui/src/platform/test/platform.rs |   2 
crates/gpui/src/platform/test/window.rs   |  14 +-
crates/gpui/src/window.rs                 |  61 ++++----
crates/search/src/buffer_search.rs        |   8 
14 files changed, 351 insertions(+), 207 deletions(-)

Detailed changes

crates/collab/src/tests/editor_tests.rs 🔗

@@ -185,31 +185,27 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
         .await
         .unwrap();
-    let window_a = cx_a.add_empty_window();
-    let editor_a =
-        window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
+    let cx_a = cx_a.add_empty_window();
+    let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
 
     let mut editor_cx_a = EditorTestContext {
-        cx: VisualTestContext::from_window(window_a, cx_a),
-        window: window_a.into(),
+        cx: cx_a.clone(),
+        window: cx_a.handle(),
         editor: editor_a,
         assertion_cx: AssertionContextManager::new(),
     };
 
-    let window_b = cx_b.add_empty_window();
-    let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
-
+    let cx_b = cx_b.add_empty_window();
     // Open a buffer as client B
     let buffer_b = project_b
-        .update(&mut cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
         .await
         .unwrap();
-    let editor_b = window_b.build_view(&mut cx_b, |cx| {
-        Editor::for_buffer(buffer_b, Some(project_b), cx)
-    });
+    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
+
     let mut editor_cx_b = EditorTestContext {
-        cx: cx_b,
-        window: window_b.into(),
+        cx: cx_b.clone(),
+        window: cx_b.handle(),
         editor: editor_b,
         assertion_cx: AssertionContextManager::new(),
     };
@@ -311,10 +307,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    let window_b = cx_b.add_empty_window();
-    let editor_b = window_b.build_view(cx_b, |cx| {
-        Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
-    });
+    let cx_b = cx_b.add_empty_window();
+    let editor_b =
+        cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx));
 
     let fake_language_server = fake_language_servers.next().await.unwrap();
     cx_a.background_executor.run_until_parked();
@@ -323,10 +318,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
         assert!(!buffer.completion_triggers().is_empty())
     });
 
-    let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
-
     // Type a completion trigger character as the guest.
-    editor_b.update(&mut cx_b, |editor, cx| {
+    editor_b.update(cx_b, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
         editor.handle_input(".", cx);
     });
@@ -392,8 +385,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
     });
 
     // Confirm a completion on the guest.
-
-    editor_b.update(&mut cx_b, |editor, cx| {
+    editor_b.update(cx_b, |editor, cx| {
         assert!(editor.context_menu_visible());
         editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
         assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
@@ -431,7 +423,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
         );
     });
 
-    buffer_b.read_with(&mut cx_b, |buffer, _| {
+    buffer_b.read_with(cx_b, |buffer, _| {
         assert_eq!(
             buffer.text(),
             "use d::SomeTrait;\nfn main() { a.first_method() }"
@@ -960,7 +952,7 @@ async fn test_share_project(
     cx_c: &mut TestAppContext,
 ) {
     let executor = cx_a.executor();
-    let window_b = cx_b.add_empty_window();
+    let cx_b = cx_b.add_empty_window();
     let mut server = TestServer::start(executor.clone()).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
@@ -1075,7 +1067,7 @@ async fn test_share_project(
         .await
         .unwrap();
 
-    let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
+    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
 
     // Client A sees client B's selection
     executor.run_until_parked();
@@ -1089,8 +1081,7 @@ async fn test_share_project(
     });
 
     // Edit the buffer as client B and see that edit as client A.
-    let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
-    editor_b.update(&mut cx_b, |editor, cx| editor.handle_input("ok, ", cx));
+    editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
     executor.run_until_parked();
 
     buffer_a.read_with(cx_a, |buffer, _| {
@@ -1099,7 +1090,7 @@ async fn test_share_project(
 
     // Client B can invite client C on a project shared by client A.
     active_call_b
-        .update(&mut cx_b, |call, cx| {
+        .update(cx_b, |call, cx| {
             call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
         })
         .await
@@ -1190,12 +1181,8 @@ async fn test_on_input_format_from_host_to_guest(
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    let window_a = cx_a.add_empty_window();
-    let editor_a = window_a
-        .update(cx_a, |_, cx| {
-            cx.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx))
-        })
-        .unwrap();
+    let cx_a = cx_a.add_empty_window();
+    let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
 
     let fake_language_server = fake_language_servers.next().await.unwrap();
     executor.run_until_parked();
@@ -1226,10 +1213,9 @@ async fn test_on_input_format_from_host_to_guest(
         .await
         .unwrap();
 
-    let mut cx_a = VisualTestContext::from_window(window_a, cx_a);
     // Type a on type formatting trigger character as the guest.
     cx_a.focus_view(&editor_a);
-    editor_a.update(&mut cx_a, |editor, cx| {
+    editor_a.update(cx_a, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
         editor.handle_input(">", cx);
     });
@@ -1241,7 +1227,7 @@ async fn test_on_input_format_from_host_to_guest(
     });
 
     // Undo should remove LSP edits first
-    editor_a.update(&mut cx_a, |editor, cx| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(editor.text(cx), "fn main() { a>~< }");
         editor.undo(&Undo, cx);
         assert_eq!(editor.text(cx), "fn main() { a> }");
@@ -1252,7 +1238,7 @@ async fn test_on_input_format_from_host_to_guest(
         assert_eq!(buffer.text(), "fn main() { a> }")
     });
 
-    editor_a.update(&mut cx_a, |editor, cx| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(editor.text(cx), "fn main() { a> }");
         editor.undo(&Undo, cx);
         assert_eq!(editor.text(cx), "fn main() { a }");
@@ -1323,17 +1309,15 @@ async fn test_on_input_format_from_guest_to_host(
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    let window_b = cx_b.add_empty_window();
-    let editor_b = window_b.build_view(cx_b, |cx| {
-        Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
-    });
+    let cx_b = cx_b.add_empty_window();
+    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
 
     let fake_language_server = fake_language_servers.next().await.unwrap();
     executor.run_until_parked();
-    let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
+
     // Type a on type formatting trigger character as the guest.
     cx_b.focus_view(&editor_b);
-    editor_b.update(&mut cx_b, |editor, cx| {
+    editor_b.update(cx_b, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
         editor.handle_input(":", cx);
     });
@@ -1374,7 +1358,7 @@ async fn test_on_input_format_from_guest_to_host(
     });
 
     // Undo should remove LSP edits first
-    editor_b.update(&mut cx_b, |editor, cx| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(editor.text(cx), "fn main() { a:~: }");
         editor.undo(&Undo, cx);
         assert_eq!(editor.text(cx), "fn main() { a: }");
@@ -1385,7 +1369,7 @@ async fn test_on_input_format_from_guest_to_host(
         assert_eq!(buffer.text(), "fn main() { a: }")
     });
 
-    editor_b.update(&mut cx_b, |editor, cx| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(editor.text(cx), "fn main() { a: }");
         editor.undo(&Undo, cx);
         assert_eq!(editor.text(cx), "fn main() { a }");

crates/gpui/src/app/test_context.rs 🔗

@@ -1,11 +1,11 @@
 #![deny(missing_docs)]
 
 use crate::{
-    div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
-    BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor,
-    IntoElement, Keystroke, Model, ModelContext, Pixels, Platform, Render, Result, Size, Task,
-    TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext,
-    WindowContext, WindowHandle, WindowOptions,
+    Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
+    AvailableSpace, BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter,
+    ForegroundExecutor, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform, Point,
+    Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View,
+    ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
@@ -167,10 +167,14 @@ impl TestAppContext {
     }
 
     /// Adds a new window with no content.
-    pub fn add_empty_window(&mut self) -> AnyWindowHandle {
+    pub fn add_empty_window(&mut self) -> &mut VisualTestContext {
         let mut cx = self.app.borrow_mut();
-        cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {}))
-            .any_handle
+        let window = cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| ()));
+        drop(cx);
+        let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
+        cx.run_until_parked();
+        // it might be nice to try and cleanup these at the end of each test.
+        Box::leak(cx)
     }
 
     /// Adds a new window, and returns its root view and a `VisualTestContext` which can be used
@@ -564,6 +568,11 @@ pub struct VisualTestContext {
 }
 
 impl<'a> VisualTestContext {
+    /// Get the underlying window handle underlying this context.
+    pub fn handle(&self) -> AnyWindowHandle {
+        self.window
+    }
+
     /// Provides the `WindowContext` for the duration of the closure.
     pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
         self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
@@ -609,6 +618,36 @@ impl<'a> VisualTestContext {
         self.cx.simulate_input(self.window, input)
     }
 
+    /// Draw an element to the window. Useful for simulating events or actions
+    pub fn draw(
+        &mut self,
+        origin: Point<Pixels>,
+        space: Size<AvailableSpace>,
+        f: impl FnOnce(&mut WindowContext) -> AnyElement,
+    ) {
+        self.update(|cx| {
+            let entity_id = cx
+                .window
+                .root_view
+                .as_ref()
+                .expect("Can't draw to this window without a root view")
+                .entity_id();
+            cx.with_view_id(entity_id, |cx| {
+                f(cx).draw(origin, space, cx);
+            });
+
+            cx.refresh();
+        })
+    }
+
+    /// Simulate an event from the platform, e.g. a SrollWheelEvent
+    /// Make sure you've called [VisualTestContext::draw] first!
+    pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
+        self.test_window(self.window)
+            .simulate_input(event.to_platform_input());
+        self.background_executor.run_until_parked();
+    }
+
     /// Simulates the user blurring the window.
     pub fn deactivate_window(&mut self) {
         if Some(self.window) == self.test_platform.active_window() {
@@ -763,12 +802,3 @@ impl AnyWindowHandle {
         self.update(cx, |_, cx| cx.new_view(build_view)).unwrap()
     }
 }
-
-/// An EmptyView for testing.
-pub struct EmptyView {}
-
-impl Render for EmptyView {
-    fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> impl IntoElement {
-        div()
-    }
-}

crates/gpui/src/element.rs 🔗

@@ -115,6 +115,12 @@ pub trait Render: 'static + Sized {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
 }
 
+impl Render for () {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        ()
+    }
+}
+
 /// You can derive [`IntoElement`] on any type that implements this trait.
 /// It is used to allow views to be expressed in terms of abstract data.
 pub trait RenderOnce: 'static {

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

@@ -30,6 +30,7 @@ struct StateInner {
     logical_scroll_top: Option<ListOffset>,
     alignment: ListAlignment,
     overdraw: Pixels,
+    reset: bool,
     #[allow(clippy::type_complexity)]
     scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
 }
@@ -92,11 +93,17 @@ impl ListState {
             alignment: orientation,
             overdraw,
             scroll_handler: None,
+            reset: false,
         })))
     }
 
+    /// Reset this instantiation of the list state.
+    ///
+    /// Note that this will cause scroll events to be dropped until the next paint.
     pub fn reset(&self, element_count: usize) {
         let state = &mut *self.0.borrow_mut();
+        state.reset = true;
+
         state.logical_scroll_top = None;
         state.items = SumTree::new();
         state
@@ -152,11 +159,13 @@ impl ListState {
             scroll_top.item_ix = item_count;
             scroll_top.offset_in_item = px(0.);
         }
+
         state.logical_scroll_top = Some(scroll_top);
     }
 
     pub fn scroll_to_reveal_item(&self, ix: usize) {
         let state = &mut *self.0.borrow_mut();
+
         let mut scroll_top = state.logical_scroll_top();
         let height = state
             .last_layout_bounds
@@ -187,9 +196,9 @@ impl ListState {
     /// Get the bounds for the given item in window coordinates.
     pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
         let state = &*self.0.borrow();
+
         let bounds = state.last_layout_bounds.unwrap_or_default();
         let scroll_top = state.logical_scroll_top();
-
         if ix < scroll_top.item_ix {
             return None;
         }
@@ -230,6 +239,12 @@ impl StateInner {
         delta: Point<Pixels>,
         cx: &mut WindowContext,
     ) {
+        // Drop scroll events after a reset, since we can't calculate
+        // the new logical scroll top without the item heights
+        if self.reset {
+            return;
+        }
+
         let scroll_max = (self.items.summary().height - height).max(px(0.));
         let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
             .max(px(0.))
@@ -325,6 +340,8 @@ impl Element for List {
     ) {
         let state = &mut *self.state.0.borrow_mut();
 
+        state.reset = false;
+
         // If the width of the list has changed, invalidate all cached item heights
         if state.last_layout_bounds.map_or(true, |last_bounds| {
             last_bounds.size.width != bounds.size.width
@@ -346,8 +363,9 @@ impl Element for List {
             height: AvailableSpace::MinContent,
         };
 
-        // Render items after the scroll top, including those in the trailing overdraw
         let mut cursor = old_items.cursor::<Count>();
+
+        // Render items after the scroll top, including those in the trailing overdraw
         cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
         for (ix, item) in cursor.by_ref().enumerate() {
             let visible_height = rendered_height - scroll_top.offset_in_item;
@@ -461,6 +479,7 @@ impl Element for List {
 
         let list_state = self.state.clone();
         let height = bounds.size.height;
+
         cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
             if phase == DispatchPhase::Bubble
                 && bounds.contains(&event.position)
@@ -562,3 +581,49 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
         self.0.partial_cmp(&other.height).unwrap()
     }
 }
+
+#[cfg(test)]
+mod test {
+
+    use gpui::{ScrollDelta, ScrollWheelEvent};
+
+    use crate::{self as gpui, TestAppContext};
+
+    #[gpui::test]
+    fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
+        use crate::{div, list, point, px, size, Element, ListState, Styled};
+
+        let cx = cx.add_empty_window();
+
+        let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _| {
+            div().h(px(10.)).w_full().into_any()
+        });
+
+        // Ensure that the list is scrolled to the top
+        state.scroll_to(gpui::ListOffset {
+            item_ix: 0,
+            offset_in_item: px(0.0),
+        });
+
+        // Paint
+        cx.draw(
+            point(px(0.), px(0.)),
+            size(px(100.), px(20.)).into(),
+            |_| list(state.clone()).w_full().h_full().z_index(10).into_any(),
+        );
+
+        // Reset
+        state.reset(5);
+
+        // And then recieve a scroll event _before_ the next paint
+        cx.simulate_event(ScrollWheelEvent {
+            position: point(px(1.), px(1.)),
+            delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
+            ..Default::default()
+        });
+
+        // Scroll position should stay at the top of the list
+        assert_eq!(state.logical_scroll_top().item_ix, 0);
+        assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
+    }
+}

crates/gpui/src/interactive.rs 🔗

@@ -1,8 +1,14 @@
 use crate::{
-    div, point, Element, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext,
+    point, seal::Sealed, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext,
 };
 use smallvec::SmallVec;
-use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
+use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf};
+
+pub trait InputEvent: Sealed + 'static {
+    fn to_platform_input(self) -> PlatformInput;
+}
+pub trait KeyEvent: InputEvent {}
+pub trait MouseEvent: InputEvent {}
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct KeyDownEvent {
@@ -10,16 +16,40 @@ pub struct KeyDownEvent {
     pub is_held: bool,
 }
 
+impl Sealed for KeyDownEvent {}
+impl InputEvent for KeyDownEvent {
+    fn to_platform_input(self) -> PlatformInput {
+        PlatformInput::KeyDown(self)
+    }
+}
+impl KeyEvent for KeyDownEvent {}
+
 #[derive(Clone, Debug)]
 pub struct KeyUpEvent {
     pub keystroke: Keystroke,
 }
 
+impl Sealed for KeyUpEvent {}
+impl InputEvent for KeyUpEvent {
+    fn to_platform_input(self) -> PlatformInput {
+        PlatformInput::KeyUp(self)
+    }
+}
+impl KeyEvent for KeyUpEvent {}
+
 #[derive(Clone, Debug, Default)]
 pub struct ModifiersChangedEvent {
     pub modifiers: Modifiers,
 }
 
+impl Sealed for ModifiersChangedEvent {}
+impl InputEvent for ModifiersChangedEvent {
+    fn to_platform_input(self) -> PlatformInput {
+        PlatformInput::ModifiersChanged(self)
+    }
+}
+impl KeyEvent for ModifiersChangedEvent {}
+
 impl Deref for ModifiersChangedEvent {
     type Target = Modifiers;
 
@@ -30,9 +60,10 @@ impl Deref for ModifiersChangedEvent {
 
 /// The phase of a touch motion event.
 /// Based on the winit enum of the same name.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Default)]
 pub enum TouchPhase {
     Started,
+    #[default]
     Moved,
     Ended,
 }
@@ -45,6 +76,14 @@ pub struct MouseDownEvent {
     pub click_count: usize,
 }
 
+impl Sealed for MouseDownEvent {}
+impl InputEvent for MouseDownEvent {
+    fn to_platform_input(self) -> PlatformInput {
+        PlatformInput::MouseDown(self)
+    }
+}
+impl MouseEvent for MouseDownEvent {}
+
 #[derive(Clone, Debug, Default)]
 pub struct MouseUpEvent {
     pub button: MouseButton,
@@ -53,38 +92,20 @@ pub struct MouseUpEvent {
     pub click_count: usize,
 }
 
+impl Sealed for MouseUpEvent {}
+impl InputEvent for MouseUpEvent {
+    fn to_platform_input(self) -> PlatformInput {
+        PlatformInput::MouseUp(self)
+    }
+}
+impl MouseEvent for MouseUpEvent {}
+
 #[derive(Clone, Debug, Default)]
 pub struct ClickEvent {
     pub down: MouseDownEvent,
     pub up: MouseUpEvent,
 }
 
-pub struct Drag<S, R, V, E>
-where
-    R: Fn(&mut V, &mut ViewContext<V>) -> E,
-    V: 'static,
-    E: IntoElement,
-{
-    pub state: S,
-    pub render_drag_handle: R,
-    view_element_types: PhantomData<(V, E)>,
-}
-
-impl<S, R, V, E> Drag<S, R, V, E>
-where
-    R: Fn(&mut V, &mut ViewContext<V>) -> E,
-    V: 'static,
-    E: Element,
-{
-    pub fn new(state: S, render_drag_handle: R) -> Self {
-        Drag {
-            state,
-            render_drag_handle,
-            view_element_types: Default::default(),
-        }
-    }
-}
-
 #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
 pub enum MouseButton {
     Left,
@@ -130,13 +151,21 @@ pub struct MouseMoveEvent {
     pub modifiers: Modifiers,
 }
 
+impl Sealed for MouseMoveEvent {}
+impl InputEvent for MouseMoveEvent {
+    fn to_platform_input(self) -> PlatformInput {
+        PlatformInput::MouseMove(self)
+    }
+}
+impl MouseEvent for MouseMoveEvent {}
+
 impl MouseMoveEvent {
     pub fn dragging(&self) -> bool {
         self.pressed_button == Some(MouseButton::Left)
     }
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Default)]
 pub struct ScrollWheelEvent {
     pub position: Point<Pixels>,
     pub delta: ScrollDelta,
@@ -144,6 +173,14 @@ pub struct ScrollWheelEvent {
     pub touch_phase: TouchPhase,
 }
 
+impl Sealed for ScrollWheelEvent {}
+impl InputEvent for ScrollWheelEvent {
+    fn to_platform_input(self) -> PlatformInput {
+        PlatformInput::ScrollWheel(self)
+    }
+}
+impl MouseEvent for ScrollWheelEvent {}
+
 impl Deref for ScrollWheelEvent {
     type Target = Modifiers;
 
@@ -201,6 +238,14 @@ pub struct MouseExitEvent {
     pub modifiers: Modifiers,
 }
 
+impl Sealed for MouseExitEvent {}
+impl InputEvent for MouseExitEvent {
+    fn to_platform_input(self) -> PlatformInput {
+        PlatformInput::MouseExited(self)
+    }
+}
+impl MouseEvent for MouseExitEvent {}
+
 impl Deref for MouseExitEvent {
     type Target = Modifiers;
 
@@ -220,7 +265,7 @@ impl ExternalPaths {
 
 impl Render for ExternalPaths {
     fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
-        div() // Intentionally left empty because the platform will render icons for the dragged files
+        () // Intentionally left empty because the platform will render icons for the dragged files
     }
 }
 
@@ -239,8 +284,16 @@ pub enum FileDropEvent {
     Exited,
 }
 
+impl Sealed for FileDropEvent {}
+impl InputEvent for FileDropEvent {
+    fn to_platform_input(self) -> PlatformInput {
+        PlatformInput::FileDrop(self)
+    }
+}
+impl MouseEvent for FileDropEvent {}
+
 #[derive(Clone, Debug)]
-pub enum InputEvent {
+pub enum PlatformInput {
     KeyDown(KeyDownEvent),
     KeyUp(KeyUpEvent),
     ModifiersChanged(ModifiersChangedEvent),
@@ -252,19 +305,19 @@ pub enum InputEvent {
     FileDrop(FileDropEvent),
 }
 
-impl InputEvent {
+impl PlatformInput {
     pub fn position(&self) -> Option<Point<Pixels>> {
         match self {
-            InputEvent::KeyDown { .. } => None,
-            InputEvent::KeyUp { .. } => None,
-            InputEvent::ModifiersChanged { .. } => None,
-            InputEvent::MouseDown(event) => Some(event.position),
-            InputEvent::MouseUp(event) => Some(event.position),
-            InputEvent::MouseMove(event) => Some(event.position),
-            InputEvent::MouseExited(event) => Some(event.position),
-            InputEvent::ScrollWheel(event) => Some(event.position),
-            InputEvent::FileDrop(FileDropEvent::Exited) => None,
-            InputEvent::FileDrop(
+            PlatformInput::KeyDown { .. } => None,
+            PlatformInput::KeyUp { .. } => None,
+            PlatformInput::ModifiersChanged { .. } => None,
+            PlatformInput::MouseDown(event) => Some(event.position),
+            PlatformInput::MouseUp(event) => Some(event.position),
+            PlatformInput::MouseMove(event) => Some(event.position),
+            PlatformInput::MouseExited(event) => Some(event.position),
+            PlatformInput::ScrollWheel(event) => Some(event.position),
+            PlatformInput::FileDrop(FileDropEvent::Exited) => None,
+            PlatformInput::FileDrop(
                 FileDropEvent::Entered { position, .. }
                 | FileDropEvent::Pending { position, .. }
                 | FileDropEvent::Submit { position, .. },
@@ -274,29 +327,29 @@ impl InputEvent {
 
     pub fn mouse_event(&self) -> Option<&dyn Any> {
         match self {
-            InputEvent::KeyDown { .. } => None,
-            InputEvent::KeyUp { .. } => None,
-            InputEvent::ModifiersChanged { .. } => None,
-            InputEvent::MouseDown(event) => Some(event),
-            InputEvent::MouseUp(event) => Some(event),
-            InputEvent::MouseMove(event) => Some(event),
-            InputEvent::MouseExited(event) => Some(event),
-            InputEvent::ScrollWheel(event) => Some(event),
-            InputEvent::FileDrop(event) => Some(event),
+            PlatformInput::KeyDown { .. } => None,
+            PlatformInput::KeyUp { .. } => None,
+            PlatformInput::ModifiersChanged { .. } => None,
+            PlatformInput::MouseDown(event) => Some(event),
+            PlatformInput::MouseUp(event) => Some(event),
+            PlatformInput::MouseMove(event) => Some(event),
+            PlatformInput::MouseExited(event) => Some(event),
+            PlatformInput::ScrollWheel(event) => Some(event),
+            PlatformInput::FileDrop(event) => Some(event),
         }
     }
 
     pub fn keyboard_event(&self) -> Option<&dyn Any> {
         match self {
-            InputEvent::KeyDown(event) => Some(event),
-            InputEvent::KeyUp(event) => Some(event),
-            InputEvent::ModifiersChanged(event) => Some(event),
-            InputEvent::MouseDown(_) => None,
-            InputEvent::MouseUp(_) => None,
-            InputEvent::MouseMove(_) => None,
-            InputEvent::MouseExited(_) => None,
-            InputEvent::ScrollWheel(_) => None,
-            InputEvent::FileDrop(_) => None,
+            PlatformInput::KeyDown(event) => Some(event),
+            PlatformInput::KeyUp(event) => Some(event),
+            PlatformInput::ModifiersChanged(event) => Some(event),
+            PlatformInput::MouseDown(_) => None,
+            PlatformInput::MouseUp(_) => None,
+            PlatformInput::MouseMove(_) => None,
+            PlatformInput::MouseExited(_) => None,
+            PlatformInput::ScrollWheel(_) => None,
+            PlatformInput::FileDrop(_) => None,
         }
     }
 }

crates/gpui/src/keymap/matcher.rs 🔗

@@ -209,7 +209,6 @@ mod tests {
         );
         assert!(!matcher.has_pending_keystrokes());
 
-        eprintln!("PROBLEM AREA");
         // If a is prefixed, C will not be dispatched because there
         // was a pending binding for it
         assert_eq!(

crates/gpui/src/platform.rs 🔗

@@ -7,7 +7,7 @@ mod test;
 
 use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics,
-    FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap, LineLayout, Pixels,
+    FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout, Pixels, PlatformInput,
     Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString,
     Size, TaskLabel,
 };
@@ -88,7 +88,7 @@ pub(crate) trait Platform: 'static {
     fn on_resign_active(&self, callback: Box<dyn FnMut()>);
     fn on_quit(&self, callback: Box<dyn FnMut()>);
     fn on_reopen(&self, callback: Box<dyn FnMut()>);
-    fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
+    fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>);
 
     fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
     fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
@@ -155,7 +155,7 @@ pub trait PlatformWindow {
     fn zoom(&self);
     fn toggle_full_screen(&self);
     fn on_request_frame(&self, callback: Box<dyn FnMut()>);
-    fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
+    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>);
     fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
     fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
     fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>);

crates/gpui/src/platform/mac/events.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
-    point, px, InputEvent, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent,
-    MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection,
-    Pixels, ScrollDelta, ScrollWheelEvent, TouchPhase,
+    point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
+    MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
+    PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
 };
 use cocoa::{
     appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
@@ -82,7 +82,7 @@ unsafe fn read_modifiers(native_event: id) -> Modifiers {
     }
 }
 
-impl InputEvent {
+impl PlatformInput {
     pub unsafe fn from_native(native_event: id, window_height: Option<Pixels>) -> Option<Self> {
         let event_type = native_event.eventType();
 

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

@@ -1,8 +1,8 @@
 use super::{events::key_to_native, BoolExt};
 use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
-    ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker,
-    MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
+    ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
+    MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
     PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
 };
 use anyhow::anyhow;
@@ -153,7 +153,7 @@ pub struct MacPlatformState {
     resign_active: Option<Box<dyn FnMut()>>,
     reopen: Option<Box<dyn FnMut()>>,
     quit: Option<Box<dyn FnMut()>>,
-    event: Option<Box<dyn FnMut(InputEvent) -> bool>>,
+    event: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
     menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
     validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
     will_open_menu: Option<Box<dyn FnMut()>>,
@@ -637,7 +637,7 @@ impl Platform for MacPlatform {
         self.0.lock().reopen = Some(callback);
     }
 
-    fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>) {
+    fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
         self.0.lock().event = Some(callback);
     }
 
@@ -976,7 +976,7 @@ unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform {
 
 extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
     unsafe {
-        if let Some(event) = InputEvent::from_native(native_event, None) {
+        if let Some(event) = PlatformInput::from_native(native_event, None) {
             let platform = get_mac_platform(this);
             let mut lock = platform.0.lock();
             if let Some(mut callback) = lock.event.take() {

crates/gpui/src/platform/mac/window.rs 🔗

@@ -1,9 +1,9 @@
 use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
 use crate::{
     display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
-    FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke,
-    Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
-    Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
+    FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, Keystroke, Modifiers,
+    ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
+    PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
     PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
 };
 use block::ConcreteBlock;
@@ -319,7 +319,7 @@ struct MacWindowState {
     renderer: MetalRenderer,
     kind: WindowKind,
     request_frame_callback: Option<Box<dyn FnMut()>>,
-    event_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
+    event_callback: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
     activate_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
     fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
@@ -333,7 +333,7 @@ struct MacWindowState {
     synthetic_drag_counter: usize,
     last_fresh_keydown: Option<Keystroke>,
     traffic_light_position: Option<Point<Pixels>>,
-    previous_modifiers_changed_event: Option<InputEvent>,
+    previous_modifiers_changed_event: Option<PlatformInput>,
     // State tracking what the IME did after the last request
     ime_state: ImeState,
     // Retains the last IME Text
@@ -928,7 +928,7 @@ impl PlatformWindow for MacWindow {
         self.0.as_ref().lock().request_frame_callback = Some(callback);
     }
 
-    fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>) {
+    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
         self.0.as_ref().lock().event_callback = Some(callback);
     }
 
@@ -1053,9 +1053,9 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
     let mut lock = window_state.as_ref().lock();
 
     let window_height = lock.content_size().height;
-    let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
+    let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
 
-    if let Some(InputEvent::KeyDown(event)) = event {
+    if let Some(PlatformInput::KeyDown(event)) = event {
         // For certain keystrokes, macOS will first dispatch a "key equivalent" event.
         // If that event isn't handled, it will then dispatch a "key down" event. GPUI
         // makes no distinction between these two types of events, so we need to ignore
@@ -1102,7 +1102,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
                         .flatten()
                         .is_some();
                 if !is_composing {
-                    handled = callback(InputEvent::KeyDown(event));
+                    handled = callback(PlatformInput::KeyDown(event));
                 }
 
                 if !handled {
@@ -1146,11 +1146,11 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
 
     let window_height = lock.content_size().height;
-    let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
+    let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
 
     if let Some(mut event) = event {
         match &mut event {
-            InputEvent::MouseDown(
+            PlatformInput::MouseDown(
                 event @ MouseDownEvent {
                     button: MouseButton::Left,
                     modifiers: Modifiers { control: true, .. },
@@ -1172,7 +1172,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
             // Because we map a ctrl-left_down to a right_down -> right_up let's ignore
             // the ctrl-left_up to avoid having a mismatch in button down/up events if the
             // user is still holding ctrl when releasing the left mouse button
-            InputEvent::MouseUp(
+            PlatformInput::MouseUp(
                 event @ MouseUpEvent {
                     button: MouseButton::Left,
                     modifiers: Modifiers { control: true, .. },
@@ -1194,7 +1194,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
         };
 
         match &event {
-            InputEvent::MouseMove(
+            PlatformInput::MouseMove(
                 event @ MouseMoveEvent {
                     pressed_button: Some(_),
                     ..
@@ -1216,15 +1216,15 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                 }
             }
 
-            InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
+            PlatformInput::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
 
-            InputEvent::MouseUp(MouseUpEvent { .. }) => {
+            PlatformInput::MouseUp(MouseUpEvent { .. }) => {
                 lock.synthetic_drag_counter += 1;
             }
 
-            InputEvent::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
+            PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
                 // Only raise modifiers changed event when they have actually changed
-                if let Some(InputEvent::ModifiersChanged(ModifiersChangedEvent {
+                if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
                     modifiers: prev_modifiers,
                 })) = &lock.previous_modifiers_changed_event
                 {
@@ -1258,7 +1258,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
         key: ".".into(),
         ime_key: None,
     };
-    let event = InputEvent::KeyDown(KeyDownEvent {
+    let event = PlatformInput::KeyDown(KeyDownEvent {
         keystroke: keystroke.clone(),
         is_held: false,
     });
@@ -1655,7 +1655,7 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr
     if send_new_event(&window_state, {
         let position = drag_event_position(&window_state, dragging_info);
         let paths = external_paths_from_event(dragging_info);
-        InputEvent::FileDrop(FileDropEvent::Entered { position, paths })
+        PlatformInput::FileDrop(FileDropEvent::Entered { position, paths })
     }) {
         window_state.lock().external_files_dragged = true;
         NSDragOperationCopy
@@ -1669,7 +1669,7 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr
     let position = drag_event_position(&window_state, dragging_info);
     if send_new_event(
         &window_state,
-        InputEvent::FileDrop(FileDropEvent::Pending { position }),
+        PlatformInput::FileDrop(FileDropEvent::Pending { position }),
     ) {
         NSDragOperationCopy
     } else {
@@ -1679,7 +1679,10 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr
 
 extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
     let window_state = unsafe { get_window_state(this) };
-    send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
+    send_new_event(
+        &window_state,
+        PlatformInput::FileDrop(FileDropEvent::Exited),
+    );
     window_state.lock().external_files_dragged = false;
 }
 
@@ -1688,7 +1691,7 @@ extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -
     let position = drag_event_position(&window_state, dragging_info);
     if send_new_event(
         &window_state,
-        InputEvent::FileDrop(FileDropEvent::Submit { position }),
+        PlatformInput::FileDrop(FileDropEvent::Submit { position }),
     ) {
         YES
     } else {
@@ -1712,7 +1715,10 @@ fn external_paths_from_event(dragging_info: *mut Object) -> ExternalPaths {
 
 extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) {
     let window_state = unsafe { get_window_state(this) };
-    send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
+    send_new_event(
+        &window_state,
+        PlatformInput::FileDrop(FileDropEvent::Exited),
+    );
 }
 
 async fn synthetic_drag(
@@ -1727,7 +1733,7 @@ async fn synthetic_drag(
             if lock.synthetic_drag_counter == drag_id {
                 if let Some(mut callback) = lock.event_callback.take() {
                     drop(lock);
-                    callback(InputEvent::MouseMove(event.clone()));
+                    callback(PlatformInput::MouseMove(event.clone()));
                     window_state.lock().event_callback = Some(callback);
                 }
             } else {
@@ -1737,7 +1743,7 @@ async fn synthetic_drag(
     }
 }
 
-fn send_new_event(window_state_lock: &Mutex<MacWindowState>, e: InputEvent) -> bool {
+fn send_new_event(window_state_lock: &Mutex<MacWindowState>, e: PlatformInput) -> bool {
     let window_state = window_state_lock.lock().event_callback.take();
     if let Some(mut callback) = window_state {
         callback(e);

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

@@ -239,7 +239,7 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn on_event(&self, _callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
+    fn on_event(&self, _callback: Box<dyn FnMut(crate::PlatformInput) -> bool>) {
         unimplemented!()
     }
 

crates/gpui/src/platform/test/window.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
-    px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, InputEvent, KeyDownEvent,
-    Keystroke, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
-    Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
+    px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke,
+    Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
+    Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -19,7 +19,7 @@ pub struct TestWindowState {
     platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
     pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
-    input_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
+    input_callback: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
     active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
     moved_callback: Option<Box<dyn FnMut()>>,
@@ -85,7 +85,7 @@ impl TestWindow {
         self.0.lock().active_status_change_callback = Some(callback);
     }
 
-    pub fn simulate_input(&mut self, event: InputEvent) -> bool {
+    pub fn simulate_input(&mut self, event: PlatformInput) -> bool {
         let mut lock = self.0.lock();
         let Some(mut callback) = lock.input_callback.take() else {
             return false;
@@ -97,7 +97,7 @@ impl TestWindow {
     }
 
     pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) {
-        if self.simulate_input(InputEvent::KeyDown(KeyDownEvent {
+        if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent {
             keystroke: keystroke.clone(),
             is_held,
         })) {
@@ -220,7 +220,7 @@ impl PlatformWindow for TestWindow {
 
     fn on_request_frame(&self, _callback: Box<dyn FnMut()>) {}
 
-    fn on_input(&self, callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
+    fn on_input(&self, callback: Box<dyn FnMut(crate::PlatformInput) -> bool>) {
         self.0.lock().input_callback = Some(callback)
     }
 

crates/gpui/src/window.rs 🔗

@@ -5,13 +5,14 @@ use crate::{
     AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
     DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect,
     Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla,
-    ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId,
-    Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent,
-    Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
-    PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
-    RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, Style, SubscriberSet,
-    Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
-    WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    ImageData, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeystrokeEvent, LayoutId,
+    Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent,
+    MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
+    PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render,
+    RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, Shadow,
+    SharedString, Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task,
+    Underline, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions,
+    SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::{FxHashMap, FxHashSet};
@@ -971,7 +972,7 @@ impl<'a> WindowContext<'a> {
     /// Register a mouse event listener on the window for the next frame. The type of event
     /// is determined by the first parameter of the given listener. When the next frame is rendered
     /// the listener will be cleared.
-    pub fn on_mouse_event<Event: 'static>(
+    pub fn on_mouse_event<Event: MouseEvent>(
         &mut self,
         mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static,
     ) {
@@ -999,7 +1000,7 @@ impl<'a> WindowContext<'a> {
     ///
     /// This is a fairly low-level method, so prefer using event handlers on elements unless you have
     /// a specific need to register a global listener.
-    pub fn on_key_event<Event: 'static>(
+    pub fn on_key_event<Event: KeyEvent>(
         &mut self,
         listener: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static,
     ) {
@@ -1620,7 +1621,7 @@ impl<'a> WindowContext<'a> {
     }
 
     /// Dispatch a mouse or keyboard event on the window.
-    pub fn dispatch_event(&mut self, event: InputEvent) -> bool {
+    pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
         // Handlers may set this to false by calling `stop_propagation`.
         self.app.propagate_event = true;
         // Handlers may set this to true by calling `prevent_default`.
@@ -1629,37 +1630,37 @@ impl<'a> WindowContext<'a> {
         let event = match event {
             // Track the mouse position with our own state, since accessing the platform
             // API for the mouse position can only occur on the main thread.
-            InputEvent::MouseMove(mouse_move) => {
+            PlatformInput::MouseMove(mouse_move) => {
                 self.window.mouse_position = mouse_move.position;
                 self.window.modifiers = mouse_move.modifiers;
-                InputEvent::MouseMove(mouse_move)
+                PlatformInput::MouseMove(mouse_move)
             }
-            InputEvent::MouseDown(mouse_down) => {
+            PlatformInput::MouseDown(mouse_down) => {
                 self.window.mouse_position = mouse_down.position;
                 self.window.modifiers = mouse_down.modifiers;
-                InputEvent::MouseDown(mouse_down)
+                PlatformInput::MouseDown(mouse_down)
             }
-            InputEvent::MouseUp(mouse_up) => {
+            PlatformInput::MouseUp(mouse_up) => {
                 self.window.mouse_position = mouse_up.position;
                 self.window.modifiers = mouse_up.modifiers;
-                InputEvent::MouseUp(mouse_up)
+                PlatformInput::MouseUp(mouse_up)
             }
-            InputEvent::MouseExited(mouse_exited) => {
+            PlatformInput::MouseExited(mouse_exited) => {
                 self.window.modifiers = mouse_exited.modifiers;
-                InputEvent::MouseExited(mouse_exited)
+                PlatformInput::MouseExited(mouse_exited)
             }
-            InputEvent::ModifiersChanged(modifiers_changed) => {
+            PlatformInput::ModifiersChanged(modifiers_changed) => {
                 self.window.modifiers = modifiers_changed.modifiers;
-                InputEvent::ModifiersChanged(modifiers_changed)
+                PlatformInput::ModifiersChanged(modifiers_changed)
             }
-            InputEvent::ScrollWheel(scroll_wheel) => {
+            PlatformInput::ScrollWheel(scroll_wheel) => {
                 self.window.mouse_position = scroll_wheel.position;
                 self.window.modifiers = scroll_wheel.modifiers;
-                InputEvent::ScrollWheel(scroll_wheel)
+                PlatformInput::ScrollWheel(scroll_wheel)
             }
             // Translate dragging and dropping of external files from the operating system
             // to internal drag and drop events.
-            InputEvent::FileDrop(file_drop) => match file_drop {
+            PlatformInput::FileDrop(file_drop) => match file_drop {
                 FileDropEvent::Entered { position, paths } => {
                     self.window.mouse_position = position;
                     if self.active_drag.is_none() {
@@ -1669,7 +1670,7 @@ impl<'a> WindowContext<'a> {
                             cursor_offset: position,
                         });
                     }
-                    InputEvent::MouseMove(MouseMoveEvent {
+                    PlatformInput::MouseMove(MouseMoveEvent {
                         position,
                         pressed_button: Some(MouseButton::Left),
                         modifiers: Modifiers::default(),
@@ -1677,7 +1678,7 @@ impl<'a> WindowContext<'a> {
                 }
                 FileDropEvent::Pending { position } => {
                     self.window.mouse_position = position;
-                    InputEvent::MouseMove(MouseMoveEvent {
+                    PlatformInput::MouseMove(MouseMoveEvent {
                         position,
                         pressed_button: Some(MouseButton::Left),
                         modifiers: Modifiers::default(),
@@ -1686,21 +1687,21 @@ impl<'a> WindowContext<'a> {
                 FileDropEvent::Submit { position } => {
                     self.activate(true);
                     self.window.mouse_position = position;
-                    InputEvent::MouseUp(MouseUpEvent {
+                    PlatformInput::MouseUp(MouseUpEvent {
                         button: MouseButton::Left,
                         position,
                         modifiers: Modifiers::default(),
                         click_count: 1,
                     })
                 }
-                FileDropEvent::Exited => InputEvent::MouseUp(MouseUpEvent {
+                FileDropEvent::Exited => PlatformInput::MouseUp(MouseUpEvent {
                     button: MouseButton::Left,
                     position: Point::default(),
                     modifiers: Modifiers::default(),
                     click_count: 1,
                 }),
             },
-            InputEvent::KeyDown(_) | InputEvent::KeyUp(_) => event,
+            PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event,
         };
 
         if let Some(any_mouse_event) = event.mouse_event() {
@@ -2983,7 +2984,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     /// Add a listener for any mouse event that occurs in the window.
     /// This is a fairly low level method.
     /// Typically, you'll want to use methods on UI elements, which perform bounds checking etc.
-    pub fn on_mouse_event<Event: 'static>(
+    pub fn on_mouse_event<Event: MouseEvent>(
         &mut self,
         handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
     ) {
@@ -2996,7 +2997,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     }
 
     /// Register a callback to be invoked when the given Key Event is dispatched to the window.
-    pub fn on_key_event<Event: 'static>(
+    pub fn on_key_event<Event: KeyEvent>(
         &mut self,
         handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
     ) {

crates/search/src/buffer_search.rs 🔗

@@ -1167,7 +1167,7 @@ mod tests {
 
     use super::*;
     use editor::{DisplayPoint, Editor};
-    use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext};
+    use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
     use language::Buffer;
     use smol::stream::StreamExt as _;
     use unindent::Unindent as _;
@@ -1200,7 +1200,7 @@ mod tests {
                 .unindent(),
             )
         });
-        let (_, cx) = cx.add_window_view(|_| EmptyView {});
+        let cx = cx.add_empty_window();
         let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
 
         let search_bar = cx.new_view(|cx| {
@@ -1547,7 +1547,7 @@ mod tests {
             "Should pick a query with multiple results"
         );
         let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
-        let window = cx.add_window(|_| EmptyView {});
+        let window = cx.add_window(|_| ());
 
         let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 
@@ -1743,7 +1743,7 @@ mod tests {
         "#
         .unindent();
         let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
-        let (_, cx) = cx.add_window_view(|_| EmptyView {});
+        let cx = cx.add_empty_window();
 
         let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));