Skip to main content

cranpose_ui/
interaction.rs

1#![expect(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_id: RefCell<u64>,
30    active_presses: RefCell<HashSet<u64>>,
31    active_hovers: RefCell<HashSet<u64>>,
32    pressed: OwnedMutableState<bool>,
33    hovered: OwnedMutableState<bool>,
34    last_interaction: OwnedMutableState<Option<Interaction>>,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub enum Interaction {
39    Press(PressInteraction),
40    Hover(HoverInteraction),
41}
42
43/// The pointer resting over a node: Compose's `HoverInteraction`.
44#[derive(Clone, Copy, Debug, PartialEq)]
45pub enum HoverInteraction {
46    Enter(HoverInteractionEnter),
47    Exit(HoverInteractionExit),
48}
49
50/// A pointer came to rest over a node.
51#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct HoverInteractionEnter {
53    id: u64,
54}
55
56/// The pointer that [`Self::enter`] reported left.
57#[derive(Clone, Copy, Debug, PartialEq)]
58pub struct HoverInteractionExit {
59    pub enter: HoverInteractionEnter,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq)]
63pub enum PressInteraction {
64    Press(PressInteractionPress),
65    Release(PressInteractionRelease),
66    Cancel(PressInteractionCancel),
67}
68
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub struct PressInteractionPress {
71    id: u64,
72    pub press_position: Point,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq)]
76pub struct PressInteractionRelease {
77    pub press: PressInteractionPress,
78}
79
80#[derive(Clone, Copy, Debug, PartialEq)]
81pub struct PressInteractionCancel {
82    pub press: PressInteractionPress,
83}
84
85impl MutableInteractionSource {
86    pub fn new() -> Self {
87        let runtime = with_current_composer(cranpose_core::Composer::runtime_handle);
88        Self::with_runtime(runtime)
89    }
90
91    pub fn with_runtime(runtime: RuntimeHandle) -> Self {
92        Self {
93            inner: MutableState::with_runtime(
94                Rc::new(MutableInteractionSourceInner {
95                    next_id: RefCell::new(1),
96                    active_presses: RefCell::new(HashSet::new()),
97                    active_hovers: RefCell::new(HashSet::new()),
98                    pressed: OwnedMutableState::with_runtime(false, runtime.clone()),
99                    hovered: OwnedMutableState::with_runtime(false, runtime.clone()),
100                    last_interaction: OwnedMutableState::with_runtime(None, runtime.clone()),
101                }),
102                runtime,
103            ),
104        }
105    }
106
107    fn inner(&self) -> Rc<MutableInteractionSourceInner> {
108        self.inner.get_non_reactive()
109    }
110
111    pub fn id(&self) -> u64 {
112        let mut hasher = std::collections::hash_map::DefaultHasher::new();
113        self.inner.runtime_state_id().hash(&mut hasher);
114        hasher.finish()
115    }
116
117    /// The next id this source hands an interaction.
118    fn next_id(&self) -> u64 {
119        let inner = self.inner();
120        let mut next_id = inner.next_id.borrow_mut();
121        let id = *next_id;
122        *next_id = id.saturating_add(1);
123        id
124    }
125
126    pub fn press(&self, press_position: Point) -> PressInteractionPress {
127        let press = PressInteractionPress {
128            id: self.next_id(),
129            press_position,
130        };
131        self.emit(Interaction::Press(PressInteraction::Press(press)));
132        press
133    }
134
135    pub fn release(&self, press: PressInteractionPress) {
136        self.emit(Interaction::Press(PressInteraction::Release(
137            PressInteractionRelease { press },
138        )));
139    }
140
141    pub fn cancel(&self, press: PressInteractionPress) {
142        self.emit(Interaction::Press(PressInteraction::Cancel(
143            PressInteractionCancel { press },
144        )));
145    }
146
147    /// Reports that a pointer came to rest over a node, Compose's
148    /// `emit(HoverInteraction.Enter())`, and returns the enter to end it
149    /// with.
150    pub fn enter_hover(&self) -> HoverInteractionEnter {
151        let enter = HoverInteractionEnter { id: self.next_id() };
152        self.emit(Interaction::Hover(HoverInteraction::Enter(enter)));
153        enter
154    }
155
156    /// Reports that the pointer `enter` reported left.
157    pub fn exit_hover(&self, enter: HoverInteractionEnter) {
158        self.emit(Interaction::Hover(HoverInteraction::Exit(
159            HoverInteractionExit { enter },
160        )));
161    }
162
163    pub fn emit(&self, interaction: Interaction) {
164        let inner = self.inner();
165        inner.last_interaction.set(Some(interaction));
166        let (active, state, id, starts) = match interaction {
167            Interaction::Press(press) => {
168                let (id, starts) = match press {
169                    PressInteraction::Press(press) => (press.id, true),
170                    PressInteraction::Release(release) => (release.press.id, false),
171                    PressInteraction::Cancel(cancel) => (cancel.press.id, false),
172                };
173                (&inner.active_presses, &inner.pressed, id, starts)
174            }
175            Interaction::Hover(hover) => {
176                let (id, starts) = match hover {
177                    HoverInteraction::Enter(enter) => (enter.id, true),
178                    HoverInteraction::Exit(exit) => (exit.enter.id, false),
179                };
180                (&inner.active_hovers, &inner.hovered, id, starts)
181            }
182        };
183        let any = {
184            let mut active = active.borrow_mut();
185            if starts {
186                active.insert(id);
187            } else {
188                active.remove(&id);
189            }
190            !active.is_empty()
191        };
192        if state.get_non_reactive() != any {
193            state.set(any);
194        }
195    }
196
197    /// Returns whether the interaction source is currently pressed as a
198    /// reactive [`State`].
199    ///
200    /// Mirrors Jetpack Compose: `InteractionSource.collectIsPressedAsState()`.
201    ///
202    /// The value flips to `true` when a `PressInteraction::Press` is emitted
203    /// and back to `false` once every active press has seen a matching
204    /// `PressInteraction::Release` or `PressInteraction::Cancel`. Reading the
205    /// returned state inside a composable subscribes the enclosing recompose
206    /// scope, so the composable recomposes whenever the pressed state changes.
207    pub fn collectIsPressedAsState(&self) -> State<bool> {
208        self.inner().pressed.as_state()
209    }
210
211    /// Returns whether a pointer rests over a node of this source as a
212    /// reactive [`State`]: `true` from a `HoverInteraction::Enter` until
213    /// every enter has seen its `HoverInteraction::Exit`.
214    ///
215    /// Mirrors Jetpack Compose: `InteractionSource.collectIsHoveredAsState()`.
216    pub fn collectIsHoveredAsState(&self) -> State<bool> {
217        self.inner().hovered.as_state()
218    }
219
220    pub fn collectLastInteractionAsState(&self) -> State<Option<Interaction>> {
221        self.inner().last_interaction.as_state()
222    }
223}
224
225impl PressInteractionPress {
226    pub fn id(&self) -> u64 {
227        self.id
228    }
229}
230
231impl HoverInteractionEnter {
232    pub fn id(&self) -> u64 {
233        self.id
234    }
235}
236
237impl std::fmt::Debug for MutableInteractionSource {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        f.debug_struct("MutableInteractionSource")
240            .field("id", &self.id())
241            .finish()
242    }
243}
244
245impl PartialEq for MutableInteractionSource {
246    fn eq(&self, other: &Self) -> bool {
247        self.inner == other.inner
248    }
249}
250
251impl Eq for MutableInteractionSource {}
252
253impl Default for MutableInteractionSource {
254    fn default() -> Self {
255        Self::new()
256    }
257}
258
259#[composable]
260pub fn rememberMutableInteractionSource() -> MutableInteractionSource {
261    let runtime = with_current_composer(|composer| composer.runtime_handle());
262    remember(move || MutableInteractionSource::with_runtime(runtime)).with(|source| *source)
263}
264
265/// Free-function form of
266/// [`MutableInteractionSource::collectIsPressedAsState`].
267///
268/// Mirrors Jetpack Compose: `InteractionSource.collectIsPressedAsState()`.
269///
270/// Returns a reactive [`State`] derived from the press interactions emitted
271/// by `interaction_source`: `true` between `PressInteraction::Press` and the
272/// matching `PressInteraction::Release`/`PressInteraction::Cancel`.
273pub fn collect_is_pressed_as_state(interaction_source: &MutableInteractionSource) -> State<bool> {
274    interaction_source.collectIsPressedAsState()
275}
276
277/// Free-function form of
278/// [`MutableInteractionSource::collectIsHoveredAsState`].
279///
280/// Mirrors Jetpack Compose: `InteractionSource.collectIsHoveredAsState()`.
281pub fn collect_is_hovered_as_state(interaction_source: &MutableInteractionSource) -> State<bool> {
282    interaction_source.collectIsHoveredAsState()
283}
284
285impl Modifier {
286    pub fn press_interaction_source(self, interaction_source: MutableInteractionSource) -> Self {
287        self.interaction_node("pressInteractionSource", interaction_source, PressTracker)
288    }
289
290    /// Reports the pointer resting over this node to `interaction_source` as
291    /// a `HoverInteraction::Enter`, and its leaving as the matching
292    /// `HoverInteraction::Exit`, while `enabled`. Disabling the node or
293    /// removing it ends a hover it reported. Compose's
294    /// `Modifier.hoverable(interactionSource, enabled)`.
295    ///
296    /// Example: `let hovered = source.collectIsHoveredAsState();` and
297    /// `Modifier::empty().hoverable(source, true)` on the row it tints.
298    pub fn hoverable(self, interaction_source: MutableInteractionSource, enabled: bool) -> Self {
299        self.interaction_node("hoverable", interaction_source, HoverTracker { enabled })
300    }
301
302    fn interaction_node<T: InteractionTracker>(
303        self,
304        name: &'static str,
305        interaction_source: MutableInteractionSource,
306        tracker: T,
307    ) -> Self {
308        let source_id = interaction_source.id();
309        let modifier = Self::with_element(InteractionElement {
310            interaction_source,
311            tracker,
312        })
313        .with_inspector_metadata(inspector_metadata(name, move |info| {
314            info.add_property("sourceId", source_id.to_string());
315        }));
316        self.then(modifier)
317    }
318}
319
320/// What a pointer node reports to an interaction source: which pointer
321/// events start and end the interaction it tracks, and how to end one
322/// early.
323trait InteractionTracker: Copy + PartialEq + std::fmt::Debug + Hash + 'static {
324    /// The interaction the node holds while it lasts.
325    type Active: Copy + 'static;
326
327    /// Feeds the primary pointer's `event` to `source`, starting or ending
328    /// the interaction `active` holds.
329    fn handle(
330        self,
331        source: MutableInteractionSource,
332        active: &RefCell<Option<Self::Active>>,
333        event: &PointerEvent,
334    );
335
336    /// Ends `active` early: the node left, or stopped tracking.
337    fn end(source: MutableInteractionSource, active: Self::Active);
338}
339
340#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
341struct PressTracker;
342
343impl InteractionTracker for PressTracker {
344    type Active = PressInteractionPress;
345
346    fn handle(
347        self,
348        source: MutableInteractionSource,
349        active: &RefCell<Option<PressInteractionPress>>,
350        event: &PointerEvent,
351    ) {
352        if event.is_consumed() {
353            if let Some(press) = active.borrow_mut().take() {
354                source.cancel(press);
355            }
356            return;
357        }
358        match event.kind {
359            PointerEventKind::Down => {
360                if active.borrow().is_none() {
361                    *active.borrow_mut() = Some(source.press(event.position));
362                }
363            }
364            PointerEventKind::Up => {
365                if let Some(press) = active.borrow_mut().take() {
366                    source.release(press);
367                }
368            }
369            PointerEventKind::Cancel => {
370                if let Some(press) = active.borrow_mut().take() {
371                    source.cancel(press);
372                }
373            }
374            PointerEventKind::Move
375            | PointerEventKind::Scroll
376            | PointerEventKind::Zoom
377            | PointerEventKind::RotaryScrollPre
378            | PointerEventKind::RotaryScroll
379            | PointerEventKind::Enter
380            | PointerEventKind::Exit => {}
381        }
382    }
383
384    fn end(source: MutableInteractionSource, active: PressInteractionPress) {
385        source.cancel(active);
386    }
387}
388
389#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
390struct HoverTracker {
391    enabled: bool,
392}
393
394impl InteractionTracker for HoverTracker {
395    type Active = HoverInteractionEnter;
396
397    fn handle(
398        self,
399        source: MutableInteractionSource,
400        active: &RefCell<Option<HoverInteractionEnter>>,
401        event: &PointerEvent,
402    ) {
403        match event.kind {
404            PointerEventKind::Enter | PointerEventKind::Move
405                if self.enabled && active.borrow().is_none() =>
406            {
407                *active.borrow_mut() = Some(source.enter_hover());
408            }
409            PointerEventKind::Exit => {
410                if let Some(enter) = active.borrow_mut().take() {
411                    source.exit_hover(enter);
412                }
413            }
414            _ => {}
415        }
416    }
417
418    fn end(source: MutableInteractionSource, active: HoverInteractionEnter) {
419        source.exit_hover(active);
420    }
421}
422
423#[derive(Clone)]
424struct InteractionElement<T> {
425    interaction_source: MutableInteractionSource,
426    tracker: T,
427}
428
429impl<T: InteractionTracker> std::fmt::Debug for InteractionElement<T> {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        f.debug_struct("InteractionElement")
432            .field("source_id", &self.interaction_source.id())
433            .field("tracker", &self.tracker)
434            .finish()
435    }
436}
437
438impl<T: InteractionTracker> PartialEq for InteractionElement<T> {
439    fn eq(&self, other: &Self) -> bool {
440        self.interaction_source == other.interaction_source && self.tracker == other.tracker
441    }
442}
443
444impl<T: InteractionTracker> Eq for InteractionElement<T> {}
445
446impl<T: InteractionTracker> Hash for InteractionElement<T> {
447    fn hash<H: Hasher>(&self, state: &mut H) {
448        self.tracker.hash(state);
449        self.interaction_source.id().hash(state);
450    }
451}
452
453impl<T: InteractionTracker> ModifierNodeElement for InteractionElement<T> {
454    type Node = InteractionNode<T>;
455
456    fn create(&self) -> Self::Node {
457        let active = Rc::new(RefCell::new(None));
458        InteractionNode {
459            interaction_source: self.interaction_source,
460            tracker: self.tracker,
461            cached_handler: InteractionNode::handler(
462                self.interaction_source,
463                self.tracker,
464                active.clone(),
465            ),
466            active,
467            state: NodeState::new(),
468        }
469    }
470
471    fn update(&self, node: &mut Self::Node) {
472        node.update(self.interaction_source, self.tracker);
473    }
474
475    fn capabilities(&self) -> NodeCapabilities {
476        NodeCapabilities::POINTER_INPUT
477    }
478}
479
480struct InteractionNode<T: InteractionTracker> {
481    interaction_source: MutableInteractionSource,
482    tracker: T,
483    active: Rc<RefCell<Option<T::Active>>>,
484    cached_handler: Rc<dyn Fn(PointerEvent)>,
485    state: NodeState,
486}
487
488impl<T: InteractionTracker> InteractionNode<T> {
489    fn update(&mut self, interaction_source: MutableInteractionSource, tracker: T) {
490        if self.interaction_source == interaction_source && self.tracker == tracker {
491            return;
492        }
493        self.end_active();
494        self.interaction_source = interaction_source;
495        self.tracker = tracker;
496        self.cached_handler = Self::handler(interaction_source, tracker, self.active.clone());
497    }
498
499    fn end_active(&self) {
500        if let Some(active) = self.active.borrow_mut().take() {
501            T::end(self.interaction_source, active);
502        }
503    }
504
505    fn handler(
506        interaction_source: MutableInteractionSource,
507        tracker: T,
508        active: Rc<RefCell<Option<T::Active>>>,
509    ) -> Rc<dyn Fn(PointerEvent)> {
510        Rc::new(move |event: PointerEvent| {
511            if event.id == 0 {
512                tracker.handle(interaction_source, &active, &event);
513            }
514        })
515    }
516}
517
518impl<T: InteractionTracker> std::fmt::Debug for InteractionNode<T> {
519    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520        f.debug_struct("InteractionNode")
521            .field("source_id", &self.interaction_source.id())
522            .field("tracker", &self.tracker)
523            .finish()
524    }
525}
526
527impl<T: InteractionTracker> DelegatableNode for InteractionNode<T> {
528    fn node_state(&self) -> &NodeState {
529        &self.state
530    }
531}
532
533impl<T: InteractionTracker> ModifierNode for InteractionNode<T> {
534    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
535        context.invalidate(InvalidationKind::PointerInput);
536    }
537
538    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
539        Some(self)
540    }
541
542    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
543        Some(self)
544    }
545
546    fn on_detach(&mut self) {
547        self.end_active();
548    }
549}
550
551impl<T: InteractionTracker> PointerInputNode for InteractionNode<T> {
552    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
553        Some(self.cached_handler.clone())
554    }
555}
556
557#[cfg(test)]
558#[path = "tests/interaction_tests.rs"]
559mod tests;