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