mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-12-07 21:59:54 +00:00
LibWeb: Add tests for Gamepad API by utilising virtual SDL3 joysticks
This commit is contained in:
parent
74e0483ea5
commit
9adf27f009
Notes:
github-actions[bot]
2025-09-01 19:11:51 +00:00
Author: https://github.com/Lubrsi
Commit: 9adf27f009
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5902
Reviewed-by: https://github.com/gmta
Reviewed-by: https://github.com/trflynn89
28 changed files with 897 additions and 0 deletions
178
Libraries/LibWeb/Internals/InternalGamepad.cpp
Normal file
178
Libraries/LibWeb/Internals/InternalGamepad.cpp
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/Bindings/InternalGamepadPrototype.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Internals/InternalGamepad.h>
|
||||
|
||||
namespace Web::Internals {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(InternalGamepad);
|
||||
|
||||
static constexpr Array<SDL_GamepadButton, 15> BUTTONS = {
|
||||
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_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 constexpr Array<SDL_GamepadAxis, 4> AXES {
|
||||
SDL_GAMEPAD_AXIS_LEFTX,
|
||||
SDL_GAMEPAD_AXIS_LEFTY,
|
||||
SDL_GAMEPAD_AXIS_RIGHTX,
|
||||
SDL_GAMEPAD_AXIS_RIGHTY,
|
||||
};
|
||||
|
||||
static constexpr Array<SDL_GamepadAxis, 2> TRIGGERS {
|
||||
SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
|
||||
SDL_GAMEPAD_AXIS_RIGHT_TRIGGER,
|
||||
};
|
||||
|
||||
static constexpr char const* VIRTUAL_GAMEPAD_NAME = "Ladybird Virtual Gamepad";
|
||||
|
||||
static SDLCALL bool rumble(void* user_data, u16 low_frequency_rumble, u16 high_frequency_rumble)
|
||||
{
|
||||
auto* internal_gamepad = static_cast<InternalGamepad*>(user_data);
|
||||
internal_gamepad->received_rumble(low_frequency_rumble, high_frequency_rumble);
|
||||
return true;
|
||||
}
|
||||
|
||||
static SDLCALL bool rumble_triggers(void* user_data, u16 left_rumble, u16 right_rumble)
|
||||
{
|
||||
auto* internal_gamepad = static_cast<InternalGamepad*>(user_data);
|
||||
internal_gamepad->received_rumble_triggers(left_rumble, right_rumble);
|
||||
return true;
|
||||
}
|
||||
|
||||
InternalGamepad::InternalGamepad(JS::Realm& realm)
|
||||
: Bindings::PlatformObject(realm)
|
||||
{
|
||||
SDL_VirtualJoystickDesc virtual_joystick_desc {};
|
||||
SDL_INIT_INTERFACE(&virtual_joystick_desc);
|
||||
|
||||
virtual_joystick_desc.type = SDL_JOYSTICK_TYPE_GAMEPAD;
|
||||
virtual_joystick_desc.naxes = AXES.size() + TRIGGERS.size();
|
||||
virtual_joystick_desc.nbuttons = BUTTONS.size();
|
||||
|
||||
u32 button_mask = 0;
|
||||
for (auto const button : BUTTONS)
|
||||
button_mask |= 1 << button;
|
||||
|
||||
virtual_joystick_desc.button_mask = button_mask;
|
||||
|
||||
u32 axis_mask = 0;
|
||||
for (auto const axis : AXES)
|
||||
axis_mask |= 1 << axis;
|
||||
|
||||
for (auto const trigger : TRIGGERS)
|
||||
axis_mask |= 1 << trigger;
|
||||
|
||||
virtual_joystick_desc.axis_mask = axis_mask;
|
||||
|
||||
virtual_joystick_desc.name = VIRTUAL_GAMEPAD_NAME;
|
||||
virtual_joystick_desc.userdata = this;
|
||||
virtual_joystick_desc.Rumble = rumble;
|
||||
virtual_joystick_desc.RumbleTriggers = rumble_triggers;
|
||||
|
||||
m_sdl_joystick_id = SDL_AttachVirtualJoystick(&virtual_joystick_desc);
|
||||
m_sdl_joystick = SDL_OpenJoystick(m_sdl_joystick_id);
|
||||
}
|
||||
|
||||
InternalGamepad::~InternalGamepad() = default;
|
||||
|
||||
void InternalGamepad::initialize(JS::Realm& realm)
|
||||
{
|
||||
WEB_SET_PROTOTYPE_FOR_INTERFACE(InternalGamepad);
|
||||
Base::initialize(realm);
|
||||
}
|
||||
|
||||
void InternalGamepad::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_received_rumble_effects);
|
||||
visitor.visit(m_received_rumble_trigger_effects);
|
||||
}
|
||||
|
||||
void InternalGamepad::finalize()
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
Array<SDL_GamepadButton, 15> const& InternalGamepad::buttons()
|
||||
{
|
||||
return BUTTONS;
|
||||
}
|
||||
|
||||
Array<SDL_GamepadAxis, 4> const& InternalGamepad::axes()
|
||||
{
|
||||
return AXES;
|
||||
}
|
||||
|
||||
Array<SDL_GamepadAxis, 2> const& InternalGamepad::triggers()
|
||||
{
|
||||
return TRIGGERS;
|
||||
}
|
||||
|
||||
void InternalGamepad::set_button(int button, bool down)
|
||||
{
|
||||
SDL_SetJoystickVirtualButton(m_sdl_joystick, button, down);
|
||||
}
|
||||
|
||||
void InternalGamepad::set_axis(int axis, short value)
|
||||
{
|
||||
SDL_SetJoystickVirtualAxis(m_sdl_joystick, axis, value);
|
||||
}
|
||||
|
||||
GC::RootVector<JS::Object*> InternalGamepad::get_received_rumble_effects() const
|
||||
{
|
||||
GC::RootVector<JS::Object*> received_rumble_effects { realm().heap() };
|
||||
for (auto const received_rumble_effect : m_received_rumble_effects)
|
||||
received_rumble_effects.append(received_rumble_effect);
|
||||
return received_rumble_effects;
|
||||
}
|
||||
|
||||
GC::RootVector<JS::Object*> InternalGamepad::get_received_rumble_trigger_effects() const
|
||||
{
|
||||
GC::RootVector<JS::Object*> received_rumble_trigger_effects { realm().heap() };
|
||||
for (auto const received_rumble_trigger_effect : m_received_rumble_trigger_effects)
|
||||
received_rumble_trigger_effects.append(received_rumble_trigger_effect);
|
||||
return received_rumble_trigger_effects;
|
||||
}
|
||||
|
||||
void InternalGamepad::received_rumble(u16 low_frequency_rumble, u16 high_frequency_rumble)
|
||||
{
|
||||
auto object = JS::Object::create(realm(), nullptr);
|
||||
object->define_direct_property("lowFrequencyRumble"_utf16, JS::Value(low_frequency_rumble), JS::default_attributes);
|
||||
object->define_direct_property("highFrequencyRumble"_utf16, JS::Value(high_frequency_rumble), JS::default_attributes);
|
||||
m_received_rumble_effects.append(object);
|
||||
}
|
||||
|
||||
void InternalGamepad::received_rumble_triggers(u16 left_rumble, u16 right_rumble)
|
||||
{
|
||||
auto object = JS::Object::create(realm(), nullptr);
|
||||
object->define_direct_property("leftRumble"_utf16, JS::Value(left_rumble), JS::default_attributes);
|
||||
object->define_direct_property("rightRumble"_utf16, JS::Value(right_rumble), JS::default_attributes);
|
||||
m_received_rumble_trigger_effects.append(object);
|
||||
}
|
||||
|
||||
void InternalGamepad::disconnect()
|
||||
{
|
||||
SDL_CloseJoystick(m_sdl_joystick);
|
||||
SDL_DetachVirtualJoystick(m_sdl_joystick_id);
|
||||
}
|
||||
|
||||
}
|
||||
51
Libraries/LibWeb/Internals/InternalGamepad.h
Normal file
51
Libraries/LibWeb/Internals/InternalGamepad.h
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
|
||||
namespace Web::Internals {
|
||||
|
||||
class InternalGamepad : public Bindings::PlatformObject {
|
||||
WEB_PLATFORM_OBJECT(InternalGamepad, Bindings::PlatformObject);
|
||||
GC_DECLARE_ALLOCATOR(InternalGamepad);
|
||||
|
||||
public:
|
||||
static GC::Ref<InternalGamepad> create(JS::Realm&);
|
||||
|
||||
virtual ~InternalGamepad() override;
|
||||
|
||||
Array<SDL_GamepadButton, 15> const& buttons();
|
||||
Array<SDL_GamepadAxis, 4> const& axes();
|
||||
Array<SDL_GamepadAxis, 2> const& triggers();
|
||||
|
||||
void set_button(int button, bool down);
|
||||
void set_axis(int axis, short value);
|
||||
|
||||
GC::RootVector<JS::Object*> get_received_rumble_effects() const;
|
||||
GC::RootVector<JS::Object*> get_received_rumble_trigger_effects() const;
|
||||
|
||||
void received_rumble(u16 low_frequency_rumble, u16 high_frequency_rumble);
|
||||
void received_rumble_triggers(u16 left_rumble, u16 right_rumble);
|
||||
|
||||
void disconnect();
|
||||
|
||||
private:
|
||||
InternalGamepad(JS::Realm&);
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
virtual void finalize() override;
|
||||
|
||||
SDL_JoystickID m_sdl_joystick_id;
|
||||
SDL_Joystick* m_sdl_joystick;
|
||||
Vector<GC::Ref<JS::Object>> m_received_rumble_effects;
|
||||
Vector<GC::Ref<JS::Object>> m_received_rumble_trigger_effects;
|
||||
};
|
||||
|
||||
}
|
||||
14
Libraries/LibWeb/Internals/InternalGamepad.idl
Normal file
14
Libraries/LibWeb/Internals/InternalGamepad.idl
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Exposed=Nobody]
|
||||
interface InternalGamepad {
|
||||
readonly attribute sequence<long> buttons;
|
||||
readonly attribute sequence<long> axes;
|
||||
readonly attribute sequence<long> triggers;
|
||||
|
||||
undefined setButton(long button, boolean down);
|
||||
undefined setAxis(long axis, short value);
|
||||
|
||||
sequence<object> getReceivedRumbleEffects();
|
||||
sequence<object> getReceivedRumbleTriggerEffects();
|
||||
|
||||
undefined disconnect();
|
||||
};
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
#include <LibWeb/DOMURL/DOMURL.h>
|
||||
#include <LibWeb/HTML/HTMLElement.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/Internals/InternalGamepad.h>
|
||||
#include <LibWeb/Internals/Internals.h>
|
||||
#include <LibWeb/Page/InputEvent.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
|
|
@ -330,4 +331,15 @@ GC::Ptr<DOM::ShadowRoot> Internals::get_shadow_root(GC::Ref<DOM::Element> elemen
|
|||
return element->shadow_root();
|
||||
}
|
||||
|
||||
void Internals::handle_sdl_input_events()
|
||||
{
|
||||
page().handle_sdl_input_events();
|
||||
}
|
||||
|
||||
GC::Ref<InternalGamepad> Internals::connect_virtual_gamepad()
|
||||
{
|
||||
auto& realm = this->realm();
|
||||
return realm.create<InternalGamepad>(realm);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,10 @@ public:
|
|||
|
||||
GC::Ptr<DOM::ShadowRoot> get_shadow_root(GC::Ref<DOM::Element>);
|
||||
|
||||
void handle_sdl_input_events();
|
||||
|
||||
GC::Ref<InternalGamepad> connect_virtual_gamepad();
|
||||
|
||||
private:
|
||||
explicit Internals(JS::Realm&);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#import <DOM/EventTarget.idl>
|
||||
#import <HTML/HTMLElement.idl>
|
||||
#import <Internals/InternalAnimationTimeline.idl>
|
||||
#import <Internals/InternalGamepad.idl>
|
||||
|
||||
[Exposed=Nobody]
|
||||
interface Internals {
|
||||
|
|
@ -58,4 +59,8 @@ interface Internals {
|
|||
// Returns the shadow root of the element, if it has one, even if it's not normally accessible to JS.
|
||||
ShadowRoot? getShadowRoot(Element element);
|
||||
|
||||
undefined handleSDLInputEvents();
|
||||
|
||||
InternalGamepad connectVirtualGamepad();
|
||||
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue