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::Enter
319                | PointerEventKind::Exit => {}
320            }
321        })
322    }
323}
324
325impl std::fmt::Debug for PressInteractionNode {
326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        f.debug_struct("PressInteractionNode")
328            .field("source_id", &self.interaction_source.id())
329            .finish()
330    }
331}
332
333impl DelegatableNode for PressInteractionNode {
334    fn node_state(&self) -> &NodeState {
335        &self.state
336    }
337}
338
339impl ModifierNode for PressInteractionNode {
340    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
341        context.invalidate(InvalidationKind::PointerInput);
342    }
343
344    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
345        Some(self)
346    }
347
348    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
349        Some(self)
350    }
351
352    fn on_detach(&mut self) {
353        if let Some(press) = self.active_press.borrow_mut().take() {
354            self.interaction_source.cancel(press);
355        }
356    }
357}
358
359impl PointerInputNode for PressInteractionNode {
360    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
361        Some(self.cached_handler.clone())
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use cranpose_core::{Composition, MemoryApplier};
369
370    #[test]
371    fn interaction_ids_do_not_use_process_global_counters() {
372        let source = include_str!("interaction.rs");
373        let source_counter = ["static ", "NEXT_SOURCE_ID"].concat();
374        let press_counter = ["static ", "NEXT_PRESS_ID"].concat();
375
376        assert!(
377            !source.contains(&source_counter) && !source.contains(&press_counter),
378            "interaction source and press ids must be owned by the interaction source instance"
379        );
380    }
381
382    #[test]
383    fn interaction_source_tracks_active_press_state() {
384        let composition = Composition::new(MemoryApplier::new());
385        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
386        let pressed = source.collectIsPressedAsState();
387
388        assert!(!pressed.get());
389
390        let first = source.press(Point { x: 1.0, y: 2.0 });
391        assert!(pressed.get());
392
393        let second = source.press(Point { x: 3.0, y: 4.0 });
394        assert_ne!(first.id(), second.id());
395        source.release(first);
396        assert!(pressed.get());
397
398        source.cancel(second);
399        assert!(!pressed.get());
400    }
401
402    #[test]
403    fn interaction_source_ids_are_instance_owned() {
404        let composition = Composition::new(MemoryApplier::new());
405        let first = MutableInteractionSource::with_runtime(composition.runtime_handle());
406        let first_clone = first.clone();
407        let second = MutableInteractionSource::with_runtime(composition.runtime_handle());
408
409        assert_eq!(first.id(), first_clone.id());
410        assert_ne!(first.id(), second.id());
411        assert_eq!(first.press(Point { x: 0.0, y: 0.0 }).id(), 1);
412        assert_eq!(first.press(Point { x: 1.0, y: 1.0 }).id(), 2);
413        assert_eq!(second.press(Point { x: 0.0, y: 0.0 }).id(), 1);
414    }
415
416    #[test]
417    fn collect_is_pressed_as_state_tracks_press_emissions() {
418        let composition = Composition::new(MemoryApplier::new());
419        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
420        let pressed = collect_is_pressed_as_state(&source);
421
422        assert!(!pressed.get());
423
424        let press = PressInteractionPress {
425            id: 7,
426            press_position: Point { x: 4.0, y: 5.0 },
427        };
428        source.emit(Interaction::Press(PressInteraction::Press(press)));
429        assert!(pressed.get(), "pressed after a Press emission");
430
431        source.emit(Interaction::Press(PressInteraction::Release(
432            PressInteractionRelease { press },
433        )));
434        assert!(!pressed.get(), "released after a Release emission");
435
436        source.emit(Interaction::Press(PressInteraction::Press(press)));
437        assert!(pressed.get(), "pressed again after a new Press emission");
438
439        source.emit(Interaction::Press(PressInteraction::Cancel(
440            PressInteractionCancel { press },
441        )));
442        assert!(!pressed.get(), "released after a Cancel emission");
443    }
444
445    #[composable]
446    fn PressedReader(
447        observed: Rc<RefCell<Vec<bool>>>,
448        source_slot: Rc<RefCell<Option<MutableInteractionSource>>>,
449    ) {
450        let source = rememberMutableInteractionSource();
451        source_slot.borrow_mut().replace(source.clone());
452        let pressed = collect_is_pressed_as_state(&source);
453        observed.borrow_mut().push(pressed.value());
454    }
455
456    #[test]
457    fn collect_is_pressed_as_state_recomposes_readers() {
458        let observed = Rc::new(RefCell::new(Vec::<bool>::new()));
459        let source_slot = Rc::new(RefCell::new(None::<MutableInteractionSource>));
460
461        let mut composition = {
462            let observed = Rc::clone(&observed);
463            let source_slot = Rc::clone(&source_slot);
464            crate::run_test_composition(move || {
465                PressedReader(Rc::clone(&observed), Rc::clone(&source_slot));
466            })
467        };
468
469        assert_eq!(observed.borrow().as_slice(), &[false]);
470
471        let source = source_slot
472            .borrow()
473            .as_ref()
474            .expect("interaction source captured")
475            .clone();
476        let press = composition.with_app_context(|| source.press(Point { x: 1.0, y: 1.0 }));
477        while composition
478            .process_invalid_scopes()
479            .expect("process press invalidation")
480        {}
481        assert_eq!(
482            observed.borrow().last(),
483            Some(&true),
484            "press emission should recompose readers with pressed=true"
485        );
486
487        composition.with_app_context(|| source.release(press));
488        while composition
489            .process_invalid_scopes()
490            .expect("process release invalidation")
491        {}
492        assert_eq!(
493            observed.borrow().last(),
494            Some(&false),
495            "release emission should recompose readers with pressed=false"
496        );
497    }
498
499    #[test]
500    fn interaction_source_exposes_latest_interaction() {
501        let composition = Composition::new(MemoryApplier::new());
502        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
503        let last_interaction = source.collectLastInteractionAsState();
504
505        assert_eq!(last_interaction.get(), None);
506
507        let press = source.press(Point { x: 8.0, y: 12.0 });
508        assert_eq!(
509            last_interaction.get(),
510            Some(Interaction::Press(PressInteraction::Press(press)))
511        );
512        assert_eq!(press.press_position, Point { x: 8.0, y: 12.0 });
513
514        source.release(press);
515        assert_eq!(
516            last_interaction.get(),
517            Some(Interaction::Press(PressInteraction::Release(
518                PressInteractionRelease { press }
519            )))
520        );
521    }
522}