1#![allow(non_snake_case)]
2
3use crate::composable;
4use crate::modifier::{inspector_metadata, Modifier, Point, PointerEvent, PointerEventKind};
5use cranpose_core::{
6 remember, with_current_composer, MutableState, OwnedMutableState, RuntimeHandle, State,
7};
8use cranpose_foundation::{
9 DelegatableNode, InvalidationKind, ModifierNode, ModifierNodeContext, ModifierNodeElement,
10 NodeCapabilities, NodeState, PointerInputNode,
11};
12use std::cell::RefCell;
13use std::collections::HashSet;
14use std::hash::{Hash, Hasher};
15use std::rc::Rc;
16
17#[derive(Clone, Copy)]
18pub struct MutableInteractionSource {
19 inner: MutableState<Rc<MutableInteractionSourceInner>>,
20}
21
22struct MutableInteractionSourceInner {
23 next_press_id: RefCell<u64>,
24 active_presses: RefCell<HashSet<u64>>,
25 pressed: OwnedMutableState<bool>,
26 last_interaction: OwnedMutableState<Option<Interaction>>,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub enum Interaction {
31 Press(PressInteraction),
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub enum PressInteraction {
36 Press(PressInteractionPress),
37 Release(PressInteractionRelease),
38 Cancel(PressInteractionCancel),
39}
40
41#[derive(Clone, Copy, Debug, PartialEq)]
42pub struct PressInteractionPress {
43 id: u64,
44 pub press_position: Point,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct PressInteractionRelease {
49 pub press: PressInteractionPress,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq)]
53pub struct PressInteractionCancel {
54 pub press: PressInteractionPress,
55}
56
57impl MutableInteractionSource {
58 pub fn new() -> Self {
59 let runtime = with_current_composer(|composer| composer.runtime_handle());
60 Self::with_runtime(runtime)
61 }
62
63 pub fn with_runtime(runtime: RuntimeHandle) -> Self {
64 Self {
65 inner: MutableState::with_runtime(
66 Rc::new(MutableInteractionSourceInner {
67 next_press_id: RefCell::new(1),
68 active_presses: RefCell::new(HashSet::new()),
69 pressed: OwnedMutableState::with_runtime(false, runtime.clone()),
70 last_interaction: OwnedMutableState::with_runtime(None, runtime.clone()),
71 }),
72 runtime,
73 ),
74 }
75 }
76
77 fn inner(&self) -> Rc<MutableInteractionSourceInner> {
78 self.inner.get_non_reactive()
79 }
80
81 pub fn id(&self) -> u64 {
82 let mut hasher = std::collections::hash_map::DefaultHasher::new();
83 self.inner.runtime_state_id().hash(&mut hasher);
84 hasher.finish()
85 }
86
87 pub fn press(&self, press_position: Point) -> PressInteractionPress {
88 let inner = self.inner();
89 let id = {
90 let mut next_press_id = inner.next_press_id.borrow_mut();
91 let id = *next_press_id;
92 *next_press_id = id.saturating_add(1);
93 id
94 };
95 let press = PressInteractionPress { id, press_position };
96 self.emit(Interaction::Press(PressInteraction::Press(press)));
97 press
98 }
99
100 pub fn release(&self, press: PressInteractionPress) {
101 self.emit(Interaction::Press(PressInteraction::Release(
102 PressInteractionRelease { press },
103 )));
104 }
105
106 pub fn cancel(&self, press: PressInteractionPress) {
107 self.emit(Interaction::Press(PressInteraction::Cancel(
108 PressInteractionCancel { press },
109 )));
110 }
111
112 pub fn emit(&self, interaction: Interaction) {
113 let inner = self.inner();
114 inner.last_interaction.set(Some(interaction));
115 let is_pressed = {
116 let mut active_presses = inner.active_presses.borrow_mut();
117 match interaction {
118 Interaction::Press(PressInteraction::Press(press)) => {
119 active_presses.insert(press.id);
120 }
121 Interaction::Press(PressInteraction::Release(release)) => {
122 active_presses.remove(&release.press.id);
123 }
124 Interaction::Press(PressInteraction::Cancel(cancel)) => {
125 active_presses.remove(&cancel.press.id);
126 }
127 }
128 !active_presses.is_empty()
129 };
130
131 if inner.pressed.get_non_reactive() != is_pressed {
132 inner.pressed.set(is_pressed);
133 }
134 }
135
136 pub fn collectIsPressedAsState(&self) -> State<bool> {
147 self.inner().pressed.as_state()
148 }
149
150 pub fn collectLastInteractionAsState(&self) -> State<Option<Interaction>> {
151 self.inner().last_interaction.as_state()
152 }
153}
154
155impl PressInteractionPress {
156 pub fn id(&self) -> u64 {
157 self.id
158 }
159}
160
161impl std::fmt::Debug for MutableInteractionSource {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 f.debug_struct("MutableInteractionSource")
164 .field("id", &self.id())
165 .finish()
166 }
167}
168
169impl PartialEq for MutableInteractionSource {
170 fn eq(&self, other: &Self) -> bool {
171 self.inner == other.inner
172 }
173}
174
175impl Eq for MutableInteractionSource {}
176
177impl Default for MutableInteractionSource {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183#[composable]
184pub fn rememberMutableInteractionSource() -> MutableInteractionSource {
185 let runtime = with_current_composer(|composer| composer.runtime_handle());
186 remember(move || MutableInteractionSource::with_runtime(runtime.clone())).with(|source| *source)
187}
188
189pub fn collect_is_pressed_as_state(interaction_source: &MutableInteractionSource) -> State<bool> {
198 interaction_source.collectIsPressedAsState()
199}
200
201impl Modifier {
202 pub fn press_interaction_source(self, interaction_source: MutableInteractionSource) -> Self {
203 let source_id = interaction_source.id();
204 let modifier = Self::with_element(PressInteractionElement::new(interaction_source))
205 .with_inspector_metadata(inspector_metadata("pressInteractionSource", move |info| {
206 info.add_property("sourceId", source_id.to_string());
207 }));
208 self.then(modifier)
209 }
210}
211
212#[derive(Clone)]
213struct PressInteractionElement {
214 interaction_source: MutableInteractionSource,
215}
216
217impl PressInteractionElement {
218 fn new(interaction_source: MutableInteractionSource) -> Self {
219 Self { interaction_source }
220 }
221}
222
223impl std::fmt::Debug for PressInteractionElement {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 f.debug_struct("PressInteractionElement")
226 .field("source_id", &self.interaction_source.id())
227 .finish()
228 }
229}
230
231impl PartialEq for PressInteractionElement {
232 fn eq(&self, other: &Self) -> bool {
233 self.interaction_source == other.interaction_source
234 }
235}
236
237impl Eq for PressInteractionElement {}
238
239impl Hash for PressInteractionElement {
240 fn hash<H: Hasher>(&self, state: &mut H) {
241 "pressInteractionSource".hash(state);
242 self.interaction_source.id().hash(state);
243 }
244}
245
246impl ModifierNodeElement for PressInteractionElement {
247 type Node = PressInteractionNode;
248
249 fn create(&self) -> Self::Node {
250 PressInteractionNode::new(self.interaction_source)
251 }
252
253 fn update(&self, node: &mut Self::Node) {
254 node.update(self.interaction_source);
255 }
256
257 fn capabilities(&self) -> NodeCapabilities {
258 NodeCapabilities::POINTER_INPUT
259 }
260}
261
262struct PressInteractionNode {
263 interaction_source: MutableInteractionSource,
264 active_press: Rc<RefCell<Option<PressInteractionPress>>>,
265 cached_handler: Rc<dyn Fn(PointerEvent)>,
266 state: NodeState,
267}
268
269impl PressInteractionNode {
270 fn new(interaction_source: MutableInteractionSource) -> Self {
271 let active_press = Rc::new(RefCell::new(None));
272 let cached_handler = Self::create_handler(interaction_source, active_press.clone());
273 Self {
274 interaction_source,
275 active_press,
276 cached_handler,
277 state: NodeState::new(),
278 }
279 }
280
281 fn update(&mut self, interaction_source: MutableInteractionSource) {
282 if self.interaction_source == interaction_source {
283 return;
284 }
285 if let Some(press) = self.active_press.borrow_mut().take() {
286 self.interaction_source.cancel(press);
287 }
288 self.interaction_source = interaction_source;
289 self.cached_handler =
290 Self::create_handler(self.interaction_source, self.active_press.clone());
291 }
292
293 fn create_handler(
294 interaction_source: MutableInteractionSource,
295 active_press: Rc<RefCell<Option<PressInteractionPress>>>,
296 ) -> Rc<dyn Fn(PointerEvent)> {
297 Rc::new(move |event: PointerEvent| {
298 if event.id != 0 {
300 return;
301 }
302
303 if event.is_consumed() {
304 if let Some(press) = active_press.borrow_mut().take() {
305 interaction_source.cancel(press);
306 }
307 return;
308 }
309
310 match event.kind {
311 PointerEventKind::Down => {
312 if active_press.borrow().is_none() {
313 let press = interaction_source.press(event.position);
314 *active_press.borrow_mut() = Some(press);
315 }
316 }
317 PointerEventKind::Up => {
318 if let Some(press) = active_press.borrow_mut().take() {
319 interaction_source.release(press);
320 }
321 }
322 PointerEventKind::Cancel => {
323 if let Some(press) = active_press.borrow_mut().take() {
324 interaction_source.cancel(press);
325 }
326 }
327 PointerEventKind::Move
328 | PointerEventKind::Scroll
329 | PointerEventKind::Zoom
330 | PointerEventKind::RotaryScrollPre
331 | PointerEventKind::RotaryScroll
332 | PointerEventKind::Enter
333 | PointerEventKind::Exit => {}
334 }
335 })
336 }
337}
338
339impl std::fmt::Debug for PressInteractionNode {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 f.debug_struct("PressInteractionNode")
342 .field("source_id", &self.interaction_source.id())
343 .finish()
344 }
345}
346
347impl DelegatableNode for PressInteractionNode {
348 fn node_state(&self) -> &NodeState {
349 &self.state
350 }
351}
352
353impl ModifierNode for PressInteractionNode {
354 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
355 context.invalidate(InvalidationKind::PointerInput);
356 }
357
358 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
359 Some(self)
360 }
361
362 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
363 Some(self)
364 }
365
366 fn on_detach(&mut self) {
367 if let Some(press) = self.active_press.borrow_mut().take() {
368 self.interaction_source.cancel(press);
369 }
370 }
371}
372
373impl PointerInputNode for PressInteractionNode {
374 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
375 Some(self.cached_handler.clone())
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use cranpose_core::{Composition, MemoryApplier};
383
384 #[test]
385 fn interaction_ids_do_not_use_process_global_counters() {
386 let source = include_str!("interaction.rs");
387 let source_counter = ["static ", "NEXT_SOURCE_ID"].concat();
388 let press_counter = ["static ", "NEXT_PRESS_ID"].concat();
389
390 assert!(
391 !source.contains(&source_counter) && !source.contains(&press_counter),
392 "interaction source and press ids must be owned by the interaction source instance"
393 );
394 }
395
396 #[test]
397 fn interaction_source_tracks_active_press_state() {
398 let composition = Composition::new(MemoryApplier::new());
399 let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
400 let pressed = source.collectIsPressedAsState();
401
402 assert!(!pressed.get());
403
404 let first = source.press(Point { x: 1.0, y: 2.0 });
405 assert!(pressed.get());
406
407 let second = source.press(Point { x: 3.0, y: 4.0 });
408 assert_ne!(first.id(), second.id());
409 source.release(first);
410 assert!(pressed.get());
411
412 source.cancel(second);
413 assert!(!pressed.get());
414 }
415
416 #[test]
417 fn interaction_source_ids_are_instance_owned() {
418 let composition = Composition::new(MemoryApplier::new());
419 let first = MutableInteractionSource::with_runtime(composition.runtime_handle());
420 let first_clone = first;
421 let second = MutableInteractionSource::with_runtime(composition.runtime_handle());
422
423 assert_eq!(first.id(), first_clone.id());
424 assert_ne!(first.id(), second.id());
425 assert_eq!(first.press(Point { x: 0.0, y: 0.0 }).id(), 1);
426 assert_eq!(first.press(Point { x: 1.0, y: 1.0 }).id(), 2);
427 assert_eq!(second.press(Point { x: 0.0, y: 0.0 }).id(), 1);
428 }
429
430 #[test]
431 fn collect_is_pressed_as_state_tracks_press_emissions() {
432 let composition = Composition::new(MemoryApplier::new());
433 let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
434 let pressed = collect_is_pressed_as_state(&source);
435
436 assert!(!pressed.get());
437
438 let press = PressInteractionPress {
439 id: 7,
440 press_position: Point { x: 4.0, y: 5.0 },
441 };
442 source.emit(Interaction::Press(PressInteraction::Press(press)));
443 assert!(pressed.get(), "pressed after a Press emission");
444
445 source.emit(Interaction::Press(PressInteraction::Release(
446 PressInteractionRelease { press },
447 )));
448 assert!(!pressed.get(), "released after a Release emission");
449
450 source.emit(Interaction::Press(PressInteraction::Press(press)));
451 assert!(pressed.get(), "pressed again after a new Press emission");
452
453 source.emit(Interaction::Press(PressInteraction::Cancel(
454 PressInteractionCancel { press },
455 )));
456 assert!(!pressed.get(), "released after a Cancel emission");
457 }
458
459 #[composable]
460 fn PressedReader(
461 observed: Rc<RefCell<Vec<bool>>>,
462 source_slot: Rc<RefCell<Option<MutableInteractionSource>>>,
463 ) {
464 let source = rememberMutableInteractionSource();
465 source_slot.borrow_mut().replace(source);
466 let pressed = collect_is_pressed_as_state(&source);
467 observed.borrow_mut().push(pressed.value());
468 }
469
470 #[test]
471 fn collect_is_pressed_as_state_recomposes_readers() {
472 let observed = Rc::new(RefCell::new(Vec::<bool>::new()));
473 let source_slot = Rc::new(RefCell::new(None::<MutableInteractionSource>));
474
475 let mut composition = {
476 let observed = Rc::clone(&observed);
477 let source_slot = Rc::clone(&source_slot);
478 crate::run_test_composition(move || {
479 PressedReader(Rc::clone(&observed), Rc::clone(&source_slot));
480 })
481 };
482
483 assert_eq!(observed.borrow().as_slice(), &[false]);
484
485 let source = *source_slot
486 .borrow()
487 .as_ref()
488 .expect("interaction source captured");
489 let press = composition.with_app_context(|| source.press(Point { x: 1.0, y: 1.0 }));
490 while composition
491 .process_invalid_scopes()
492 .expect("process press invalidation")
493 {}
494 assert_eq!(
495 observed.borrow().last(),
496 Some(&true),
497 "press emission should recompose readers with pressed=true"
498 );
499
500 composition.with_app_context(|| source.release(press));
501 while composition
502 .process_invalid_scopes()
503 .expect("process release invalidation")
504 {}
505 assert_eq!(
506 observed.borrow().last(),
507 Some(&false),
508 "release emission should recompose readers with pressed=false"
509 );
510 }
511
512 #[test]
513 fn interaction_source_exposes_latest_interaction() {
514 let composition = Composition::new(MemoryApplier::new());
515 let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
516 let last_interaction = source.collectLastInteractionAsState();
517
518 assert_eq!(last_interaction.get(), None);
519
520 let press = source.press(Point { x: 8.0, y: 12.0 });
521 assert_eq!(
522 last_interaction.get(),
523 Some(Interaction::Press(PressInteraction::Press(press)))
524 );
525 assert_eq!(press.press_position, Point { x: 8.0, y: 12.0 });
526
527 source.release(press);
528 assert_eq!(
529 last_interaction.get(),
530 Some(Interaction::Press(PressInteraction::Release(
531 PressInteractionRelease { press }
532 )))
533 );
534 }
535}