diff --git a/packages/tui/Cargo.toml b/packages/tui/Cargo.toml index bfc1c8902..88584cc4b 100644 --- a/packages/tui/Cargo.toml +++ b/packages/tui/Cargo.toml @@ -27,3 +27,8 @@ taffy = "0.1.0" smallvec = "1.6" fxhash = "0.2" anymap = "0.12.1" + +[dev-dependencies] +dioxus-core-macro = { path = "../core-macro", version = "^0.2.1" } +dioxus-hooks = { path = "../hooks", version = "^0.2.1" } +tokio = { version = "1" } \ No newline at end of file diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 22d70524e..a8f681a25 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -52,6 +52,12 @@ impl TuiContext { pub fn quit(&self) { self.tx.unbounded_send(InputEvent::Close).unwrap(); } + + pub fn inject_event(&self, event: crossterm::event::Event) { + self.tx + .unbounded_send(InputEvent::UserInput(event)) + .unwrap(); + } } pub fn launch(app: Component<()>) { @@ -71,7 +77,6 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) { let tick_rate = Duration::from_millis(1000); loop { if crossterm::event::poll(tick_rate).unwrap() { - // if crossterm::event::poll(timeout).unwrap() { let evt = crossterm::event::read().unwrap(); if event_tx.unbounded_send(InputEvent::UserInput(evt)).is_err() { break; @@ -180,8 +185,8 @@ fn render_vdom( Rect { x: 0, y: 0, - width: 300, - height: 300, + width: 100, + height: 100, }, &mut taffy.borrow_mut(), &rdom, @@ -251,6 +256,7 @@ fn render_vdom( }) } +#[derive(Debug)] enum InputEvent { UserInput(TermEvent), Close, diff --git a/packages/tui/tests/events.rs b/packages/tui/tests/events.rs new file mode 100644 index 000000000..ffe9ea9ec --- /dev/null +++ b/packages/tui/tests/events.rs @@ -0,0 +1,349 @@ +use std::time::Duration; + +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent}; +use dioxus_core::VNode; +use dioxus_core::*; +use dioxus_core_macro::*; +use dioxus_hooks::*; +use dioxus_html as dioxus_elements; +use dioxus_tui::TuiContext; + +#[test] +fn key_down() { + dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless()); + + fn app(cx: Scope) -> Element { + let render_count = use_state(&cx, || 0); + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + let render_count_handle = render_count.clone(); + cx.spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + render_count_handle.modify(|x| *x + 1); + }); + if *render_count.get() > 2 { + panic!("Event was not received"); + } + tui_ctx.inject_event(Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::NONE, + })); + cx.render(rsx! { + div { + width: "100%", + height: "100%", + onkeydown: move |evt| { + assert_eq!(evt.data.key_code, dioxus_html::KeyCode::A); + tui_ctx.quit(); + }, + } + }) + } +} + +#[test] +fn mouse_down() { + dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless()); + + fn app(cx: Scope) -> Element { + let render_count = use_state(&cx, || 0); + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + let render_count_handle = render_count.clone(); + cx.spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + render_count_handle.modify(|x| *x + 1); + }); + if *render_count.get() > 2 { + panic!("Event was not received"); + } + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 0, + row: 0, + kind: crossterm::event::MouseEventKind::Down(MouseButton::Left), + modifiers: KeyModifiers::NONE, + })); + cx.render(rsx! { + div { + width: "100%", + height: "100%", + onmousedown: move |evt| { + assert!(evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); + tui_ctx.quit(); + }, + } + }) + } +} + +#[test] +fn mouse_up() { + dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless()); + + fn app(cx: Scope) -> Element { + let render_count = use_state(&cx, || 0); + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + let render_count_handle = render_count.clone(); + cx.spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + render_count_handle.modify(|x| *x + 1); + }); + if *render_count.get() > 2 { + panic!("Event was not received"); + } + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 0, + row: 0, + kind: crossterm::event::MouseEventKind::Down(MouseButton::Left), + modifiers: KeyModifiers::NONE, + })); + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 0, + row: 0, + kind: crossterm::event::MouseEventKind::Up(MouseButton::Left), + modifiers: KeyModifiers::NONE, + })); + cx.render(rsx! { + div { + width: "100%", + height: "100%", + onmouseup: move |_| { + tui_ctx.quit(); + }, + } + }) + } +} + +#[test] +fn mouse_enter() { + dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless()); + + fn app(cx: Scope) -> Element { + let render_count = use_state(&cx, || 0); + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + let render_count_handle = render_count.clone(); + cx.spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + render_count_handle.modify(|x| *x + 1); + }); + if *render_count.get() > 2 { + panic!("Event was not received"); + } + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 100, + row: 100, + kind: crossterm::event::MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + })); + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 0, + row: 0, + kind: crossterm::event::MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + })); + cx.render(rsx! { + div { + width: "50%", + height: "50%", + onmouseenter: move |_| { + tui_ctx.quit(); + }, + } + }) + } +} + +#[test] +fn mouse_exit() { + dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless()); + + fn app(cx: Scope) -> Element { + let render_count = use_state(&cx, || 0); + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + let render_count_handle = render_count.clone(); + cx.spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + render_count_handle.modify(|x| *x + 1); + }); + if *render_count.get() > 2 { + panic!("Event was not received"); + } + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 0, + row: 0, + kind: crossterm::event::MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + })); + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 100, + row: 100, + kind: crossterm::event::MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + })); + cx.render(rsx! { + div { + width: "50%", + height: "50%", + onmouseenter: move |_| { + tui_ctx.quit(); + }, + } + }) + } +} + +#[test] +fn mouse_move() { + dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless()); + + fn app(cx: Scope) -> Element { + let render_count = use_state(&cx, || 0); + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + let render_count_handle = render_count.clone(); + cx.spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + render_count_handle.modify(|x| *x + 1); + }); + if *render_count.get() > 2 { + panic!("Event was not received"); + } + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 40, + row: 40, + kind: crossterm::event::MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + })); + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 60, + row: 60, + kind: crossterm::event::MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + })); + cx.render(rsx! { + div { + width: "100%", + height: "100%", + onmousemove: move |_|{ + tui_ctx.quit(); + }, + } + }) + } +} + +#[test] +fn wheel() { + dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless()); + + fn app(cx: Scope) -> Element { + let render_count = use_state(&cx, || 0); + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + let render_count_handle = render_count.clone(); + cx.spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + render_count_handle.modify(|x| *x + 1); + }); + if *render_count.get() > 2 { + panic!("Event was not received"); + } + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 50, + row: 50, + kind: crossterm::event::MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + })); + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 50, + row: 50, + kind: crossterm::event::MouseEventKind::ScrollDown, + modifiers: KeyModifiers::NONE, + })); + cx.render(rsx! { + div { + width: "100%", + height: "100%", + onwheel: move |evt| { + assert!(evt.data.delta_y > 0.0); + tui_ctx.quit(); + }, + } + }) + } +} + +#[test] +fn click() { + dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless()); + + fn app(cx: Scope) -> Element { + let render_count = use_state(&cx, || 0); + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + let render_count_handle = render_count.clone(); + cx.spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + render_count_handle.modify(|x| *x + 1); + }); + if *render_count.get() > 2 { + panic!("Event was not received"); + } + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 50, + row: 50, + kind: crossterm::event::MouseEventKind::Down(MouseButton::Left), + modifiers: KeyModifiers::NONE, + })); + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 50, + row: 50, + kind: crossterm::event::MouseEventKind::Up(MouseButton::Left), + modifiers: KeyModifiers::NONE, + })); + cx.render(rsx! { + div { + width: "100%", + height: "100%", + onclick: move |_|{ + tui_ctx.quit(); + }, + } + }) + } +} + +#[test] +fn context_menu() { + dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless()); + + fn app(cx: Scope) -> Element { + let render_count = use_state(&cx, || 0); + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + let render_count_handle = render_count.clone(); + cx.spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + render_count_handle.modify(|x| *x + 1); + }); + if *render_count.get() > 2 { + panic!("Event was not received"); + } + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 50, + row: 50, + kind: crossterm::event::MouseEventKind::Down(MouseButton::Right), + modifiers: KeyModifiers::NONE, + })); + tui_ctx.inject_event(Event::Mouse(MouseEvent { + column: 50, + row: 50, + kind: crossterm::event::MouseEventKind::Up(MouseButton::Right), + modifiers: KeyModifiers::NONE, + })); + cx.render(rsx! { + div { + width: "100%", + height: "100%", + oncontextmenu: move |_|{ + tui_ctx.quit(); + }, + } + }) + } +}