1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
//! Audio and Haptic Feedback -- Item 14
//!
//! OS-agnostic audio and haptic feedback abstractions.
//! Platform implementations are behind feature flags.
//!
//! # OS-agnostic design
//! The traits here use no platform-specific types. Platform backends
//! are selected via cfg flags in the renderer, not here.
use std::sync::Arc;
use std::sync::Mutex;
/// Audio engine trait for playing sounds and spatial audio.
///
/// Implementations are platform-specific:
/// - Desktop: rodio or cpal backend
/// - Web: Web Audio API via wasm-bindgen
pub trait AudioEngine: Send + Sync {
/// Play a named sound at the given volume (0.0 to 1.0).
fn play_sound(&self, name: &str, volume: f32);
/// Play a spatial sound at a 3D position relative to the listener.
fn play_spatial(&self, name: &str, position: [f32; 3], volume: f32);
/// Set the listener's position in 3D space for spatial audio.
fn set_listener_position(&self, _position: [f32; 3]) {}
/// Stop all currently playing sounds.
fn stop_all(&self) {}
/// Play an embedded audio buffer (e.g., from include_bytes!) at the given volume.
///
/// The data slice must contain a valid WAV, OGG, or other supported audio format.
/// Default implementation is a no-op; backends that support buffer playback
/// should override this method.
fn play_buffer(&self, _data: &[u8], _volume: f32) {}
}
/// No-op audio engine used when no audio backend is available.
pub struct NullAudioEngine;
impl AudioEngine for NullAudioEngine {
fn play_sound(&self, _name: &str, _volume: f32) {}
fn play_spatial(&self, _name: &str, _position: [f32; 3], _volume: f32) {}
fn set_listener_position(&self, _position: [f32; 3]) {}
fn stop_all(&self) {}
}
/// Haptic feedback engine trait.
///
/// Implementations are platform-specific:
/// - macOS: Core Haptics via objc2
/// - iOS: UIImpactFeedbackGenerator via objc2
/// - Web: Vibration API via wasm-bindgen
/// - Other: no-op
pub trait HapticEngine: Send + Sync {
/// Trigger an impact haptic with the given intensity.
fn impact(&self, _intensity: HapticIntensity) {}
/// Light tap for selection changes (e.g., picker wheel, toggle).
fn selection(&self) {}
/// Success notification haptic.
fn success(&self) {}
/// Warning notification haptic.
fn warning(&self) {}
/// Error notification haptic.
fn error(&self) {}
/// Visual micro-feedback tick -- subtle visual pulse for UI interactions.
///
/// Unlike haptic methods which trigger physical feedback, this triggers
/// a brief visual animation (e.g., a glow or scale pulse) synchronized
/// with the interaction. Intensity ranges from 0.0 (barely visible) to
/// 1.0 (strong). Default implementation is a no-op.
fn visual_tick(&self, _intensity: f32) {}
}
/// Haptic impact intensity levels.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HapticIntensity {
Light,
Medium,
Heavy,
}
/// No-op haptic engine used when no haptic backend is available.
pub struct NullHapticEngine;
impl HapticEngine for NullHapticEngine {
fn impact(&self, _intensity: HapticIntensity) {}
fn selection(&self) {}
fn success(&self) {}
fn warning(&self) {}
fn error(&self) {}
fn visual_tick(&self, _intensity: f32) {}
}
/// Named sound constants for the design system.
///
/// These are string identifiers for platform-loaded sounds.
pub mod sounds {
pub const CLICK: &str = "click";
pub const TOGGLE_ON: &str = "toggle_on";
pub const TOGGLE_OFF: &str = "toggle_off";
pub const SUCCESS: &str = "success";
pub const ERROR: &str = "error";
pub const WARNING: &str = "warning";
pub const SCRUB: &str = "scrub";
pub const SELECTION: &str = "selection";
/// Embedded WAV data for the navigation tick sound.
///
/// Play via `AudioEngine::play_buffer(sounds::NAVIGATION_TICK, 1.0)`.
pub const NAVIGATION_TICK: &[u8] = include_bytes!("../assets/sounds/nav_tick.wav");
/// Embedded WAV data for the success chime sound.
pub const SUCCESS_CHIME: &[u8] = include_bytes!("../assets/sounds/success_chime.wav");
/// Embedded WAV data for the warning tone sound.
pub const WARNING_TONE: &[u8] = include_bytes!("../assets/sounds/warning_tone.wav");
}
/// Global audio engine instance (wrapped in Mutex for runtime replacement).
static AUDIO_ENGINE: once_cell::sync::Lazy<Mutex<Arc<dyn AudioEngine>>> =
once_cell::sync::Lazy::new(|| Mutex::new(Arc::new(NullAudioEngine)));
/// Global haptic engine instance (wrapped in Mutex for runtime replacement).
static HAPTIC_ENGINE: once_cell::sync::Lazy<Mutex<Arc<dyn HapticEngine>>> =
once_cell::sync::Lazy::new(|| Mutex::new(Arc::new(NullHapticEngine)));
/// Set the global audio engine.
pub fn set_audio_engine(engine: Arc<dyn AudioEngine>) {
if let Ok(mut guard) = AUDIO_ENGINE.lock() {
*guard = engine;
}
}
/// Set the global haptic engine.
pub fn set_haptic_engine(engine: Arc<dyn HapticEngine>) {
if let Ok(mut guard) = HAPTIC_ENGINE.lock() {
*guard = engine;
}
}
/// Play a sound using the global audio engine.
pub fn play_sound(name: &str, volume: f32) {
if let Ok(guard) = AUDIO_ENGINE.lock() {
guard.play_sound(name, volume);
}
}
/// Trigger a haptic using the global haptic engine.
pub fn haptic_impact(intensity: HapticIntensity) {
if let Ok(guard) = HAPTIC_ENGINE.lock() {
guard.impact(intensity);
}
}
/// Trigger selection haptic.
pub fn haptic_selection() {
if let Ok(guard) = HAPTIC_ENGINE.lock() {
guard.selection();
}
}
/// Trigger success haptic.
pub fn haptic_success() {
if let Ok(guard) = HAPTIC_ENGINE.lock() {
guard.success();
}
}
/// Trigger error haptic.
pub fn haptic_error() {
if let Ok(guard) = HAPTIC_ENGINE.lock() {
guard.error();
}
}