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