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