1use std::cell::Cell;
6
7use dom_struct::dom_struct;
8use embedder_traits::{DualRumbleEffectParams, EmbedderMsg, GamepadSupportedHapticEffects};
9use js::context::JSContext;
10use js::realm::CurrentRealm;
11use js::rust::MutableHandleValue;
12use script_bindings::cell::DomRefCell;
13use script_bindings::reflector::{Reflector, reflect_dom_object};
14use servo_base::generic_channel::GenericCallback;
15
16use crate::dom::bindings::codegen::Bindings::GamepadHapticActuatorBinding::{
17 GamepadEffectParameters, GamepadHapticActuatorMethods, GamepadHapticEffectType,
18};
19use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
20use crate::dom::bindings::error::Error;
21use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
22use crate::dom::bindings::reflector::DomGlobal;
23use crate::dom::bindings::root::DomRoot;
24use crate::dom::bindings::str::DOMString;
25use crate::dom::bindings::utils::to_frozen_array;
26use crate::dom::promise::{Promise, RootedPromise, TracedPromise};
27use crate::dom::window::Window;
28use crate::tasks::task_source::SendableTaskSource;
29
30struct HapticEffectListener {
31 task_source: SendableTaskSource,
32 context: Trusted<GamepadHapticActuator>,
33}
34
35impl HapticEffectListener {
36 fn handle_stopped(&self, stopped_successfully: bool) {
37 let context = self.context.clone();
38 self.task_source
39 .queue(task!(handle_haptic_effect_stopped: move |cx| {
40 let actuator = context.root();
41 actuator.handle_haptic_effect_stopped(cx, stopped_successfully);
42 }));
43 }
44
45 fn handle_completed(&self, completed_successfully: bool) {
46 let context = self.context.clone();
47 self.task_source
48 .queue(task!(handle_haptic_effect_completed: move |cx| {
49 let actuator = context.root();
50 actuator.handle_haptic_effect_completed(cx, completed_successfully);
51 }));
52 }
53}
54
55#[dom_struct]
57pub(crate) struct GamepadHapticActuator {
58 reflector_: Reflector,
59 gamepad_index: u32,
60 effects: Vec<GamepadHapticEffectType>,
62 playing_effect_promise: DomRefCell<Option<TracedPromise>>,
64 sequence_id: Cell<u32>,
69 effect_sequence_id: Cell<u32>,
71 reset_sequence_id: Cell<u32>,
73}
74
75impl GamepadHapticActuator {
76 fn new_inherited(
77 gamepad_index: u32,
78 supported_haptic_effects: GamepadSupportedHapticEffects,
79 ) -> GamepadHapticActuator {
80 let mut effects = vec![];
81 if supported_haptic_effects.supports_dual_rumble {
82 effects.push(GamepadHapticEffectType::Dual_rumble);
83 }
84 if supported_haptic_effects.supports_trigger_rumble {
85 effects.push(GamepadHapticEffectType::Trigger_rumble);
86 }
87 Self {
88 reflector_: Reflector::new(),
89 gamepad_index,
90 effects,
91 playing_effect_promise: DomRefCell::new(None),
92 sequence_id: Cell::new(0),
93 effect_sequence_id: Cell::new(0),
94 reset_sequence_id: Cell::new(0),
95 }
96 }
97
98 pub(crate) fn new(
99 cx: &mut JSContext,
100 window: &Window,
101 gamepad_index: u32,
102 supported_haptic_effects: GamepadSupportedHapticEffects,
103 ) -> DomRoot<GamepadHapticActuator> {
104 reflect_dom_object(
105 cx,
106 Box::new(GamepadHapticActuator::new_inherited(
107 gamepad_index,
108 supported_haptic_effects,
109 )),
110 window,
111 )
112 }
113}
114
115impl GamepadHapticActuatorMethods<crate::DomTypeHolder> for GamepadHapticActuator {
116 fn Effects(&self, cx: &mut JSContext, retval: MutableHandleValue) {
118 to_frozen_array(cx, self.effects.as_slice(), retval)
119 }
120
121 fn PlayEffect(
123 &self,
124 cx: &mut CurrentRealm,
125 type_: GamepadHapticEffectType,
126 params: &GamepadEffectParameters,
127 ) -> RootedPromise {
128 let playing_effect_promise = Promise::new_in_realm_rooted(cx);
129
130 match type_ {
132 GamepadHapticEffectType::Dual_rumble => {
134 if *params.strongMagnitude < 0.0 || *params.strongMagnitude > 1.0 {
135 playing_effect_promise.reject_error(
136 cx,
137 Error::Type(
138 c"Strong magnitude value is not within range of 0.0 to 1.0.".to_owned(),
139 ),
140 );
141 return playing_effect_promise;
142 } else if *params.weakMagnitude < 0.0 || *params.weakMagnitude > 1.0 {
143 playing_effect_promise.reject_error(
144 cx,
145 Error::Type(
146 c"Weak magnitude value is not within range of 0.0 to 1.0.".to_owned(),
147 ),
148 );
149 return playing_effect_promise;
150 }
151 },
152 GamepadHapticEffectType::Trigger_rumble => {
154 if *params.strongMagnitude < 0.0 || *params.strongMagnitude > 1.0 {
155 playing_effect_promise.reject_error(
156 cx,
157 Error::Type(
158 c"Strong magnitude value is not within range of 0.0 to 1.0.".to_owned(),
159 ),
160 );
161 return playing_effect_promise;
162 } else if *params.weakMagnitude < 0.0 || *params.weakMagnitude > 1.0 {
163 playing_effect_promise.reject_error(
164 cx,
165 Error::Type(
166 c"Weak magnitude value is not within range of 0.0 to 1.0.".to_owned(),
167 ),
168 );
169 return playing_effect_promise;
170 } else if *params.leftTrigger < 0.0 || *params.leftTrigger > 1.0 {
171 playing_effect_promise.reject_error(
172 cx,
173 Error::Type(
174 c"Left trigger value is not within range of 0.0 to 1.0.".to_owned(),
175 ),
176 );
177 return playing_effect_promise;
178 } else if *params.rightTrigger < 0.0 || *params.rightTrigger > 1.0 {
179 playing_effect_promise.reject_error(
180 cx,
181 Error::Type(
182 c"Right trigger value is not within range of 0.0 to 1.0.".to_owned(),
183 ),
184 );
185 return playing_effect_promise;
186 }
187 },
188 }
189
190 let document = self.global().as_window().Document();
191 if !document.is_fully_active() {
192 playing_effect_promise.reject_error(cx, Error::InvalidState(None));
193 }
194
195 self.sequence_id.set(self.sequence_id.get().wrapping_add(1));
196
197 let previous_promise = self
198 .playing_effect_promise
199 .borrow_mut()
200 .take()
201 .map(|promise| promise.root(cx));
202 if let Some(promise) = previous_promise {
203 let trusted_promise = TrustedPromise::from(&promise);
204 self.global().task_manager().gamepad_task_source().queue(
205 task!(preempt_promise: move |cx| {
206 let promise = trusted_promise.root(cx);
207 let message = DOMString::from_static("preempted");
208 promise.resolve_native(cx, &message);
209 }),
210 );
211 }
212
213 if !self.effects.contains(&type_) {
214 playing_effect_promise.reject_error(cx, Error::NotSupported(None));
215 return playing_effect_promise;
216 }
217
218 *self.playing_effect_promise.borrow_mut() = Some(playing_effect_promise.to_traced());
219 self.effect_sequence_id.set(self.sequence_id.get());
220
221 let context = Trusted::new(self);
222 let listener = HapticEffectListener {
223 task_source: self.global().task_manager().gamepad_task_source().into(),
224 context,
225 };
226
227 let callback = GenericCallback::new(move |message| match message {
228 Ok(msg) => listener.handle_completed(msg),
229 Err(error) => warn!("Error receiving a GamepadMsg: {error:?}"),
230 })
231 .expect("Could not create generic callback");
232
233 let params = DualRumbleEffectParams {
238 duration: params.duration as f64,
239 start_delay: params.startDelay as f64,
240 strong_magnitude: *params.strongMagnitude,
241 weak_magnitude: *params.weakMagnitude,
242 };
243 let event = EmbedderMsg::PlayGamepadHapticEffect(
244 document.webview_id(),
245 self.gamepad_index as usize,
246 embedder_traits::GamepadHapticEffectType::DualRumble(params),
247 callback,
248 );
249 self.global().as_window().send_to_embedder(event);
250
251 playing_effect_promise
252 }
253
254 fn Reset(&self, cx: &mut CurrentRealm) -> RootedPromise {
256 let promise = Promise::new_in_realm_rooted(cx);
257
258 let document = self.global().as_window().Document();
259 if !document.is_fully_active() {
260 promise.reject_error(cx, Error::InvalidState(None));
261 return promise;
262 }
263
264 self.sequence_id.set(self.sequence_id.get().wrapping_add(1));
265
266 let previous_promise = self
267 .playing_effect_promise
268 .borrow_mut()
269 .take()
270 .map(|promise| promise.root(cx));
271 if let Some(previous_promise) = previous_promise {
272 let trusted_promise = TrustedPromise::from(&previous_promise);
273 self.global().task_manager().gamepad_task_source().queue(
274 task!(preempt_promise: move |cx| {
275 let promise = trusted_promise.root(cx);
276 let message = DOMString::from_static("preempted");
277 promise.resolve_native(cx, &message);
278 }),
279 );
280 }
281
282 *self.playing_effect_promise.borrow_mut() = Some(promise.to_traced());
283
284 self.reset_sequence_id.set(self.sequence_id.get());
285
286 let context = Trusted::new(self);
287 let listener = HapticEffectListener {
288 task_source: self.global().task_manager().gamepad_task_source().into(),
289 context,
290 };
291
292 let callback = GenericCallback::new(move |message| match message {
293 Ok(success) => listener.handle_stopped(success),
294 Err(error) => warn!("Error receiving a GamepadMsg: {error:?}"),
295 })
296 .expect("Could not create callback");
297
298 let event = EmbedderMsg::StopGamepadHapticEffect(
299 document.webview_id(),
300 self.gamepad_index as usize,
301 callback,
302 );
303 self.global().as_window().send_to_embedder(event);
304
305 promise
306 }
307}
308
309impl GamepadHapticActuator {
310 fn handle_haptic_effect_completed(&self, cx: &mut JSContext, completed_successfully: bool) {
313 if self.effect_sequence_id.get() != self.sequence_id.get() || !completed_successfully {
314 return;
315 }
316 let playing_effect_promise = self
317 .playing_effect_promise
318 .borrow_mut()
319 .take()
320 .map(|promise| promise.root(cx));
321 if let Some(promise) = playing_effect_promise {
322 let message = DOMString::from_static("complete");
323 promise.resolve_native(cx, &message);
324 }
325 }
326
327 pub(crate) fn handle_haptic_effect_stopped(
330 &self,
331 cx: &mut JSContext,
332 stopped_successfully: bool,
333 ) {
334 if !stopped_successfully {
335 return;
336 }
337
338 let playing_effect_promise = self
339 .playing_effect_promise
340 .borrow_mut()
341 .take()
342 .map(|promise| promise.root(cx));
343
344 if let Some(promise) = playing_effect_promise {
345 let trusted_promise = TrustedPromise::from(&promise);
346 let sequence_id = self.sequence_id.get();
347 let reset_sequence_id = self.reset_sequence_id.get();
348 self.global().task_manager().gamepad_task_source().queue(
349 task!(complete_promise: move |cx| {
350 if sequence_id != reset_sequence_id {
351 warn!("Mismatched sequence/reset sequence ids: {} != {}", sequence_id, reset_sequence_id);
352 return;
353 }
354 let promise = trusted_promise.root(cx);
355 let message = DOMString::from_static("complete");
356 promise.resolve_native(cx, &message);
357 })
358 );
359 }
360 }
361
362 pub(crate) fn handle_visibility_change(&self) {
364 if self.playing_effect_promise.borrow().is_none() {
365 return;
366 }
367
368 let this = Trusted::new(self);
369 self.global().task_manager().gamepad_task_source().queue(
370 task!(stop_playing_effect: move |cx| {
371 let actuator = this.root();
372 let playing_effect_promise = actuator
373 .playing_effect_promise
374 .borrow_mut()
375 .take()
376 .map(|promise| promise.root(cx));
377 let Some(promise) = playing_effect_promise else {
378 return;
379 };
380 let message = DOMString::from_static("preempted");
381 promise.resolve_native(cx, &message);
382 }),
383 );
384
385 let callback = GenericCallback::new(|_| ()).expect("Could not create callback");
386
387 let document = self.global().as_window().Document();
388 let event = EmbedderMsg::StopGamepadHapticEffect(
389 document.webview_id(),
390 self.gamepad_index as usize,
391 callback,
392 );
393 self.global().as_window().send_to_embedder(event);
394 }
395}