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