gpui event test (#3249)

- Flesh out gpui2 test support
- Smoke test for event handling
This commit is contained in:
Conrad Irwin 2023-11-07 08:43:15 -07:00 committed by GitHub
commit 2e43015664
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 346 additions and 18 deletions

View file

@ -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();

View file

@ -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
}

View file

@ -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()),

View file

@ -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();
}
}

View file

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

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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!()
}
}

View file

@ -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.