ladybird/Libraries/LibWeb/Gamepad/NavigatorGamepad.cpp
ayeteadoe 454e6a6f7f LibWeb/Gamepad: Forward declare SDL components to fix Windows build
We have to prevent from including any SDL headers in LibWeb headers.
Otherwise there will be transitive Windows.h includes that will
re-declare some of our existing forward decls/defines in
LibCore/SocketAddressWindows.h
2025-09-02 11:11:12 +01:00

253 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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>
#include <SDL3/SDL_gamepad.h>
namespace Web::Gamepad {
// https://w3c.github.io/gamepad/#dom-navigator-getgamepads
WebIDL::ExceptionOr<GC::RootVector<GC::Ptr<Gamepad>>> NavigatorGamepadPartial::get_gamepads()
{
auto& navigator = as<HTML::Navigator>(*this);
auto& realm = navigator.realm();
auto& heap = realm.heap();
// 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();
// 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;
// 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);
// 4. If this.[[hasGamepadGesture]] is false, then return an empty list.
if (!m_has_gamepad_gesture)
return gamepads;
// 5. Let now be the current high resolution time given the current global object.
auto now = HighResolutionTime::current_high_resolution_time(window);
// 6. Let gamepads be an empty list.
// NOTE: Already done.
// 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);
// 2. Set gamepad.[[timestamp]] to now.
gamepad->set_timestamp({}, now);
}
// 2. Append gamepad to gamepads.
gamepads.append(gamepad);
}
// 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 };
}
}