use std::{
cell::RefCell,
sync::{Arc, OnceLock},
};
use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
use cranpose_macros::composable;
use crate::registry::ServiceRegistry;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum HapticFeedback {
ImpactLight,
ImpactMedium,
ImpactHeavy,
Selection,
Success,
Warning,
Error,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum HapticEffect {
Click,
Tick,
DoubleClick,
HeavyClick,
}
impl HapticEffect {
pub fn closest_feedback(self) -> HapticFeedback {
match self {
HapticEffect::Tick => HapticFeedback::Selection,
HapticEffect::Click => HapticFeedback::ImpactLight,
HapticEffect::DoubleClick => HapticFeedback::ImpactMedium,
HapticEffect::HeavyClick => HapticFeedback::ImpactHeavy,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum HapticError {
#[error("waveform has {timings} timings and {amplitudes} amplitudes; they must match")]
LengthMismatch {
timings: usize,
amplitudes: usize,
},
#[error("waveform has no steps")]
Empty,
#[error("waveform has a total duration of zero")]
ZeroDuration,
#[error("repeat index {index} is out of range for a {len}-step waveform")]
RepeatOutOfRange {
index: usize,
len: usize,
},
#[error("waveform has {len} steps, more than the maximum of {max}")]
TooManySteps {
len: usize,
max: usize,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HapticPattern {
timings_ms: Vec<u32>,
amplitudes: Vec<u8>,
repeat: Option<usize>,
}
impl HapticPattern {
pub const MAX_STEPS: usize = 512;
pub fn new(timings_ms: &[u32], amplitudes: &[u8]) -> Result<HapticPattern, HapticError> {
Self::build(timings_ms, amplitudes, None)
}
pub fn repeating(
timings_ms: &[u32],
amplitudes: &[u8],
repeat_index: usize,
) -> Result<HapticPattern, HapticError> {
Self::build(timings_ms, amplitudes, Some(repeat_index))
}
fn build(
timings_ms: &[u32],
amplitudes: &[u8],
repeat: Option<usize>,
) -> Result<HapticPattern, HapticError> {
if timings_ms.len() != amplitudes.len() {
return Err(HapticError::LengthMismatch {
timings: timings_ms.len(),
amplitudes: amplitudes.len(),
});
}
if timings_ms.is_empty() {
return Err(HapticError::Empty);
}
if timings_ms.len() > HapticPattern::MAX_STEPS {
return Err(HapticError::TooManySteps {
len: timings_ms.len(),
max: HapticPattern::MAX_STEPS,
});
}
if timings_ms.iter().all(|step| *step == 0) {
return Err(HapticError::ZeroDuration);
}
if let Some(index) = repeat
&& index >= timings_ms.len()
{
return Err(HapticError::RepeatOutOfRange {
index,
len: timings_ms.len(),
});
}
Ok(HapticPattern {
timings_ms: timings_ms.to_vec(),
amplitudes: amplitudes.to_vec(),
repeat,
})
}
pub fn timings_ms(&self) -> &[u32] {
&self.timings_ms
}
pub fn amplitudes(&self) -> &[u8] {
&self.amplitudes
}
pub fn repeat(&self) -> Option<usize> {
self.repeat
}
pub fn len(&self) -> usize {
self.timings_ms.len()
}
pub fn is_empty(&self) -> bool {
false
}
pub fn total_duration_ms(&self) -> u32 {
self.timings_ms
.iter()
.fold(0u32, |sum, step| sum.saturating_add(*step))
}
pub fn peak_amplitude(&self) -> u8 {
self.amplitudes.iter().copied().max().unwrap_or(0)
}
pub fn closest_feedback(&self) -> HapticFeedback {
let peak = u32::from(self.peak_amplitude());
let duration = self.total_duration_ms();
if peak >= 200 || duration >= 120 {
HapticFeedback::ImpactHeavy
} else if peak >= 110 || duration >= 40 {
HapticFeedback::ImpactMedium
} else {
HapticFeedback::ImpactLight
}
}
}
pub trait Haptics: Send + Sync {
fn perform(&self, feedback: HapticFeedback);
fn vibrate(&self, duration_ms: u32, amplitude: u8) {
let feedback = if amplitude >= 200 || duration_ms >= 120 {
HapticFeedback::ImpactHeavy
} else if amplitude >= 110 || duration_ms >= 40 {
HapticFeedback::ImpactMedium
} else {
HapticFeedback::ImpactLight
};
self.perform(feedback);
}
fn play_pattern(&self, pattern: &HapticPattern) {
self.perform(pattern.closest_feedback());
}
fn perform_effect(&self, effect: HapticEffect) {
self.perform(effect.closest_feedback());
}
fn cancel(&self) {}
fn has_amplitude_control(&self) -> bool {
false
}
}
pub type HapticsRef = Arc<dyn Haptics>;
struct NoopHaptics;
impl Haptics for NoopHaptics {
fn perform(&self, _feedback: HapticFeedback) {}
}
static PLATFORM_HAPTICS: ServiceRegistry<dyn Haptics> = ServiceRegistry::new();
static NOOP_HAPTICS: OnceLock<HapticsRef> = OnceLock::new();
static DEFAULT_HAPTICS: OnceLock<HapticsRef> = OnceLock::new();
struct PlatformHaptics;
fn registered_haptics() -> HapticsRef {
PLATFORM_HAPTICS
.get_or_warn("haptics")
.unwrap_or_else(|| NOOP_HAPTICS.get_or_init(|| Arc::new(NoopHaptics)).clone())
}
impl Haptics for PlatformHaptics {
fn perform(&self, feedback: HapticFeedback) {
registered_haptics().perform(feedback);
}
fn vibrate(&self, duration_ms: u32, amplitude: u8) {
registered_haptics().vibrate(duration_ms, amplitude);
}
fn play_pattern(&self, pattern: &HapticPattern) {
registered_haptics().play_pattern(pattern);
}
fn perform_effect(&self, effect: HapticEffect) {
registered_haptics().perform_effect(effect);
}
fn cancel(&self) {
registered_haptics().cancel();
}
fn has_amplitude_control(&self) -> bool {
registered_haptics().has_amplitude_control()
}
}
pub fn set_platform_haptics(haptics: HapticsRef) {
PLATFORM_HAPTICS.set(haptics);
}
pub fn clear_platform_haptics() {
PLATFORM_HAPTICS.clear();
}
pub fn default_haptics() -> HapticsRef {
DEFAULT_HAPTICS
.get_or_init(|| Arc::new(PlatformHaptics))
.clone()
}
pub fn local_haptics() -> CompositionLocal<HapticsRef> {
thread_local! {
static LOCAL_HAPTICS: RefCell<Option<CompositionLocal<HapticsRef>>> = const { RefCell::new(None) };
}
LOCAL_HAPTICS.with(|cell| {
let mut local = cell.borrow_mut();
local
.get_or_insert_with(|| compositionLocalOfWithPolicy(default_haptics, Arc::ptr_eq))
.clone()
})
}
#[composable]
pub fn ProvideHaptics(content: impl FnOnce()) {
let haptics = cranpose_core::remember(default_haptics).with(|state| state.clone());
let local = local_haptics();
CompositionLocalProvider(vec![local.provides(haptics)], move || {
content();
});
}
#[cfg(test)]
#[path = "tests/haptics_tests.rs"]
mod tests;