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#[derive(Clone, Copy, Debug, PartialEq)]
45pub enum HoverInteraction {
46 Enter(HoverInteractionEnter),
47 Exit(HoverInteractionExit),
48}
49
50#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct HoverInteractionEnter {
53 id: u64,
54}
55
56#[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 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 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 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 pub fn collectIsPressedAsState(&self) -> State<bool> {
208 self.inner().pressed.as_state()
209 }
210
211 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
265pub fn collect_is_pressed_as_state(interaction_source: &MutableInteractionSource) -> State<bool> {
274 interaction_source.collectIsPressedAsState()
275}
276
277pub 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 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
320trait InteractionTracker: Copy + PartialEq + std::fmt::Debug + Hash + 'static {
324 type Active: Copy + 'static;
326
327 fn handle(
330 self,
331 source: MutableInteractionSource,
332 active: &RefCell<Option<Self::Active>>,
333 event: &PointerEvent,
334 );
335
336 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;