Skip to main content

cranpose_ui/modifier/
rotary_input.rs

1use std::{
2    cell::Cell,
3    fmt,
4    hash::{Hash, Hasher},
5    rc::Rc,
6};
7
8use cranpose_foundation::{
9    DelegatableNode, ModifierNode, ModifierNodeElement, NodeCapabilities, NodeState, PointerEvent,
10    PointerEventKind, PointerInputNode, RotaryScrollEvent, impl_pointer_input_node,
11};
12
13use super::{Modifier, inspector_metadata};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16enum RotaryPass {
17    Pre,
18    Bubble,
19}
20
21impl RotaryPass {
22    fn matches(self, kind: PointerEventKind) -> bool {
23        matches!(
24            (self, kind),
25            (RotaryPass::Pre, PointerEventKind::RotaryScrollPre)
26                | (RotaryPass::Bubble, PointerEventKind::RotaryScroll)
27        )
28    }
29}
30
31type RotaryHandler = Rc<dyn Fn(RotaryScrollEvent) -> bool>;
32
33impl Modifier {
34    /// Handles rotary scroll events (Pixel Watch crown, Galaxy Watch rotating
35    /// bezel) during the **bubble** pass.
36    ///
37    /// The handler receives a [`RotaryScrollEvent`] whose scroll amounts are
38    /// already in pixels. Return `true` to consume the event and stop it
39    /// propagating to ancestors; return `false` to let it keep bubbling.
40    ///
41    /// Equivalent to Compose's `Modifier.onRotaryScrollEvent`.
42    ///
43    /// ```ignore
44    /// Modifier::new().on_rotary_scroll_event(move |event| {
45    ///     offset.set(offset.get() + event.vertical_scroll_pixels);
46    ///     true
47    /// })
48    /// ```
49    pub fn on_rotary_scroll_event<F>(self, handler: F) -> Self
50    where
51        F: Fn(RotaryScrollEvent) -> bool + 'static,
52    {
53        self.rotary_element(RotaryPass::Bubble, Rc::new(handler), "onRotaryScrollEvent")
54    }
55
56    /// Handles rotary scroll events during the **capture** pass, before the
57    /// focused node sees them.
58    ///
59    /// Return `true` to consume the event and stop it reaching descendants.
60    ///
61    /// Equivalent to Compose's `Modifier.onPreRotaryScrollEvent`.
62    pub fn on_pre_rotary_scroll_event<F>(self, handler: F) -> Self
63    where
64        F: Fn(RotaryScrollEvent) -> bool + 'static,
65    {
66        self.rotary_element(RotaryPass::Pre, Rc::new(handler), "onPreRotaryScrollEvent")
67    }
68
69    fn rotary_element(
70        self,
71        pass: RotaryPass,
72        handler: RotaryHandler,
73        inspector_name: &'static str,
74    ) -> Self {
75        let element = RotaryInputElement::new(pass, handler);
76        let handler_id = element.handler_id;
77        self.then(
78            Self::with_element(element).with_inspector_metadata(inspector_metadata(
79                inspector_name,
80                move |info| {
81                    info.add_property("handlerId", handler_id.to_string());
82                },
83            )),
84        )
85    }
86}
87
88#[derive(Clone)]
89struct RotaryInputElement {
90    pass: RotaryPass,
91    handler: RotaryHandler,
92    handler_id: u64,
93}
94
95impl RotaryInputElement {
96    fn new(pass: RotaryPass, handler: RotaryHandler) -> Self {
97        let handler_id = Rc::as_ptr(&handler) as *const () as usize as u64;
98        Self {
99            pass,
100            handler,
101            handler_id,
102        }
103    }
104}
105
106impl fmt::Debug for RotaryInputElement {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.debug_struct("RotaryInputElement")
109            .field("pass", &self.pass)
110            .field("handler_id", &self.handler_id)
111            .finish()
112    }
113}
114
115impl PartialEq for RotaryInputElement {
116    fn eq(&self, other: &Self) -> bool {
117        self.pass == other.pass
118    }
119}
120
121impl Eq for RotaryInputElement {}
122
123impl Hash for RotaryInputElement {
124    fn hash<H: Hasher>(&self, state: &mut H) {
125        self.pass.hash(state);
126    }
127}
128
129impl ModifierNodeElement for RotaryInputElement {
130    type Node = RotaryInputModifierNode;
131
132    fn create(&self) -> Self::Node {
133        RotaryInputModifierNode::new(self.pass, self.handler.clone())
134    }
135
136    fn update(&self, node: &mut Self::Node) {
137        node.handler.set(Some(self.handler.clone()));
138    }
139
140    fn always_update(&self) -> bool {
141        true
142    }
143
144    fn capabilities(&self) -> NodeCapabilities {
145        NodeCapabilities::POINTER_INPUT
146    }
147}
148
149/// Modifier node that forwards rotary scroll events to an app handler.
150///
151/// Named after Compose's `RotaryInputModifierNode`. It rides the existing
152/// pointer dispatch path: the shell sends rotary passes as
153/// [`PointerEventKind::RotaryScrollPre`]/[`PointerEventKind::RotaryScroll`]
154/// pointer events, and this node filters for its own pass.
155pub struct RotaryInputModifierNode {
156    handler: Rc<Cell<Option<RotaryHandler>>>,
157    dispatch: Rc<dyn Fn(PointerEvent)>,
158    state: NodeState,
159}
160
161impl RotaryInputModifierNode {
162    fn new(pass: RotaryPass, handler: RotaryHandler) -> Self {
163        let handler_cell: Rc<Cell<Option<RotaryHandler>>> = Rc::new(Cell::new(Some(handler)));
164        let handler_for_dispatch = Rc::clone(&handler_cell);
165        let dispatch = Rc::new(move |event: PointerEvent| {
166            if !pass.matches(event.kind) || event.is_consumed() {
167                return;
168            }
169            let Some(rotary) = event.rotary_scroll_event() else {
170                return;
171            };
172            let Some(handler) = handler_for_dispatch.take() else {
173                return;
174            };
175            let consumed = handler(rotary);
176            if handler_for_dispatch.take().is_none() {
177                handler_for_dispatch.set(Some(handler));
178            }
179            if consumed {
180                event.consume();
181            }
182        });
183
184        Self {
185            handler: handler_cell,
186            dispatch,
187            state: NodeState::new(),
188        }
189    }
190}
191
192impl ModifierNode for RotaryInputModifierNode {
193    impl_pointer_input_node!();
194}
195
196impl DelegatableNode for RotaryInputModifierNode {
197    fn node_state(&self) -> &NodeState {
198        &self.state
199    }
200}
201
202impl PointerInputNode for RotaryInputModifierNode {
203    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
204        Some(Rc::clone(&self.dispatch))
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use std::cell::RefCell;
211
212    use cranpose_ui_graphics::Point;
213
214    use super::*;
215
216    fn rotary_event(kind: PointerEventKind, vertical: f32) -> PointerEvent {
217        PointerEvent::rotary(
218            kind,
219            RotaryScrollEvent::new(vertical, 0.0, 42),
220            Point { x: 0.0, y: 0.0 },
221        )
222    }
223
224    fn node(pass: RotaryPass, handler: RotaryHandler) -> RotaryInputModifierNode {
225        RotaryInputModifierNode::new(pass, handler)
226    }
227
228    #[test]
229    fn bubble_node_receives_bubble_events_only() {
230        let seen = Rc::new(RefCell::new(Vec::new()));
231        let sink = Rc::clone(&seen);
232        let node = node(
233            RotaryPass::Bubble,
234            Rc::new(move |event: RotaryScrollEvent| {
235                sink.borrow_mut().push(event.vertical_scroll_pixels);
236                false
237            }),
238        );
239        let dispatch = node.pointer_input_handler().expect("handler");
240
241        dispatch(rotary_event(PointerEventKind::RotaryScrollPre, -1.0));
242        dispatch(rotary_event(PointerEventKind::RotaryScroll, -2.0));
243        dispatch(PointerEvent::new(
244            PointerEventKind::Scroll,
245            Point { x: 0.0, y: 0.0 },
246            Point { x: 0.0, y: 0.0 },
247        ));
248
249        assert_eq!(*seen.borrow(), vec![-2.0]);
250    }
251
252    #[test]
253    fn pre_node_receives_capture_events_only() {
254        let seen = Rc::new(RefCell::new(Vec::new()));
255        let sink = Rc::clone(&seen);
256        let node = node(
257            RotaryPass::Pre,
258            Rc::new(move |event: RotaryScrollEvent| {
259                sink.borrow_mut().push(event.vertical_scroll_pixels);
260                false
261            }),
262        );
263        let dispatch = node.pointer_input_handler().expect("handler");
264
265        dispatch(rotary_event(PointerEventKind::RotaryScrollPre, -1.0));
266        dispatch(rotary_event(PointerEventKind::RotaryScroll, -2.0));
267
268        assert_eq!(*seen.borrow(), vec![-1.0]);
269    }
270
271    #[test]
272    fn returning_true_consumes_the_event() {
273        let node = node(RotaryPass::Bubble, Rc::new(|_| true));
274        let dispatch = node.pointer_input_handler().expect("handler");
275
276        let event = rotary_event(PointerEventKind::RotaryScroll, -8.0);
277        dispatch(event.clone());
278
279        assert!(event.is_consumed());
280    }
281
282    #[test]
283    fn returning_false_leaves_the_event_unconsumed() {
284        let node = node(RotaryPass::Bubble, Rc::new(|_| false));
285        let dispatch = node.pointer_input_handler().expect("handler");
286
287        let event = rotary_event(PointerEventKind::RotaryScroll, -8.0);
288        dispatch(event.clone());
289
290        assert!(!event.is_consumed());
291    }
292
293    #[test]
294    fn an_already_consumed_event_never_reaches_the_handler() {
295        let calls = Rc::new(Cell::new(0));
296        let counter = Rc::clone(&calls);
297        let node = node(
298            RotaryPass::Bubble,
299            Rc::new(move |_| {
300                counter.set(counter.get() + 1);
301                false
302            }),
303        );
304        let dispatch = node.pointer_input_handler().expect("handler");
305
306        let event = rotary_event(PointerEventKind::RotaryScroll, -8.0);
307        event.consume();
308        dispatch(event);
309
310        assert_eq!(calls.get(), 0);
311    }
312
313    #[test]
314    fn handler_sees_the_full_rotary_payload() {
315        let captured = Rc::new(Cell::new(None));
316        let sink = Rc::clone(&captured);
317        let node = node(
318            RotaryPass::Bubble,
319            Rc::new(move |event: RotaryScrollEvent| {
320                sink.set(Some(event));
321                true
322            }),
323        );
324        let dispatch = node.pointer_input_handler().expect("handler");
325
326        dispatch(PointerEvent::rotary(
327            PointerEventKind::RotaryScroll,
328            RotaryScrollEvent::new(-64.0, 32.0, 777),
329            Point { x: 0.0, y: 0.0 },
330        ));
331
332        assert_eq!(
333            captured.get(),
334            Some(RotaryScrollEvent::new(-64.0, 32.0, 777))
335        );
336    }
337
338    #[test]
339    fn element_reuses_the_node_across_recomposition() {
340        let first = RotaryInputElement::new(RotaryPass::Bubble, Rc::new(|_| false));
341        let second = RotaryInputElement::new(RotaryPass::Bubble, Rc::new(|_| true));
342        assert_eq!(first, second);
343        assert!(first.always_update());
344
345        let mut node = first.create();
346        second.update(&mut node);
347
348        let event = rotary_event(PointerEventKind::RotaryScroll, -1.0);
349        node.pointer_input_handler().expect("handler")(event.clone());
350
351        assert!(event.is_consumed(), "updated handler should be in effect");
352    }
353
354    #[test]
355    fn pre_and_bubble_elements_are_distinct() {
356        let pre = RotaryInputElement::new(RotaryPass::Pre, Rc::new(|_| false));
357        let bubble = RotaryInputElement::new(RotaryPass::Bubble, Rc::new(|_| false));
358
359        assert_ne!(pre, bubble);
360    }
361
362    #[test]
363    fn modifier_builders_add_pointer_input_elements() {
364        let modifier = Modifier::empty()
365            .on_pre_rotary_scroll_event(|_| false)
366            .on_rotary_scroll_event(|_| true);
367
368        assert_eq!(modifier.elements().len(), 2);
369    }
370}