1use fission_core::{
2 HapticError, HapticImpactRequest, HapticNotificationRequest, HapticPatternRequest,
3 HAPTIC_IMPACT, HAPTIC_NOTIFICATION, HAPTIC_PATTERN, HAPTIC_SELECTION,
4};
5use fission_shell::async_host::AsyncRegistry;
6use std::sync::{Arc, Mutex};
7
8#[cfg(any(target_os = "macos", target_os = "ios"))]
9use objc::{class, msg_send, sel, sel_impl};
10
11#[cfg(target_os = "ios")]
12#[link(name = "UIKit", kind = "framework")]
13extern "C" {}
14
15pub trait HapticHost: Send + Sync + 'static {
17 fn impact(&self, request: HapticImpactRequest) -> Result<(), HapticError>;
19 fn notification(&self, request: HapticNotificationRequest) -> Result<(), HapticError>;
21 fn selection(&self) -> Result<(), HapticError>;
23 fn pattern(&self, request: HapticPatternRequest) -> Result<(), HapticError>;
25}
26
27#[derive(Debug, Default)]
28pub struct UnsupportedHapticHost;
29
30#[derive(Debug, Default)]
31pub struct NativeHapticHost;
32
33pub(crate) fn native_haptic_host() -> NativeHapticHost {
34 NativeHapticHost
35}
36
37impl NativeHapticHost {
38 #[cfg(target_os = "macos")]
39 fn perform_macos_haptic(pattern: usize) -> Result<(), HapticError> {
40 unsafe {
41 let manager = class!(NSHapticFeedbackManager);
42 let performer: *mut objc::runtime::Object = msg_send![manager, defaultPerformer];
43 if performer.is_null() {
44 return Err(HapticError::unsupported("macos_haptic_feedback"));
45 }
46 let _: () = msg_send![
47 performer,
48 performFeedbackPattern: pattern
49 performanceTime: 0usize
50 ];
51 }
52 Ok(())
53 }
54
55 #[cfg(target_os = "ios")]
56 fn perform_ios_impact(style: isize) -> Result<(), HapticError> {
57 unsafe {
58 let class = class!(UIImpactFeedbackGenerator);
59 let generator: *mut objc::runtime::Object = msg_send![class, alloc];
60 let generator: *mut objc::runtime::Object = msg_send![generator, initWithStyle: style];
61 let _: () = msg_send![generator, prepare];
62 let _: () = msg_send![generator, impactOccurred];
63 }
64 Ok(())
65 }
66
67 #[cfg(target_os = "ios")]
68 fn perform_ios_selection() -> Result<(), HapticError> {
69 unsafe {
70 let class = class!(UISelectionFeedbackGenerator);
71 let generator: *mut objc::runtime::Object = msg_send![class, new];
72 let _: () = msg_send![generator, prepare];
73 let _: () = msg_send![generator, selectionChanged];
74 }
75 Ok(())
76 }
77
78 #[cfg(target_os = "ios")]
79 fn perform_ios_notification(kind: isize) -> Result<(), HapticError> {
80 unsafe {
81 let class = class!(UINotificationFeedbackGenerator);
82 let generator: *mut objc::runtime::Object = msg_send![class, new];
83 let _: () = msg_send![generator, prepare];
84 let _: () = msg_send![generator, notificationOccurred: kind];
85 }
86 Ok(())
87 }
88}
89
90impl HapticHost for NativeHapticHost {
91 fn impact(&self, request: HapticImpactRequest) -> Result<(), HapticError> {
92 #[cfg(target_os = "ios")]
93 {
94 let style = match request.style {
95 fission_core::HapticImpactStyle::Light | fission_core::HapticImpactStyle::Soft => 0,
96 fission_core::HapticImpactStyle::Medium => 1,
97 fission_core::HapticImpactStyle::Heavy | fission_core::HapticImpactStyle::Rigid => {
98 2
99 }
100 };
101 return Self::perform_ios_impact(style);
102 }
103
104 #[cfg(target_os = "macos")]
105 {
106 let pattern = match request.style {
107 fission_core::HapticImpactStyle::Light | fission_core::HapticImpactStyle::Soft => 1,
108 fission_core::HapticImpactStyle::Medium => 0,
109 fission_core::HapticImpactStyle::Heavy | fission_core::HapticImpactStyle::Rigid => {
110 2
111 }
112 };
113 return Self::perform_macos_haptic(pattern);
114 }
115
116 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
117 {
118 let _ = request;
119 Err(HapticError::unsupported("impact"))
120 }
121 }
122
123 fn notification(&self, request: HapticNotificationRequest) -> Result<(), HapticError> {
124 #[cfg(target_os = "ios")]
125 {
126 let kind = match request.kind {
127 fission_core::HapticNotificationKind::Success => 0,
128 fission_core::HapticNotificationKind::Warning => 1,
129 fission_core::HapticNotificationKind::Error => 2,
130 };
131 return Self::perform_ios_notification(kind);
132 }
133
134 #[cfg(target_os = "macos")]
135 {
136 let _ = request;
137 return Self::perform_macos_haptic(0);
138 }
139
140 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
141 {
142 let _ = request;
143 Err(HapticError::unsupported("notification"))
144 }
145 }
146
147 fn selection(&self) -> Result<(), HapticError> {
148 #[cfg(target_os = "ios")]
149 {
150 return Self::perform_ios_selection();
151 }
152
153 #[cfg(target_os = "macos")]
154 {
155 return Self::perform_macos_haptic(1);
156 }
157
158 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
159 {
160 Err(HapticError::unsupported("selection"))
161 }
162 }
163
164 fn pattern(&self, request: HapticPatternRequest) -> Result<(), HapticError> {
165 #[cfg(target_os = "ios")]
166 {
167 let _ = request;
168 return Self::perform_ios_impact(1);
169 }
170
171 #[cfg(target_os = "macos")]
172 {
173 let _ = request;
174 return Self::perform_macos_haptic(0);
175 }
176
177 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
178 {
179 let _ = request;
180 Err(HapticError::unsupported("pattern"))
181 }
182 }
183}
184
185impl HapticHost for UnsupportedHapticHost {
186 fn impact(&self, _request: HapticImpactRequest) -> Result<(), HapticError> {
187 Err(HapticError::unsupported("impact"))
188 }
189
190 fn notification(&self, _request: HapticNotificationRequest) -> Result<(), HapticError> {
191 Err(HapticError::unsupported("notification"))
192 }
193
194 fn selection(&self) -> Result<(), HapticError> {
195 Err(HapticError::unsupported("selection"))
196 }
197
198 fn pattern(&self, _request: HapticPatternRequest) -> Result<(), HapticError> {
199 Err(HapticError::unsupported("pattern"))
200 }
201}
202
203#[derive(Debug, Default)]
204pub struct MemoryHapticHost {
205 calls: Arc<Mutex<Vec<String>>>,
206}
207
208impl MemoryHapticHost {
209 pub fn calls(&self) -> Vec<String> {
210 self.calls
211 .lock()
212 .map(|calls| calls.clone())
213 .unwrap_or_default()
214 }
215}
216
217impl HapticHost for MemoryHapticHost {
218 fn impact(&self, _request: HapticImpactRequest) -> Result<(), HapticError> {
219 self.calls.lock().unwrap().push("impact".into());
220 Ok(())
221 }
222
223 fn notification(&self, _request: HapticNotificationRequest) -> Result<(), HapticError> {
224 self.calls.lock().unwrap().push("notification".into());
225 Ok(())
226 }
227
228 fn selection(&self) -> Result<(), HapticError> {
229 self.calls.lock().unwrap().push("selection".into());
230 Ok(())
231 }
232
233 fn pattern(&self, _request: HapticPatternRequest) -> Result<(), HapticError> {
234 self.calls.lock().unwrap().push("pattern".into());
235 Ok(())
236 }
237}
238
239pub(crate) fn register_haptic_capabilities(
240 async_registry: &mut AsyncRegistry,
241 host: Arc<dyn HapticHost>,
242) {
243 let impact_host = host.clone();
244 async_registry.register_operation_capability(HAPTIC_IMPACT, move |request, _| {
245 let host = impact_host.clone();
246 async move { host.impact(request) }
247 });
248
249 let notification_host = host.clone();
250 async_registry.register_operation_capability(HAPTIC_NOTIFICATION, move |request, _| {
251 let host = notification_host.clone();
252 async move { host.notification(request) }
253 });
254
255 let selection_host = host.clone();
256 async_registry.register_operation_capability(HAPTIC_SELECTION, move |(), _| {
257 let host = selection_host.clone();
258 async move { host.selection() }
259 });
260
261 async_registry.register_operation_capability(HAPTIC_PATTERN, move |request, _| {
262 let host = host.clone();
263 async move { host.pattern(request) }
264 });
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use fission_core::HapticImpactStyle;
271
272 #[test]
273 fn unsupported_host_reports_errors() {
274 let host = UnsupportedHapticHost;
275 assert!(host.selection().is_err());
276 }
277
278 #[test]
279 fn memory_host_records_calls() {
280 let host = MemoryHapticHost::default();
281 host.impact(HapticImpactRequest {
282 style: HapticImpactStyle::Heavy,
283 })
284 .unwrap();
285 host.selection().unwrap();
286 assert_eq!(host.calls(), vec!["impact", "selection"]);
287 }
288}