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