mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-09-21 08:48:57 +00:00
LibWeb: Implement the Gamepad API with SDL3
This commit is contained in:
parent
50dcd8fc85
commit
74e0483ea5
Notes:
github-actions[bot]
2025-09-01 19:11:57 +00:00
Author: https://github.com/Lubrsi
Commit: 74e0483ea5
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5902
Reviewed-by: https://github.com/gmta
Reviewed-by: https://github.com/trflynn89
36 changed files with 1848 additions and 50 deletions
|
@ -382,7 +382,11 @@ set(SOURCES
|
|||
FileAPI/FileList.cpp
|
||||
FileAPI/FileReader.cpp
|
||||
FileAPI/FileReaderSync.cpp
|
||||
Gamepad/EventNames.cpp
|
||||
Gamepad/Gamepad.cpp
|
||||
Gamepad/GamepadButton.cpp
|
||||
Gamepad/GamepadEvent.cpp
|
||||
Gamepad/GamepadHapticActuator.cpp
|
||||
Gamepad/NavigatorGamepad.cpp
|
||||
Geolocation/Geolocation.cpp
|
||||
Serial/Serial.cpp
|
||||
|
|
|
@ -4453,6 +4453,9 @@ bool Document::is_allowed_to_use_feature(PolicyControlledFeature feature) const
|
|||
case PolicyControlledFeature::EncryptedMedia:
|
||||
// FIXME: Implement allowlist for this.
|
||||
return true;
|
||||
case PolicyControlledFeature::Gamepad:
|
||||
// FIXME: Implement allowlist for this.
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. Return false.
|
||||
|
|
|
@ -160,6 +160,7 @@ enum class PolicyControlledFeature : u8 {
|
|||
Autoplay,
|
||||
EncryptedMedia,
|
||||
FocusWithoutUserActivation,
|
||||
Gamepad,
|
||||
};
|
||||
|
||||
class WEB_API Document
|
||||
|
|
|
@ -128,4 +128,8 @@ interface mixin WindowEventHandlers {
|
|||
attribute EventHandler onstorage;
|
||||
attribute EventHandler onunhandledrejection;
|
||||
attribute EventHandler onunload;
|
||||
|
||||
// https://w3c.github.io/gamepad/#extensions-to-the-windoweventhandlers-interface-mixin
|
||||
attribute EventHandler ongamepadconnected;
|
||||
attribute EventHandler ongamepaddisconnected;
|
||||
};
|
||||
|
|
|
@ -537,7 +537,11 @@ class FileList;
|
|||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
class NavigatorGamepadPartial;
|
||||
class Gamepad;
|
||||
class GamepadButton;
|
||||
class GamepadEvent;
|
||||
class GamepadHapticActuator;
|
||||
|
||||
}
|
||||
|
||||
|
|
16
Libraries/LibWeb/Gamepad/EventNames.cpp
Normal file
16
Libraries/LibWeb/Gamepad/EventNames.cpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/Gamepad/EventNames.h>
|
||||
|
||||
namespace Web::Gamepad::EventNames {
|
||||
|
||||
#define __ENUMERATE_GAMEPAD_EVENT(name) \
|
||||
FlyString name = #name##_fly_string;
|
||||
ENUMERATE_GAMEPAD_EVENTS
|
||||
#undef __ENUMERATE_GAMEPAD_EVENT
|
||||
|
||||
}
|
21
Libraries/LibWeb/Gamepad/EventNames.h
Normal file
21
Libraries/LibWeb/Gamepad/EventNames.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/FlyString.h>
|
||||
|
||||
namespace Web::Gamepad::EventNames {
|
||||
|
||||
#define ENUMERATE_GAMEPAD_EVENTS \
|
||||
__ENUMERATE_GAMEPAD_EVENT(gamepadconnected) \
|
||||
__ENUMERATE_GAMEPAD_EVENT(gamepaddisconnected)
|
||||
|
||||
#define __ENUMERATE_GAMEPAD_EVENT(name) extern FlyString name;
|
||||
ENUMERATE_GAMEPAD_EVENTS
|
||||
#undef __ENUMERATE_GAMEPAD_EVENT
|
||||
|
||||
}
|
|
@ -1,18 +1,146 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/Bindings/GamepadPrototype.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Gamepad/EventNames.h>
|
||||
#include <LibWeb/Gamepad/Gamepad.h>
|
||||
#include <LibWeb/Gamepad/GamepadButton.h>
|
||||
#include <LibWeb/Gamepad/GamepadEvent.h>
|
||||
#include <LibWeb/Gamepad/GamepadHapticActuator.h>
|
||||
#include <LibWeb/HTML/Navigator.h>
|
||||
#include <LibWeb/HTML/Scripting/Environments.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
Gamepad::Gamepad(JS::Realm& realm)
|
||||
: PlatformObject(realm)
|
||||
GC_DEFINE_ALLOCATOR(Gamepad);
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-standard-gamepad
|
||||
// Type Index Location
|
||||
// Button 0 Bottom button in right cluster
|
||||
// 1 Right button in right cluster
|
||||
// 2 Left button in right cluster
|
||||
// 3 Top button in right cluster
|
||||
// 4 Top left front button
|
||||
// 5 Top right front button
|
||||
// 6 Bottom left front button
|
||||
// 7 Bottom right front button
|
||||
// 8 Left button in center cluster
|
||||
// 9 Right button in center cluster
|
||||
// 10 Left stick pressed button
|
||||
// 11 Right stick pressed button
|
||||
// 12 Top button in left cluster
|
||||
// 13 Bottom button in left cluster
|
||||
// 14 Left button in left cluster
|
||||
// 15 Right button in left cluster
|
||||
// 16 Center button in center cluster
|
||||
static Array<Variant<SDL_GamepadButton, SDL_GamepadAxis, Empty>, 17> standard_gamepad_button_layout {
|
||||
SDL_GAMEPAD_BUTTON_SOUTH,
|
||||
SDL_GAMEPAD_BUTTON_EAST,
|
||||
SDL_GAMEPAD_BUTTON_WEST,
|
||||
SDL_GAMEPAD_BUTTON_NORTH,
|
||||
SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
|
||||
SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER,
|
||||
SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
|
||||
SDL_GAMEPAD_AXIS_RIGHT_TRIGGER,
|
||||
SDL_GAMEPAD_BUTTON_BACK,
|
||||
SDL_GAMEPAD_BUTTON_START,
|
||||
SDL_GAMEPAD_BUTTON_LEFT_STICK,
|
||||
SDL_GAMEPAD_BUTTON_RIGHT_STICK,
|
||||
SDL_GAMEPAD_BUTTON_DPAD_UP,
|
||||
SDL_GAMEPAD_BUTTON_DPAD_DOWN,
|
||||
SDL_GAMEPAD_BUTTON_DPAD_LEFT,
|
||||
SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
|
||||
SDL_GAMEPAD_BUTTON_GUIDE,
|
||||
};
|
||||
|
||||
static Array<SDL_GamepadButton, 11> non_standard_gamepad_button_layout {
|
||||
SDL_GAMEPAD_BUTTON_MISC1,
|
||||
SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1,
|
||||
SDL_GAMEPAD_BUTTON_LEFT_PADDLE1,
|
||||
SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2,
|
||||
SDL_GAMEPAD_BUTTON_LEFT_PADDLE2,
|
||||
SDL_GAMEPAD_BUTTON_TOUCHPAD,
|
||||
SDL_GAMEPAD_BUTTON_MISC2,
|
||||
SDL_GAMEPAD_BUTTON_MISC3,
|
||||
SDL_GAMEPAD_BUTTON_MISC4,
|
||||
SDL_GAMEPAD_BUTTON_MISC5,
|
||||
SDL_GAMEPAD_BUTTON_MISC6,
|
||||
};
|
||||
|
||||
// axes 0 Horizontal axis for left stick (negative left/positive right)
|
||||
// 1 Vertical axis for left stick (negative up/positive down)
|
||||
// 2 Horizontal axis for right stick (negative left/positive right)
|
||||
// 3 Vertical axis for right stick (negative up/positive down)
|
||||
static Array<SDL_GamepadAxis, 4> standard_gamepad_axes_layout {
|
||||
SDL_GAMEPAD_AXIS_LEFTX,
|
||||
SDL_GAMEPAD_AXIS_LEFTY,
|
||||
SDL_GAMEPAD_AXIS_RIGHTX,
|
||||
SDL_GAMEPAD_AXIS_RIGHTY,
|
||||
};
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-button-press-threshold
|
||||
// For buttons which do not have a digital switch to indicate a pure pressed or released state, the user
|
||||
// agent MUST choose a button press threshold to indicate the button as pressed when its value is above a
|
||||
// certain amount. If the platform API gives a recommended value, the user agent SHOULD use that. In other
|
||||
// cases, the user agent SHOULD choose some other reasonable value.
|
||||
static constexpr double ANALOG_BUTTON_PRESS_THRESHOLD = 0.1;
|
||||
|
||||
static constexpr double GAMEPAD_EXPOSURE_AXIS_THRESHOLD = 0.5;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-a-new-gamepad
|
||||
GC::Ref<Gamepad> Gamepad::create(JS::Realm& realm, SDL_JoystickID sdl_joystick_id)
|
||||
{
|
||||
// 1. Let gamepad be a newly created Gamepad instance:
|
||||
auto gamepad = realm.create<Gamepad>(realm, sdl_joystick_id);
|
||||
|
||||
// 1. Initialize gamepad's id attribute to an identification string for the gamepad.
|
||||
// FIXME: What is the encoding used by SDL?
|
||||
auto const* name = SDL_GetGamepadNameForID(sdl_joystick_id);
|
||||
if (name) {
|
||||
gamepad->m_id = Utf16String::from_utf8(StringView { name, strlen(name) });
|
||||
}
|
||||
|
||||
// 2. Initialize gamepad's index attribute to the result of selecting an unused gamepad index for gamepad.
|
||||
// https://w3c.github.io/gamepad/#dfn-selecting-an-unused-gamepad-index
|
||||
// 1. Let navigator be gamepad's relevant global object's Navigator object.
|
||||
// The rest of the steps are implemented in NavigatorGamepad.
|
||||
// NOTE: Gamepad is only exposed on Window.
|
||||
auto& window = as<HTML::Window>(HTML::relevant_global_object(gamepad));
|
||||
gamepad->m_index = window.navigator()->select_an_unused_gamepad_index({});
|
||||
|
||||
// 3. Initialize gamepad's mapping attribute to the result of selecting a mapping for the gamepad device.
|
||||
gamepad->select_a_mapping();
|
||||
|
||||
// 4. Set gamepad.[[connected]] to true.
|
||||
gamepad->m_connected = true;
|
||||
|
||||
// 5. Set gamepad.[[timestamp]] to the current high resolution time given gamepad's relevant global object.
|
||||
gamepad->m_timestamp = HighResolutionTime::current_high_resolution_time(HTML::relevant_global_object(gamepad));
|
||||
|
||||
// 6. Set gamepad.[[axes]] to the result of initializing axes for gamepad.
|
||||
gamepad->initialize_axes();
|
||||
|
||||
// 7. Set gamepad.[[buttons]] to the result of initializing buttons for gamepad.
|
||||
gamepad->initialize_buttons();
|
||||
|
||||
// 8. Set gamepad.[[vibrationActuator]] to the result of constructing a GamepadHapticActuator for gamepad.
|
||||
gamepad->m_vibration_actuator = GamepadHapticActuator::create(realm, gamepad);
|
||||
|
||||
// 2. Return gamepad.
|
||||
return gamepad;
|
||||
}
|
||||
|
||||
Gamepad::Gamepad(JS::Realm& realm, SDL_JoystickID sdl_joystick_id)
|
||||
: PlatformObject(realm)
|
||||
, m_sdl_joystick_id(sdl_joystick_id)
|
||||
{
|
||||
m_sdl_gamepad = SDL_OpenGamepad(m_sdl_joystick_id);
|
||||
}
|
||||
|
||||
void Gamepad::initialize(JS::Realm& realm)
|
||||
|
@ -21,4 +149,478 @@ void Gamepad::initialize(JS::Realm& realm)
|
|||
Base::initialize(realm);
|
||||
}
|
||||
|
||||
void Gamepad::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_buttons);
|
||||
visitor.visit(m_vibration_actuator);
|
||||
}
|
||||
|
||||
void Gamepad::finalize()
|
||||
{
|
||||
SDL_CloseGamepad(m_sdl_gamepad);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-initializing-axes
|
||||
void Gamepad::initialize_axes()
|
||||
{
|
||||
// 1. Let inputCount be the number of axis inputs exposed by the device represented by gamepad.
|
||||
Vector<SDL_GamepadAxis> inputs;
|
||||
|
||||
// 2. Set gamepad.[[axisMinimums]] to a list of unsigned long values with size equal to inputCount containing minimum logical values for each of the axis inputs.
|
||||
// 3. Set gamepad.[[axisMaximums]] to a list of unsigned long values with size equal to inputCount containing maximum logical values for each of the axis inputs.
|
||||
for (auto const standard_gamepad_axis : standard_gamepad_axes_layout) {
|
||||
if (SDL_GamepadHasAxis(m_sdl_gamepad, standard_gamepad_axis)) {
|
||||
inputs.append(standard_gamepad_axis);
|
||||
m_axis_minimums.append(SDL_JOYSTICK_AXIS_MIN);
|
||||
m_axis_maximums.append(SDL_JOYSTICK_AXIS_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Let unmappedInputList be an empty list.
|
||||
Vector<size_t> unmapped_input_list;
|
||||
|
||||
// 5. Let mappedIndexList be an empty list.
|
||||
Vector<size_t> mapped_index_list;
|
||||
|
||||
// 6. Let axesSize be 0.
|
||||
size_t axes_size = 0;
|
||||
|
||||
// 7. For each rawInputIndex of the range from 0 to inputCount − 1:
|
||||
for (size_t raw_input_index = 0; raw_input_index < inputs.size(); ++raw_input_index) {
|
||||
// 1. If the gamepad axis at index rawInputIndex represents a Standard Gamepad axis:
|
||||
auto const axis = inputs[raw_input_index];
|
||||
if (auto maybe_index = standard_gamepad_axes_layout.first_index_of(axis); maybe_index.has_value()) {
|
||||
// 1. Let canonicalIndex be the canonical index for the axis.
|
||||
auto canonical_index = maybe_index.value();
|
||||
|
||||
// 2. If mappedIndexList contains canonicalIndex, then append rawInputIndex to unmappedInputList.
|
||||
if (mapped_index_list.contains_slow(canonical_index)) {
|
||||
unmapped_input_list.append(raw_input_index);
|
||||
} else {
|
||||
// Otherwise:
|
||||
// 1. Set gamepad.[[axisMapping]][rawInputIndex] to canonicalIndex.
|
||||
m_axis_mapping.set(raw_input_index, canonical_index);
|
||||
|
||||
// 2. Append canonicalIndex to mappedIndexList.
|
||||
mapped_index_list.append(canonical_index);
|
||||
|
||||
// 3. If canonicalIndex + 1 is greater than axesSize, then set axesSize to canonicalIndex + 1.
|
||||
if (canonical_index + 1 > axes_size)
|
||||
axes_size = canonical_index + 1;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, append rawInputIndex to unmappedInputList.
|
||||
unmapped_input_list.append(raw_input_index);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Let axisIndex be 0.
|
||||
size_t axis_index = 0;
|
||||
|
||||
// 9. For each rawInputIndex of unmappedInputList:
|
||||
for (size_t raw_input_index : unmapped_input_list) {
|
||||
// 1. While mappedIndexList contains axisIndex:
|
||||
while (mapped_index_list.contains_slow(axis_index)) {
|
||||
// 1. Increment axisIndex.
|
||||
++axis_index;
|
||||
}
|
||||
|
||||
// 2. Set gamepad.[[axisMapping]][rawInputIndex] to axisIndex.
|
||||
m_axis_mapping.set(raw_input_index, axis_index);
|
||||
|
||||
// 3. Append axisIndex to mappedIndexList.
|
||||
mapped_index_list.append(axis_index);
|
||||
|
||||
// 4. If axisIndex + 1 is greater than axesSize, then set axesSize to axisIndex + 1.
|
||||
if (axis_index + 1 > axes_size)
|
||||
axes_size = axis_index + 1;
|
||||
}
|
||||
|
||||
// NOTE: Instead of returning a list, we can just directly update m_buttons.
|
||||
// 10. Let axes be an empty list.
|
||||
// 11. For each axisIndex of the range from 0 to axesSize − 1, append 0 to axes.
|
||||
// 12. Return axes.
|
||||
for (size_t final_axis_index = 0; final_axis_index < axes_size; ++final_axis_index)
|
||||
m_axes.append(0.0);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-initializing-buttons
|
||||
void Gamepad::initialize_buttons()
|
||||
{
|
||||
auto& realm = this->realm();
|
||||
|
||||
// 1. Let inputCount be the number of button inputs exposed by the device represented by gamepad.
|
||||
Vector<Variant<SDL_GamepadButton, SDL_GamepadAxis>> inputs;
|
||||
|
||||
// 2. Set gamepad.[[buttonMinimums]] to be a list of unsigned long values with size equal to inputCount containing minimum logical values for each of the button inputs.
|
||||
// 3. Set gamepad.[[buttonMaximums]] to be a list of unsigned long values with size equal to inputCount containing maximum logical values for each of the button inputs.
|
||||
for (auto const& standard_gamepad_button : standard_gamepad_button_layout) {
|
||||
standard_gamepad_button.visit(
|
||||
[this, &inputs](SDL_GamepadButton button) {
|
||||
if (SDL_GamepadHasButton(m_sdl_gamepad, button)) {
|
||||
inputs.append(button);
|
||||
|
||||
// Buttons are binary inputs with SDL.
|
||||
m_button_minimums.append(0);
|
||||
m_button_maximums.append(1);
|
||||
}
|
||||
},
|
||||
[this, &inputs](SDL_GamepadAxis axis) {
|
||||
if (SDL_GamepadHasAxis(m_sdl_gamepad, axis)) {
|
||||
inputs.append(axis);
|
||||
|
||||
// "Trigger axis values range from 0 (released) to SDL_JOYSTICK_AXIS_MAX (fully
|
||||
// pressed) when reported by SDL_GetGamepadAxis(). Note that this is not the
|
||||
// same range that will be reported by the lower-level SDL_GetJoystickAxis()."
|
||||
m_button_minimums.append(0);
|
||||
m_button_maximums.append(SDL_JOYSTICK_AXIS_MAX);
|
||||
}
|
||||
},
|
||||
[](Empty) {
|
||||
VERIFY_NOT_REACHED();
|
||||
});
|
||||
}
|
||||
|
||||
for (auto const non_standard_gamepad_button : non_standard_gamepad_button_layout) {
|
||||
if (SDL_GamepadHasButton(m_sdl_gamepad, non_standard_gamepad_button)) {
|
||||
inputs.append(non_standard_gamepad_button);
|
||||
|
||||
// Buttons are binary inputs with SDL.
|
||||
m_button_minimums.append(0);
|
||||
m_button_maximums.append(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Let unmappedInputList be an empty list.
|
||||
Vector<size_t> unmapped_input_list;
|
||||
|
||||
// 5. Let mappedIndexList be an empty list.
|
||||
Vector<size_t> mapped_index_list;
|
||||
|
||||
// 6. Let buttonsSize be 0.
|
||||
size_t buttons_size = 0;
|
||||
|
||||
// 7. For each rawInputIndex of the range from 0 to inputCount − 1:
|
||||
for (size_t raw_input_index = 0; raw_input_index < inputs.size(); ++raw_input_index) {
|
||||
auto const& input = inputs[raw_input_index];
|
||||
|
||||
// 1. If the gamepad button at index rawInputIndex represents a Standard Gamepad button:
|
||||
if (auto maybe_index = standard_gamepad_button_layout.first_index_of(input); maybe_index.has_value()) {
|
||||
// 1. Let canonicalIndex be the canonical index for the button.
|
||||
auto canonical_index = maybe_index.value();
|
||||
|
||||
// 2. If mappedIndexList contains canonicalIndex, then append rawInputIndex to unmappedInputList.
|
||||
if (mapped_index_list.contains_slow(canonical_index)) {
|
||||
unmapped_input_list.append(raw_input_index);
|
||||
} else {
|
||||
// Otherwise:
|
||||
// 1. Set gamepad.[[buttonMapping]][rawInputIndex] to canonicalIndex.
|
||||
m_button_mapping.set(raw_input_index, canonical_index);
|
||||
|
||||
// 2. Append canonicalIndex to mappedIndexList.
|
||||
mapped_index_list.append(canonical_index);
|
||||
|
||||
// 3. If canonicalIndex + 1 is greater than buttonsSize, then set buttonsSize to canonicalIndex + 1.
|
||||
if (canonical_index + 1 > buttons_size)
|
||||
buttons_size = canonical_index + 1;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, append rawInputIndex to unmappedInputList.
|
||||
unmapped_input_list.append(raw_input_index);
|
||||
}
|
||||
|
||||
// 2. Increment rawInputIndex.
|
||||
}
|
||||
|
||||
// 8. Let buttonIndex be 0.
|
||||
size_t button_index = 0;
|
||||
|
||||
// 9. For each rawInputIndex of unmappedInputList:
|
||||
for (size_t raw_input_index : unmapped_input_list) {
|
||||
// 1. While mappedIndexList contains buttonIndex:
|
||||
while (mapped_index_list.contains_slow(button_index)) {
|
||||
// 1. Increment buttonIndex.
|
||||
++button_index;
|
||||
}
|
||||
|
||||
// 2. Set gamepad.[[buttonMapping]][rawInputIndex] to buttonIndex.
|
||||
m_button_mapping.set(raw_input_index, button_index);
|
||||
|
||||
// 3. Append buttonIndex to mappedIndexList.
|
||||
mapped_index_list.append(button_index);
|
||||
|
||||
// 4. If buttonIndex + 1 is greater than buttonsSize, then set buttonsSize to buttonIndex + 1.
|
||||
if (button_index + 1 > buttons_size)
|
||||
buttons_size = button_index + 1;
|
||||
}
|
||||
|
||||
// NOTE: Instead of returning a list (and thus needing to use RootVector), we can just directly update m_buttons.
|
||||
// 10. Let buttons be an empty list.
|
||||
// 11. For each buttonIndex of the range from 0 to buttonsSize − 1, append a new GamepadButton to buttons.
|
||||
// 12. Return buttons.
|
||||
for (size_t final_button_index = 0; final_button_index < buttons_size; ++final_button_index) {
|
||||
auto gamepad_button = realm.create<GamepadButton>(realm);
|
||||
m_buttons.append(gamepad_button);
|
||||
}
|
||||
}
|
||||
|
||||
GC::Ref<GamepadHapticActuator> Gamepad::vibration_actuator() const
|
||||
{
|
||||
VERIFY(m_vibration_actuator);
|
||||
return *m_vibration_actuator;
|
||||
}
|
||||
|
||||
void Gamepad::set_connected(Badge<NavigatorGamepadPartial>, bool value)
|
||||
{
|
||||
m_connected = value;
|
||||
}
|
||||
|
||||
void Gamepad::set_exposed(Badge<NavigatorGamepadPartial>, bool value)
|
||||
{
|
||||
m_exposed = value;
|
||||
}
|
||||
|
||||
void Gamepad::set_timestamp(Badge<NavigatorGamepadPartial>, HighResolutionTime::DOMHighResTimeStamp value)
|
||||
{
|
||||
m_timestamp = value;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-selecting-a-mapping
|
||||
void Gamepad::select_a_mapping()
|
||||
{
|
||||
// 1. If the button and axis layout of the gamepad device corresponds with the Standard Gamepad layout, then
|
||||
// return "standard".
|
||||
// 2. Return "".
|
||||
for (auto const& standard_gamepad_button : standard_gamepad_button_layout) {
|
||||
bool has_standard_button = standard_gamepad_button.visit(
|
||||
[this](SDL_GamepadButton button) -> bool {
|
||||
return SDL_GamepadHasButton(m_sdl_gamepad, button);
|
||||
},
|
||||
[this](SDL_GamepadAxis axis) -> bool {
|
||||
return SDL_GamepadHasAxis(m_sdl_gamepad, axis);
|
||||
},
|
||||
[](Empty) -> bool {
|
||||
VERIFY_NOT_REACHED();
|
||||
});
|
||||
|
||||
if (!has_standard_button) {
|
||||
m_mapping = Bindings::GamepadMappingType::Empty;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const standard_gamepad_axis : standard_gamepad_axes_layout) {
|
||||
if (!SDL_GamepadHasAxis(m_sdl_gamepad, standard_gamepad_axis)) {
|
||||
m_mapping = Bindings::GamepadMappingType::Empty;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_mapping = Bindings::GamepadMappingType::Standard;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-map-and-normalize-axes
|
||||
void Gamepad::map_and_normalize_axes()
|
||||
{
|
||||
// 1. Let axisValues be a list of unsigned long values representing the most recent logical axis input values for
|
||||
// each axis input of the device represented by gamepad.
|
||||
// NOTE: While the Gamepad API internally uses u32 to represent raw axis values, SDL uses i16 for axes.
|
||||
Vector<i16> axis_values;
|
||||
for (auto const standard_gamepad_axis : standard_gamepad_axes_layout) {
|
||||
if (SDL_GamepadHasAxis(m_sdl_gamepad, standard_gamepad_axis))
|
||||
axis_values.append(SDL_GetGamepadAxis(m_sdl_gamepad, standard_gamepad_axis));
|
||||
}
|
||||
|
||||
// 2. Let maxRawAxisIndex be the size of axisValues − 1.
|
||||
// 3. For each rawAxisIndex of the range from 0 to maxRawAxisIndex:
|
||||
for (size_t raw_axis_index = 0; raw_axis_index < axis_values.size(); ++raw_axis_index) {
|
||||
// 1. Let mappedIndex be gamepad.[[axisMapping]][rawAxisIndex].
|
||||
auto mapped_index = m_axis_mapping.get(raw_axis_index).value();
|
||||
|
||||
// 2. Let logicalValue be axisValues[rawAxisIndex].
|
||||
auto logical_value = axis_values[raw_axis_index];
|
||||
|
||||
// 3. Let logicalMinimum be gamepad.[[axisMinimums]][rawAxisIndex].
|
||||
auto logical_minimum = m_axis_minimums[raw_axis_index];
|
||||
|
||||
// 4. Let logicalMaximum be gamepad.[[axisMaximums]][rawAxisIndex].
|
||||
auto logical_maximum = m_axis_maximums[raw_axis_index];
|
||||
|
||||
// 5. Let normalizedValue be 2 (logicalValue − logicalMinimum) / (logicalMaximum − logicalMinimum) − 1.
|
||||
double normalized_value = 2.0 * static_cast<double>(logical_value - logical_minimum) / static_cast<double>(logical_maximum - logical_minimum) - 1.0;
|
||||
|
||||
// 6. Set gamepad.[[axes]][axisIndex] to be normalizedValue.
|
||||
// FIXME: axisIndex should be mappedIndex.
|
||||
m_axes[mapped_index] = normalized_value;
|
||||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-map-and-normalize-buttons
|
||||
void Gamepad::map_and_normalize_buttons()
|
||||
{
|
||||
// 1. Let buttonValues be a list of unsigned long values representing the most recent logical button input values
|
||||
// for each button input of the device represented by gamepad.
|
||||
// NOTE: While the Gamepad API internally uses u32 to represent raw button values, SDL uses bool for buttons and
|
||||
// i16 for axes. The left and right triggers are buttons in the Gamepad API.
|
||||
Vector<i16> button_values;
|
||||
|
||||
for (auto const& standard_gamepad_button : standard_gamepad_button_layout) {
|
||||
standard_gamepad_button.visit(
|
||||
[this, &button_values](SDL_GamepadButton button) {
|
||||
if (SDL_GamepadHasButton(m_sdl_gamepad, button)) {
|
||||
bool button_pressed = SDL_GetGamepadButton(m_sdl_gamepad, button);
|
||||
button_values.append(button_pressed ? 1 : 0);
|
||||
}
|
||||
},
|
||||
[this, &button_values](SDL_GamepadAxis axis) {
|
||||
if (SDL_GamepadHasAxis(m_sdl_gamepad, axis))
|
||||
button_values.append(SDL_GetGamepadAxis(m_sdl_gamepad, axis));
|
||||
},
|
||||
[](Empty) {
|
||||
VERIFY_NOT_REACHED();
|
||||
});
|
||||
}
|
||||
|
||||
for (auto const non_standard_gamepad_button : non_standard_gamepad_button_layout) {
|
||||
if (SDL_GamepadHasButton(m_sdl_gamepad, non_standard_gamepad_button)) {
|
||||
bool button_pressed = SDL_GetGamepadButton(m_sdl_gamepad, non_standard_gamepad_button);
|
||||
button_values.append(button_pressed ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Let maxRawButtonIndex be the size of buttonValues − 1.
|
||||
// 3. For each rawButtonIndex of the range from 0 to maxRawButtonIndex:
|
||||
for (size_t raw_button_index = 0; raw_button_index < button_values.size(); ++raw_button_index) {
|
||||
// 1. Let mappedIndex be gamepad.[[buttonMapping]][rawButtonIndex].
|
||||
auto mapped_index = m_button_mapping.get(raw_button_index).value();
|
||||
|
||||
// 2. Let logicalValue be buttonValues[rawButtonIndex].
|
||||
auto logical_value = button_values[raw_button_index];
|
||||
|
||||
// 3. Let logicalMinimum be gamepad.[[buttonMinimums]][rawButtonIndex].
|
||||
auto logical_minimum = m_button_minimums[raw_button_index];
|
||||
|
||||
// 4. Let logicalMaximum be gamepad.[[buttonMaximums]][rawButtonIndex].
|
||||
auto logical_maximum = m_button_maximums[raw_button_index];
|
||||
|
||||
// 5. Let normalizedValue be (logicalValue − logicalMinimum) / (logicalMaximum − logicalMinimum).
|
||||
double value = static_cast<double>(logical_value - logical_minimum) / static_cast<double>(logical_maximum - logical_minimum);
|
||||
|
||||
// 6. Let button be gamepad.[[buttons]][mappedIndex].
|
||||
auto button = m_buttons[mapped_index];
|
||||
|
||||
// 7. Set button.[[value]] to normalizedValue.
|
||||
button->set_value({}, value);
|
||||
|
||||
// 8. If the button has a digital switch to indicate a pure pressed or released state, set button.[[pressed]]
|
||||
// to true if the button is pressed or false if it is not pressed.
|
||||
// Otherwise, set button.[[pressed]] to true if the value is above the button press threshold or false if
|
||||
// it is not above the threshold.
|
||||
if (logical_maximum == 1) {
|
||||
button->set_pressed({}, logical_value == 1);
|
||||
} else {
|
||||
button->set_pressed({}, value > ANALOG_BUTTON_PRESS_THRESHOLD);
|
||||
}
|
||||
|
||||
// 9. If the button is capable of detecting touch, set button.[[touched]] to true if the button is currently being touched.
|
||||
// Otherwise, set button.[[touched]] to button.[[pressed]].
|
||||
// FIXME: Support the PS4/PS5 controller which has a touchpad, which is a button that can be touched and not pressed in at the same time.
|
||||
button->set_touched({}, button->pressed());
|
||||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-update-gamepad-state
|
||||
void Gamepad::update_gamepad_state(Badge<NavigatorGamepadPartial>)
|
||||
{
|
||||
auto& realm = this->realm();
|
||||
|
||||
// 1. Let now be the current high resolution time given gamepad's relevant global object.
|
||||
auto& window = as<HTML::Window>(HTML::relevant_global_object(*this));
|
||||
auto now = HighResolutionTime::current_high_resolution_time(window);
|
||||
|
||||
// 2. Set gamepad.[[timestamp]] to now.
|
||||
m_timestamp = now;
|
||||
|
||||
// 3. Run the steps to map and normalize axes for gamepad.
|
||||
map_and_normalize_axes();
|
||||
|
||||
// 4. Run the steps to map and normalize buttons for gamepad.
|
||||
map_and_normalize_buttons();
|
||||
|
||||
// FIXME: 5. Run the steps to record touches for gamepad.
|
||||
|
||||
// 6. Let navigator be gamepad's relevant global object's Navigator object.
|
||||
auto navigator = window.navigator();
|
||||
|
||||
// 7. If navigator.[[hasGamepadGesture]] is false and gamepad contains a gamepad user gesture:
|
||||
if (!navigator->has_gamepad_gesture() && contains_gamepad_user_gesture()) {
|
||||
// 1. Set navigator.[[hasGamepadGesture]] to true.
|
||||
navigator->set_has_gamepad_gesture({}, true);
|
||||
|
||||
// 2. For each connectedGamepad of navigator.[[gamepads]]:
|
||||
for (auto connected_gamepad : navigator->gamepads({})) {
|
||||
// 1. If connectedGamepad is not equal to null:
|
||||
if (connected_gamepad) {
|
||||
// 1. Set connectedGamepad.[[exposed]] to true.
|
||||
connected_gamepad->m_exposed = true;
|
||||
|
||||
// 2. Set connectedGamepad.[[timestamp]] to now.
|
||||
connected_gamepad->m_timestamp = now;
|
||||
|
||||
// 3. Let document be gamepad's relevant global object's associated Document; otherwise null.
|
||||
auto& document = window.associated_document();
|
||||
|
||||
// 4. If document is not null and is fully active, then queue a global task on the gamepad task source
|
||||
// to fire an event named gamepadconnected at gamepad's relevant global object using GamepadEvent
|
||||
// with its gamepad attribute initialized to connectedGamepad.
|
||||
if (document.is_fully_active()) {
|
||||
auto gamepad_connected_event_init = GamepadEventInit {
|
||||
{
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
.composed = false,
|
||||
},
|
||||
connected_gamepad,
|
||||
};
|
||||
auto gamepad_connected_event = MUST(GamepadEvent::construct_impl(realm, EventNames::gamepadconnected, gamepad_connected_event_init));
|
||||
window.dispatch_event(gamepad_connected_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-gamepad-user-gesture
|
||||
bool Gamepad::contains_gamepad_user_gesture()
|
||||
{
|
||||
// A gamepad contains a gamepad user gesture if the current input state indicates that the user is currently
|
||||
// interacting with the gamepad. The user agent MUST provide an algorithm to check if the input state contains a
|
||||
// gamepad user gesture. For buttons that support a neutral default value and have reported a pressed value of
|
||||
// false at least once, a pressed value of true SHOULD be considered interaction. If a button does not support a
|
||||
// neutral default value (for example, a toggle switch), then a pressed value of true SHOULD NOT be considered
|
||||
// interaction. If a button has never reported a pressed value of false then it SHOULD NOT be considered
|
||||
// interaction. Axis movements SHOULD be considered interaction if the axis supports a neutral default value, the
|
||||
// current displacement from neutral is greater than a threshold chosen by the user agent, and the axis has
|
||||
// reported a value below the threshold at least once. If an axis does not support a neutral default value (for
|
||||
// example, an axis for a joystick that does not self-center), or an axis has never reported a value below the axis
|
||||
// gesture threshold, then the axis SHOULD NOT be considered when checking for interaction. The axis gesture
|
||||
// threshold SHOULD be large enough that random jitter is not considered interaction.
|
||||
|
||||
// NOTE: This roughly follows Chrome, where it exposes gamepads if a button is pressed (even if it's held across
|
||||
// a refresh) or an absolute axis is above 0.5.
|
||||
auto pressed_button = m_buttons.find_if([](GC::Ref<GamepadButton> gamepad_button) {
|
||||
return gamepad_button->pressed();
|
||||
});
|
||||
|
||||
if (!pressed_button.is_end())
|
||||
return true;
|
||||
|
||||
auto axis_above_threshold = m_axes.find_if([](double value) {
|
||||
return abs(value) > GAMEPAD_EXPOSURE_AXIS_THRESHOLD;
|
||||
});
|
||||
|
||||
return !axis_above_threshold.is_end();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,24 +1,156 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/Bindings/GamepadPrototype.h>
|
||||
#include <LibWeb/Bindings/PlatformObject.h>
|
||||
#include <LibWeb/HighResolutionTime/DOMHighResTimeStamp.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepad
|
||||
class Gamepad : public Bindings::PlatformObject {
|
||||
class Gamepad final : public Bindings::PlatformObject {
|
||||
WEB_PLATFORM_OBJECT(Gamepad, Bindings::PlatformObject);
|
||||
GC_DECLARE_ALLOCATOR(Gamepad);
|
||||
|
||||
public:
|
||||
static GC::Ref<Gamepad> create(JS::Realm&, SDL_JoystickID);
|
||||
|
||||
SDL_JoystickID sdl_joystick_id() const { return m_sdl_joystick_id; }
|
||||
SDL_Gamepad* sdl_gamepad() const { return m_sdl_gamepad; }
|
||||
|
||||
Utf16String const& id() const { return m_id; }
|
||||
|
||||
size_t index() const { return m_index; }
|
||||
|
||||
bool connected() const { return m_connected; }
|
||||
void set_connected(Badge<NavigatorGamepadPartial>, bool);
|
||||
|
||||
HighResolutionTime::DOMHighResTimeStamp timestamp() const { return m_timestamp; }
|
||||
void set_timestamp(Badge<NavigatorGamepadPartial>, HighResolutionTime::DOMHighResTimeStamp);
|
||||
|
||||
bool exposed() const { return m_exposed; }
|
||||
void set_exposed(Badge<NavigatorGamepadPartial>, bool);
|
||||
|
||||
Bindings::GamepadMappingType mapping() const { return m_mapping; }
|
||||
|
||||
Vector<double> const& axes() const { return m_axes; }
|
||||
Vector<GC::Ref<GamepadButton>> const& buttons() const { return m_buttons; }
|
||||
|
||||
GC::Ref<GamepadHapticActuator> vibration_actuator() const;
|
||||
|
||||
void update_gamepad_state(Badge<NavigatorGamepadPartial>);
|
||||
|
||||
private:
|
||||
explicit Gamepad(JS::Realm&);
|
||||
explicit Gamepad(JS::Realm&, SDL_JoystickID);
|
||||
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
virtual void finalize() override;
|
||||
|
||||
void select_a_mapping();
|
||||
void initialize_axes();
|
||||
void initialize_buttons();
|
||||
|
||||
void map_and_normalize_axes();
|
||||
void map_and_normalize_buttons();
|
||||
|
||||
bool contains_gamepad_user_gesture();
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepad-id
|
||||
// An identification string for the gamepad. This string identifies the brand or style of connected gamepad device.
|
||||
// The exact format of the id string is left unspecified. It is RECOMMENDED that the user agent select a string
|
||||
// that identifies the product without uniquely identifying the device. For example, a USB gamepad may be
|
||||
// identified by its idVendor and idProduct values. Unique identifiers like serial numbers or Bluetooth device
|
||||
// addresses MUST NOT be included in the id string.
|
||||
Utf16String m_id;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepad-index
|
||||
// The index of the gamepad in the Navigator. When multiple gamepads are connected to a user agent, indices MUST be
|
||||
// assigned on a first-come, first-serve basis, starting at zero. If a gamepad is disconnected, previously assigned
|
||||
// indices MUST NOT be reassigned to gamepads that continue to be connected. However, if a gamepad is disconnected,
|
||||
// and subsequently the same or a different gamepad is then connected, the lowest previously used index MUST be
|
||||
// reused.
|
||||
size_t m_index { 0 };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-connected
|
||||
// A flag indicating that the device is connected to the system
|
||||
bool m_connected { false };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-timestamp
|
||||
// The last time data for this Gamepad was updated
|
||||
HighResolutionTime::DOMHighResTimeStamp m_timestamp { 0.0 };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-axes
|
||||
// A sequence of double values representing the current state of axes exposed by this device.
|
||||
// https://w3c.github.io/gamepad/#dom-gamepad-axes
|
||||
// Array of values for all axes of the gamepad. All axis values MUST be linearly normalized to the range [-1 .. 1].
|
||||
// If the controller is perpendicular to the ground with the directional stick pointing up, -1 SHOULD correspond to
|
||||
// "forward" or "left", and 1 SHOULD correspond to "backward" or "right". Axes that are drawn from a 2D input
|
||||
// device SHOULD appear next to each other in the axes array, X then Y. It is RECOMMENDED that axes appear in
|
||||
// decreasing order of importance, such that element 0 and 1 typically represent the X and Y axis of a directional
|
||||
// stick. The same object MUST be returned until the user agent needs to return different values (or values in a
|
||||
// different order).
|
||||
// FIXME: Our current FrozenArray implementation only supports returning new objects everytime.
|
||||
Vector<double> m_axes;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-axismapping
|
||||
// Mapping from unmapped axis index to an index in the axes array
|
||||
HashMap<size_t, size_t> m_axis_mapping;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-axisminimums
|
||||
// A list containing the minimum logical value for each axis
|
||||
// NOTE: While the Gamepad API internally uses u32 to represent raw axis values, SDL uses i16 for axes.
|
||||
Vector<i16> m_axis_minimums;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-axismaximums
|
||||
// A list containing the maximum logical value for each axis
|
||||
// NOTE: While the Gamepad API internally uses u32 to represent raw axis values, SDL uses i16 for axes.
|
||||
Vector<i16> m_axis_maximums;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-buttons
|
||||
// A sequence of GamepadButton objects representing the current state of buttons exposed by this device
|
||||
// Array of button states for all buttons of the gamepad. It is RECOMMENDED that buttons appear in decreasing
|
||||
// importance such that the primary button, secondary button, tertiary button, and so on appear as elements 0, 1,
|
||||
// 2, ... in the buttons array. The same object MUST be returned until the user agent needs to return different
|
||||
// values (or values in a different order).
|
||||
// FIXME: Our current FrozenArray implementation only supports returning new objects everytime.
|
||||
Vector<GC::Ref<GamepadButton>> m_buttons;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-buttonmapping
|
||||
// Mapping from unmapped button index to an index in the buttons array
|
||||
HashMap<size_t, size_t> m_button_mapping;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-buttonminimums
|
||||
// A list containing the minimum logical value for each button.
|
||||
// NOTE: While the Gamepad API internally uses u32 to represent raw button values, SDL uses bool for buttons and
|
||||
// i16 for axes. The left and right triggers are buttons in the Gamepad API.
|
||||
Vector<i16> m_button_minimums;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-buttonmaximums
|
||||
// A list containing the maximum logical value for each button
|
||||
Vector<i16> m_button_maximums;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-exposed
|
||||
// A flag indicating that the Gamepad object has been exposed to script
|
||||
bool m_exposed { false };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-vibrationactuator
|
||||
GC::Ptr<GamepadHapticActuator> m_vibration_actuator;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepad-mapping
|
||||
// The mapping in use for this device. If the user agent has knowledge of the layout of the device, then it SHOULD
|
||||
// indicate that a mapping is in use by setting mapping to the corresponding GamepadMappingType value.
|
||||
Bindings::GamepadMappingType m_mapping { Bindings::GamepadMappingType::Standard };
|
||||
|
||||
SDL_JoystickID m_sdl_joystick_id { 0 };
|
||||
SDL_Gamepad* m_sdl_gamepad { nullptr };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
#import <HighResolutionTime/DOMHighResTimeStamp.idl>
|
||||
#import <Gamepad/GamepadButton.idl>
|
||||
#import <Gamepad/GamepadHapticActuator.idl>
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadmappingtype
|
||||
enum GamepadMappingType {
|
||||
"",
|
||||
"standard",
|
||||
"xr-standard",
|
||||
};
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepad
|
||||
[Exposed=Window]
|
||||
interface Gamepad {
|
||||
[FIXME] readonly attribute DOMString id;
|
||||
[FIXME] readonly attribute long index;
|
||||
[FIXME] readonly attribute boolean connected;
|
||||
[FIXME] readonly attribute DOMHighResTimeStamp timestamp;
|
||||
[FIXME] readonly attribute GamepadMappingType mapping;
|
||||
[FIXME] readonly attribute FrozenArray<double> axes;
|
||||
[FIXME] readonly attribute FrozenArray<GamepadButton> buttons;
|
||||
readonly attribute Utf16DOMString id;
|
||||
readonly attribute long index;
|
||||
readonly attribute boolean connected;
|
||||
readonly attribute DOMHighResTimeStamp timestamp;
|
||||
readonly attribute GamepadMappingType mapping;
|
||||
readonly attribute FrozenArray<double> axes;
|
||||
readonly attribute FrozenArray<GamepadButton> buttons;
|
||||
[FIXME] readonly attribute FrozenArray<GamepadTouch> touches;
|
||||
[FIXME, SameObject] readonly attribute GamepadHapticActuator vibrationActuator;
|
||||
[SameObject] readonly attribute GamepadHapticActuator vibrationActuator;
|
||||
};
|
||||
|
||||
// https://w3c.github.io/gamepad/#idl-def-navigator-partial-1
|
||||
|
|
43
Libraries/LibWeb/Gamepad/GamepadButton.cpp
Normal file
43
Libraries/LibWeb/Gamepad/GamepadButton.cpp
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/Bindings/GamepadButtonPrototype.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Gamepad/GamepadButton.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(GamepadButton);
|
||||
|
||||
GamepadButton::GamepadButton(JS::Realm& realm)
|
||||
: Bindings::PlatformObject(realm)
|
||||
{
|
||||
}
|
||||
|
||||
GamepadButton::~GamepadButton() = default;
|
||||
|
||||
void GamepadButton::initialize(JS::Realm& realm)
|
||||
{
|
||||
WEB_SET_PROTOTYPE_FOR_INTERFACE(GamepadButton);
|
||||
Base::initialize(realm);
|
||||
}
|
||||
|
||||
void GamepadButton::set_pressed(Badge<Gamepad>, bool value)
|
||||
{
|
||||
m_pressed = value;
|
||||
}
|
||||
|
||||
void GamepadButton::set_touched(Badge<Gamepad>, bool value)
|
||||
{
|
||||
m_touched = value;
|
||||
}
|
||||
|
||||
void GamepadButton::set_value(Badge<Gamepad>, double value)
|
||||
{
|
||||
m_value = value;
|
||||
}
|
||||
|
||||
}
|
47
Libraries/LibWeb/Gamepad/GamepadButton.h
Normal file
47
Libraries/LibWeb/Gamepad/GamepadButton.h
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/Bindings/PlatformObject.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
class GamepadButton final : public Bindings::PlatformObject {
|
||||
WEB_PLATFORM_OBJECT(GamepadButton, Bindings::PlatformObject);
|
||||
GC_DECLARE_ALLOCATOR(GamepadButton);
|
||||
|
||||
public:
|
||||
virtual ~GamepadButton() override;
|
||||
|
||||
bool pressed() const { return m_pressed; }
|
||||
bool touched() const { return m_touched; }
|
||||
double value() const { return m_value; }
|
||||
|
||||
void set_pressed(Badge<Gamepad>, bool);
|
||||
void set_touched(Badge<Gamepad>, bool);
|
||||
void set_value(Badge<Gamepad>, double);
|
||||
|
||||
private:
|
||||
GamepadButton(JS::Realm&);
|
||||
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-pressed
|
||||
// A flag indicating that the button is pressed
|
||||
bool m_pressed { false };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-touched
|
||||
// A flag indicating that the button is touched
|
||||
bool m_touched { false };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-value
|
||||
// A double representing the button value scaled to the range [0 .. 1]
|
||||
double m_value { 0.0 };
|
||||
};
|
||||
|
||||
}
|
7
Libraries/LibWeb/Gamepad/GamepadButton.idl
Normal file
7
Libraries/LibWeb/Gamepad/GamepadButton.idl
Normal file
|
@ -0,0 +1,7 @@
|
|||
// https://w3c.github.io/gamepad/#dom-gamepadbutton
|
||||
[Exposed=Window]
|
||||
interface GamepadButton {
|
||||
readonly attribute boolean pressed;
|
||||
readonly attribute boolean touched;
|
||||
readonly attribute double value;
|
||||
};
|
41
Libraries/LibWeb/Gamepad/GamepadEvent.cpp
Normal file
41
Libraries/LibWeb/Gamepad/GamepadEvent.cpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/Bindings/GamepadEventPrototype.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Gamepad/Gamepad.h>
|
||||
#include <LibWeb/Gamepad/GamepadEvent.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(GamepadEvent);
|
||||
|
||||
WebIDL::ExceptionOr<GC::Ref<GamepadEvent>> GamepadEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, GamepadEventInit const& gamepad_event_init)
|
||||
{
|
||||
return realm.create<GamepadEvent>(realm, event_name, gamepad_event_init);
|
||||
}
|
||||
|
||||
GamepadEvent::GamepadEvent(JS::Realm& realm, FlyString const& event_name, GamepadEventInit const& event_init)
|
||||
: DOM::Event(realm, event_name, event_init)
|
||||
, m_gamepad(*event_init.gamepad)
|
||||
{
|
||||
}
|
||||
|
||||
GamepadEvent::~GamepadEvent() = default;
|
||||
|
||||
void GamepadEvent::initialize(JS::Realm& realm)
|
||||
{
|
||||
WEB_SET_PROTOTYPE_FOR_INTERFACE(GamepadEvent);
|
||||
Base::initialize(realm);
|
||||
}
|
||||
|
||||
void GamepadEvent::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_gamepad);
|
||||
}
|
||||
|
||||
}
|
36
Libraries/LibWeb/Gamepad/GamepadEvent.h
Normal file
36
Libraries/LibWeb/Gamepad/GamepadEvent.h
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/DOM/Event.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
struct GamepadEventInit : public DOM::EventInit {
|
||||
GC::Root<Gamepad> gamepad;
|
||||
};
|
||||
|
||||
class GamepadEvent final : public DOM::Event {
|
||||
WEB_PLATFORM_OBJECT(GamepadEvent, DOM::Event);
|
||||
GC_DECLARE_ALLOCATOR(GamepadEvent);
|
||||
|
||||
public:
|
||||
[[nodiscard]] static WebIDL::ExceptionOr<GC::Ref<GamepadEvent>> construct_impl(JS::Realm&, FlyString const& event_name, GamepadEventInit const&);
|
||||
|
||||
virtual ~GamepadEvent() override;
|
||||
|
||||
GC::Ref<Gamepad> gamepad() const { return m_gamepad; }
|
||||
|
||||
private:
|
||||
GamepadEvent(JS::Realm&, FlyString const& event_name, GamepadEventInit const& event_init);
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
|
||||
GC::Ref<Gamepad> m_gamepad;
|
||||
};
|
||||
|
||||
}
|
14
Libraries/LibWeb/Gamepad/GamepadEvent.idl
Normal file
14
Libraries/LibWeb/Gamepad/GamepadEvent.idl
Normal file
|
@ -0,0 +1,14 @@
|
|||
#import <DOM/Event.idl>
|
||||
#import <Gamepad/Gamepad.idl>
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadevent
|
||||
[Exposed=Window]
|
||||
interface GamepadEvent : Event {
|
||||
constructor(DOMString type, GamepadEventInit eventInitDict);
|
||||
[SameObject] readonly attribute Gamepad gamepad;
|
||||
};
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadeventinit
|
||||
dictionary GamepadEventInit : EventInit {
|
||||
required Gamepad gamepad;
|
||||
};
|
378
Libraries/LibWeb/Gamepad/GamepadHapticActuator.cpp
Normal file
378
Libraries/LibWeb/Gamepad/GamepadHapticActuator.cpp
Normal file
|
@ -0,0 +1,378 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/DOM/DocumentObserver.h>
|
||||
#include <LibWeb/Gamepad/Gamepad.h>
|
||||
#include <LibWeb/Gamepad/GamepadHapticActuator.h>
|
||||
#include <LibWeb/HTML/Scripting/Environments.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||||
#include <LibWeb/Platform/Timer.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(GamepadHapticActuator);
|
||||
|
||||
// FIXME: What is a valid duration and startDelay? The spec doesn't define that.
|
||||
// Safari: clamps any duration above 5000ms to 5000ms and doesn't seem to clamp or reject any startDelay.
|
||||
// Chrome: rejects if duration + startDelay > 5000ms.
|
||||
// Firefox doesn't support vibration at the time of writing.
|
||||
static constexpr u64 MAX_VIBRATION_DURATION = 5000;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-constructing-a-gamepadhapticactuator
|
||||
GC::Ref<GamepadHapticActuator> GamepadHapticActuator::create(JS::Realm& realm, GC::Ref<Gamepad> gamepad)
|
||||
{
|
||||
auto& window = as<HTML::Window>(realm.global_object());
|
||||
auto document_became_hidden_observer = realm.create<DOM::DocumentObserver>(realm, window.associated_document());
|
||||
|
||||
// 1. Let gamepadHapticActuator be a newly created GamepadHapticActuator instance.
|
||||
auto gamepad_haptic_actuator = realm.create<GamepadHapticActuator>(realm, gamepad, document_became_hidden_observer);
|
||||
|
||||
// 2. Let supportedEffectsList be an empty list.
|
||||
Vector<Bindings::GamepadHapticEffectType> supported_effects_list;
|
||||
|
||||
// 3. For each enum value type of GamepadHapticEffectType, if the user agent can send a command to initiate effects
|
||||
// of that type on that actuator, append type to supportedEffectsList.
|
||||
SDL_PropertiesID sdl_gamepad_properties = SDL_GetGamepadProperties(gamepad->sdl_gamepad());
|
||||
|
||||
if (SDL_GetBooleanProperty(sdl_gamepad_properties, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, /* default_value= */ false))
|
||||
supported_effects_list.append(Bindings::GamepadHapticEffectType::DualRumble);
|
||||
|
||||
if (SDL_GetBooleanProperty(sdl_gamepad_properties, SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN, /* default_value= */ false))
|
||||
supported_effects_list.append(Bindings::GamepadHapticEffectType::TriggerRumble);
|
||||
|
||||
// 4. Set gamepadHapticActuator.[[effects]] to supportedEffectsList.
|
||||
gamepad_haptic_actuator->m_effects = move(supported_effects_list);
|
||||
|
||||
return gamepad_haptic_actuator;
|
||||
}
|
||||
|
||||
GamepadHapticActuator::GamepadHapticActuator(JS::Realm& realm, GC::Ref<Gamepad> gamepad, GC::Ref<DOM::DocumentObserver> document_became_hidden_observer)
|
||||
: Bindings::PlatformObject(realm)
|
||||
, m_gamepad(gamepad)
|
||||
, m_document_became_hidden_observer(document_became_hidden_observer)
|
||||
{
|
||||
m_document_became_hidden_observer->set_document_visibility_state_observer([this](HTML::VisibilityState visibility_state) {
|
||||
if (visibility_state == HTML::VisibilityState::Hidden)
|
||||
document_became_hidden();
|
||||
});
|
||||
}
|
||||
|
||||
GamepadHapticActuator::~GamepadHapticActuator() = default;
|
||||
|
||||
void GamepadHapticActuator::initialize(JS::Realm& realm)
|
||||
{
|
||||
WEB_SET_PROTOTYPE_FOR_INTERFACE(GamepadHapticActuator);
|
||||
Base::initialize(realm);
|
||||
}
|
||||
|
||||
void GamepadHapticActuator::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_gamepad);
|
||||
visitor.visit(m_document_became_hidden_observer);
|
||||
visitor.visit(m_playing_effect_promise);
|
||||
visitor.visit(m_playing_effect_timer);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-valid-effect
|
||||
static bool is_valid_effect(Bindings::GamepadHapticEffectType type, GamepadEffectParameters const& params)
|
||||
{
|
||||
// 1. Given the value of GamepadHapticEffectType type, switch on:
|
||||
// "dual-rumble"
|
||||
// If params does not describe a valid dual-rumble effect, return false.
|
||||
// "trigger-rumble"
|
||||
// If params does not describe a valid trigger-rumble effect, return false.
|
||||
// 2. Return true
|
||||
switch (type) {
|
||||
case Bindings::GamepadHapticEffectType::DualRumble:
|
||||
// https://w3c.github.io/gamepad/#dfn-valid-dual-rumble-effect
|
||||
// Given GamepadEffectParameters params, a valid dual-rumble effect must have a valid duration, a valid
|
||||
// startDelay, and both the strongMagnitude and the weakMagnitude must be in the range [0 .. 1].
|
||||
if (Checked<u64>::addition_would_overflow(params.duration, params.start_delay))
|
||||
return false;
|
||||
|
||||
if (params.duration + params.start_delay > MAX_VIBRATION_DURATION)
|
||||
return false;
|
||||
|
||||
if (params.strong_magnitude < 0.0 || params.strong_magnitude > 1.0)
|
||||
return false;
|
||||
|
||||
if (params.weak_magnitude < 0.0 || params.weak_magnitude > 1.0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
case Bindings::GamepadHapticEffectType::TriggerRumble:
|
||||
// https://w3c.github.io/gamepad/#dfn-valid-trigger-rumble-effect
|
||||
// Given GamepadEffectParameters params, a valid trigger-rumble effect must have a valid duration, a valid
|
||||
// startDelay, and the strongMagnitude, weakMagnitude, leftTrigger, and rightTrigger must be in the range
|
||||
// [0 .. 1].
|
||||
if (Checked<u64>::addition_would_overflow(params.duration, params.start_delay))
|
||||
return false;
|
||||
|
||||
if (params.duration + params.start_delay > MAX_VIBRATION_DURATION)
|
||||
return false;
|
||||
|
||||
if (params.strong_magnitude < 0.0 || params.strong_magnitude > 1.0)
|
||||
return false;
|
||||
|
||||
if (params.weak_magnitude < 0.0 || params.weak_magnitude > 1.0)
|
||||
return false;
|
||||
|
||||
if (params.left_trigger < 0.0 || params.left_trigger > 1.0)
|
||||
return false;
|
||||
|
||||
if (params.right_trigger < 0.0 || params.right_trigger > 1.0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadhapticactuator-playeffect
|
||||
GC::Ref<WebIDL::Promise> GamepadHapticActuator::play_effect(Bindings::GamepadHapticEffectType type, GamepadEffectParameters const& params)
|
||||
{
|
||||
auto& realm = this->realm();
|
||||
|
||||
// 1. If params does not describe a valid effect of type type, return a promise rejected with a TypeError.
|
||||
if (!is_valid_effect(type, params))
|
||||
return WebIDL::create_rejected_promise_from_exception(realm, WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Invalid effect"_string });
|
||||
|
||||
// 2. Let document be the current settings object's relevant global object's associated Document.
|
||||
auto& window = as<HTML::Window>(HTML::current_principal_settings_object().global_object());
|
||||
auto& document = window.associated_document();
|
||||
|
||||
// 3. If document is null or document is not fully active or document's visibility state is "hidden", return a
|
||||
// promise rejected with an "InvalidStateError" DOMException.
|
||||
if (!document.is_fully_active() || document.visibility_state_value() == HTML::VisibilityState::Hidden)
|
||||
return WebIDL::create_rejected_promise_from_exception(realm, WebIDL::InvalidStateError::create(realm, "Haptics are not allowed in a hidden document"_utf16));
|
||||
|
||||
// 4. If this.[[playingEffectPromise]] is not null:
|
||||
if (m_playing_effect_promise) {
|
||||
// 1. Let effectPromise be this.[[playingEffectPromise]].
|
||||
auto effect_promise = GC::Ref { *m_playing_effect_promise };
|
||||
|
||||
// 2. Set this.[[playingEffectPromise]] to null.
|
||||
m_playing_effect_promise = nullptr;
|
||||
clear_playing_effect_timers();
|
||||
|
||||
// 3. Queue a global task on the gamepad task source with the relevant global object of this to resolve
|
||||
// effectPromise with "preempted".
|
||||
HTML::queue_global_task(HTML::Task::Source::Gamepad, HTML::relevant_global_object(*this), GC::create_function(realm.heap(), [effect_promise, &realm] {
|
||||
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||||
|
||||
auto preempted_string = JS::PrimitiveString::create(realm.vm(), Bindings::idl_enum_to_string(Bindings::GamepadHapticsResult::Preempted));
|
||||
WebIDL::resolve_promise(realm, effect_promise, preempted_string);
|
||||
}));
|
||||
}
|
||||
|
||||
// 5. If this GamepadHapticActuator cannot play effects with type type, return a promise rejected with reason
|
||||
// NotSupportedError.
|
||||
// https://w3c.github.io/gamepad/#ref-for-dfn-play-effects-with-type-1
|
||||
// A GamepadHapticActuator can play effects with type type if type can be found in the [[effects]] list.
|
||||
if (!m_effects.contains_slow(type))
|
||||
return WebIDL::create_rejected_promise_from_exception(realm, WebIDL::NotSupportedError::create(realm, "Gamepad does not support this effect"_utf16));
|
||||
|
||||
// 6. Let [[playingEffectPromise]] be a new promise.
|
||||
m_playing_effect_promise = WebIDL::create_promise(realm);
|
||||
|
||||
// 7. Let playEffectTimestamp be the current high resolution time given the document's relevant global object.
|
||||
// NOTE: Unused.
|
||||
|
||||
// 8. Do the following steps in parallel:
|
||||
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm, type, params] {
|
||||
// 1. Issue a haptic effect to the actuator with type, params, and the playEffectTimestamp.
|
||||
issue_haptic_effect(type, params, GC::create_function(realm.heap(), [this, &realm] {
|
||||
// 2. When the effect completes, if this.[[playingEffectPromise]] is not null, queue a global task on the
|
||||
// gamepad task source with the relevant global object of this to run the following steps:
|
||||
if (m_playing_effect_promise) {
|
||||
HTML::queue_global_task(HTML::Task::Source::Gamepad, HTML::relevant_global_object(*this), GC::create_function(realm.heap(), [this, &realm] {
|
||||
// 1. If this.[[playingEffectPromise]] is null, abort these steps.
|
||||
if (!m_playing_effect_promise)
|
||||
return;
|
||||
|
||||
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||||
|
||||
// 2. Resolve this.[[playingEffectPromise]] with "complete".
|
||||
auto complete_string = JS::PrimitiveString::create(realm.vm(), Bindings::idl_enum_to_string(Bindings::GamepadHapticsResult::Complete));
|
||||
WebIDL::resolve_promise(realm, *m_playing_effect_promise, complete_string);
|
||||
|
||||
// 3. Set this.[[playingEffectPromise]] to null.
|
||||
m_playing_effect_promise = nullptr;
|
||||
clear_playing_effect_timers();
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}));
|
||||
|
||||
// 9. Return [[playingEffectPromise]].
|
||||
VERIFY(m_playing_effect_promise);
|
||||
return *m_playing_effect_promise;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadhapticactuator-reset
|
||||
GC::Ref<WebIDL::Promise> GamepadHapticActuator::reset()
|
||||
{
|
||||
auto& realm = this->realm();
|
||||
|
||||
// 1. Let document be the current settings object's relevant global object's associated Document.
|
||||
auto& window = as<HTML::Window>(HTML::current_principal_settings_object().global_object());
|
||||
auto& document = window.associated_document();
|
||||
|
||||
// 2. If document is null or document is not fully active or document's visibility state is "hidden", return a
|
||||
// promise rejected with an "InvalidStateError" DOMException.
|
||||
if (!document.is_fully_active() || document.visibility_state_value() == HTML::VisibilityState::Hidden)
|
||||
return WebIDL::create_rejected_promise_from_exception(realm, WebIDL::InvalidStateError::create(realm, "Haptics are not allowed in a hidden document"_utf16));
|
||||
|
||||
// 3. Let resetResultPromise be a new promise.
|
||||
auto reset_result_promise = WebIDL::create_promise(realm);
|
||||
|
||||
// 4. If this.[[playingEffectPromise]] is not null, do the following steps in parallel:
|
||||
if (m_playing_effect_promise) {
|
||||
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm, reset_result_promise] {
|
||||
// 1. Let effectPromise be this.[[playingEffectPromise]].
|
||||
auto effect_promise = m_playing_effect_promise;
|
||||
|
||||
// 2. Stop haptic effects on this's gamepad's actuator.
|
||||
bool stopped_all = stop_haptic_effects();
|
||||
|
||||
// 3. If the effect has been successfully stopped, do:
|
||||
if (stopped_all) {
|
||||
// 1. If effectPromise and this.[[playingEffectPromise]] are still the same,
|
||||
// set this.[[playingEffectPromise]] to null.
|
||||
if (effect_promise == m_playing_effect_promise)
|
||||
m_playing_effect_promise = nullptr;
|
||||
|
||||
// 2. Queue a global task on the gamepad task source with the relevant global object of this to resolve
|
||||
// effectPromise with "preempted".
|
||||
// AD-HOC: With doing this in parallel, there is a chance effect_promise is null. Don't try to resolve it
|
||||
// if so.
|
||||
if (effect_promise) {
|
||||
HTML::queue_global_task(HTML::Task::Source::Gamepad, HTML::relevant_global_object(*this), GC::create_function(realm.heap(), [&realm, effect_promise = GC::Ref { *effect_promise }] {
|
||||
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||||
|
||||
auto preempted_string = JS::PrimitiveString::create(realm.vm(), Bindings::idl_enum_to_string(Bindings::GamepadHapticsResult::Preempted));
|
||||
WebIDL::resolve_promise(realm, effect_promise, preempted_string);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Resolve resetResultPromise with "complete"
|
||||
auto complete_string = JS::PrimitiveString::create(realm.vm(), Bindings::idl_enum_to_string(Bindings::GamepadHapticsResult::Complete));
|
||||
WebIDL::resolve_promise(realm, reset_result_promise, complete_string);
|
||||
}));
|
||||
}
|
||||
|
||||
// 5. Return resetResultPromise.
|
||||
return reset_result_promise;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#handling-visibility-change
|
||||
void GamepadHapticActuator::document_became_hidden()
|
||||
{
|
||||
auto& realm = this->realm();
|
||||
|
||||
// When the document's visibility state becomes "hidden", run these steps for each GamepadHapticActuator actuator:
|
||||
// 1. If actuator.[[playingEffectPromise]] is null, abort these steps.
|
||||
if (!m_playing_effect_promise)
|
||||
return;
|
||||
|
||||
// 2. Queue a global task on the gamepad task source with the relevant global object of actuator to run the
|
||||
// following steps:
|
||||
auto& global = HTML::relevant_global_object(*this);
|
||||
HTML::queue_global_task(HTML::Task::Source::Gamepad, global, GC::create_function(global.heap(), [this, &realm] {
|
||||
// 1. If actuator.[[playingEffectPromise]] is null, abort these steps.
|
||||
if (!m_playing_effect_promise)
|
||||
return;
|
||||
|
||||
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||||
|
||||
// 2. Resolve actuator.[[playingEffectPromise]] with "preempted".
|
||||
auto preempted_string = JS::PrimitiveString::create(realm.vm(), Bindings::idl_enum_to_string(Bindings::GamepadHapticsResult::Preempted));
|
||||
WebIDL::resolve_promise(realm, *m_playing_effect_promise, preempted_string);
|
||||
|
||||
// 3. Set actuator.[[playingEffectPromise]] to null.
|
||||
m_playing_effect_promise = nullptr;
|
||||
clear_playing_effect_timers();
|
||||
}));
|
||||
|
||||
// 3. Stop haptic effects on actuator.
|
||||
stop_haptic_effects();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-issue-a-haptic-effect
|
||||
void GamepadHapticActuator::issue_haptic_effect(Bindings::GamepadHapticEffectType type, GamepadEffectParameters const& params, GC::Ref<GC::Function<void()>> on_complete)
|
||||
{
|
||||
auto& heap = this->heap();
|
||||
|
||||
// To issue a haptic effect on an actuator, the user agent MUST send a command to the device to render an effect
|
||||
// of type and try to make it use the provided params. The user agent SHOULD use the provided playEffectTimestamp
|
||||
// for more precise playback timing when params.startDelay is not 0.0. The user agent MAY modify the effect to
|
||||
// increase compatibility. For example, an effect intended for a rumble motor may be transformed into a
|
||||
// waveform-based effect for a device that supports waveform haptics but lacks rumble motors.
|
||||
m_playing_effect_timer = Platform::Timer::create_single_shot(heap, static_cast<int>(params.start_delay), GC::create_function(heap, [this, type, params, on_complete, &heap] {
|
||||
switch (type) {
|
||||
case Bindings::GamepadHapticEffectType::DualRumble:
|
||||
SDL_RumbleGamepad(m_gamepad->sdl_gamepad(), params.strong_magnitude * NumericLimits<u16>::max(), params.weak_magnitude * NumericLimits<u16>::max(), params.duration);
|
||||
break;
|
||||
case Bindings::GamepadHapticEffectType::TriggerRumble:
|
||||
SDL_RumbleGamepadTriggers(m_gamepad->sdl_gamepad(), params.left_trigger * NumericLimits<u16>::max(), params.right_trigger * NumericLimits<u16>::max(), params.duration);
|
||||
break;
|
||||
}
|
||||
|
||||
m_playing_effect_timer = Platform::Timer::create_single_shot(heap, params.duration, GC::create_function(heap, [on_complete] {
|
||||
on_complete->function()();
|
||||
}));
|
||||
|
||||
m_playing_effect_timer->start();
|
||||
}));
|
||||
|
||||
m_playing_effect_timer->start();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-stop-haptic-effects
|
||||
bool GamepadHapticActuator::stop_haptic_effects()
|
||||
{
|
||||
// To stop haptic effects on an actuator, the user agent MUST send a command to the device to abort any effects
|
||||
// currently being played. If a haptic effect was interrupted, the actuator SHOULD return to a motionless state
|
||||
// as quickly as possible.
|
||||
bool stopped_all = true;
|
||||
|
||||
// https://wiki.libsdl.org/SDL3/SDL_RumbleGamepad
|
||||
// "Each call to this function cancels any previous rumble effect, and calling it with 0 intensity stops any
|
||||
// rumbling."
|
||||
if (m_effects.contains_slow(Bindings::GamepadHapticEffectType::DualRumble)) {
|
||||
bool success = SDL_RumbleGamepad(m_gamepad->sdl_gamepad(), 0, 0, 0);
|
||||
if (!success)
|
||||
stopped_all = false;
|
||||
}
|
||||
|
||||
// https://wiki.libsdl.org/SDL3/SDL_RumbleGamepadTriggers
|
||||
// "Each call to this function cancels any previous trigger rumble effect, and calling it with 0 intensity stops
|
||||
// any rumbling."
|
||||
if (m_effects.contains_slow(Bindings::GamepadHapticEffectType::TriggerRumble)) {
|
||||
bool success = SDL_RumbleGamepadTriggers(m_gamepad->sdl_gamepad(), 0, 0, 0);
|
||||
if (!success)
|
||||
stopped_all = false;
|
||||
}
|
||||
|
||||
return stopped_all;
|
||||
}
|
||||
|
||||
void GamepadHapticActuator::clear_playing_effect_timers()
|
||||
{
|
||||
if (m_playing_effect_timer) {
|
||||
m_playing_effect_timer->stop();
|
||||
m_playing_effect_timer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
81
Libraries/LibWeb/Gamepad/GamepadHapticActuator.h
Normal file
81
Libraries/LibWeb/Gamepad/GamepadHapticActuator.h
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/Bindings/GamepadHapticActuatorPrototype.h>
|
||||
#include <LibWeb/Bindings/PlatformObject.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadeffectparameters
|
||||
struct GamepadEffectParameters {
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadeffectparameters-duration
|
||||
// duration sets the duration of the vibration effect in milliseconds.
|
||||
u64 duration { 0 };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadeffectparameters-startdelay
|
||||
// startDelay sets the duration of the delay after playEffect() is called until vibration is started, in
|
||||
// milliseconds. During the delay interval, the actuator SHOULD NOT vibrate.
|
||||
u64 start_delay { 0 };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadeffectparameters-strongmagnitude
|
||||
// The vibration magnitude for the low frequency rumble in a "dual-rumble" or "trigger-rumble" effect.
|
||||
double strong_magnitude { 0.0 };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadeffectparameters-weakmagnitude
|
||||
// The vibration magnitude for the high frequency rumble in a "dual-rumble" or "trigger-rumble" effect.
|
||||
double weak_magnitude { 0.0 };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadeffectparameters-lefttrigger
|
||||
// The vibration magnitude for the bottom left front button (canonical index 6) rumble in a "trigger-rumble"
|
||||
// effect.
|
||||
double left_trigger { 0.0 };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dom-gamepadeffectparameters-righttrigger
|
||||
// The vibration magnitude for the bottom right front button (canonical index 7) rumble in a "trigger-rumble"
|
||||
// effect.
|
||||
double right_trigger { 0.0 };
|
||||
};
|
||||
|
||||
class GamepadHapticActuator final : public Bindings::PlatformObject {
|
||||
WEB_PLATFORM_OBJECT(GamepadHapticActuator, Bindings::PlatformObject);
|
||||
GC_DECLARE_ALLOCATOR(GamepadHapticActuator);
|
||||
|
||||
public:
|
||||
static GC::Ref<GamepadHapticActuator> create(JS::Realm&, GC::Ref<Gamepad>);
|
||||
|
||||
virtual ~GamepadHapticActuator() override;
|
||||
|
||||
Vector<Bindings::GamepadHapticEffectType> const& effects() const { return m_effects; }
|
||||
|
||||
GC::Ref<WebIDL::Promise> play_effect(Bindings::GamepadHapticEffectType type, GamepadEffectParameters const& params);
|
||||
GC::Ref<WebIDL::Promise> reset();
|
||||
|
||||
private:
|
||||
GamepadHapticActuator(JS::Realm&, GC::Ref<Gamepad>, GC::Ref<DOM::DocumentObserver>);
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
|
||||
void document_became_hidden();
|
||||
void issue_haptic_effect(Bindings::GamepadHapticEffectType type, GamepadEffectParameters const& params, GC::Ref<GC::Function<void()>> on_complete);
|
||||
bool stop_haptic_effects();
|
||||
void clear_playing_effect_timers();
|
||||
|
||||
GC::Ref<Gamepad> m_gamepad;
|
||||
GC::Ref<DOM::DocumentObserver> m_document_became_hidden_observer;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-effects
|
||||
// Represents the effects supported by the actuator.
|
||||
Vector<Bindings::GamepadHapticEffectType> m_effects;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-playingeffectpromise
|
||||
// The Promise to play some effect, or null if no effect is playing.
|
||||
GC::Ptr<WebIDL::Promise> m_playing_effect_promise;
|
||||
GC::Ptr<Platform::Timer> m_playing_effect_timer;
|
||||
};
|
||||
|
||||
}
|
29
Libraries/LibWeb/Gamepad/GamepadHapticActuator.idl
Normal file
29
Libraries/LibWeb/Gamepad/GamepadHapticActuator.idl
Normal file
|
@ -0,0 +1,29 @@
|
|||
// https://w3c.github.io/gamepad/#gamepadhapticsresult-enum
|
||||
enum GamepadHapticsResult {
|
||||
"complete",
|
||||
"preempted"
|
||||
};
|
||||
|
||||
// https://w3c.github.io/gamepad/#gamepadhapticeffecttype-enum
|
||||
enum GamepadHapticEffectType {
|
||||
"dual-rumble",
|
||||
"trigger-rumble"
|
||||
};
|
||||
|
||||
// https://w3c.github.io/gamepad/#gamepadeffectparameters-dictionary
|
||||
dictionary GamepadEffectParameters {
|
||||
unsigned long long duration = 0;
|
||||
unsigned long long startDelay = 0;
|
||||
double strongMagnitude = 0.0;
|
||||
double weakMagnitude = 0.0;
|
||||
double leftTrigger = 0.0;
|
||||
double rightTrigger = 0.0;
|
||||
};
|
||||
|
||||
// https://w3c.github.io/gamepad/#gamepadhapticactuator-interface
|
||||
[Exposed=Window]
|
||||
interface GamepadHapticActuator {
|
||||
[SameObject] readonly attribute FrozenArray<GamepadHapticEffectType> effects;
|
||||
Promise<GamepadHapticsResult> playEffect(GamepadHapticEffectType type, optional GamepadEffectParameters params = {});
|
||||
Promise<GamepadHapticsResult> reset();
|
||||
};
|
|
@ -1,13 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/TypeCasts.h>
|
||||
#include <LibWeb/Gamepad/EventNames.h>
|
||||
#include <LibWeb/Gamepad/Gamepad.h>
|
||||
#include <LibWeb/Gamepad/GamepadEvent.h>
|
||||
#include <LibWeb/Gamepad/NavigatorGamepad.h>
|
||||
#include <LibWeb/HTML/Navigator.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
|
@ -15,35 +20,232 @@ namespace Web::Gamepad {
|
|||
WebIDL::ExceptionOr<GC::RootVector<GC::Ptr<Gamepad>>> NavigatorGamepadPartial::get_gamepads()
|
||||
{
|
||||
auto& navigator = as<HTML::Navigator>(*this);
|
||||
auto& realm = navigator.realm();
|
||||
auto& heap = realm.heap();
|
||||
|
||||
// FIXME: 1. Let doc be the current global object's associated Document.
|
||||
// 1. Let doc be the current global object's associated Document.
|
||||
auto& window = as<HTML::Window>(HTML::current_principal_global_object());
|
||||
auto& document = window.associated_document();
|
||||
|
||||
// FIXME: 2. If doc is null or doc is not fully active, then return an empty list.
|
||||
// 2. If doc is null or doc is not fully active, then return an empty list.
|
||||
GC::RootVector<GC::Ptr<Gamepad>> gamepads { heap };
|
||||
if (!document.is_fully_active())
|
||||
return gamepads;
|
||||
|
||||
// FIXME: 3. If doc is not allowed to use the "gamepad" permission, then throw a "SecurityError" DOMException and abort
|
||||
// these steps.
|
||||
// 3. If doc is not allowed to use the "gamepad" permission, then throw a "SecurityError" DOMException and abort these steps.
|
||||
if (!document.is_allowed_to_use_feature(DOM::PolicyControlledFeature::Gamepad))
|
||||
return WebIDL::SecurityError::create(realm, "Not allowed to use gamepads"_utf16);
|
||||
|
||||
// FIXME: 4. If this.[[hasGamepadGesture]] is false, then return an empty list.
|
||||
// 4. If this.[[hasGamepadGesture]] is false, then return an empty list.
|
||||
if (!m_has_gamepad_gesture)
|
||||
return gamepads;
|
||||
|
||||
// FIXME: 5. Let now be the current high resolution time given the current global object.
|
||||
// 5. Let now be the current high resolution time given the current global object.
|
||||
auto now = HighResolutionTime::current_high_resolution_time(window);
|
||||
|
||||
// FIXME: 6. Let gamepads be an empty list.
|
||||
// 6. Let gamepads be an empty list.
|
||||
// NOTE: Already done.
|
||||
|
||||
// FIXME: 7. For each gamepad of this.[[gamepads]]:
|
||||
{
|
||||
// FIXME: 1. If gamepad is not null and gamepad.[[exposed]] is false:
|
||||
if (false) {
|
||||
// FIXME: 1. Set gamepad.[[exposed]] to true.
|
||||
// 7. For each gamepad of this.[[gamepads]]:
|
||||
for (auto gamepad : m_gamepads) {
|
||||
// 1. If gamepad is not null and gamepad.[[exposed]] is false:
|
||||
if (gamepad && !gamepad->exposed()) {
|
||||
// 1. Set gamepad.[[exposed]] to true.
|
||||
gamepad->set_exposed({}, true);
|
||||
|
||||
// FIXME: 2. Set gamepad.[[timestamp]] to now.
|
||||
// 2. Set gamepad.[[timestamp]] to now.
|
||||
gamepad->set_timestamp({}, now);
|
||||
}
|
||||
|
||||
// FIXME: 2. Append gamepad to gamepads.
|
||||
// 2. Append gamepad to gamepads.
|
||||
gamepads.append(gamepad);
|
||||
}
|
||||
|
||||
// FIXME: 8. Return gamepads.
|
||||
dbgln("FIXME: Unimplemented NavigatorGamepadPartial::get_gamepads()");
|
||||
return GC::RootVector<GC::Ptr<Gamepad>> { navigator.heap() };
|
||||
// 8. Return gamepads.
|
||||
return gamepads;
|
||||
}
|
||||
|
||||
void NavigatorGamepadPartial::visit_edges(GC::Cell::Visitor& visitor)
|
||||
{
|
||||
visitor.visit(m_gamepads);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-selecting-an-unused-gamepad-index
|
||||
size_t NavigatorGamepadPartial::select_an_unused_gamepad_index(Badge<Gamepad>)
|
||||
{
|
||||
// 2. Let maxGamepadIndex be the size of navigator.[[gamepads]] − 1.
|
||||
// 3. For each gamepadIndex of the range from 0 to maxGamepadIndex:
|
||||
for (size_t gamepad_index = 0; gamepad_index < m_gamepads.size(); ++gamepad_index) {
|
||||
// 1. If navigator.[[gamepads]][gamepadIndex] is null, then return gamepadIndex.
|
||||
if (!m_gamepads[gamepad_index])
|
||||
return gamepad_index;
|
||||
}
|
||||
|
||||
// 4. Append null to navigator.[[gamepads]].
|
||||
m_gamepads.append(nullptr);
|
||||
|
||||
// 5. Return the size of navigator.[[gamepads]] − 1.
|
||||
return m_gamepads.size() - 1;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#event-gamepadconnected
|
||||
void NavigatorGamepadPartial::handle_gamepad_connected(SDL_JoystickID sdl_joystick_id)
|
||||
{
|
||||
// When a gamepad becomes available on the system, run the following steps:
|
||||
if (m_available_gamepads.contains_slow(sdl_joystick_id))
|
||||
return;
|
||||
|
||||
m_available_gamepads.append(sdl_joystick_id);
|
||||
|
||||
// 1. Let document be the current global object's associated Document; otherwise null.
|
||||
// FIXME: We can't use the current global object here, since it's not executing in a scripting context.
|
||||
// NOTE: NavigatorGamepad is only available on Window.
|
||||
// NOTE: document is never null.
|
||||
auto& navigator = as<HTML::Navigator>(*this);
|
||||
auto& realm = navigator.realm();
|
||||
auto& window = as<HTML::Window>(HTML::relevant_global_object(navigator));
|
||||
auto& document = window.associated_document();
|
||||
|
||||
// 2. If document is not null and is not allowed to use the "gamepad" permission, then abort these steps.
|
||||
if (!document.is_allowed_to_use_feature(DOM::PolicyControlledFeature::Gamepad))
|
||||
return;
|
||||
|
||||
// 3. Queue a global task on the gamepad task source with the current global object to perform the following steps:
|
||||
HTML::queue_global_task(HTML::Task::Source::Gamepad, window, GC::create_function(realm.heap(), [&realm, &document, sdl_joystick_id] mutable {
|
||||
// 1. Let gamepad be a new Gamepad representing the gamepad.
|
||||
auto gamepad = Gamepad::create(realm, sdl_joystick_id);
|
||||
|
||||
// 2. Let navigator be gamepad's relevant global object's Navigator object.
|
||||
auto& gamepad_window = as<HTML::Window>(HTML::relevant_global_object(gamepad));
|
||||
auto navigator = gamepad_window.navigator();
|
||||
|
||||
// 3. Set navigator.[[gamepads]][gamepad.index] to gamepad.
|
||||
navigator->m_gamepads[gamepad->index()] = gamepad;
|
||||
|
||||
// 4. If navigator.[[hasGamepadGesture]] is true:
|
||||
if (navigator->m_has_gamepad_gesture) {
|
||||
// 1. Set gamepad.[[exposed]] to true.
|
||||
gamepad->set_exposed({}, true);
|
||||
|
||||
// 2. If document is not null and is fully active, then fire an event named gamepadconnected at gamepad's
|
||||
// relevant global object using GamepadEvent with its gamepad attribute initialized to gamepad.
|
||||
if (document.is_fully_active()) {
|
||||
auto gamepad_connected_event_init = GamepadEventInit {
|
||||
{
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
.composed = false,
|
||||
},
|
||||
gamepad,
|
||||
};
|
||||
auto gamepad_connected_event = MUST(GamepadEvent::construct_impl(realm, EventNames::gamepadconnected, gamepad_connected_event_init));
|
||||
gamepad_window.dispatch_event(gamepad_connected_event);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-receives-new-button-or-axis-input-values
|
||||
void NavigatorGamepadPartial::handle_gamepad_updated(Badge<EventHandler>, SDL_JoystickID sdl_joystick_id)
|
||||
{
|
||||
// When the system receives new button or axis input values, run the following steps:
|
||||
// 1. Let gamepad be the Gamepad object representing the device that received new button or axis input values.
|
||||
auto gamepad = m_gamepads.find_if([&sdl_joystick_id](GC::Ptr<Gamepad> gamepad) {
|
||||
return gamepad && gamepad->sdl_joystick_id() == sdl_joystick_id;
|
||||
});
|
||||
|
||||
if (gamepad.is_end())
|
||||
return;
|
||||
|
||||
// 2. Queue a global task on the gamepad task source with gamepad's relevant global object to update gamepad state
|
||||
// for gamepad.
|
||||
auto& global = HTML::relevant_global_object(**gamepad);
|
||||
HTML::queue_global_task(HTML::Task::Source::Gamepad, global, GC::create_function(global.heap(), [gamepad = GC::Ref { **gamepad }] {
|
||||
gamepad->update_gamepad_state({});
|
||||
}));
|
||||
}
|
||||
|
||||
void NavigatorGamepadPartial::handle_gamepad_disconnected(Badge<EventHandler>, SDL_JoystickID sdl_joystick_id)
|
||||
{
|
||||
// When a gamepad becomes unavailable on the system, run the following steps:
|
||||
m_available_gamepads.remove_first_matching([&sdl_joystick_id](SDL_JoystickID available_gamepad) {
|
||||
return sdl_joystick_id == available_gamepad;
|
||||
});
|
||||
|
||||
// 1. Let gamepad be the Gamepad representing the unavailable device.
|
||||
auto gamepad = m_gamepads.find_if([&sdl_joystick_id](GC::Ptr<Gamepad> gamepad) {
|
||||
return gamepad && gamepad->sdl_joystick_id() == sdl_joystick_id;
|
||||
});
|
||||
|
||||
if (gamepad.is_end())
|
||||
return;
|
||||
|
||||
// 2. Queue a global task on the gamepad task source with gamepad's relevant global object to perform the
|
||||
// following steps:
|
||||
auto& window = as<HTML::Window>(HTML::relevant_global_object(**gamepad));
|
||||
HTML::queue_global_task(HTML::Task::Source::Gamepad, window, GC::create_function(window.heap(), [gamepad = GC::Ref { **gamepad }, &window] {
|
||||
// 1. Set gamepad.[[connected]] to false.
|
||||
gamepad->set_connected({}, false);
|
||||
|
||||
// 2. Let document be gamepad's relevant global object's associated Document; otherwise null.
|
||||
auto& document = window.associated_document();
|
||||
|
||||
// 3. If gamepad.[[exposed]] is true and document is not null and is fully active, then fire an event named
|
||||
// gamepaddisconnected at gamepad's relevant global object using GamepadEvent with its gamepad attribute
|
||||
// initialized to gamepad.
|
||||
if (gamepad->exposed() && document.is_fully_active()) {
|
||||
auto gamepad_disconnected_event_init = GamepadEventInit {
|
||||
{
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
.composed = false,
|
||||
},
|
||||
gamepad,
|
||||
};
|
||||
auto gamepad_disconnected_event = MUST(GamepadEvent::construct_impl(window.realm(), EventNames::gamepaddisconnected, gamepad_disconnected_event_init));
|
||||
window.dispatch_event(gamepad_disconnected_event);
|
||||
}
|
||||
|
||||
// 4. Let navigator be gamepad's relevant global object's Navigator object.
|
||||
auto navigator = window.navigator();
|
||||
|
||||
// 5. Set navigator.[[gamepads]][gamepad.index] to null.
|
||||
navigator->m_gamepads[gamepad->index()] = nullptr;
|
||||
|
||||
// 6. While navigator.[[gamepads]] is not empty and the last item of navigator.[[gamepads]] is null, remove the
|
||||
// last item of navigator.[[gamepads]].
|
||||
while (!navigator->m_gamepads.is_empty() && navigator->m_gamepads.last() == nullptr) {
|
||||
(void)navigator->m_gamepads.take_last();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void NavigatorGamepadPartial::check_for_connected_gamepads()
|
||||
{
|
||||
// "(SDL_JoystickID *) Returns a 0 terminated array of joystick instance IDs or NULL on failure; call
|
||||
// SDL_GetError() for more information. This should be freed with SDL_free() when it is no longer needed."
|
||||
int gamepad_count = 0;
|
||||
SDL_JoystickID* connected_gamepads = SDL_GetGamepads(&gamepad_count);
|
||||
if (!connected_gamepads)
|
||||
return;
|
||||
|
||||
for (int gamepad_index = 0; gamepad_index < gamepad_count; ++gamepad_index) {
|
||||
handle_gamepad_connected(connected_gamepads[gamepad_index]);
|
||||
}
|
||||
|
||||
SDL_free(connected_gamepads);
|
||||
}
|
||||
|
||||
void NavigatorGamepadPartial::set_has_gamepad_gesture(Badge<Gamepad>, bool value)
|
||||
{
|
||||
m_has_gamepad_gesture = value;
|
||||
}
|
||||
|
||||
GC::RootVector<GC::Ptr<Gamepad>> NavigatorGamepadPartial::gamepads(Badge<Gamepad>) const
|
||||
{
|
||||
auto& navigator = as<HTML::Navigator>(*this);
|
||||
auto& realm = navigator.realm();
|
||||
return { realm.heap(), m_gamepads };
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,16 +11,47 @@
|
|||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebIDL/ExceptionOr.h>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
namespace Web::Gamepad {
|
||||
|
||||
class NavigatorGamepadPartial {
|
||||
public:
|
||||
WebIDL::ExceptionOr<GC::RootVector<GC::Ptr<Gamepad>>> get_gamepads();
|
||||
|
||||
size_t select_an_unused_gamepad_index(Badge<Gamepad>);
|
||||
|
||||
void handle_gamepad_connected(SDL_JoystickID sdl_joystick_id);
|
||||
void handle_gamepad_updated(Badge<EventHandler>, SDL_JoystickID sdl_joystick_id);
|
||||
void handle_gamepad_disconnected(Badge<EventHandler>, SDL_JoystickID sdl_joystick_id);
|
||||
|
||||
bool has_gamepad_gesture() const { return m_has_gamepad_gesture; }
|
||||
void set_has_gamepad_gesture(Badge<Gamepad>, bool);
|
||||
|
||||
GC::RootVector<GC::Ptr<Gamepad>> gamepads(Badge<Gamepad>) const;
|
||||
|
||||
protected:
|
||||
void visit_edges(GC::Cell::Visitor& visitor);
|
||||
|
||||
void check_for_connected_gamepads();
|
||||
|
||||
private:
|
||||
virtual ~NavigatorGamepadPartial() = default;
|
||||
|
||||
friend class HTML::Navigator;
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-hasgamepadgesture
|
||||
// A flag indicating that a gamepad user gesture has been observed
|
||||
bool m_has_gamepad_gesture { false };
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-gamepads
|
||||
// Each Gamepad present at the index specified by its index attribute, or null for unassigned indices.
|
||||
Vector<GC::Ptr<Gamepad>> m_gamepads;
|
||||
|
||||
// Non-standard attribute to know which gamepads are available to the system. This is used to prevent duplicate
|
||||
// connections for the same gamepad ID (e.g. if the navigator object is initialized and checks for connected gamepads
|
||||
// and also receives an SDL gamepad connected event)
|
||||
Vector<SDL_JoystickID> m_available_gamepads;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -179,6 +179,8 @@ namespace AttributeNames {
|
|||
__ENUMERATE_HTML_ATTRIBUTE(onfocusin, "onfocusin") \
|
||||
__ENUMERATE_HTML_ATTRIBUTE(onfocusout, "onfocusout") \
|
||||
__ENUMERATE_HTML_ATTRIBUTE(onformdata, "onformdata") \
|
||||
__ENUMERATE_HTML_ATTRIBUTE(ongamepadconnected, "ongamepadconnected") \
|
||||
__ENUMERATE_HTML_ATTRIBUTE(ongamepaddisconnected, "ongamepaddisconnected") \
|
||||
__ENUMERATE_HTML_ATTRIBUTE(ongotpointercapture, "ongotpointercapture") \
|
||||
__ENUMERATE_HTML_ATTRIBUTE(onhashchange, "onhashchange") \
|
||||
__ENUMERATE_HTML_ATTRIBUTE(oninput, "oninput") \
|
||||
|
|
|
@ -296,6 +296,8 @@ void EventLoop::process_input_events() const
|
|||
page_client.report_finished_handling_input_event(event.page_id, EventResult::Dropped);
|
||||
page_client.report_finished_handling_input_event(event.page_id, result);
|
||||
}
|
||||
|
||||
page.handle_sdl_input_events();
|
||||
};
|
||||
|
||||
auto documents_of_traversable_navigables = documents_in_this_event_loop_matching([&](auto const& document) {
|
||||
|
|
|
@ -81,6 +81,9 @@ public:
|
|||
// https://w3c.github.io/media-capabilities/#media-capabilities-task-source
|
||||
MediaCapabilities,
|
||||
|
||||
// https://w3c.github.io/gamepad/#dfn-gamepad-task-source
|
||||
Gamepad,
|
||||
|
||||
// !!! IMPORTANT: Keep this field last!
|
||||
// This serves as the base value of all unique task sources.
|
||||
// Some elements, such as the HTMLMediaElement, must have a unique task source per instance.
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <LibWeb/CSS/StyleValues/ImageStyleValue.h>
|
||||
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/Gamepad/EventNames.h>
|
||||
#include <LibWeb/HTML/HTMLBodyElement.h>
|
||||
#include <LibWeb/HTML/Numbers.h>
|
||||
#include <LibWeb/HTML/Parser/HTMLParser.h>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include <LibWeb/CSS/ComputedProperties.h>
|
||||
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/Gamepad/EventNames.h>
|
||||
#include <LibWeb/HTML/HTMLFrameSetElement.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ void Navigator::initialize(JS::Realm& realm)
|
|||
{
|
||||
WEB_SET_PROTOTYPE_FOR_INTERFACE(Navigator);
|
||||
Base::initialize(realm);
|
||||
NavigatorGamepadPartial::check_for_connected_gamepads();
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/system-state.html#dom-navigator-pdfviewerenabled
|
||||
|
@ -65,6 +66,7 @@ bool Navigator::webdriver() const
|
|||
void Navigator::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
NavigatorGamepadPartial::visit_edges(visitor);
|
||||
visitor.visit(m_mime_type_array);
|
||||
visitor.visit(m_plugin_array);
|
||||
visitor.visit(m_clipboard);
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
|
||||
* Copyright (c) 2022-2025, Luke Wilde <lukew@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/DOM/EventTarget.h>
|
||||
#include <LibWeb/Gamepad/EventNames.h>
|
||||
#include <LibWeb/HTML/EventNames.h>
|
||||
#include <LibWeb/HTML/WindowEventHandlers.h>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
|
||||
* Copyright (c) 2022-2025, Luke Wilde <lukew@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -9,24 +9,26 @@
|
|||
#include <AK/Forward.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
#define ENUMERATE_WINDOW_EVENT_HANDLERS(E) \
|
||||
E(onafterprint, HTML::EventNames::afterprint) \
|
||||
E(onbeforeprint, HTML::EventNames::beforeprint) \
|
||||
E(onbeforeunload, HTML::EventNames::beforeunload) \
|
||||
E(onhashchange, HTML::EventNames::hashchange) \
|
||||
E(onlanguagechange, HTML::EventNames::languagechange) \
|
||||
E(onmessage, HTML::EventNames::message) \
|
||||
E(onmessageerror, HTML::EventNames::messageerror) \
|
||||
E(onoffline, HTML::EventNames::offline) \
|
||||
E(ononline, HTML::EventNames::online) \
|
||||
E(onpagehide, HTML::EventNames::pagehide) \
|
||||
E(onpagereveal, HTML::EventNames::pagereveal) \
|
||||
E(onpageshow, HTML::EventNames::pageshow) \
|
||||
E(onpageswap, HTML::EventNames::pageswap) \
|
||||
E(onpopstate, HTML::EventNames::popstate) \
|
||||
E(onrejectionhandled, HTML::EventNames::rejectionhandled) \
|
||||
E(onstorage, HTML::EventNames::storage) \
|
||||
E(onunhandledrejection, HTML::EventNames::unhandledrejection) \
|
||||
#define ENUMERATE_WINDOW_EVENT_HANDLERS(E) \
|
||||
E(onafterprint, HTML::EventNames::afterprint) \
|
||||
E(onbeforeprint, HTML::EventNames::beforeprint) \
|
||||
E(onbeforeunload, HTML::EventNames::beforeunload) \
|
||||
E(ongamepadconnected, Gamepad::EventNames::gamepadconnected) \
|
||||
E(ongamepaddisconnected, Gamepad::EventNames::gamepaddisconnected) \
|
||||
E(onhashchange, HTML::EventNames::hashchange) \
|
||||
E(onlanguagechange, HTML::EventNames::languagechange) \
|
||||
E(onmessage, HTML::EventNames::message) \
|
||||
E(onmessageerror, HTML::EventNames::messageerror) \
|
||||
E(onoffline, HTML::EventNames::offline) \
|
||||
E(ononline, HTML::EventNames::online) \
|
||||
E(onpagehide, HTML::EventNames::pagehide) \
|
||||
E(onpagereveal, HTML::EventNames::pagereveal) \
|
||||
E(onpageshow, HTML::EventNames::pageshow) \
|
||||
E(onpageswap, HTML::EventNames::pageswap) \
|
||||
E(onpopstate, HTML::EventNames::popstate) \
|
||||
E(onrejectionhandled, HTML::EventNames::rejectionhandled) \
|
||||
E(onstorage, HTML::EventNames::storage) \
|
||||
E(onunhandledrejection, HTML::EventNames::unhandledrejection) \
|
||||
E(onunload, HTML::EventNames::unload)
|
||||
|
||||
namespace Web::HTML {
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include <LibWeb/HTML/HTMLImageElement.h>
|
||||
#include <LibWeb/HTML/HTMLMediaElement.h>
|
||||
#include <LibWeb/HTML/HTMLVideoElement.h>
|
||||
#include <LibWeb/HTML/Navigator.h>
|
||||
#include <LibWeb/Layout/Label.h>
|
||||
#include <LibWeb/Layout/Viewport.h>
|
||||
#include <LibWeb/Page/DragAndDropEventHandler.h>
|
||||
|
@ -37,6 +38,8 @@
|
|||
#include <LibWeb/UIEvents/PointerEvent.h>
|
||||
#include <LibWeb/UIEvents/WheelEvent.h>
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
namespace Web {
|
||||
|
||||
#define FIRE(expression) \
|
||||
|
@ -1459,6 +1462,56 @@ EventResult EventHandler::handle_paste(String const& text)
|
|||
return EventResult::Handled;
|
||||
}
|
||||
|
||||
void EventHandler::handle_gamepad_connected(SDL_JoystickID sdl_joystick_id)
|
||||
{
|
||||
auto active_document = m_navigable->active_document();
|
||||
if (active_document)
|
||||
active_document->window()->navigator()->handle_gamepad_connected(sdl_joystick_id);
|
||||
|
||||
for (auto child_navigable : m_navigable->child_navigables())
|
||||
child_navigable->event_handler().handle_gamepad_connected(sdl_joystick_id);
|
||||
}
|
||||
|
||||
void EventHandler::handle_gamepad_updated(SDL_JoystickID sdl_joystick_id)
|
||||
{
|
||||
auto active_document = m_navigable->active_document();
|
||||
if (active_document)
|
||||
active_document->window()->navigator()->handle_gamepad_updated({}, sdl_joystick_id);
|
||||
|
||||
for (auto child_navigable : m_navigable->child_navigables())
|
||||
child_navigable->event_handler().handle_gamepad_updated(sdl_joystick_id);
|
||||
}
|
||||
|
||||
void EventHandler::handle_gamepad_disconnected(SDL_JoystickID sdl_joystick_id)
|
||||
{
|
||||
auto active_document = m_navigable->active_document();
|
||||
if (active_document)
|
||||
active_document->window()->navigator()->handle_gamepad_disconnected({}, sdl_joystick_id);
|
||||
|
||||
for (auto child_navigable : m_navigable->child_navigables())
|
||||
child_navigable->event_handler().handle_gamepad_disconnected(sdl_joystick_id);
|
||||
}
|
||||
|
||||
void EventHandler::handle_sdl_input_events()
|
||||
{
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_GAMEPAD_ADDED:
|
||||
handle_gamepad_connected(event.gdevice.which);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_UPDATE_COMPLETE:
|
||||
handle_gamepad_updated(event.gdevice.which);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||
handle_gamepad_disconnected(event.gdevice.which);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EventHandler::set_mouse_event_tracking_paintable(GC::Ptr<Painting::Paintable> paintable)
|
||||
{
|
||||
m_mouse_event_tracking_paintable = paintable;
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
#include <LibWeb/PixelUnits.h>
|
||||
#include <LibWeb/UIEvents/KeyCode.h>
|
||||
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
|
||||
namespace Web {
|
||||
|
||||
class WEB_API EventHandler {
|
||||
|
@ -43,6 +45,8 @@ public:
|
|||
|
||||
EventResult handle_paste(String const& text);
|
||||
|
||||
void handle_sdl_input_events();
|
||||
|
||||
void visit_edges(JS::Cell::Visitor& visitor) const;
|
||||
|
||||
Unicode::Segmenter& word_segmenter();
|
||||
|
@ -68,6 +72,10 @@ private:
|
|||
|
||||
bool should_ignore_device_input_event() const;
|
||||
|
||||
void handle_gamepad_connected(SDL_JoystickID);
|
||||
void handle_gamepad_updated(SDL_JoystickID);
|
||||
void handle_gamepad_disconnected(SDL_JoystickID);
|
||||
|
||||
GC::Ref<HTML::Navigable> m_navigable;
|
||||
|
||||
bool m_in_mouse_selection { false };
|
||||
|
|
|
@ -244,6 +244,11 @@ EventResult Page::handle_keyup(UIEvents::KeyCode key, unsigned modifiers, u32 co
|
|||
return focused_navigable().event_handler().handle_keyup(key, modifiers, code_point, repeat);
|
||||
}
|
||||
|
||||
void Page::handle_sdl_input_events()
|
||||
{
|
||||
top_level_traversable()->event_handler().handle_sdl_input_events();
|
||||
}
|
||||
|
||||
void Page::set_top_level_traversable(GC::Ref<HTML::TraversableNavigable> navigable)
|
||||
{
|
||||
VERIFY(!m_top_level_traversable); // Replacement is not allowed!
|
||||
|
|
|
@ -102,6 +102,8 @@ public:
|
|||
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
EventResult handle_keyup(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
|
||||
void handle_sdl_input_events();
|
||||
|
||||
Gfx::Palette palette() const;
|
||||
CSSPixelRect web_exposed_screen_area() const;
|
||||
CSS::PreferredColorScheme preferred_color_scheme() const;
|
||||
|
|
|
@ -126,6 +126,9 @@ libweb_js_bindings(FileAPI/FileList)
|
|||
libweb_js_bindings(FileAPI/FileReader)
|
||||
libweb_js_bindings(FileAPI/FileReaderSync)
|
||||
libweb_js_bindings(Gamepad/Gamepad)
|
||||
libweb_js_bindings(Gamepad/GamepadButton)
|
||||
libweb_js_bindings(Gamepad/GamepadEvent)
|
||||
libweb_js_bindings(Gamepad/GamepadHapticActuator)
|
||||
libweb_js_bindings(Geolocation/Geolocation)
|
||||
libweb_js_bindings(Geolocation/GeolocationCoordinates)
|
||||
libweb_js_bindings(Geolocation/GeolocationPosition)
|
||||
|
|
|
@ -74,6 +74,9 @@ static bool is_platform_object(Type const& type)
|
|||
"FileList"sv,
|
||||
"FontFace"sv,
|
||||
"FormData"sv,
|
||||
"Gamepad"sv,
|
||||
"GamepadButton"sv,
|
||||
"GamepadHapticActuator"sv,
|
||||
"HTMLCollection"sv,
|
||||
"IDBCursor"sv,
|
||||
"IDBCursorWithValue"sv,
|
||||
|
|
|
@ -157,6 +157,9 @@ FormDataEvent
|
|||
Function
|
||||
GainNode
|
||||
Gamepad
|
||||
GamepadButton
|
||||
GamepadEvent
|
||||
GamepadHapticActuator
|
||||
Geolocation
|
||||
GeolocationCoordinates
|
||||
GeolocationPosition
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue