1use std::{
16 cell::RefCell,
17 sync::{Arc, OnceLock},
18};
19
20use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
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 && index >= timings_ms.len()
175 {
176 return Err(HapticError::RepeatOutOfRange {
177 index,
178 len: timings_ms.len(),
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#[composable]
369pub fn ProvideHaptics(content: impl FnOnce()) {
370 let haptics = cranpose_core::remember(default_haptics).with(|state| state.clone());
371 let local = local_haptics();
372 CompositionLocalProvider(vec![local.provides(haptics)], move || {
373 content();
374 });
375}
376
377#[cfg(test)]
378#[path = "tests/haptics_tests.rs"]
379mod tests;