use crate::event::{Event, InputOutcome};
use crate::focus::FocusRegistry;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum RouteStage {
Pre,
Always,
Target,
Fallback,
Undelivered,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Delivery<'a> {
pub target: Option<&'a str>,
pub stage: RouteStage,
pub outcome: InputOutcome,
}
impl Delivery<'_> {
pub fn consumed(&self) -> bool {
self.outcome.consumed()
}
pub fn reached(&self, id: &str) -> bool {
self.target == Some(id)
}
}
pub trait InputTarget {
fn handle(&mut self, event: &Event) -> InputOutcome;
}
macro_rules! impl_input_target {
($($ty:path),* $(,)?) => {
$(impl InputTarget for $ty {
fn handle(&mut self, event: &Event) -> InputOutcome {
<$ty>::handle(self, event)
}
})*
};
}
impl_input_target!(
crate::components::CompletionState,
crate::components::ConfirmDialogState,
crate::components::InputDialogState,
crate::components::SingleLineInputState,
crate::components::SliderState,
crate::components::TextInputState,
);
impl<T: InputTarget + ?Sized> InputTarget for &mut T {
fn handle(&mut self, event: &Event) -> InputOutcome {
(**self).handle(event)
}
}
pub struct Router<'a, 'e> {
active: Option<String>,
event: &'e Event,
delivery: Delivery<'a>,
last_stage: RouteStage,
}
impl<'a, 'e> Router<'a, 'e> {
pub fn new(focus: &FocusRegistry, event: &'e Event) -> Self {
Self {
active: focus.active().map(str::to_owned),
event,
delivery: Delivery {
target: None,
stage: RouteStage::Undelivered,
outcome: InputOutcome::Ignored,
},
last_stage: RouteStage::Pre,
}
}
pub fn event(&self) -> &'e Event {
self.event
}
pub fn delivery(&self) -> Delivery<'a> {
self.delivery
}
pub fn finish(self) -> Delivery<'a> {
self.delivery
}
pub fn pre_fn(&mut self, handler: impl FnOnce(&Event) -> InputOutcome) -> &mut Self {
self.stage_fn(RouteStage::Pre, None, handler)
}
pub fn target(&mut self, id: &'a str, target: &mut impl InputTarget) -> &mut Self {
self.target_fn(id, |event| target.handle(event))
}
pub fn target_fn(
&mut self,
id: &'a str,
handler: impl FnOnce(&Event) -> InputOutcome,
) -> &mut Self {
if self.active.as_deref() != Some(id) {
self.enter(RouteStage::Target);
return self;
}
self.stage_fn(RouteStage::Target, Some(id), handler)
}
pub fn always(&mut self, id: &'a str, target: &mut impl InputTarget) -> &mut Self {
self.always_fn(id, |event| target.handle(event))
}
pub fn always_fn(
&mut self,
id: &'a str,
handler: impl FnOnce(&Event) -> InputOutcome,
) -> &mut Self {
self.stage_fn(RouteStage::Always, Some(id), handler)
}
pub fn fallback_fn(&mut self, handler: impl FnOnce(&Event) -> InputOutcome) -> &mut Self {
self.stage_fn(RouteStage::Fallback, None, handler)
}
fn stage_fn(
&mut self,
stage: RouteStage,
id: Option<&'a str>,
handler: impl FnOnce(&Event) -> InputOutcome,
) -> &mut Self {
self.enter(stage);
if self.delivery.consumed() {
return self;
}
let outcome = handler(self.event);
if id.is_some() || outcome.consumed() {
self.delivery = Delivery {
target: id,
stage,
outcome,
};
}
self
}
fn enter(&mut self, stage: RouteStage) {
debug_assert!(
stage >= self.last_stage,
"routing stages run in order: {:?} was called after {:?}",
stage,
self.last_stage
);
self.last_stage = stage;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::SingleLineInputState;
use crate::event::{Key, KeyCode};
fn frame(ids: &[&str]) -> FocusRegistry {
let mut focus = FocusRegistry::new();
focus.begin_frame();
for id in ids {
focus.register(*id);
}
focus
}
fn paste(text: &str) -> Event {
Event::Paste(text.to_string())
}
fn ctrl(ch: char) -> Event {
Event::Key(Key {
code: KeyCode::Char(ch),
ctrl: true,
alt: false,
shift: false,
})
}
#[test]
fn overlay_owner_receives_every_event_kind() {
for event in [
paste("hunter2"),
ctrl('v'),
Event::Key(Key::new(KeyCode::Char('x'))),
] {
let mut composer = SingleLineInputState::new();
let mut prompt = SingleLineInputState::new();
let mut focus = frame(&["composer"]);
focus.set_owner("prompt");
let mut router = Router::new(&focus, &event);
router.target("prompt", &mut prompt);
router.target("composer", &mut composer);
let delivery = router.finish();
assert_eq!(delivery.target, Some("prompt"), "{event:?}");
assert_eq!(delivery.stage, RouteStage::Target, "{event:?}");
assert!(composer.text().is_empty(), "{event:?}");
}
}
#[test]
fn paste_reaches_the_owning_prompt_intact() {
let mut composer = SingleLineInputState::new();
let mut prompt = SingleLineInputState::new();
let mut focus = frame(&["composer"]);
focus.set_owner("prompt");
let event = paste("secret-token");
let mut router = Router::new(&focus, &event);
router.target("prompt", &mut prompt);
router.target("composer", &mut composer);
let delivery = router.finish();
assert!(delivery.consumed());
assert_eq!(delivery.outcome, InputOutcome::Changed);
assert_eq!(prompt.text(), "secret-token");
assert!(composer.text().is_empty());
}
#[test]
fn without_an_owner_the_focused_surface_receives() {
let mut composer = SingleLineInputState::new();
let mut prompt = SingleLineInputState::new();
let focus = frame(&["composer"]);
let event = paste("draft");
let mut router = Router::new(&focus, &event);
router.target("prompt", &mut prompt);
router.target("composer", &mut composer);
assert!(router.finish().reached("composer"));
assert_eq!(composer.text(), "draft");
assert!(prompt.text().is_empty());
}
#[test]
fn nothing_recognized_the_event_is_observable() {
let mut composer = SingleLineInputState::new();
let focus = frame(&["composer"]);
let event = Event::Resize {
width: 10,
height: 4,
};
let mut router = Router::new(&focus, &event);
router.pre_fn(|_| InputOutcome::Ignored);
router.target("composer", &mut composer);
let delivery = router.finish();
assert!(!delivery.consumed());
assert_eq!(delivery.target, Some("composer"));
assert_eq!(delivery.stage, RouteStage::Target);
}
#[test]
fn an_unregistered_route_delivers_nothing() {
let focus = frame(&["composer"]);
let event = ctrl('v');
let mut router = Router::new(&focus, &event);
router.pre_fn(|_| InputOutcome::Ignored);
router.fallback_fn(|_| InputOutcome::Ignored);
let delivery = router.finish();
assert_eq!(delivery.target, None);
assert_eq!(delivery.stage, RouteStage::Undelivered);
}
#[test]
fn pre_stage_wins_over_the_owning_surface() {
let mut prompt = SingleLineInputState::new();
let mut focus = frame(&["composer"]);
focus.set_owner("prompt");
let event = ctrl('c');
let mut interrupted = false;
let mut router = Router::new(&focus, &event);
router.pre_fn(|_| {
interrupted = true;
InputOutcome::Cancelled
});
router.target("prompt", &mut prompt);
let delivery = router.finish();
assert!(interrupted);
assert_eq!(delivery.stage, RouteStage::Pre);
assert_eq!(delivery.target, None);
assert!(prompt.text().is_empty());
}
#[test]
fn fallback_runs_only_when_nothing_consumed() {
let mut composer = SingleLineInputState::new();
let focus = frame(&["composer"]);
let typed = Event::Key(Key::new(KeyCode::Char('a')));
let mut fell_through = false;
let mut router = Router::new(&focus, &typed);
router.target("composer", &mut composer);
router.fallback_fn(|_| {
fell_through = true;
InputOutcome::Consumed
});
assert_eq!(router.finish().stage, RouteStage::Target);
assert!(!fell_through);
let page = Event::Key(Key::new(KeyCode::PageDown));
let mut router = Router::new(&focus, &page);
router.target("composer", &mut composer);
router.fallback_fn(|_| {
fell_through = true;
InputOutcome::Consumed
});
assert_eq!(router.finish().stage, RouteStage::Fallback);
assert!(fell_through);
}
#[test]
fn always_reaches_a_surface_the_owner_left_inactive() {
let mut prompt = SingleLineInputState::new();
let mut focus = frame(&["transcript"]);
focus.set_owner("prompt");
let event = Event::Key(Key::new(KeyCode::PageDown));
let mut scrolled = false;
let mut router = Router::new(&focus, &event);
router.always_fn("transcript", |_| {
scrolled = true;
InputOutcome::Changed
});
router.target("prompt", &mut prompt);
let delivery = router.finish();
assert!(scrolled);
assert_eq!(delivery.target, Some("transcript"));
assert_eq!(delivery.stage, RouteStage::Always);
}
#[test]
fn a_consumed_event_stops_at_the_first_receiver() {
let mut first = SingleLineInputState::new();
let mut second = SingleLineInputState::new();
let focus = frame(&["first"]);
let event = paste("once");
let mut router = Router::new(&focus, &event);
router.always("second", &mut second);
router.target("first", &mut first);
assert!(router.finish().reached("second"));
assert_eq!(second.text(), "once");
assert!(first.text().is_empty());
}
#[test]
fn an_always_surface_that_ignores_leaves_the_active_one_its_event() {
let mut composer = SingleLineInputState::new();
let focus = frame(&["composer"]);
let typed = paste("filter");
let mut picker_saw = 0;
let mut router = Router::new(&focus, &typed);
router.always_fn("picker", |_| {
picker_saw += 1;
InputOutcome::Ignored
});
router.target("composer", &mut composer);
let delivery = router.finish();
assert_eq!(picker_saw, 1);
assert!(delivery.reached("composer"));
assert_eq!(composer.text(), "filter");
}
#[test]
#[should_panic(expected = "routing stages run in order")]
fn stages_called_out_of_order_are_a_bug() {
let focus = frame(&["composer"]);
let event = ctrl('v');
let mut router = Router::new(&focus, &event);
router.fallback_fn(|_| InputOutcome::Ignored);
router.pre_fn(|_| InputOutcome::Ignored);
}
}