gpui event test (#3249)

Conrad Irwin created

- Flesh out gpui2 test support
- Smoke test for event handling

Change summary

crates/gpui2/src/app/test_context.rs       |  21 ++
crates/gpui2/src/elements/div.rs           |  10 
crates/gpui2/src/geometry.rs               |   4 
crates/gpui2/src/interactive.rs            |  70 +++++++++
crates/gpui2/src/platform/test.rs          |   4 
crates/gpui2/src/platform/test/display.rs  |  41 +++++
crates/gpui2/src/platform/test/platform.rs |  33 +++-
crates/gpui2/src/platform/test/window.rs   | 179 ++++++++++++++++++++++++
crates/gpui2/src/window.rs                 |   2 
9 files changed, 346 insertions(+), 18 deletions(-)

Detailed changes

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

@@ -1,7 +1,7 @@
 use crate::{
     AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context,
-    EventEmitter, ForegroundExecutor, Model, ModelContext, Result, Task, TestDispatcher,
-    TestPlatform, WindowContext,
+    EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext,
+    Result, Task, TestDispatcher, TestPlatform, WindowContext,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
@@ -129,6 +129,23 @@ impl TestAppContext {
         }
     }
 
+    pub fn dispatch_keystroke(
+        &mut self,
+        window: AnyWindowHandle,
+        keystroke: Keystroke,
+        is_held: bool,
+    ) {
+        let handled = window
+            .update(self, |_, cx| {
+                cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held }))
+            })
+            .is_ok_and(|handled| handled);
+
+        if !handled {
+            // todo!() simluate input here
+        }
+    }
+
     pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> {
         let (tx, rx) = futures::channel::mpsc::unbounded();
 

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

@@ -209,15 +209,15 @@ where
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
         let mut element_state = element_state.unwrap_or_default();
-        self.focus
-            .initialize(element_state.focus_handle.take(), cx, |focus_handle, cx| {
-                element_state.focus_handle = focus_handle;
-                self.interaction.initialize(cx, |cx| {
+        self.interaction.initialize(cx, |cx| {
+            self.focus
+                .initialize(element_state.focus_handle.take(), cx, |focus_handle, cx| {
+                    element_state.focus_handle = focus_handle;
                     for child in &mut self.children {
                         child.initialize(view_state, cx);
                     }
                 })
-            });
+        });
         element_state
     }
 

crates/gpui2/src/geometry.rs 🔗

@@ -25,6 +25,10 @@ impl<T: Clone + Debug + Default> Point<T> {
         Self { x, y }
     }
 
+    pub fn zero() -> Self {
+        Self::new(T::default(), T::default())
+    }
+
     pub fn map<U: Clone + Default + Debug>(&self, f: impl Fn(T) -> U) -> Point<U> {
         Point {
             x: f(self.x.clone()),

crates/gpui2/src/interactive.rs 🔗

@@ -1230,3 +1230,73 @@ pub type KeyListener<V> = Box<
         ) -> Option<Box<dyn Action>>
         + 'static,
 >;
+
+#[cfg(test)]
+mod test {
+    use serde_derive::Deserialize;
+
+    use crate::{
+        self as gpui, div, Div, FocusHandle, KeyBinding, Keystroke, ParentElement, Render,
+        StatefulInteraction, StatelessInteractive, TestAppContext, VisualContext,
+    };
+
+    struct TestView {
+        saw_key_down: bool,
+        saw_action: bool,
+        focus_handle: FocusHandle,
+    }
+
+    #[derive(PartialEq, Clone, Default, Deserialize)]
+    struct TestAction;
+
+    impl Render for TestView {
+        type Element = Div<Self, StatefulInteraction<Self>>;
+
+        fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> Self::Element {
+            div().id("testview").child(
+                div()
+                    .on_key_down(|this: &mut TestView, _, _, _| {
+                        dbg!("ola!");
+                        this.saw_key_down = true
+                    })
+                    .on_action(|this: &mut TestView, _: &TestAction, _, _| {
+                        dbg!("ola!");
+                        this.saw_action = true
+                    })
+                    .track_focus(&self.focus_handle),
+            )
+        }
+    }
+
+    #[gpui::test]
+    fn test_on_events(cx: &mut TestAppContext) {
+        let window = cx.update(|cx| {
+            cx.open_window(Default::default(), |cx| {
+                cx.build_view(|cx| TestView {
+                    saw_key_down: false,
+                    saw_action: false,
+                    focus_handle: cx.focus_handle(),
+                })
+            })
+        });
+
+        cx.update(|cx| {
+            cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, None)]);
+        });
+
+        window
+            .update(cx, |test_view, cx| cx.focus(&test_view.focus_handle))
+            .unwrap();
+
+        cx.dispatch_keystroke(*window, Keystroke::parse("space").unwrap(), false);
+        cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false);
+
+        window
+            .update(cx, |test_view, _| {
+                assert!(test_view.saw_key_down || test_view.saw_action);
+                assert!(test_view.saw_key_down);
+                assert!(test_view.saw_action);
+            })
+            .unwrap();
+    }
+}

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

@@ -1,5 +1,9 @@
 mod dispatcher;
+mod display;
 mod platform;
+mod window;
 
 pub use dispatcher::*;
+pub use display::*;
 pub use platform::*;
+pub use window::*;

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

@@ -0,0 +1,41 @@
+use anyhow::{Ok, Result};
+
+use crate::{Bounds, DisplayId, GlobalPixels, PlatformDisplay, Point};
+
+#[derive(Debug)]
+pub struct TestDisplay {
+    id: DisplayId,
+    uuid: uuid::Uuid,
+    bounds: Bounds<GlobalPixels>,
+}
+
+impl TestDisplay {
+    pub fn new() -> Self {
+        TestDisplay {
+            id: DisplayId(1),
+            uuid: uuid::Uuid::new_v4(),
+            bounds: Bounds::from_corners(
+                Point::zero(),
+                Point::new(GlobalPixels(1920.), GlobalPixels(1080.)),
+            ),
+        }
+    }
+}
+
+impl PlatformDisplay for TestDisplay {
+    fn id(&self) -> crate::DisplayId {
+        self.id
+    }
+
+    fn uuid(&self) -> Result<uuid::Uuid> {
+        Ok(self.uuid)
+    }
+
+    fn as_any(&self) -> &dyn std::any::Any {
+        todo!()
+    }
+
+    fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {
+        self.bounds
+    }
+}

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

@@ -1,10 +1,18 @@
-use crate::{BackgroundExecutor, DisplayId, ForegroundExecutor, Platform, PlatformTextSystem};
+use crate::{
+    AnyWindowHandle, BackgroundExecutor, CursorStyle, DisplayId, ForegroundExecutor, Platform,
+    PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
+};
 use anyhow::{anyhow, Result};
-use std::sync::Arc;
+use parking_lot::Mutex;
+use std::{rc::Rc, sync::Arc};
 
 pub struct TestPlatform {
     background_executor: BackgroundExecutor,
     foreground_executor: ForegroundExecutor,
+
+    active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+    active_display: Rc<dyn PlatformDisplay>,
+    active_cursor: Mutex<CursorStyle>,
 }
 
 impl TestPlatform {
@@ -12,6 +20,10 @@ impl TestPlatform {
         TestPlatform {
             background_executor: executor,
             foreground_executor,
+
+            active_cursor: Default::default(),
+            active_display: Rc::new(TestDisplay::new()),
+            active_window: Default::default(),
         }
     }
 }
@@ -59,11 +71,11 @@ impl Platform for TestPlatform {
     }
 
     fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
-        unimplemented!()
+        vec![self.active_display.clone()]
     }
 
-    fn display(&self, _id: DisplayId) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
-        unimplemented!()
+    fn display(&self, id: DisplayId) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
+        self.displays().iter().find(|d| d.id() == id).cloned()
     }
 
     fn main_window(&self) -> Option<crate::AnyWindowHandle> {
@@ -72,10 +84,11 @@ impl Platform for TestPlatform {
 
     fn open_window(
         &self,
-        _handle: crate::AnyWindowHandle,
-        _options: crate::WindowOptions,
+        handle: AnyWindowHandle,
+        options: WindowOptions,
     ) -> Box<dyn crate::PlatformWindow> {
-        unimplemented!()
+        *self.active_window.lock() = Some(handle);
+        Box::new(TestWindow::new(options, self.active_display.clone()))
     }
 
     fn set_display_link_output_callback(
@@ -164,8 +177,8 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn set_cursor_style(&self, _style: crate::CursorStyle) {
-        unimplemented!()
+    fn set_cursor_style(&self, style: crate::CursorStyle) {
+        *self.active_cursor.lock() = style;
     }
 
     fn should_auto_hide_scrollbars(&self) -> bool {

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

@@ -0,0 +1,179 @@
+use std::{rc::Rc, sync::Arc};
+
+use parking_lot::Mutex;
+
+use crate::{
+    px, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, Scene, Size,
+    WindowAppearance, WindowBounds, WindowOptions,
+};
+
+#[derive(Default)]
+struct Handlers {
+    active_status_change: Vec<Box<dyn FnMut(bool)>>,
+    input: Vec<Box<dyn FnMut(crate::InputEvent) -> bool>>,
+    moved: Vec<Box<dyn FnMut()>>,
+    resize: Vec<Box<dyn FnMut(Size<Pixels>, f32)>>,
+}
+
+pub struct TestWindow {
+    bounds: WindowBounds,
+    current_scene: Mutex<Option<Scene>>,
+    display: Rc<dyn PlatformDisplay>,
+
+    handlers: Mutex<Handlers>,
+    sprite_atlas: Arc<dyn PlatformAtlas>,
+}
+impl TestWindow {
+    pub fn new(options: WindowOptions, display: Rc<dyn PlatformDisplay>) -> Self {
+        Self {
+            bounds: options.bounds,
+            current_scene: Default::default(),
+            display,
+
+            sprite_atlas: Arc::new(TestAtlas),
+            handlers: Default::default(),
+        }
+    }
+}
+
+impl PlatformWindow for TestWindow {
+    fn bounds(&self) -> WindowBounds {
+        self.bounds
+    }
+
+    fn content_size(&self) -> Size<Pixels> {
+        let bounds = match self.bounds {
+            WindowBounds::Fixed(bounds) => bounds,
+            WindowBounds::Maximized | WindowBounds::Fullscreen => self.display().bounds(),
+        };
+        bounds.size.map(|p| px(p.0))
+    }
+
+    fn scale_factor(&self) -> f32 {
+        2.0
+    }
+
+    fn titlebar_height(&self) -> Pixels {
+        todo!()
+    }
+
+    fn appearance(&self) -> WindowAppearance {
+        todo!()
+    }
+
+    fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
+        self.display.clone()
+    }
+
+    fn mouse_position(&self) -> Point<Pixels> {
+        Point::zero()
+    }
+
+    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
+        todo!()
+    }
+
+    fn set_input_handler(&mut self, _input_handler: Box<dyn crate::PlatformInputHandler>) {
+        todo!()
+    }
+
+    fn prompt(
+        &self,
+        _level: crate::PromptLevel,
+        _msg: &str,
+        _answers: &[&str],
+    ) -> futures::channel::oneshot::Receiver<usize> {
+        todo!()
+    }
+
+    fn activate(&self) {
+        todo!()
+    }
+
+    fn set_title(&mut self, _title: &str) {
+        todo!()
+    }
+
+    fn set_edited(&mut self, _edited: bool) {
+        todo!()
+    }
+
+    fn show_character_palette(&self) {
+        todo!()
+    }
+
+    fn minimize(&self) {
+        todo!()
+    }
+
+    fn zoom(&self) {
+        todo!()
+    }
+
+    fn toggle_full_screen(&self) {
+        todo!()
+    }
+
+    fn on_input(&self, callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
+        self.handlers.lock().input.push(callback)
+    }
+
+    fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
+        self.handlers.lock().active_status_change.push(callback)
+    }
+
+    fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
+        self.handlers.lock().resize.push(callback)
+    }
+
+    fn on_fullscreen(&self, _callback: Box<dyn FnMut(bool)>) {
+        todo!()
+    }
+
+    fn on_moved(&self, callback: Box<dyn FnMut()>) {
+        self.handlers.lock().moved.push(callback)
+    }
+
+    fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) {
+        todo!()
+    }
+
+    fn on_close(&self, _callback: Box<dyn FnOnce()>) {
+        todo!()
+    }
+
+    fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {
+        todo!()
+    }
+
+    fn is_topmost_for_position(&self, _position: crate::Point<Pixels>) -> bool {
+        todo!()
+    }
+
+    fn draw(&self, scene: crate::Scene) {
+        self.current_scene.lock().replace(scene);
+    }
+
+    fn sprite_atlas(&self) -> std::sync::Arc<dyn crate::PlatformAtlas> {
+        self.sprite_atlas.clone()
+    }
+}
+
+pub struct TestAtlas;
+
+impl PlatformAtlas for TestAtlas {
+    fn get_or_insert_with<'a>(
+        &self,
+        _key: &crate::AtlasKey,
+        _build: &mut dyn FnMut() -> anyhow::Result<(
+            Size<crate::DevicePixels>,
+            std::borrow::Cow<'a, [u8]>,
+        )>,
+    ) -> anyhow::Result<crate::AtlasTile> {
+        todo!()
+    }
+
+    fn clear(&self) {
+        todo!()
+    }
+}

crates/gpui2/src/window.rs 🔗

@@ -1047,7 +1047,7 @@ impl<'a> WindowContext<'a> {
     }
 
     /// Dispatch a mouse or keyboard event on the window.
-    fn dispatch_event(&mut self, event: InputEvent) -> bool {
+    pub fn dispatch_event(&mut self, event: InputEvent) -> bool {
         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.