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