use crate::{ComponentUpdater, FullscreenMouseEvent, Hook, Hooks, TerminalEvent, TerminalEvents};
use core::{
pin::{pin, Pin},
task::{Context, Poll},
};
use futures::stream::Stream;
use taffy::{Point, Size};
mod private {
pub trait Sealed {}
impl Sealed for crate::Hooks<'_, '_> {}
}
pub trait UseTerminalEvents: private::Sealed {
fn use_terminal_events<F>(&mut self, f: F)
where
F: FnMut(TerminalEvent) + Send + 'static;
fn use_local_terminal_events<F>(&mut self, f: F)
where
F: FnMut(TerminalEvent) + Send + 'static;
}
impl UseTerminalEvents for Hooks<'_, '_> {
fn use_terminal_events<F>(&mut self, f: F)
where
F: FnMut(TerminalEvent) + Send + 'static,
{
let h = self.use_hook(move || UseTerminalEventsImpl {
events: None,
component_location: Default::default(),
in_component: false,
f: None,
});
h.f = Some(Box::new(f));
}
fn use_local_terminal_events<F>(&mut self, f: F)
where
F: FnMut(TerminalEvent) + Send + 'static,
{
let h = self.use_hook(move || UseTerminalEventsImpl {
events: None,
component_location: Default::default(),
in_component: true,
f: None,
});
h.f = Some(Box::new(f));
}
}
struct UseTerminalEventsImpl {
events: Option<TerminalEvents>,
component_location: (Point<i16>, Size<u16>),
in_component: bool,
f: Option<Box<dyn FnMut(TerminalEvent) + Send + 'static>>,
}
impl Hook for UseTerminalEventsImpl {
fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
while let Some(Poll::Ready(Some(event))) = self
.events
.as_mut()
.map(|events| pin!(events).poll_next(cx))
{
if self.in_component {
let (location, size) = self.component_location;
match event {
TerminalEvent::FullscreenMouse(event) => {
if event.row as i16 >= location.y && event.column as i16 >= location.x {
let row = (event.row as i16 - location.y) as u16;
let column = (event.column as i16 - location.x) as u16;
if row < size.height && column < size.width {
if let Some(f) = &mut self.f {
f(TerminalEvent::FullscreenMouse(FullscreenMouseEvent {
row,
column,
..event
}));
}
}
}
}
TerminalEvent::Key(_) | TerminalEvent::Resize(..) => {
if let Some(f) = &mut self.f {
f(event);
}
}
}
} else if let Some(f) = &mut self.f {
f(event);
}
}
Poll::Pending
}
fn post_component_update(&mut self, updater: &mut ComponentUpdater) {
if self.events.is_none() {
self.events = updater.terminal_events();
}
}
fn post_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
self.component_location = (drawer.canvas_position(), drawer.size());
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use crossterm::event::MouseButton;
use futures::stream::{self, StreamExt};
use macro_rules_attribute::apply;
use smol_macros::test;
#[component]
fn MyComponent(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let mut system = hooks.use_context_mut::<SystemContext>();
let mut should_exit = hooks.use_state(|| false);
hooks.use_terminal_events(move |_event| {
should_exit.set(true);
});
if should_exit.get() {
system.exit();
element!(Text(content:"received event")).into_any()
} else {
element!(View).into_any()
}
}
#[apply(test!)]
async fn test_use_terminal_events() {
let canvases: Vec<_> = element!(MyComponent)
.mock_terminal_render_loop(MockTerminalConfig::with_events(stream::iter(vec![
TerminalEvent::Key(KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::empty(),
kind: KeyEventKind::Press,
}),
])))
.collect()
.await;
let actual = canvases.iter().map(|c| c.to_string()).collect::<Vec<_>>();
let expected = vec!["", "received event\n"];
assert_eq!(actual, expected);
}
#[component]
fn MyClickableComponent(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let mut system = hooks.use_context_mut::<SystemContext>();
let mut should_exit = hooks.use_state(|| false);
hooks.use_local_terminal_events(move |event| {
if let TerminalEvent::FullscreenMouse(FullscreenMouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
row,
column,
..
}) = event
{
assert_eq!(row, 8);
assert_eq!(column, 8);
should_exit.set(true);
}
});
if should_exit.get() {
system.exit();
element!(Text(content:"received click")).into_any()
} else {
element!(View(width: 10, height: 10)).into_any()
}
}
#[apply(test!)]
async fn test_use_local_terminal_events() {
let canvases: Vec<_> = element! {
View(padding: 2) {
MyClickableComponent
}
}
.mock_terminal_render_loop(MockTerminalConfig::with_events(stream::iter(vec![
TerminalEvent::FullscreenMouse(FullscreenMouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
10,
10,
)),
])))
.collect()
.await;
let actual = canvases
.iter()
.map(|c| c.to_string().trim().to_string())
.collect::<Vec<_>>();
assert_eq!(actual, vec!["", "received click"]);
}
}