1#![expect(non_snake_case)]
2
3use std::{
4 cell::RefCell,
5 hash::{Hash, Hasher},
6 rc::Rc,
7};
8
9use cranpose_core::{
10 OwnedMutableState, RuntimeCell, 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: RuntimeCell<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: RuntimeCell::with_runtime(
113 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()
128 }
129
130 pub fn id(&self) -> u64 {
132 let mut hasher = cranpose_core::hash::default::new();
133 self.inner.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 | PointerEventKind::ButtonDown(_)
417 | PointerEventKind::ButtonUp(_) => {}
418 }
419 }
420
421 fn end(source: MutableInteractionSource, active: PressInteractionPress) {
422 source.cancel(active);
423 }
424}
425
426#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
427struct HoverTracker {
428 enabled: bool,
429}
430
431impl InteractionTracker for HoverTracker {
432 type Active = HoverInteractionEnter;
433
434 fn handle(
435 self,
436 source: MutableInteractionSource,
437 active: &RefCell<Option<HoverInteractionEnter>>,
438 event: &PointerEvent,
439 ) {
440 match event.kind {
441 PointerEventKind::Enter | PointerEventKind::Move
442 if self.enabled && active.borrow().is_none() =>
443 {
444 *active.borrow_mut() = Some(source.enter_hover());
445 }
446 PointerEventKind::Exit => {
447 if let Some(enter) = active.borrow_mut().take() {
448 source.exit_hover(enter);
449 }
450 }
451 _ => {}
452 }
453 }
454
455 fn end(source: MutableInteractionSource, active: HoverInteractionEnter) {
456 source.exit_hover(active);
457 }
458}
459
460#[derive(Clone)]
461struct InteractionElement<T> {
462 interaction_source: MutableInteractionSource,
463 tracker: T,
464}
465
466impl<T: InteractionTracker> std::fmt::Debug for InteractionElement<T> {
467 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468 f.debug_struct("InteractionElement")
469 .field("source_id", &self.interaction_source.id())
470 .field("tracker", &self.tracker)
471 .finish()
472 }
473}
474
475impl<T: InteractionTracker> PartialEq for InteractionElement<T> {
476 fn eq(&self, other: &Self) -> bool {
477 self.interaction_source == other.interaction_source && self.tracker == other.tracker
478 }
479}
480
481impl<T: InteractionTracker> Eq for InteractionElement<T> {}
482
483impl<T: InteractionTracker> Hash for InteractionElement<T> {
484 fn hash<H: Hasher>(&self, state: &mut H) {
485 self.tracker.hash(state);
486 self.interaction_source.id().hash(state);
487 }
488}
489
490impl<T: InteractionTracker> ModifierNodeElement for InteractionElement<T> {
491 type Node = InteractionNode<T>;
492
493 fn create(&self) -> Self::Node {
494 let active = Rc::new(RefCell::new(None));
495 InteractionNode {
496 interaction_source: self.interaction_source,
497 tracker: self.tracker,
498 cached_handler: InteractionNode::handler(
499 self.interaction_source,
500 self.tracker,
501 active.clone(),
502 ),
503 active,
504 state: NodeState::new(),
505 }
506 }
507
508 fn update(&self, node: &mut Self::Node) {
509 node.update(self.interaction_source, self.tracker);
510 }
511
512 fn capabilities(&self) -> NodeCapabilities {
513 NodeCapabilities::POINTER_INPUT
514 }
515}
516
517struct InteractionNode<T: InteractionTracker> {
518 interaction_source: MutableInteractionSource,
519 tracker: T,
520 active: Rc<RefCell<Option<T::Active>>>,
521 cached_handler: Rc<dyn Fn(PointerEvent)>,
522 state: NodeState,
523}
524
525impl<T: InteractionTracker> InteractionNode<T> {
526 fn update(&mut self, interaction_source: MutableInteractionSource, tracker: T) {
527 if self.interaction_source == interaction_source && self.tracker == tracker {
528 return;
529 }
530 self.end_active();
531 self.interaction_source = interaction_source;
532 self.tracker = tracker;
533 self.cached_handler = Self::handler(interaction_source, tracker, self.active.clone());
534 }
535
536 fn end_active(&self) {
537 if let Some(active) = self.active.borrow_mut().take() {
538 T::end(self.interaction_source, active);
539 }
540 }
541
542 fn handler(
543 interaction_source: MutableInteractionSource,
544 tracker: T,
545 active: Rc<RefCell<Option<T::Active>>>,
546 ) -> Rc<dyn Fn(PointerEvent)> {
547 Rc::new(move |event: PointerEvent| {
548 if event.id == 0 {
549 tracker.handle(interaction_source, &active, &event);
550 }
551 })
552 }
553}
554
555impl<T: InteractionTracker> std::fmt::Debug for InteractionNode<T> {
556 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557 f.debug_struct("InteractionNode")
558 .field("source_id", &self.interaction_source.id())
559 .field("tracker", &self.tracker)
560 .finish()
561 }
562}
563
564impl<T: InteractionTracker> DelegatableNode for InteractionNode<T> {
565 fn node_state(&self) -> &NodeState {
566 &self.state
567 }
568}
569
570impl<T: InteractionTracker> ModifierNode for InteractionNode<T> {
571 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
572 context.invalidate(InvalidationKind::PointerInput);
573 }
574
575 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
576 Some(self)
577 }
578
579 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
580 Some(self)
581 }
582
583 fn on_detach(&mut self) {
584 self.end_active();
585 }
586}
587
588impl<T: InteractionTracker> PointerInputNode for InteractionNode<T> {
589 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
590 Some(self.cached_handler.clone())
591 }
592}
593
594#[cfg(test)]
595#[path = "tests/interaction_tests.rs"]
596mod tests;