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)]
209#[path = "tests/rotary_input_tests.rs"]
210mod tests;