Skip to main content

cranpose_ui/
interaction.rs

1#![allow(non_snake_case)]
2
3use crate::composable;
4use crate::modifier::{inspector_metadata, Modifier, Point, PointerEvent, PointerEventKind};
5use cranpose_core::{remember, with_current_composer, OwnedMutableState, RuntimeHandle, State};
6use cranpose_foundation::{
7    DelegatableNode, InvalidationKind, ModifierNode, ModifierNodeContext, ModifierNodeElement,
8    NodeCapabilities, NodeState, PointerInputNode,
9};
10use std::cell::RefCell;
11use std::collections::HashSet;
12use std::hash::{Hash, Hasher};
13use std::rc::Rc;
14
15#[derive(Clone)]
16pub struct MutableInteractionSource {
17    inner: Rc<MutableInteractionSourceInner>,
18}
19
20struct MutableInteractionSourceInner {
21    next_press_id: RefCell<u64>,
22    active_presses: RefCell<HashSet<u64>>,
23    pressed: OwnedMutableState<bool>,
24    last_interaction: OwnedMutableState<Option<Interaction>>,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub enum Interaction {
29    Press(PressInteraction),
30}
31
32#[derive(Clone, Copy, Debug, PartialEq)]
33pub enum PressInteraction {
34    Press(PressInteractionPress),
35    Release(PressInteractionRelease),
36    Cancel(PressInteractionCancel),
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct PressInteractionPress {
41    id: u64,
42    pub press_position: Point,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq)]
46pub struct PressInteractionRelease {
47    pub press: PressInteractionPress,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct PressInteractionCancel {
52    pub press: PressInteractionPress,
53}
54
55impl MutableInteractionSource {
56    pub fn new() -> Self {
57        let runtime = with_current_composer(|composer| composer.runtime_handle());
58        Self::with_runtime(runtime)
59    }
60
61    pub fn with_runtime(runtime: RuntimeHandle) -> Self {
62        Self {
63            inner: Rc::new(MutableInteractionSourceInner {
64                next_press_id: RefCell::new(1),
65                active_presses: RefCell::new(HashSet::new()),
66                pressed: OwnedMutableState::with_runtime(false, runtime.clone()),
67                last_interaction: OwnedMutableState::with_runtime(None, runtime),
68            }),
69        }
70    }
71
72    pub fn id(&self) -> u64 {
73        Rc::as_ptr(&self.inner) as usize as u64
74    }
75
76    pub fn press(&self, press_position: Point) -> PressInteractionPress {
77        let id = {
78            let mut next_press_id = self.inner.next_press_id.borrow_mut();
79            let id = *next_press_id;
80            *next_press_id = next_press_id.saturating_add(1);
81            id
82        };
83        let press = PressInteractionPress { id, press_position };
84        self.emit(Interaction::Press(PressInteraction::Press(press)));
85        press
86    }
87
88    pub fn release(&self, press: PressInteractionPress) {
89        self.emit(Interaction::Press(PressInteraction::Release(
90            PressInteractionRelease { press },
91        )));
92    }
93
94    pub fn cancel(&self, press: PressInteractionPress) {
95        self.emit(Interaction::Press(PressInteraction::Cancel(
96            PressInteractionCancel { press },
97        )));
98    }
99
100    pub fn emit(&self, interaction: Interaction) {
101        self.inner.last_interaction.set(Some(interaction));
102        let is_pressed = {
103            let mut active_presses = self.inner.active_presses.borrow_mut();
104            match interaction {
105                Interaction::Press(PressInteraction::Press(press)) => {
106                    active_presses.insert(press.id);
107                }
108                Interaction::Press(PressInteraction::Release(release)) => {
109                    active_presses.remove(&release.press.id);
110                }
111                Interaction::Press(PressInteraction::Cancel(cancel)) => {
112                    active_presses.remove(&cancel.press.id);
113                }
114            }
115            !active_presses.is_empty()
116        };
117
118        if self.inner.pressed.get_non_reactive() != is_pressed {
119            self.inner.pressed.set(is_pressed);
120        }
121    }
122
123    pub fn collectIsPressedAsState(&self) -> State<bool> {
124        self.inner.pressed.as_state()
125    }
126
127    pub fn collectLastInteractionAsState(&self) -> State<Option<Interaction>> {
128        self.inner.last_interaction.as_state()
129    }
130}
131
132impl PressInteractionPress {
133    pub fn id(&self) -> u64 {
134        self.id
135    }
136}
137
138impl std::fmt::Debug for MutableInteractionSource {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.debug_struct("MutableInteractionSource")
141            .field("id", &self.id())
142            .finish()
143    }
144}
145
146impl PartialEq for MutableInteractionSource {
147    fn eq(&self, other: &Self) -> bool {
148        self.id() == other.id()
149    }
150}
151
152impl Eq for MutableInteractionSource {}
153
154impl Default for MutableInteractionSource {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160#[composable]
161pub fn rememberMutableInteractionSource() -> MutableInteractionSource {
162    let runtime = with_current_composer(|composer| composer.runtime_handle());
163    remember(move || MutableInteractionSource::with_runtime(runtime.clone()))
164        .with(|source| source.clone())
165}
166
167impl Modifier {
168    pub fn press_interaction_source(self, interaction_source: MutableInteractionSource) -> Self {
169        let source_id = interaction_source.id();
170        let modifier = Self::with_element(PressInteractionElement::new(interaction_source))
171            .with_inspector_metadata(inspector_metadata("pressInteractionSource", move |info| {
172                info.add_property("sourceId", source_id.to_string());
173            }));
174        self.then(modifier)
175    }
176}
177
178#[derive(Clone)]
179struct PressInteractionElement {
180    interaction_source: MutableInteractionSource,
181}
182
183impl PressInteractionElement {
184    fn new(interaction_source: MutableInteractionSource) -> Self {
185        Self { interaction_source }
186    }
187}
188
189impl std::fmt::Debug for PressInteractionElement {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        f.debug_struct("PressInteractionElement")
192            .field("source_id", &self.interaction_source.id())
193            .finish()
194    }
195}
196
197impl PartialEq for PressInteractionElement {
198    fn eq(&self, other: &Self) -> bool {
199        self.interaction_source == other.interaction_source
200    }
201}
202
203impl Eq for PressInteractionElement {}
204
205impl Hash for PressInteractionElement {
206    fn hash<H: Hasher>(&self, state: &mut H) {
207        "pressInteractionSource".hash(state);
208        self.interaction_source.id().hash(state);
209    }
210}
211
212impl ModifierNodeElement for PressInteractionElement {
213    type Node = PressInteractionNode;
214
215    fn create(&self) -> Self::Node {
216        PressInteractionNode::new(self.interaction_source.clone())
217    }
218
219    fn update(&self, node: &mut Self::Node) {
220        node.update(self.interaction_source.clone());
221    }
222
223    fn capabilities(&self) -> NodeCapabilities {
224        NodeCapabilities::POINTER_INPUT
225    }
226}
227
228struct PressInteractionNode {
229    interaction_source: MutableInteractionSource,
230    active_press: Rc<RefCell<Option<PressInteractionPress>>>,
231    cached_handler: Rc<dyn Fn(PointerEvent)>,
232    state: NodeState,
233}
234
235impl PressInteractionNode {
236    fn new(interaction_source: MutableInteractionSource) -> Self {
237        let active_press = Rc::new(RefCell::new(None));
238        let cached_handler = Self::create_handler(interaction_source.clone(), active_press.clone());
239        Self {
240            interaction_source,
241            active_press,
242            cached_handler,
243            state: NodeState::new(),
244        }
245    }
246
247    fn update(&mut self, interaction_source: MutableInteractionSource) {
248        if self.interaction_source == interaction_source {
249            return;
250        }
251        if let Some(press) = self.active_press.borrow_mut().take() {
252            self.interaction_source.cancel(press);
253        }
254        self.interaction_source = interaction_source;
255        self.cached_handler =
256            Self::create_handler(self.interaction_source.clone(), self.active_press.clone());
257    }
258
259    fn create_handler(
260        interaction_source: MutableInteractionSource,
261        active_press: Rc<RefCell<Option<PressInteractionPress>>>,
262    ) -> Rc<dyn Fn(PointerEvent)> {
263        Rc::new(move |event: PointerEvent| {
264            // Press interactions track the primary pointer only.
265            if event.id != 0 {
266                return;
267            }
268
269            if event.is_consumed() {
270                if let Some(press) = active_press.borrow_mut().take() {
271                    interaction_source.cancel(press);
272                }
273                return;
274            }
275
276            match event.kind {
277                PointerEventKind::Down => {
278                    if active_press.borrow().is_none() {
279                        let press = interaction_source.press(event.position);
280                        *active_press.borrow_mut() = Some(press);
281                    }
282                }
283                PointerEventKind::Up => {
284                    if let Some(press) = active_press.borrow_mut().take() {
285                        interaction_source.release(press);
286                    }
287                }
288                PointerEventKind::Cancel => {
289                    if let Some(press) = active_press.borrow_mut().take() {
290                        interaction_source.cancel(press);
291                    }
292                }
293                PointerEventKind::Move
294                | PointerEventKind::Scroll
295                | PointerEventKind::Zoom
296                | PointerEventKind::Enter
297                | PointerEventKind::Exit => {}
298            }
299        })
300    }
301}
302
303impl std::fmt::Debug for PressInteractionNode {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        f.debug_struct("PressInteractionNode")
306            .field("source_id", &self.interaction_source.id())
307            .finish()
308    }
309}
310
311impl DelegatableNode for PressInteractionNode {
312    fn node_state(&self) -> &NodeState {
313        &self.state
314    }
315}
316
317impl ModifierNode for PressInteractionNode {
318    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
319        context.invalidate(InvalidationKind::PointerInput);
320    }
321
322    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
323        Some(self)
324    }
325
326    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
327        Some(self)
328    }
329
330    fn on_detach(&mut self) {
331        if let Some(press) = self.active_press.borrow_mut().take() {
332            self.interaction_source.cancel(press);
333        }
334    }
335}
336
337impl PointerInputNode for PressInteractionNode {
338    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
339        Some(self.cached_handler.clone())
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use cranpose_core::{Composition, MemoryApplier};
347
348    #[test]
349    fn interaction_ids_do_not_use_process_global_counters() {
350        let source = include_str!("interaction.rs");
351        let source_counter = ["static ", "NEXT_SOURCE_ID"].concat();
352        let press_counter = ["static ", "NEXT_PRESS_ID"].concat();
353
354        assert!(
355            !source.contains(&source_counter) && !source.contains(&press_counter),
356            "interaction source and press ids must be owned by the interaction source instance"
357        );
358    }
359
360    #[test]
361    fn interaction_source_tracks_active_press_state() {
362        let composition = Composition::new(MemoryApplier::new());
363        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
364        let pressed = source.collectIsPressedAsState();
365
366        assert!(!pressed.get());
367
368        let first = source.press(Point { x: 1.0, y: 2.0 });
369        assert!(pressed.get());
370
371        let second = source.press(Point { x: 3.0, y: 4.0 });
372        assert_ne!(first.id(), second.id());
373        source.release(first);
374        assert!(pressed.get());
375
376        source.cancel(second);
377        assert!(!pressed.get());
378    }
379
380    #[test]
381    fn interaction_source_ids_are_instance_owned() {
382        let composition = Composition::new(MemoryApplier::new());
383        let first = MutableInteractionSource::with_runtime(composition.runtime_handle());
384        let first_clone = first.clone();
385        let second = MutableInteractionSource::with_runtime(composition.runtime_handle());
386
387        assert_eq!(first.id(), first_clone.id());
388        assert_ne!(first.id(), second.id());
389        assert_eq!(first.press(Point { x: 0.0, y: 0.0 }).id(), 1);
390        assert_eq!(first.press(Point { x: 1.0, y: 1.0 }).id(), 2);
391        assert_eq!(second.press(Point { x: 0.0, y: 0.0 }).id(), 1);
392    }
393
394    #[test]
395    fn interaction_source_exposes_latest_interaction() {
396        let composition = Composition::new(MemoryApplier::new());
397        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
398        let last_interaction = source.collectLastInteractionAsState();
399
400        assert_eq!(last_interaction.get(), None);
401
402        let press = source.press(Point { x: 8.0, y: 12.0 });
403        assert_eq!(
404            last_interaction.get(),
405            Some(Interaction::Press(PressInteraction::Press(press)))
406        );
407        assert_eq!(press.press_position, Point { x: 8.0, y: 12.0 });
408
409        source.release(press);
410        assert_eq!(
411            last_interaction.get(),
412            Some(Interaction::Press(PressInteraction::Release(
413                PressInteractionRelease { press }
414            )))
415        );
416    }
417}