1use std::{
16 cell::RefCell,
17 sync::{Arc, OnceLock},
18};
19
20use cranpose_core::{compositionLocalOfWithPolicy, CompositionLocal, CompositionLocalProvider};
21use cranpose_macros::composable;
22
23use crate::registry::ServiceRegistry;
24
25#[derive(Clone, Copy, PartialEq, Eq, Debug)]
27pub enum HapticFeedback {
28 ImpactLight,
30 ImpactMedium,
32 ImpactHeavy,
34 Selection,
36 Success,
38 Warning,
40 Error,
42}
43
44#[derive(Clone, Copy, PartialEq, Eq, Debug)]
50pub enum HapticEffect {
51 Click,
53 Tick,
55 DoubleClick,
57 HeavyClick,
59}
60
61impl HapticEffect {
62 pub fn closest_feedback(self) -> HapticFeedback {
65 match self {
66 HapticEffect::Tick => HapticFeedback::Selection,
67 HapticEffect::Click => HapticFeedback::ImpactLight,
68 HapticEffect::DoubleClick => HapticFeedback::ImpactMedium,
69 HapticEffect::HeavyClick => HapticFeedback::ImpactHeavy,
70 }
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
76pub enum HapticError {
77 #[error("waveform has {timings} timings and {amplitudes} amplitudes; they must match")]
79 LengthMismatch {
80 timings: usize,
82 amplitudes: usize,
84 },
85 #[error("waveform has no steps")]
87 Empty,
88 #[error("waveform has a total duration of zero")]
90 ZeroDuration,
91 #[error("repeat index {index} is out of range for a {len}-step waveform")]
93 RepeatOutOfRange {
94 index: usize,
96 len: usize,
98 },
99 #[error("waveform has {len} steps, more than the maximum of {max}")]
101 TooManySteps {
102 len: usize,
104 max: usize,
106 },
107}
108
109#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct HapticPattern {
121 timings_ms: Vec<u32>,
122 amplitudes: Vec<u8>,
123 repeat: Option<usize>,
124}
125
126impl HapticPattern {
127 pub const MAX_STEPS: usize = 512;
131
132 pub fn new(timings_ms: &[u32], amplitudes: &[u8]) -> Result<HapticPattern, HapticError> {
137 Self::build(timings_ms, amplitudes, None)
138 }
139
140 pub fn repeating(
143 timings_ms: &[u32],
144 amplitudes: &[u8],
145 repeat_index: usize,
146 ) -> Result<HapticPattern, HapticError> {
147 Self::build(timings_ms, amplitudes, Some(repeat_index))
148 }
149
150 fn build(
151 timings_ms: &[u32],
152 amplitudes: &[u8],
153 repeat: Option<usize>,
154 ) -> Result<HapticPattern, HapticError> {
155 if timings_ms.len() != amplitudes.len() {
156 return Err(HapticError::LengthMismatch {
157 timings: timings_ms.len(),
158 amplitudes: amplitudes.len(),
159 });
160 }
161 if timings_ms.is_empty() {
162 return Err(HapticError::Empty);
163 }
164 if timings_ms.len() > HapticPattern::MAX_STEPS {
165 return Err(HapticError::TooManySteps {
166 len: timings_ms.len(),
167 max: HapticPattern::MAX_STEPS,
168 });
169 }
170 if timings_ms.iter().all(|step| *step == 0) {
171 return Err(HapticError::ZeroDuration);
172 }
173 if let Some(index) = repeat {
174 if index >= timings_ms.len() {
175 return Err(HapticError::RepeatOutOfRange {
176 index,
177 len: timings_ms.len(),
178 });
179 }
180 }
181 Ok(HapticPattern {
182 timings_ms: timings_ms.to_vec(),
183 amplitudes: amplitudes.to_vec(),
184 repeat,
185 })
186 }
187
188 pub fn timings_ms(&self) -> &[u32] {
190 &self.timings_ms
191 }
192
193 pub fn amplitudes(&self) -> &[u8] {
195 &self.amplitudes
196 }
197
198 pub fn repeat(&self) -> Option<usize> {
200 self.repeat
201 }
202
203 pub fn len(&self) -> usize {
205 self.timings_ms.len()
206 }
207
208 pub fn is_empty(&self) -> bool {
210 false
211 }
212
213 pub fn total_duration_ms(&self) -> u32 {
215 self.timings_ms
216 .iter()
217 .fold(0u32, |sum, step| sum.saturating_add(*step))
218 }
219
220 pub fn peak_amplitude(&self) -> u8 {
223 self.amplitudes.iter().copied().max().unwrap_or(0)
224 }
225
226 pub fn closest_feedback(&self) -> HapticFeedback {
229 let peak = u32::from(self.peak_amplitude());
230 let duration = self.total_duration_ms();
231 if peak >= 200 || duration >= 120 {
232 HapticFeedback::ImpactHeavy
233 } else if peak >= 110 || duration >= 40 {
234 HapticFeedback::ImpactMedium
235 } else {
236 HapticFeedback::ImpactLight
237 }
238 }
239}
240
241pub trait Haptics: Send + Sync {
247 fn perform(&self, feedback: HapticFeedback);
249
250 fn vibrate(&self, duration_ms: u32, amplitude: u8) {
256 let feedback = if amplitude >= 200 || duration_ms >= 120 {
257 HapticFeedback::ImpactHeavy
258 } else if amplitude >= 110 || duration_ms >= 40 {
259 HapticFeedback::ImpactMedium
260 } else {
261 HapticFeedback::ImpactLight
262 };
263 self.perform(feedback);
264 }
265
266 fn play_pattern(&self, pattern: &HapticPattern) {
271 self.perform(pattern.closest_feedback());
272 }
273
274 fn perform_effect(&self, effect: HapticEffect) {
279 self.perform(effect.closest_feedback());
280 }
281
282 fn cancel(&self) {}
285
286 fn has_amplitude_control(&self) -> bool {
289 false
290 }
291}
292
293pub type HapticsRef = Arc<dyn Haptics>;
294
295struct NoopHaptics;
296
297impl Haptics for NoopHaptics {
298 fn perform(&self, _feedback: HapticFeedback) {}
299}
300
301static PLATFORM_HAPTICS: ServiceRegistry<dyn Haptics> = ServiceRegistry::new();
302static NOOP_HAPTICS: OnceLock<HapticsRef> = OnceLock::new();
303static DEFAULT_HAPTICS: OnceLock<HapticsRef> = OnceLock::new();
304
305struct PlatformHaptics;
306
307fn registered_haptics() -> HapticsRef {
308 PLATFORM_HAPTICS
309 .get_or_warn("haptics")
310 .unwrap_or_else(|| NOOP_HAPTICS.get_or_init(|| Arc::new(NoopHaptics)).clone())
311}
312
313impl Haptics for PlatformHaptics {
314 fn perform(&self, feedback: HapticFeedback) {
315 registered_haptics().perform(feedback);
316 }
317
318 fn vibrate(&self, duration_ms: u32, amplitude: u8) {
319 registered_haptics().vibrate(duration_ms, amplitude);
320 }
321
322 fn play_pattern(&self, pattern: &HapticPattern) {
323 registered_haptics().play_pattern(pattern);
324 }
325
326 fn perform_effect(&self, effect: HapticEffect) {
327 registered_haptics().perform_effect(effect);
328 }
329
330 fn cancel(&self) {
331 registered_haptics().cancel();
332 }
333
334 fn has_amplitude_control(&self) -> bool {
335 registered_haptics().has_amplitude_control()
336 }
337}
338
339pub fn set_platform_haptics(haptics: HapticsRef) {
341 PLATFORM_HAPTICS.set(haptics);
342}
343
344pub fn clear_platform_haptics() {
346 PLATFORM_HAPTICS.clear();
347}
348
349pub fn default_haptics() -> HapticsRef {
350 DEFAULT_HAPTICS
351 .get_or_init(|| Arc::new(PlatformHaptics))
352 .clone()
353}
354
355pub fn local_haptics() -> CompositionLocal<HapticsRef> {
356 thread_local! {
357 static LOCAL_HAPTICS: RefCell<Option<CompositionLocal<HapticsRef>>> = const { RefCell::new(None) };
358 }
359
360 LOCAL_HAPTICS.with(|cell| {
361 let mut local = cell.borrow_mut();
362 local
363 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_haptics, Arc::ptr_eq))
364 .clone()
365 })
366}
367
368#[allow(non_snake_case)]
369#[composable]
370pub fn ProvideHaptics(content: impl FnOnce()) {
371 let haptics = cranpose_core::remember(default_haptics).with(|state| state.clone());
372 let local = local_haptics();
373 CompositionLocalProvider(vec![local.provides(haptics)], move || {
374 content();
375 });
376}
377
378#[cfg(test)]
379mod tests {
380 use parking_lot::Mutex;
381
382 use super::*;
383 use crate::run_test_composition;
384
385 #[derive(Default)]
388 struct Rec {
389 events: Mutex<Vec<HapticFeedback>>,
390 }
391
392 impl Haptics for Rec {
393 fn perform(&self, feedback: HapticFeedback) {
394 self.events.lock().push(feedback);
395 }
396 }
397
398 #[test]
399 fn registered_haptics_receives_events() {
400 let _guard = crate::registry::test_service_guard();
401 clear_platform_haptics();
402 default_haptics().perform(HapticFeedback::Selection); struct Counter(Arc<Mutex<u32>>);
405 impl Haptics for Counter {
406 fn perform(&self, _f: HapticFeedback) {
407 *self.0.lock() += 1;
408 }
409 }
410 let count = Arc::new(Mutex::new(0));
411 set_platform_haptics(Arc::new(Counter(count.clone())));
412 default_haptics().perform(HapticFeedback::ImpactMedium);
413 assert_eq!(*count.lock(), 1);
414 clear_platform_haptics();
415 }
416
417 #[test]
418 fn noop_backend_answers_every_method_without_panicking() {
419 let _guard = crate::registry::test_service_guard();
420 clear_platform_haptics();
421 let haptics = default_haptics();
422 haptics.perform(HapticFeedback::Error);
423 haptics.vibrate(30, 128);
424 haptics.perform_effect(HapticEffect::DoubleClick);
425 haptics.play_pattern(&HapticPattern::new(&[0, 20, 10, 20], &[0, 255, 0, 120]).unwrap());
426 haptics.cancel();
427 assert!(!haptics.has_amplitude_control());
428 }
429
430 #[test]
431 fn waveform_rejects_length_mismatch_instead_of_panicking() {
432 assert_eq!(
433 HapticPattern::new(&[0, 20, 10], &[0, 255]),
434 Err(HapticError::LengthMismatch {
435 timings: 3,
436 amplitudes: 2
437 })
438 );
439 assert_eq!(
440 HapticPattern::repeating(&[0, 20], &[0, 255, 128], 0),
441 Err(HapticError::LengthMismatch {
442 timings: 2,
443 amplitudes: 3
444 })
445 );
446 }
447
448 #[test]
449 fn waveform_rejects_empty_zero_and_out_of_range_repeat() {
450 assert_eq!(HapticPattern::new(&[], &[]), Err(HapticError::Empty));
451 assert_eq!(
452 HapticPattern::new(&[0, 0, 0], &[0, 255, 0]),
453 Err(HapticError::ZeroDuration)
454 );
455 assert_eq!(
456 HapticPattern::repeating(&[0, 20], &[0, 255], 2),
457 Err(HapticError::RepeatOutOfRange { index: 2, len: 2 })
458 );
459 let long = vec![1u32; HapticPattern::MAX_STEPS + 1];
460 let amps = vec![1u8; HapticPattern::MAX_STEPS + 1];
461 assert_eq!(
462 HapticPattern::new(&long, &s),
463 Err(HapticError::TooManySteps {
464 len: HapticPattern::MAX_STEPS + 1,
465 max: HapticPattern::MAX_STEPS
466 })
467 );
468 }
469
470 #[test]
471 fn waveform_exposes_its_shape() {
472 let pattern = HapticPattern::repeating(&[0, 40, 30, 40], &[0, 200, 0, 90], 1)
473 .expect("valid waveform");
474 assert_eq!(pattern.timings_ms(), &[0, 40, 30, 40]);
475 assert_eq!(pattern.amplitudes(), &[0, 200, 0, 90]);
476 assert_eq!(pattern.repeat(), Some(1));
477 assert_eq!(pattern.len(), 4);
478 assert!(!pattern.is_empty());
479 assert_eq!(pattern.total_duration_ms(), 110);
480 assert_eq!(pattern.peak_amplitude(), 200);
481 assert_eq!(pattern.closest_feedback(), HapticFeedback::ImpactHeavy);
482
483 let light = HapticPattern::new(&[0, 8], &[0, 40]).expect("valid waveform");
484 assert_eq!(light.closest_feedback(), HapticFeedback::ImpactLight);
485 let medium = HapticPattern::new(&[0, 50], &[0, 120]).expect("valid waveform");
486 assert_eq!(medium.closest_feedback(), HapticFeedback::ImpactMedium);
487 assert_eq!(light.repeat(), None);
488 }
489
490 #[test]
491 fn total_duration_saturates_instead_of_overflowing() {
492 let pattern = HapticPattern::new(&[u32::MAX, u32::MAX], &[255, 255]).expect("valid");
493 assert_eq!(pattern.total_duration_ms(), u32::MAX);
494 }
495
496 #[test]
497 fn defaulted_methods_fall_back_to_perform() {
498 let backend = Arc::new(Rec::default());
499 let haptics: HapticsRef = backend.clone();
500
501 haptics.vibrate(10, 20);
502 haptics.vibrate(60, 20);
503 haptics.vibrate(10, 220);
504 haptics.perform_effect(HapticEffect::Tick);
505 haptics.perform_effect(HapticEffect::Click);
506 haptics.perform_effect(HapticEffect::DoubleClick);
507 haptics.perform_effect(HapticEffect::HeavyClick);
508 haptics.play_pattern(&HapticPattern::new(&[0, 200], &[0, 255]).unwrap());
509 haptics.cancel();
510
511 assert_eq!(
512 *backend.events.lock(),
513 vec![
514 HapticFeedback::ImpactLight,
515 HapticFeedback::ImpactMedium,
516 HapticFeedback::ImpactHeavy,
517 HapticFeedback::Selection,
518 HapticFeedback::ImpactLight,
519 HapticFeedback::ImpactMedium,
520 HapticFeedback::ImpactHeavy,
521 HapticFeedback::ImpactHeavy,
522 ]
523 );
524 }
525
526 #[test]
527 fn provide_haptics_publishes_the_platform_backend() {
528 let _guard = crate::registry::test_service_guard();
529 clear_platform_haptics();
530 let backend = Arc::new(Rec::default());
531 let haptics: HapticsRef = backend.clone();
532 set_platform_haptics(haptics);
533
534 run_test_composition(move || {
535 ProvideHaptics(|| {
536 local_haptics().current().perform(HapticFeedback::Success);
537 });
538 });
539
540 assert_eq!(*backend.events.lock(), vec![HapticFeedback::Success]);
541 clear_platform_haptics();
542 }
543}