Skip to main content

cranpose_ui/
interaction.rs

1#![allow(non_snake_case)]
2
3use std::{
4    cell::RefCell,
5    collections::HashSet,
6    hash::{Hash, Hasher},
7    rc::Rc,
8};
9
10use cranpose_core::{
11    MutableState, OwnedMutableState, RuntimeHandle, State, remember, with_current_composer,
12};
13use cranpose_foundation::{
14    DelegatableNode, InvalidationKind, ModifierNode, ModifierNodeContext, ModifierNodeElement,
15    NodeCapabilities, NodeState, PointerInputNode,
16};
17
18use crate::{
19    composable,
20    modifier::{Modifier, Point, PointerEvent, PointerEventKind, inspector_metadata},
21};
22
23#[derive(Clone, Copy)]
24pub struct MutableInteractionSource {
25    inner: MutableState<Rc<MutableInteractionSourceInner>>,
26}
27
28struct MutableInteractionSourceInner {
29    next_press_id: RefCell<u64>,
30    active_presses: RefCell<HashSet<u64>>,
31    pressed: OwnedMutableState<bool>,
32    last_interaction: OwnedMutableState<Option<Interaction>>,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq)]
36pub enum Interaction {
37    Press(PressInteraction),
38}
39
40#[derive(Clone, Copy, Debug, PartialEq)]
41pub enum PressInteraction {
42    Press(PressInteractionPress),
43    Release(PressInteractionRelease),
44    Cancel(PressInteractionCancel),
45}
46
47#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct PressInteractionPress {
49    id: u64,
50    pub press_position: Point,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq)]
54pub struct PressInteractionRelease {
55    pub press: PressInteractionPress,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct PressInteractionCancel {
60    pub press: PressInteractionPress,
61}
62
63impl MutableInteractionSource {
64    pub fn new() -> Self {
65        let runtime = with_current_composer(|composer| composer.runtime_handle());
66        Self::with_runtime(runtime)
67    }
68
69    pub fn with_runtime(runtime: RuntimeHandle) -> Self {
70        Self {
71            inner: MutableState::with_runtime(
72                Rc::new(MutableInteractionSourceInner {
73                    next_press_id: RefCell::new(1),
74                    active_presses: RefCell::new(HashSet::new()),
75                    pressed: OwnedMutableState::with_runtime(false, runtime.clone()),
76                    last_interaction: OwnedMutableState::with_runtime(None, runtime.clone()),
77                }),
78                runtime,
79            ),
80        }
81    }
82
83    fn inner(&self) -> Rc<MutableInteractionSourceInner> {
84        self.inner.get_non_reactive()
85    }
86
87    pub fn id(&self) -> u64 {
88        let mut hasher = std::collections::hash_map::DefaultHasher::new();
89        self.inner.runtime_state_id().hash(&mut hasher);
90        hasher.finish()
91    }
92
93    pub fn press(&self, press_position: Point) -> PressInteractionPress {
94        let inner = self.inner();
95        let id = {
96            let mut next_press_id = inner.next_press_id.borrow_mut();
97            let id = *next_press_id;
98            *next_press_id = id.saturating_add(1);
99            id
100        };
101        let press = PressInteractionPress { id, press_position };
102        self.emit(Interaction::Press(PressInteraction::Press(press)));
103        press
104    }
105
106    pub fn release(&self, press: PressInteractionPress) {
107        self.emit(Interaction::Press(PressInteraction::Release(
108            PressInteractionRelease { press },
109        )));
110    }
111
112    pub fn cancel(&self, press: PressInteractionPress) {
113        self.emit(Interaction::Press(PressInteraction::Cancel(
114            PressInteractionCancel { press },
115        )));
116    }
117
118    pub fn emit(&self, interaction: Interaction) {
119        let inner = self.inner();
120        inner.last_interaction.set(Some(interaction));
121        let is_pressed = {
122            let mut active_presses = inner.active_presses.borrow_mut();
123            match interaction {
124                Interaction::Press(PressInteraction::Press(press)) => {
125                    active_presses.insert(press.id);
126                }
127                Interaction::Press(PressInteraction::Release(release)) => {
128                    active_presses.remove(&release.press.id);
129                }
130                Interaction::Press(PressInteraction::Cancel(cancel)) => {
131                    active_presses.remove(&cancel.press.id);
132                }
133            }
134            !active_presses.is_empty()
135        };
136
137        if inner.pressed.get_non_reactive() != is_pressed {
138            inner.pressed.set(is_pressed);
139        }
140    }
141
142    /// Returns whether the interaction source is currently pressed as a
143    /// reactive [`State`].
144    ///
145    /// Mirrors Jetpack Compose: `InteractionSource.collectIsPressedAsState()`.
146    ///
147    /// The value flips to `true` when a `PressInteraction::Press` is emitted
148    /// and back to `false` once every active press has seen a matching
149    /// `PressInteraction::Release` or `PressInteraction::Cancel`. Reading the
150    /// returned state inside a composable subscribes the enclosing recompose
151    /// scope, so the composable recomposes whenever the pressed state changes.
152    pub fn collectIsPressedAsState(&self) -> State<bool> {
153        self.inner().pressed.as_state()
154    }
155
156    pub fn collectLastInteractionAsState(&self) -> State<Option<Interaction>> {
157        self.inner().last_interaction.as_state()
158    }
159}
160
161impl PressInteractionPress {
162    pub fn id(&self) -> u64 {
163        self.id
164    }
165}
166
167impl std::fmt::Debug for MutableInteractionSource {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        f.debug_struct("MutableInteractionSource")
170            .field("id", &self.id())
171            .finish()
172    }
173}
174
175impl PartialEq for MutableInteractionSource {
176    fn eq(&self, other: &Self) -> bool {
177        self.inner == other.inner
178    }
179}
180
181impl Eq for MutableInteractionSource {}
182
183impl Default for MutableInteractionSource {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189#[composable]
190pub fn rememberMutableInteractionSource() -> MutableInteractionSource {
191    let runtime = with_current_composer(|composer| composer.runtime_handle());
192    remember(move || MutableInteractionSource::with_runtime(runtime.clone())).with(|source| *source)
193}
194
195/// Free-function form of
196/// [`MutableInteractionSource::collectIsPressedAsState`].
197///
198/// Mirrors Jetpack Compose: `InteractionSource.collectIsPressedAsState()`.
199///
200/// Returns a reactive [`State`] derived from the press interactions emitted
201/// by `interaction_source`: `true` between `PressInteraction::Press` and the
202/// matching `PressInteraction::Release`/`PressInteraction::Cancel`.
203pub fn collect_is_pressed_as_state(interaction_source: &MutableInteractionSource) -> State<bool> {
204    interaction_source.collectIsPressedAsState()
205}
206
207impl Modifier {
208    pub fn press_interaction_source(self, interaction_source: MutableInteractionSource) -> Self {
209        let source_id = interaction_source.id();
210        let modifier = Self::with_element(PressInteractionElement::new(interaction_source))
211            .with_inspector_metadata(inspector_metadata("pressInteractionSource", move |info| {
212                info.add_property("sourceId", source_id.to_string());
213            }));
214        self.then(modifier)
215    }
216}
217
218#[derive(Clone)]
219struct PressInteractionElement {
220    interaction_source: MutableInteractionSource,
221}
222
223impl PressInteractionElement {
224    fn new(interaction_source: MutableInteractionSource) -> Self {
225        Self { interaction_source }
226    }
227}
228
229impl std::fmt::Debug for PressInteractionElement {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        f.debug_struct("PressInteractionElement")
232            .field("source_id", &self.interaction_source.id())
233            .finish()
234    }
235}
236
237impl PartialEq for PressInteractionElement {
238    fn eq(&self, other: &Self) -> bool {
239        self.interaction_source == other.interaction_source
240    }
241}
242
243impl Eq for PressInteractionElement {}
244
245impl Hash for PressInteractionElement {
246    fn hash<H: Hasher>(&self, state: &mut H) {
247        "pressInteractionSource".hash(state);
248        self.interaction_source.id().hash(state);
249    }
250}
251
252impl ModifierNodeElement for PressInteractionElement {
253    type Node = PressInteractionNode;
254
255    fn create(&self) -> Self::Node {
256        PressInteractionNode::new(self.interaction_source)
257    }
258
259    fn update(&self, node: &mut Self::Node) {
260        node.update(self.interaction_source);
261    }
262
263    fn capabilities(&self) -> NodeCapabilities {
264        NodeCapabilities::POINTER_INPUT
265    }
266}
267
268struct PressInteractionNode {
269    interaction_source: MutableInteractionSource,
270    active_press: Rc<RefCell<Option<PressInteractionPress>>>,
271    cached_handler: Rc<dyn Fn(PointerEvent)>,
272    state: NodeState,
273}
274
275impl PressInteractionNode {
276    fn new(interaction_source: MutableInteractionSource) -> Self {
277        let active_press = Rc::new(RefCell::new(None));
278        let cached_handler = Self::create_handler(interaction_source, active_press.clone());
279        Self {
280            interaction_source,
281            active_press,
282            cached_handler,
283            state: NodeState::new(),
284        }
285    }
286
287    fn update(&mut self, interaction_source: MutableInteractionSource) {
288        if self.interaction_source == interaction_source {
289            return;
290        }
291        if let Some(press) = self.active_press.borrow_mut().take() {
292            self.interaction_source.cancel(press);
293        }
294        self.interaction_source = interaction_source;
295        self.cached_handler =
296            Self::create_handler(self.interaction_source, self.active_press.clone());
297    }
298
299    fn create_handler(
300        interaction_source: MutableInteractionSource,
301        active_press: Rc<RefCell<Option<PressInteractionPress>>>,
302    ) -> Rc<dyn Fn(PointerEvent)> {
303        Rc::new(move |event: PointerEvent| {
304            // Press interactions track the primary pointer only.
305            if event.id != 0 {
306                return;
307            }
308
309            if event.is_consumed() {
310                if let Some(press) = active_press.borrow_mut().take() {
311                    interaction_source.cancel(press);
312                }
313                return;
314            }
315
316            match event.kind {
317                PointerEventKind::Down => {
318                    if active_press.borrow().is_none() {
319                        let press = interaction_source.press(event.position);
320                        *active_press.borrow_mut() = Some(press);
321                    }
322                }
323                PointerEventKind::Up => {
324                    if let Some(press) = active_press.borrow_mut().take() {
325                        interaction_source.release(press);
326                    }
327                }
328                PointerEventKind::Cancel => {
329                    if let Some(press) = active_press.borrow_mut().take() {
330                        interaction_source.cancel(press);
331                    }
332                }
333                PointerEventKind::Move
334                | PointerEventKind::Scroll
335                | PointerEventKind::Zoom
336                | PointerEventKind::RotaryScrollPre
337                | PointerEventKind::RotaryScroll
338                | PointerEventKind::Enter
339                | PointerEventKind::Exit => {}
340            }
341        })
342    }
343}
344
345impl std::fmt::Debug for PressInteractionNode {
346    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347        f.debug_struct("PressInteractionNode")
348            .field("source_id", &self.interaction_source.id())
349            .finish()
350    }
351}
352
353impl DelegatableNode for PressInteractionNode {
354    fn node_state(&self) -> &NodeState {
355        &self.state
356    }
357}
358
359impl ModifierNode for PressInteractionNode {
360    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
361        context.invalidate(InvalidationKind::PointerInput);
362    }
363
364    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
365        Some(self)
366    }
367
368    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
369        Some(self)
370    }
371
372    fn on_detach(&mut self) {
373        if let Some(press) = self.active_press.borrow_mut().take() {
374            self.interaction_source.cancel(press);
375        }
376    }
377}
378
379impl PointerInputNode for PressInteractionNode {
380    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
381        Some(self.cached_handler.clone())
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use cranpose_core::{Composition, MemoryApplier};
388
389    use super::*;
390
391    #[test]
392    fn interaction_ids_do_not_use_process_global_counters() {
393        let source = include_str!("interaction.rs");
394        let source_counter = ["static ", "NEXT_SOURCE_ID"].concat();
395        let press_counter = ["static ", "NEXT_PRESS_ID"].concat();
396
397        assert!(
398            !source.contains(&source_counter) && !source.contains(&press_counter),
399            "interaction source and press ids must be owned by the interaction source instance"
400        );
401    }
402
403    #[test]
404    fn interaction_source_tracks_active_press_state() {
405        let composition = Composition::new(MemoryApplier::new());
406        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
407        let pressed = source.collectIsPressedAsState();
408
409        assert!(!pressed.get());
410
411        let first = source.press(Point { x: 1.0, y: 2.0 });
412        assert!(pressed.get());
413
414        let second = source.press(Point { x: 3.0, y: 4.0 });
415        assert_ne!(first.id(), second.id());
416        source.release(first);
417        assert!(pressed.get());
418
419        source.cancel(second);
420        assert!(!pressed.get());
421    }
422
423    #[test]
424    fn interaction_source_ids_are_instance_owned() {
425        let composition = Composition::new(MemoryApplier::new());
426        let first = MutableInteractionSource::with_runtime(composition.runtime_handle());
427        let first_clone = first;
428        let second = MutableInteractionSource::with_runtime(composition.runtime_handle());
429
430        assert_eq!(first.id(), first_clone.id());
431        assert_ne!(first.id(), second.id());
432        assert_eq!(first.press(Point { x: 0.0, y: 0.0 }).id(), 1);
433        assert_eq!(first.press(Point { x: 1.0, y: 1.0 }).id(), 2);
434        assert_eq!(second.press(Point { x: 0.0, y: 0.0 }).id(), 1);
435    }
436
437    #[test]
438    fn collect_is_pressed_as_state_tracks_press_emissions() {
439        let composition = Composition::new(MemoryApplier::new());
440        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
441        let pressed = collect_is_pressed_as_state(&source);
442
443        assert!(!pressed.get());
444
445        let press = PressInteractionPress {
446            id: 7,
447            press_position: Point { x: 4.0, y: 5.0 },
448        };
449        source.emit(Interaction::Press(PressInteraction::Press(press)));
450        assert!(pressed.get(), "pressed after a Press emission");
451
452        source.emit(Interaction::Press(PressInteraction::Release(
453            PressInteractionRelease { press },
454        )));
455        assert!(!pressed.get(), "released after a Release emission");
456
457        source.emit(Interaction::Press(PressInteraction::Press(press)));
458        assert!(pressed.get(), "pressed again after a new Press emission");
459
460        source.emit(Interaction::Press(PressInteraction::Cancel(
461            PressInteractionCancel { press },
462        )));
463        assert!(!pressed.get(), "released after a Cancel emission");
464    }
465
466    #[composable]
467    fn PressedReader(
468        observed: Rc<RefCell<Vec<bool>>>,
469        source_slot: Rc<RefCell<Option<MutableInteractionSource>>>,
470    ) {
471        let source = rememberMutableInteractionSource();
472        source_slot.borrow_mut().replace(source);
473        let pressed = collect_is_pressed_as_state(&source);
474        observed.borrow_mut().push(pressed.value());
475    }
476
477    #[test]
478    fn collect_is_pressed_as_state_recomposes_readers() {
479        let observed = Rc::new(RefCell::new(Vec::<bool>::new()));
480        let source_slot = Rc::new(RefCell::new(None::<MutableInteractionSource>));
481
482        let mut composition = {
483            let observed = Rc::clone(&observed);
484            let source_slot = Rc::clone(&source_slot);
485            crate::run_test_composition(move || {
486                PressedReader(Rc::clone(&observed), Rc::clone(&source_slot));
487            })
488        };
489
490        assert_eq!(observed.borrow().as_slice(), &[false]);
491
492        let source = *source_slot
493            .borrow()
494            .as_ref()
495            .expect("interaction source captured");
496        let press = composition.with_app_context(|| source.press(Point { x: 1.0, y: 1.0 }));
497        while composition
498            .process_invalid_scopes()
499            .expect("process press invalidation")
500        {}
501        assert_eq!(
502            observed.borrow().last(),
503            Some(&true),
504            "press emission should recompose readers with pressed=true"
505        );
506
507        composition.with_app_context(|| source.release(press));
508        while composition
509            .process_invalid_scopes()
510            .expect("process release invalidation")
511        {}
512        assert_eq!(
513            observed.borrow().last(),
514            Some(&false),
515            "release emission should recompose readers with pressed=false"
516        );
517    }
518
519    #[test]
520    fn interaction_source_exposes_latest_interaction() {
521        let composition = Composition::new(MemoryApplier::new());
522        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
523        let last_interaction = source.collectLastInteractionAsState();
524
525        assert_eq!(last_interaction.get(), None);
526
527        let press = source.press(Point { x: 8.0, y: 12.0 });
528        assert_eq!(
529            last_interaction.get(),
530            Some(Interaction::Press(PressInteraction::Press(press)))
531        );
532        assert_eq!(press.press_position, Point { x: 8.0, y: 12.0 });
533
534        source.release(press);
535        assert_eq!(
536            last_interaction.get(),
537            Some(Interaction::Press(PressInteraction::Release(
538                PressInteractionRelease { press }
539            )))
540        );
541    }
542}