1use std::sync::{Arc, OnceLock};
16
17use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
18use cranpose_macros::composable;
19
20use crate::registry::ServiceRegistry;
21
22#[derive(Clone, Copy, PartialEq, Eq, Debug)]
24pub enum HapticFeedback {
25 ImpactLight,
27 ImpactMedium,
29 ImpactHeavy,
31 Selection,
33 Success,
35 Warning,
37 Error,
39}
40
41#[derive(Clone, Copy, PartialEq, Eq, Debug)]
47pub enum HapticEffect {
48 Click,
50 Tick,
52 DoubleClick,
54 HeavyClick,
56}
57
58impl HapticEffect {
59 pub fn closest_feedback(self) -> HapticFeedback {
62 match self {
63 HapticEffect::Tick => HapticFeedback::Selection,
64 HapticEffect::Click => HapticFeedback::ImpactLight,
65 HapticEffect::DoubleClick => HapticFeedback::ImpactMedium,
66 HapticEffect::HeavyClick => HapticFeedback::ImpactHeavy,
67 }
68 }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
73pub enum HapticError {
74 #[error("waveform has {timings} timings and {amplitudes} amplitudes; they must match")]
76 LengthMismatch {
77 timings: usize,
79 amplitudes: usize,
81 },
82 #[error("waveform has no steps")]
84 Empty,
85 #[error("waveform has a total duration of zero")]
87 ZeroDuration,
88 #[error("repeat index {index} is out of range for a {len}-step waveform")]
90 RepeatOutOfRange {
91 index: usize,
93 len: usize,
95 },
96 #[error("waveform has {len} steps, more than the maximum of {max}")]
98 TooManySteps {
99 len: usize,
101 max: usize,
103 },
104}
105
106#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct HapticPattern {
118 timings_ms: Vec<u32>,
119 amplitudes: Vec<u8>,
120 repeat: Option<usize>,
121}
122
123impl HapticPattern {
124 pub const MAX_STEPS: usize = 512;
128
129 pub fn new(timings_ms: &[u32], amplitudes: &[u8]) -> Result<HapticPattern, HapticError> {
134 Self::build(timings_ms, amplitudes, None)
135 }
136
137 pub fn repeating(
140 timings_ms: &[u32],
141 amplitudes: &[u8],
142 repeat_index: usize,
143 ) -> Result<HapticPattern, HapticError> {
144 Self::build(timings_ms, amplitudes, Some(repeat_index))
145 }
146
147 fn build(
148 timings_ms: &[u32],
149 amplitudes: &[u8],
150 repeat: Option<usize>,
151 ) -> Result<HapticPattern, HapticError> {
152 if timings_ms.len() != amplitudes.len() {
153 return Err(HapticError::LengthMismatch {
154 timings: timings_ms.len(),
155 amplitudes: amplitudes.len(),
156 });
157 }
158 if timings_ms.is_empty() {
159 return Err(HapticError::Empty);
160 }
161 if timings_ms.len() > HapticPattern::MAX_STEPS {
162 return Err(HapticError::TooManySteps {
163 len: timings_ms.len(),
164 max: HapticPattern::MAX_STEPS,
165 });
166 }
167 if timings_ms.iter().all(|step| *step == 0) {
168 return Err(HapticError::ZeroDuration);
169 }
170 if let Some(index) = repeat
171 && index >= timings_ms.len()
172 {
173 return Err(HapticError::RepeatOutOfRange {
174 index,
175 len: timings_ms.len(),
176 });
177 }
178 Ok(HapticPattern {
179 timings_ms: timings_ms.to_vec(),
180 amplitudes: amplitudes.to_vec(),
181 repeat,
182 })
183 }
184
185 pub fn timings_ms(&self) -> &[u32] {
187 &self.timings_ms
188 }
189
190 pub fn amplitudes(&self) -> &[u8] {
192 &self.amplitudes
193 }
194
195 pub fn repeat(&self) -> Option<usize> {
197 self.repeat
198 }
199
200 pub fn len(&self) -> usize {
202 self.timings_ms.len()
203 }
204
205 pub fn is_empty(&self) -> bool {
207 false
208 }
209
210 pub fn total_duration_ms(&self) -> u32 {
212 self.timings_ms
213 .iter()
214 .fold(0u32, |sum, step| sum.saturating_add(*step))
215 }
216
217 pub fn peak_amplitude(&self) -> u8 {
220 self.amplitudes.iter().copied().max().unwrap_or(0)
221 }
222
223 pub fn closest_feedback(&self) -> HapticFeedback {
226 let peak = u32::from(self.peak_amplitude());
227 let duration = self.total_duration_ms();
228 if peak >= 200 || duration >= 120 {
229 HapticFeedback::ImpactHeavy
230 } else if peak >= 110 || duration >= 40 {
231 HapticFeedback::ImpactMedium
232 } else {
233 HapticFeedback::ImpactLight
234 }
235 }
236}
237
238pub trait Haptics: Send + Sync {
244 fn perform(&self, feedback: HapticFeedback);
246
247 fn vibrate(&self, duration_ms: u32, amplitude: u8) {
253 let feedback = if amplitude >= 200 || duration_ms >= 120 {
254 HapticFeedback::ImpactHeavy
255 } else if amplitude >= 110 || duration_ms >= 40 {
256 HapticFeedback::ImpactMedium
257 } else {
258 HapticFeedback::ImpactLight
259 };
260 self.perform(feedback);
261 }
262
263 fn play_pattern(&self, pattern: &HapticPattern) {
268 self.perform(pattern.closest_feedback());
269 }
270
271 fn perform_effect(&self, effect: HapticEffect) {
276 self.perform(effect.closest_feedback());
277 }
278
279 fn cancel(&self) {}
282
283 fn has_amplitude_control(&self) -> bool {
286 false
287 }
288}
289
290pub type HapticsRef = Arc<dyn Haptics>;
291
292struct NoopHaptics;
293
294impl Haptics for NoopHaptics {
295 fn perform(&self, _feedback: HapticFeedback) {}
296}
297
298static PLATFORM_HAPTICS: ServiceRegistry<dyn Haptics> = ServiceRegistry::new();
299static NOOP_HAPTICS: OnceLock<HapticsRef> = OnceLock::new();
300static DEFAULT_HAPTICS: OnceLock<HapticsRef> = OnceLock::new();
301
302struct PlatformHaptics;
303
304fn registered_haptics() -> HapticsRef {
305 PLATFORM_HAPTICS
306 .get_or_warn("haptics")
307 .unwrap_or_else(|| NOOP_HAPTICS.get_or_init(|| Arc::new(NoopHaptics)).clone())
308}
309
310impl Haptics for PlatformHaptics {
311 fn perform(&self, feedback: HapticFeedback) {
312 registered_haptics().perform(feedback);
313 }
314
315 fn vibrate(&self, duration_ms: u32, amplitude: u8) {
316 registered_haptics().vibrate(duration_ms, amplitude);
317 }
318
319 fn play_pattern(&self, pattern: &HapticPattern) {
320 registered_haptics().play_pattern(pattern);
321 }
322
323 fn perform_effect(&self, effect: HapticEffect) {
324 registered_haptics().perform_effect(effect);
325 }
326
327 fn cancel(&self) {
328 registered_haptics().cancel();
329 }
330
331 fn has_amplitude_control(&self) -> bool {
332 registered_haptics().has_amplitude_control()
333 }
334}
335
336pub fn set_platform_haptics(haptics: HapticsRef) {
338 PLATFORM_HAPTICS.set(haptics);
339}
340
341pub fn clear_platform_haptics() {
343 PLATFORM_HAPTICS.clear();
344}
345
346pub fn default_haptics() -> HapticsRef {
347 DEFAULT_HAPTICS
348 .get_or_init(|| Arc::new(PlatformHaptics))
349 .clone()
350}
351
352pub fn local_haptics() -> CompositionLocal<HapticsRef> {
353 crate::composition_locals::cached_local(
354 |locals| &locals.haptics,
355 || compositionLocalOfWithPolicy(default_haptics, Arc::ptr_eq),
356 )
357}
358
359#[composable]
360pub fn ProvideHaptics(content: impl FnOnce()) {
361 let haptics = cranpose_core::remember(default_haptics).with(|state| state.clone());
362 let local = local_haptics();
363 CompositionLocalProvider([local.provides(haptics)], move || {
364 content();
365 });
366}
367
368#[cfg(test)]
369#[path = "tests/haptics_tests.rs"]
370mod tests;