use std::cell::RefCell;
use std::rc::Rc;
pub use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crate::hooks::{HookSlot, Hooks};
pub struct InputCtx {
pub(crate) stopped: bool,
}
impl InputCtx {
pub fn stop_propagation(&mut self) {
self.stopped = true;
}
}
pub(crate) type InputHandler = Rc<RefCell<dyn FnMut(KeyEvent, &mut InputCtx)>>;
pub(crate) type PasteHandler = Rc<RefCell<dyn FnMut(&str, &mut InputCtx)>>;
impl<'a> Hooks<'a> {
pub fn use_input(&mut self, handler: impl FnMut(KeyEvent, &mut InputCtx) + 'static) {
let h: InputHandler = Rc::new(RefCell::new(handler));
let slot = self.next_slot(|| HookSlot::Input(h.clone()));
let HookSlot::Input(existing) = slot else {
self.hook_mismatch("use_input")
};
*existing = h;
}
pub fn use_paste(&mut self, handler: impl FnMut(&str, &mut InputCtx) + 'static) {
let h: PasteHandler = Rc::new(RefCell::new(handler));
let slot = self.next_slot(|| HookSlot::Paste(h.clone()));
let HookSlot::Paste(existing) = slot else {
self.hook_mismatch("use_paste")
};
*existing = h;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::component::Component;
use crate::element::Element;
use crate::hooks::Hooks;
use crate::hooks::state::State;
use crate::props::TextProps;
use crate::test_util::Shared;
use crate::testing::TestTerminal;
struct Inner;
#[derive(Clone, PartialEq, Default)]
struct InnerProps {
log: Shared<Vec<String>>,
stop: bool,
}
impl Component for Inner {
type Props = InnerProps;
fn render(props: &InnerProps, hooks: &mut Hooks) -> Element {
let log = props.log.clone();
let stop = props.stop;
hooks.use_input(move |ev, ctx| {
log.lock().push(format!("inner:{:?}", ev.code));
if stop {
ctx.stop_propagation();
}
});
Element::text(TextProps {
content: "inner".into(),
..Default::default()
})
}
}
struct Outer;
#[derive(Clone, PartialEq, Default)]
struct OuterProps {
log: Shared<Vec<String>>,
stop_inner: bool,
}
impl Component for Outer {
type Props = OuterProps;
fn render(props: &OuterProps, hooks: &mut Hooks) -> Element {
let log = props.log.clone();
hooks.use_input(move |ev, _| log.lock().push(format!("outer:{:?}", ev.code)));
Element::fragment(vec![Element::component::<Inner>(InnerProps {
log: props.log.clone(),
stop: props.stop_inner,
})])
}
}
#[tokio::test]
async fn deepest_handler_runs_first_then_bubbles() {
let log = Shared::default();
let mut t = TestTerminal::new(
10,
2,
Element::component::<Outer>(OuterProps {
log: log.clone(),
stop_inner: false,
}),
)
.unwrap();
t.send_key(KeyCode::Char('x')).unwrap();
assert_eq!(*log.lock(), vec!["inner:Char('x')", "outer:Char('x')"]);
}
#[tokio::test]
async fn stop_propagation_blocks_ancestors() {
let log = Shared::default();
let mut t = TestTerminal::new(
10,
2,
Element::component::<Outer>(OuterProps {
log: log.clone(),
stop_inner: true,
}),
)
.unwrap();
t.send_key(KeyCode::Char('x')).unwrap();
assert_eq!(*log.lock(), vec!["inner:Char('x')"]);
}
struct Fresh;
#[derive(Clone, PartialEq, Default)]
struct FreshProps {
seen: Shared<Vec<i32>>,
handle: Shared<Option<State<i32>>>,
}
impl Component for Fresh {
type Props = FreshProps;
fn render(props: &FreshProps, hooks: &mut Hooks) -> Element {
let n = hooks.use_state(|| 0);
*props.handle.lock() = Some(n.clone());
let cur = n.get();
let seen = props.seen.clone();
hooks.use_input(move |_ev, _| seen.lock().push(cur));
Element::text(TextProps {
content: cur.to_string(),
..Default::default()
})
}
}
#[tokio::test]
async fn use_input_handler_captures_fresh_state_after_rerender() {
let props = FreshProps::default();
let mut t = TestTerminal::new(10, 1, Element::component::<Fresh>(props.clone())).unwrap();
t.send_key(KeyCode::Char('a')).unwrap();
assert_eq!(*props.seen.lock(), vec![0]);
props.handle.lock().clone().unwrap().set(42); t.tick().await.unwrap();
t.send_key(KeyCode::Char('b')).unwrap();
assert_eq!(*props.seen.lock(), vec![0, 42]); }
struct PasteInner;
#[derive(Clone, PartialEq, Default)]
struct PasteInnerProps {
log: Shared<Vec<String>>,
stop: bool,
}
impl Component for PasteInner {
type Props = PasteInnerProps;
fn render(props: &PasteInnerProps, hooks: &mut Hooks) -> Element {
let log = props.log.clone();
let stop = props.stop;
hooks.use_paste(move |text, ctx| {
log.lock().push(format!("inner:{text}"));
if stop {
ctx.stop_propagation();
}
});
Element::text(TextProps {
content: "inner".into(),
..Default::default()
})
}
}
struct PasteOuter;
#[derive(Clone, PartialEq, Default)]
struct PasteOuterProps {
log: Shared<Vec<String>>,
stop_inner: bool,
}
impl Component for PasteOuter {
type Props = PasteOuterProps;
fn render(props: &PasteOuterProps, hooks: &mut Hooks) -> Element {
let log = props.log.clone();
hooks.use_paste(move |text, _| log.lock().push(format!("outer:{text}")));
Element::fragment(vec![Element::component::<PasteInner>(PasteInnerProps {
log: props.log.clone(),
stop: props.stop_inner,
})])
}
}
#[tokio::test]
async fn paste_deepest_handler_runs_first_then_bubbles() {
let log = Shared::default();
let mut t = TestTerminal::new(
10,
2,
Element::component::<PasteOuter>(PasteOuterProps {
log: log.clone(),
stop_inner: false,
}),
)
.unwrap();
t.send_paste("hello\npaste").unwrap();
assert_eq!(
*log.lock(),
vec!["inner:hello\npaste", "outer:hello\npaste"]
);
}
#[tokio::test]
async fn paste_stop_propagation_blocks_ancestors() {
let log = Shared::default();
let mut t = TestTerminal::new(
10,
2,
Element::component::<PasteOuter>(PasteOuterProps {
log: log.clone(),
stop_inner: true,
}),
)
.unwrap();
t.send_paste("x").unwrap();
assert_eq!(*log.lock(), vec!["inner:x"]);
}
struct PasteFresh;
#[derive(Clone, PartialEq, Default)]
struct PasteFreshProps {
seen: Shared<Vec<i32>>,
handle: Shared<Option<State<i32>>>,
}
impl Component for PasteFresh {
type Props = PasteFreshProps;
fn render(props: &PasteFreshProps, hooks: &mut Hooks) -> Element {
let n = hooks.use_state(|| 0);
*props.handle.lock() = Some(n.clone());
let cur = n.get();
let seen = props.seen.clone();
hooks.use_paste(move |_text, _| seen.lock().push(cur));
Element::text(TextProps {
content: cur.to_string(),
..Default::default()
})
}
}
#[tokio::test]
async fn use_paste_handler_captures_fresh_state_after_rerender() {
let props = PasteFreshProps::default();
let mut t =
TestTerminal::new(10, 1, Element::component::<PasteFresh>(props.clone())).unwrap();
t.send_paste("a").unwrap();
assert_eq!(*props.seen.lock(), vec![0]);
props.handle.lock().clone().unwrap().set(42);
t.tick().await.unwrap();
t.send_paste("b").unwrap();
assert_eq!(*props.seen.lock(), vec![0, 42]);
}
}