use super::{inspector_metadata, Modifier};
use cranpose_foundation::{
impl_pointer_input_node, DelegatableNode, ModifierNode, ModifierNodeElement, NodeCapabilities,
NodeState, PointerEvent, PointerEventKind, PointerInputNode, RotaryScrollEvent,
};
use std::cell::Cell;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum RotaryPass {
Pre,
Bubble,
}
impl RotaryPass {
fn matches(self, kind: PointerEventKind) -> bool {
matches!(
(self, kind),
(RotaryPass::Pre, PointerEventKind::RotaryScrollPre)
| (RotaryPass::Bubble, PointerEventKind::RotaryScroll)
)
}
}
type RotaryHandler = Rc<dyn Fn(RotaryScrollEvent) -> bool>;
impl Modifier {
pub fn on_rotary_scroll_event<F>(self, handler: F) -> Self
where
F: Fn(RotaryScrollEvent) -> bool + 'static,
{
self.rotary_element(RotaryPass::Bubble, Rc::new(handler), "onRotaryScrollEvent")
}
pub fn on_pre_rotary_scroll_event<F>(self, handler: F) -> Self
where
F: Fn(RotaryScrollEvent) -> bool + 'static,
{
self.rotary_element(RotaryPass::Pre, Rc::new(handler), "onPreRotaryScrollEvent")
}
fn rotary_element(
self,
pass: RotaryPass,
handler: RotaryHandler,
inspector_name: &'static str,
) -> Self {
let element = RotaryInputElement::new(pass, handler);
let handler_id = element.handler_id;
self.then(
Self::with_element(element).with_inspector_metadata(inspector_metadata(
inspector_name,
move |info| {
info.add_property("handlerId", handler_id.to_string());
},
)),
)
}
}
#[derive(Clone)]
struct RotaryInputElement {
pass: RotaryPass,
handler: RotaryHandler,
handler_id: u64,
}
impl RotaryInputElement {
fn new(pass: RotaryPass, handler: RotaryHandler) -> Self {
let handler_id = Rc::as_ptr(&handler) as *const () as usize as u64;
Self {
pass,
handler,
handler_id,
}
}
}
impl fmt::Debug for RotaryInputElement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RotaryInputElement")
.field("pass", &self.pass)
.field("handler_id", &self.handler_id)
.finish()
}
}
impl PartialEq for RotaryInputElement {
fn eq(&self, other: &Self) -> bool {
self.pass == other.pass
}
}
impl Eq for RotaryInputElement {}
impl Hash for RotaryInputElement {
fn hash<H: Hasher>(&self, state: &mut H) {
self.pass.hash(state);
}
}
impl ModifierNodeElement for RotaryInputElement {
type Node = RotaryInputModifierNode;
fn create(&self) -> Self::Node {
RotaryInputModifierNode::new(self.pass, self.handler.clone())
}
fn update(&self, node: &mut Self::Node) {
node.handler.set(Some(self.handler.clone()));
}
fn always_update(&self) -> bool {
true
}
fn capabilities(&self) -> NodeCapabilities {
NodeCapabilities::POINTER_INPUT
}
}
pub struct RotaryInputModifierNode {
handler: Rc<Cell<Option<RotaryHandler>>>,
dispatch: Rc<dyn Fn(PointerEvent)>,
state: NodeState,
}
impl RotaryInputModifierNode {
fn new(pass: RotaryPass, handler: RotaryHandler) -> Self {
let handler_cell: Rc<Cell<Option<RotaryHandler>>> = Rc::new(Cell::new(Some(handler)));
let handler_for_dispatch = Rc::clone(&handler_cell);
let dispatch = Rc::new(move |event: PointerEvent| {
if !pass.matches(event.kind) || event.is_consumed() {
return;
}
let Some(rotary) = event.rotary_scroll_event() else {
return;
};
let Some(handler) = handler_for_dispatch.take() else {
return;
};
let consumed = handler(rotary);
if handler_for_dispatch.take().is_none() {
handler_for_dispatch.set(Some(handler));
}
if consumed {
event.consume();
}
});
Self {
handler: handler_cell,
dispatch,
state: NodeState::new(),
}
}
}
impl ModifierNode for RotaryInputModifierNode {
impl_pointer_input_node!();
}
impl DelegatableNode for RotaryInputModifierNode {
fn node_state(&self) -> &NodeState {
&self.state
}
}
impl PointerInputNode for RotaryInputModifierNode {
fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
Some(Rc::clone(&self.dispatch))
}
}
#[cfg(test)]
mod tests {
use super::*;
use cranpose_ui_graphics::Point;
use std::cell::RefCell;
fn rotary_event(kind: PointerEventKind, vertical: f32) -> PointerEvent {
PointerEvent::rotary(
kind,
RotaryScrollEvent::new(vertical, 0.0, 42),
Point { x: 0.0, y: 0.0 },
)
}
fn node(pass: RotaryPass, handler: RotaryHandler) -> RotaryInputModifierNode {
RotaryInputModifierNode::new(pass, handler)
}
#[test]
fn bubble_node_receives_bubble_events_only() {
let seen = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&seen);
let node = node(
RotaryPass::Bubble,
Rc::new(move |event: RotaryScrollEvent| {
sink.borrow_mut().push(event.vertical_scroll_pixels);
false
}),
);
let dispatch = node.pointer_input_handler().expect("handler");
dispatch(rotary_event(PointerEventKind::RotaryScrollPre, -1.0));
dispatch(rotary_event(PointerEventKind::RotaryScroll, -2.0));
dispatch(PointerEvent::new(
PointerEventKind::Scroll,
Point { x: 0.0, y: 0.0 },
Point { x: 0.0, y: 0.0 },
));
assert_eq!(*seen.borrow(), vec![-2.0]);
}
#[test]
fn pre_node_receives_capture_events_only() {
let seen = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&seen);
let node = node(
RotaryPass::Pre,
Rc::new(move |event: RotaryScrollEvent| {
sink.borrow_mut().push(event.vertical_scroll_pixels);
false
}),
);
let dispatch = node.pointer_input_handler().expect("handler");
dispatch(rotary_event(PointerEventKind::RotaryScrollPre, -1.0));
dispatch(rotary_event(PointerEventKind::RotaryScroll, -2.0));
assert_eq!(*seen.borrow(), vec![-1.0]);
}
#[test]
fn returning_true_consumes_the_event() {
let node = node(RotaryPass::Bubble, Rc::new(|_| true));
let dispatch = node.pointer_input_handler().expect("handler");
let event = rotary_event(PointerEventKind::RotaryScroll, -8.0);
dispatch(event.clone());
assert!(event.is_consumed());
}
#[test]
fn returning_false_leaves_the_event_unconsumed() {
let node = node(RotaryPass::Bubble, Rc::new(|_| false));
let dispatch = node.pointer_input_handler().expect("handler");
let event = rotary_event(PointerEventKind::RotaryScroll, -8.0);
dispatch(event.clone());
assert!(!event.is_consumed());
}
#[test]
fn an_already_consumed_event_never_reaches_the_handler() {
let calls = Rc::new(Cell::new(0));
let counter = Rc::clone(&calls);
let node = node(
RotaryPass::Bubble,
Rc::new(move |_| {
counter.set(counter.get() + 1);
false
}),
);
let dispatch = node.pointer_input_handler().expect("handler");
let event = rotary_event(PointerEventKind::RotaryScroll, -8.0);
event.consume();
dispatch(event);
assert_eq!(calls.get(), 0);
}
#[test]
fn handler_sees_the_full_rotary_payload() {
let captured = Rc::new(Cell::new(None));
let sink = Rc::clone(&captured);
let node = node(
RotaryPass::Bubble,
Rc::new(move |event: RotaryScrollEvent| {
sink.set(Some(event));
true
}),
);
let dispatch = node.pointer_input_handler().expect("handler");
dispatch(PointerEvent::rotary(
PointerEventKind::RotaryScroll,
RotaryScrollEvent::new(-64.0, 32.0, 777),
Point { x: 0.0, y: 0.0 },
));
assert_eq!(
captured.get(),
Some(RotaryScrollEvent::new(-64.0, 32.0, 777))
);
}
#[test]
fn element_reuses_the_node_across_recomposition() {
let first = RotaryInputElement::new(RotaryPass::Bubble, Rc::new(|_| false));
let second = RotaryInputElement::new(RotaryPass::Bubble, Rc::new(|_| true));
assert_eq!(first, second);
assert!(first.always_update());
let mut node = first.create();
second.update(&mut node);
let event = rotary_event(PointerEventKind::RotaryScroll, -1.0);
node.pointer_input_handler().expect("handler")(event.clone());
assert!(event.is_consumed(), "updated handler should be in effect");
}
#[test]
fn pre_and_bubble_elements_are_distinct() {
let pre = RotaryInputElement::new(RotaryPass::Pre, Rc::new(|_| false));
let bubble = RotaryInputElement::new(RotaryPass::Bubble, Rc::new(|_| false));
assert_ne!(pre, bubble);
}
#[test]
fn modifier_builders_add_pointer_input_elements() {
let modifier = Modifier::empty()
.on_pre_rotary_scroll_event(|_| false)
.on_rotary_scroll_event(|_| true);
assert_eq!(modifier.elements().len(), 2);
}
}