1#![expect(non_snake_case)]
2
3use std::{
4 cell::RefCell,
5 hash::{Hash, Hasher},
6 rc::Rc,
7};
8
9use cranpose_core::{
10 MutableState, OwnedMutableState, RuntimeHandle, State, collections::map::HashSet, remember,
11 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)]
25pub struct MutableInteractionSource {
26 inner: MutableState<Rc<MutableInteractionSourceInner>>,
27}
28
29struct MutableInteractionSourceInner {
30 next_id: RefCell<u64>,
31 active_presses: RefCell<HashSet<u64>>,
32 active_hovers: RefCell<HashSet<u64>>,
33 pressed: OwnedMutableState<bool>,
34 hovered: OwnedMutableState<bool>,
35 last_interaction: OwnedMutableState<Option<Interaction>>,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
40pub enum Interaction {
41 Press(PressInteraction),
43 Hover(HoverInteraction),
45}
46
47#[derive(Clone, Copy, Debug, PartialEq)]
49pub enum HoverInteraction {
50 Enter(HoverInteractionEnter),
52 Exit(HoverInteractionExit),
54}
55
56#[derive(Clone, Copy, Debug, PartialEq)]
58pub struct HoverInteractionEnter {
59 id: u64,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq)]
64pub struct HoverInteractionExit {
65 pub enter: HoverInteractionEnter,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq)]
71pub enum PressInteraction {
72 Press(PressInteractionPress),
74 Release(PressInteractionRelease),
76 Cancel(PressInteractionCancel),
78}
79
80#[derive(Clone, Copy, Debug, PartialEq)]
82pub struct PressInteractionPress {
83 id: u64,
84 pub press_position: Point,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq)]
90pub struct PressInteractionRelease {
91 pub press: PressInteractionPress,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq)]
97pub struct PressInteractionCancel {
98 pub press: PressInteractionPress,
100}
101
102impl MutableInteractionSource {
103 pub fn new() -> Self {
105 let runtime = with_current_composer(cranpose_core::Composer::runtime_handle);
106 Self::with_runtime(runtime)
107 }
108
109 pub fn with_runtime(runtime: RuntimeHandle) -> Self {
111 Self {
112 inner: MutableState::with_runtime(
113 Rc::new(MutableInteractionSourceInner {
114 next_id: RefCell::new(1),
115 active_presses: RefCell::new(HashSet::default()),
116 active_hovers: RefCell::new(HashSet::default()),
117 pressed: OwnedMutableState::with_runtime(false, runtime.clone()),
118 hovered: OwnedMutableState::with_runtime(false, runtime.clone()),
119 last_interaction: OwnedMutableState::with_runtime(None, runtime.clone()),
120 }),
121 runtime,
122 ),
123 }
124 }
125
126 fn inner(&self) -> Rc<MutableInteractionSourceInner> {
127 self.inner.get_non_reactive()
128 }
129
130 pub fn id(&self) -> u64 {
132 let mut hasher = cranpose_core::hash::default::new();
133 self.inner.runtime_state_id().hash(&mut hasher);
134 hasher.finish()
135 }
136
137 fn next_id(&self) -> u64 {
139 let inner = self.inner();
140 let mut next_id = inner.next_id.borrow_mut();
141 let id = *next_id;
142 *next_id = id.saturating_add(1);
143 id
144 }
145
146 pub fn press(&self, press_position: Point) -> PressInteractionPress {
149 let press = PressInteractionPress {
150 id: self.next_id(),
151 press_position,
152 };
153 self.emit(Interaction::Press(PressInteraction::Press(press)));
154 press
155 }
156
157 pub fn release(&self, press: PressInteractionPress) {
159 self.emit(Interaction::Press(PressInteraction::Release(
160 PressInteractionRelease { press },
161 )));
162 }
163
164 pub fn cancel(&self, press: PressInteractionPress) {
166 self.emit(Interaction::Press(PressInteraction::Cancel(
167 PressInteractionCancel { press },
168 )));
169 }
170
171 pub fn enter_hover(&self) -> HoverInteractionEnter {
175 let enter = HoverInteractionEnter { id: self.next_id() };
176 self.emit(Interaction::Hover(HoverInteraction::Enter(enter)));
177 enter
178 }
179
180 pub fn exit_hover(&self, enter: HoverInteractionEnter) {
182 self.emit(Interaction::Hover(HoverInteraction::Exit(
183 HoverInteractionExit { enter },
184 )));
185 }
186
187 pub fn emit(&self, interaction: Interaction) {
189 let inner = self.inner();
190 inner.last_interaction.set(Some(interaction));
191 let (active, state, id, starts) = match interaction {
192 Interaction::Press(press) => {
193 let (id, starts) = match press {
194 PressInteraction::Press(press) => (press.id, true),
195 PressInteraction::Release(release) => (release.press.id, false),
196 PressInteraction::Cancel(cancel) => (cancel.press.id, false),
197 };
198 (&inner.active_presses, &inner.pressed, id, starts)
199 }
200 Interaction::Hover(hover) => {
201 let (id, starts) = match hover {
202 HoverInteraction::Enter(enter) => (enter.id, true),
203 HoverInteraction::Exit(exit) => (exit.enter.id, false),
204 };
205 (&inner.active_hovers, &inner.hovered, id, starts)
206 }
207 };
208 let any = {
209 let mut active = active.borrow_mut();
210 if starts {
211 active.insert(id);
212 } else {
213 active.remove(&id);
214 }
215 !active.is_empty()
216 };
217 if state.get_non_reactive() != any {
218 state.set(any);
219 }
220 }
221
222 pub fn collectIsPressedAsState(&self) -> State<bool> {
233 self.inner().pressed.as_state()
234 }
235
236 pub fn collectIsHoveredAsState(&self) -> State<bool> {
242 self.inner().hovered.as_state()
243 }
244
245 pub fn collectLastInteractionAsState(&self) -> State<Option<Interaction>> {
248 self.inner().last_interaction.as_state()
249 }
250}
251
252impl PressInteractionPress {
253 pub fn id(&self) -> u64 {
256 self.id
257 }
258}
259
260impl HoverInteractionEnter {
261 pub fn id(&self) -> u64 {
264 self.id
265 }
266}
267
268impl std::fmt::Debug for MutableInteractionSource {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 f.debug_struct("MutableInteractionSource")
271 .field("id", &self.id())
272 .finish()
273 }
274}
275
276impl PartialEq for MutableInteractionSource {
277 fn eq(&self, other: &Self) -> bool {
278 self.inner == other.inner
279 }
280}
281
282impl Eq for MutableInteractionSource {}
283
284impl Default for MutableInteractionSource {
285 fn default() -> Self {
286 Self::new()
287 }
288}
289
290#[composable]
292pub fn rememberMutableInteractionSource() -> MutableInteractionSource {
293 let runtime = with_current_composer(|composer| composer.runtime_handle());
294 remember(move || MutableInteractionSource::with_runtime(runtime)).with(|source| *source)
295}
296
297pub fn collect_is_pressed_as_state(interaction_source: &MutableInteractionSource) -> State<bool> {
306 interaction_source.collectIsPressedAsState()
307}
308
309pub fn collect_is_hovered_as_state(interaction_source: &MutableInteractionSource) -> State<bool> {
314 interaction_source.collectIsHoveredAsState()
315}
316
317impl Modifier {
318 pub fn press_interaction_source(self, interaction_source: MutableInteractionSource) -> Self {
322 self.interaction_node("pressInteractionSource", interaction_source, PressTracker)
323 }
324
325 pub fn hoverable(self, interaction_source: MutableInteractionSource, enabled: bool) -> Self {
334 self.interaction_node("hoverable", interaction_source, HoverTracker { enabled })
335 }
336
337 fn interaction_node<T: InteractionTracker>(
338 self,
339 name: &'static str,
340 interaction_source: MutableInteractionSource,
341 tracker: T,
342 ) -> Self {
343 let source_id = interaction_source.id();
344 let modifier = Self::with_element(InteractionElement {
345 interaction_source,
346 tracker,
347 })
348 .with_inspector_metadata(inspector_metadata(name, move |info| {
349 info.add_property("sourceId", source_id.to_string());
350 }));
351 self.then(modifier)
352 }
353}
354
355trait InteractionTracker: Copy + PartialEq + std::fmt::Debug + Hash + 'static {
359 type Active: Copy + 'static;
361
362 fn handle(
365 self,
366 source: MutableInteractionSource,
367 active: &RefCell<Option<Self::Active>>,
368 event: &PointerEvent,
369 );
370
371 fn end(source: MutableInteractionSource, active: Self::Active);
373}
374
375#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
376struct PressTracker;
377
378impl InteractionTracker for PressTracker {
379 type Active = PressInteractionPress;
380
381 fn handle(
382 self,
383 source: MutableInteractionSource,
384 active: &RefCell<Option<PressInteractionPress>>,
385 event: &PointerEvent,
386 ) {
387 if event.is_consumed() {
388 if let Some(press) = active.borrow_mut().take() {
389 source.cancel(press);
390 }
391 return;
392 }
393 match event.kind {
394 PointerEventKind::Down => {
395 if active.borrow().is_none() {
396 *active.borrow_mut() = Some(source.press(event.position));
397 }
398 }
399 PointerEventKind::Up => {
400 if let Some(press) = active.borrow_mut().take() {
401 source.release(press);
402 }
403 }
404 PointerEventKind::Cancel => {
405 if let Some(press) = active.borrow_mut().take() {
406 source.cancel(press);
407 }
408 }
409 PointerEventKind::Move
410 | PointerEventKind::Scroll
411 | PointerEventKind::Zoom
412 | PointerEventKind::RotaryScrollPre
413 | PointerEventKind::RotaryScroll
414 | PointerEventKind::Enter
415 | PointerEventKind::Exit => {}
416 }
417 }
418
419 fn end(source: MutableInteractionSource, active: PressInteractionPress) {
420 source.cancel(active);
421 }
422}
423
424#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
425struct HoverTracker {
426 enabled: bool,
427}
428
429impl InteractionTracker for HoverTracker {
430 type Active = HoverInteractionEnter;
431
432 fn handle(
433 self,
434 source: MutableInteractionSource,
435 active: &RefCell<Option<HoverInteractionEnter>>,
436 event: &PointerEvent,
437 ) {
438 match event.kind {
439 PointerEventKind::Enter | PointerEventKind::Move
440 if self.enabled && active.borrow().is_none() =>
441 {
442 *active.borrow_mut() = Some(source.enter_hover());
443 }
444 PointerEventKind::Exit => {
445 if let Some(enter) = active.borrow_mut().take() {
446 source.exit_hover(enter);
447 }
448 }
449 _ => {}
450 }
451 }
452
453 fn end(source: MutableInteractionSource, active: HoverInteractionEnter) {
454 source.exit_hover(active);
455 }
456}
457
458#[derive(Clone)]
459struct InteractionElement<T> {
460 interaction_source: MutableInteractionSource,
461 tracker: T,
462}
463
464impl<T: InteractionTracker> std::fmt::Debug for InteractionElement<T> {
465 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466 f.debug_struct("InteractionElement")
467 .field("source_id", &self.interaction_source.id())
468 .field("tracker", &self.tracker)
469 .finish()
470 }
471}
472
473impl<T: InteractionTracker> PartialEq for InteractionElement<T> {
474 fn eq(&self, other: &Self) -> bool {
475 self.interaction_source == other.interaction_source && self.tracker == other.tracker
476 }
477}
478
479impl<T: InteractionTracker> Eq for InteractionElement<T> {}
480
481impl<T: InteractionTracker> Hash for InteractionElement<T> {
482 fn hash<H: Hasher>(&self, state: &mut H) {
483 self.tracker.hash(state);
484 self.interaction_source.id().hash(state);
485 }
486}
487
488impl<T: InteractionTracker> ModifierNodeElement for InteractionElement<T> {
489 type Node = InteractionNode<T>;
490
491 fn create(&self) -> Self::Node {
492 let active = Rc::new(RefCell::new(None));
493 InteractionNode {
494 interaction_source: self.interaction_source,
495 tracker: self.tracker,
496 cached_handler: InteractionNode::handler(
497 self.interaction_source,
498 self.tracker,
499 active.clone(),
500 ),
501 active,
502 state: NodeState::new(),
503 }
504 }
505
506 fn update(&self, node: &mut Self::Node) {
507 node.update(self.interaction_source, self.tracker);
508 }
509
510 fn capabilities(&self) -> NodeCapabilities {
511 NodeCapabilities::POINTER_INPUT
512 }
513}
514
515struct InteractionNode<T: InteractionTracker> {
516 interaction_source: MutableInteractionSource,
517 tracker: T,
518 active: Rc<RefCell<Option<T::Active>>>,
519 cached_handler: Rc<dyn Fn(PointerEvent)>,
520 state: NodeState,
521}
522
523impl<T: InteractionTracker> InteractionNode<T> {
524 fn update(&mut self, interaction_source: MutableInteractionSource, tracker: T) {
525 if self.interaction_source == interaction_source && self.tracker == tracker {
526 return;
527 }
528 self.end_active();
529 self.interaction_source = interaction_source;
530 self.tracker = tracker;
531 self.cached_handler = Self::handler(interaction_source, tracker, self.active.clone());
532 }
533
534 fn end_active(&self) {
535 if let Some(active) = self.active.borrow_mut().take() {
536 T::end(self.interaction_source, active);
537 }
538 }
539
540 fn handler(
541 interaction_source: MutableInteractionSource,
542 tracker: T,
543 active: Rc<RefCell<Option<T::Active>>>,
544 ) -> Rc<dyn Fn(PointerEvent)> {
545 Rc::new(move |event: PointerEvent| {
546 if event.id == 0 {
547 tracker.handle(interaction_source, &active, &event);
548 }
549 })
550 }
551}
552
553impl<T: InteractionTracker> std::fmt::Debug for InteractionNode<T> {
554 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555 f.debug_struct("InteractionNode")
556 .field("source_id", &self.interaction_source.id())
557 .field("tracker", &self.tracker)
558 .finish()
559 }
560}
561
562impl<T: InteractionTracker> DelegatableNode for InteractionNode<T> {
563 fn node_state(&self) -> &NodeState {
564 &self.state
565 }
566}
567
568impl<T: InteractionTracker> ModifierNode for InteractionNode<T> {
569 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
570 context.invalidate(InvalidationKind::PointerInput);
571 }
572
573 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
574 Some(self)
575 }
576
577 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
578 Some(self)
579 }
580
581 fn on_detach(&mut self) {
582 self.end_active();
583 }
584}
585
586impl<T: InteractionTracker> PointerInputNode for InteractionNode<T> {
587 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
588 Some(self.cached_handler.clone())
589 }
590}
591
592#[cfg(test)]
593#[path = "tests/interaction_tests.rs"]
594mod tests;