Skip to main content

fission_shell_winit/
haptics.rs

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
15/// Host-side haptic feedback provider used by shell capability registration.
16pub trait HapticHost: Send + Sync + 'static {
17    /// Plays impact feedback with the requested strength.
18    fn impact(&self, request: HapticImpactRequest) -> Result<(), HapticError>;
19    /// Plays success, warning, or error notification feedback.
20    fn notification(&self, request: HapticNotificationRequest) -> Result<(), HapticError>;
21    /// Plays lightweight selection-change feedback.
22    fn selection(&self) -> Result<(), HapticError>;
23    /// Plays a bounded custom haptic pattern.
24    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}