New input handling system

The new system allows to register all function objects as Input::Handler
not only function pointers.
To not get dangling references a handler is unsubscribed on it's
destruction.
The order in with the handlers are executed has to be specified by the
slot-argument, instead of the reverse order of registration.
This commit is contained in:
phosit 2025-04-13 18:27:40 +02:00
parent 1ab55e7f2e
commit 9890d3eb8a
No known key found for this signature in database
GPG key ID: C9430B600671C268
14 changed files with 616 additions and 230 deletions

View file

@ -32,6 +32,7 @@
#include "ps/GameSetup/GameSetup.h"
#include "ps/Hotkey.h"
#include "ps/XML/Xeromyces.h"
#include "ps/VideoMode.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptConversions.h"
@ -166,11 +167,10 @@ public:
hotkeyNotification.key.repeat = 0;
// Init input and poll the event.
InitInput();
in_push_priority_event(hotkeyNotification);
SDL_Event ev;
while (in_poll_event(ev))
in_dispatch_event(ev);
const std::unique_ptr<InputHandlers> _{MakeInputHandlers()};
g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification);
for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents())
g_VideoMode.m_InputManager.DispatchEvent(ev);
const ScriptInterface& pageScriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface());
ScriptRequest prq(pageScriptInterface);
@ -191,9 +191,9 @@ public:
// We are listening to KeyDown events, so repeat shouldn't matter.
hotkeyNotification.key.repeat = 1;
in_push_priority_event(hotkeyNotification);
while (in_poll_event(ev))
in_dispatch_event(ev);
g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification);
for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents())
g_VideoMode.m_InputManager.DispatchEvent(ev);
hotkey_pressed_value = false;
Script::GetProperty(prq, global, "state_before", &js_hotkey_pressed_value);
@ -206,9 +206,9 @@ public:
TS_ASSERT_EQUALS(hotkey_pressed_value, true);
hotkeyNotification.type = SDL_KEYUP;
in_push_priority_event(hotkeyNotification);
while (in_poll_event(ev))
in_dispatch_event(ev);
g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification);
for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents())
g_VideoMode.m_InputManager.DispatchEvent(ev);
hotkey_pressed_value = true;
Script::GetProperty(prq, global, "state_before", &js_hotkey_pressed_value);

View file

@ -1,97 +0,0 @@
/* Copyright (C) 2026 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* SDL input redirector; dispatches to multiple handlers.
*/
#include "precompiled.h"
#include "input.h"
#include "lib/debug.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/status.h"
#include <SDL_events.h>
#include <cstddef>
#include <list>
const size_t MAX_HANDLERS = 10;
static InHandler handler_stack[MAX_HANDLERS];
static size_t handler_stack_top = 0;
static std::list<SDL_Event> priority_events;
void in_add_handler(InHandler handler)
{
ENSURE(handler);
if(handler_stack_top >= MAX_HANDLERS)
WARN_IF_ERR(ERR::LIMIT);
handler_stack[handler_stack_top++] = handler;
}
void in_reset_handlers()
{
handler_stack_top = 0;
}
// send ev to each handler until one returns IN_HANDLED
void in_dispatch_event(const SDL_Event& ev)
{
for(int i = (int)handler_stack_top-1; i >= 0; i--)
{
ENSURE(handler_stack[i]);
InReaction ret = handler_stack[i](ev);
// .. done, return
if(ret == IN_HANDLED)
return;
// .. next handler
else if(ret == IN_PASS)
continue;
// .. invalid return value
else
DEBUG_WARN_ERR(ERR::LOGIC); // invalid handler return value
}
}
void in_push_priority_event(const SDL_Event& event)
{
priority_events.push_back(event);
}
int in_poll_priority_event(SDL_Event& event)
{
if (priority_events.empty())
return 0;
event = priority_events.front();
priority_events.pop_front();
return 1;
}
int in_poll_event(SDL_Event& event)
{
return in_poll_priority_event(event) ? 1 : SDL_PollEvent(&event);
}

View file

@ -42,29 +42,4 @@ enum InReaction
IN_HANDLED = 2
};
typedef InReaction (*InHandler)(const SDL_Event&);
// register an input handler, which will receive all subsequent events first.
// events are passed to other handlers if handler returns IN_PASS.
extern void in_add_handler(InHandler handler);
// remove all registered input handlers
extern void in_reset_handlers();
// send event to each handler (newest first) until one returns true
extern void in_dispatch_event(const SDL_Event& event);
// push an event onto the back of a high-priority queue - the new event will
// be returned by in_poll_event before any standard SDL events
extern void in_push_priority_event(const SDL_Event& event);
// reads events that were pushed by in_push_priority_event
// returns 1 if an event was read, 0 otherwise.
extern int in_poll_priority_event(SDL_Event& event);
// reads events that were pushed by in_push_priority_event, or, if there are
// no high-priority events) reads from the SDL event queue with SDL_PollEvent.
// returns 1 if an event was read, 0 otherwise.
extern int in_poll_event(SDL_Event& event);
#endif // #ifndef INCLUDED_INPUT

View file

@ -41,7 +41,6 @@ that of Atlas depending on commandline parameters.
#include "lib/file/file_system.h"
#include "lib/file/vfs/vfs.h"
#include "lib/frequency_filter.h"
#include "lib/input.h"
#include "lib/path.h"
#include "lib/posix/posix_types.h"
#include "lib/secure_crt.h"
@ -66,6 +65,7 @@ that of Atlas depending on commandline parameters.
#include "ps/GameSetup/Paths.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Input.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
#include "ps/ModInstaller.h"
@ -281,8 +281,7 @@ static void PumpEvents()
PROFILE3("dispatch events");
SDL_Event ev{};
while (in_poll_event(ev))
for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents())
{
PROFILE2("event");
if (g_GUI)
@ -292,7 +291,7 @@ static void PumpEvents()
std::string data = Script::StringifyJSON(rq, &tmpVal);
PROFILE2_ATTR("%s", data.c_str());
}
in_dispatch_event(ev);
g_VideoMode.m_InputManager.DispatchEvent(ev);
}
g_TouchInput.Frame();
@ -524,19 +523,6 @@ static void NonVisualFrame()
QuitEngine(EXIT_SUCCESS);
}
static void MainControllerInit()
{
// add additional input handlers only needed by this controller:
// must be registered after gui_handler. Should mayhap even be last.
in_add_handler(MainInputHandler);
}
static void MainControllerShutdown()
{
in_reset_handlers();
}
static std::optional<RL::Interface> CreateRLInterface(const CmdLineArgs& args)
{
if (!args.Has("rl-interface"))
@ -751,58 +737,82 @@ static void RunGameOrAtlas(const std::span<const char* const> argv)
std::optional<DAP::Interface> dapInterface{CreateDAPInterface(args)};
#endif // CONFIG2_DAP_INTERFACE
std::optional<ScriptInterface> guiScriptInterface;
if (isVisual)
{
guiScriptInterface.emplace("Engine", "gui", *g_ScriptContext);
InitGraphics(args, 0, installedMods, *g_ScriptContext, *guiScriptInterface);
MainControllerInit();
}
else if (!InitNonVisual(args))
g_Shutdown = ShutdownType::Quit;
try
{
// MSVC doesn't support copy elision in ternary expressions. So we use a lambda instead.
std::optional<RL::Interface> rlInterface{[&]() -> std::optional<RL::Interface>
class VisualData
{
public:
VisualData(const CmdLineArgs& args, std::vector<CStr>& installedMods) :
inputHandlers{InitGraphics(args, 0, installedMods, *g_ScriptContext,
scriptInterface)}
{
if (g_Shutdown == ShutdownType::None)
return CreateRLInterface(args);
}
VisualData(const VisualData&) = delete;
VisualData& operator=(const VisualData&) = delete;
VisualData(VisualData&&) = delete;
VisualData& operator=(VisualData&) = delete;
~VisualData() = default;
private:
ScriptInterface scriptInterface{"Engine", "gui", *g_ScriptContext};
std::unique_ptr<InputHandlers> inputHandlers;
Input::Handler<InReaction(&)(const SDL_Event&)> mainInputHandler{
g_VideoMode.m_InputManager, Input::Slot::PRIMARY, MainInputHandler};
};
// MSVC doesn't support copy elision in ternary expressions. So we use a lambda instead.
const std::optional<VisualData> visualData{[&]() -> std::optional<VisualData>
{
if (isVisual)
return std::make_optional<VisualData>(args, installedMods);
else
return std::nullopt;
}()};
if (!isVisual && !InitNonVisual(args))
g_Shutdown = ShutdownType::Quit;
while (g_Shutdown == ShutdownType::None)
try
{
if (isVisual)
// MSVC doesn't support copy elision in ternary expressions. So we use a lambda instead.
std::optional<RL::Interface> rlInterface{[&]() -> std::optional<RL::Interface>
{
if (g_Shutdown == ShutdownType::None)
return CreateRLInterface(args);
else
return std::nullopt;
}()};
while (g_Shutdown == ShutdownType::None)
{
if (isVisual)
{
#if CONFIG2_DAP_INTERFACE
Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency, dapInterface ? &*dapInterface : nullptr);
Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency,
dapInterface ? &*dapInterface : nullptr);
#else
Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency);
Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency);
#endif
else if(rlInterface)
rlInterface->TryApplyMessage();
else
NonVisualFrame();
}
else if(rlInterface)
rlInterface->TryApplyMessage();
else
NonVisualFrame();
}
}
catch (const RL::SetupError&)
{
rlInterfaceError = true;
}
ShutdownNetworkAndUI();
}
catch (const RL::SetupError&)
{
rlInterfaceError = true;
}
ShutdownNetworkAndUI();
guiScriptInterface.reset();
#if CONFIG2_DAP_INTERFACE
dapInterface.reset();
#endif // CONFIG2_DAP_INTERFACE
ShutdownConfigAndSubsequent();
MainControllerShutdown();
} while (g_Shutdown == ShutdownType::Restart);
if (rlInterfaceError)

View file

@ -35,7 +35,6 @@
#include "lib/file/vfs/vfs.h"
#include "lib/file/vfs/vfs_path.h"
#include "lib/file/vfs/vfs_util.h"
#include "lib/input.h"
#include "lib/path.h"
#include "lib/status.h"
#include "lib/sysdep/os.h"
@ -254,44 +253,41 @@ static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcSc
g_GUI->SwitchPage(gui_page, srcScriptInterface, initData);
}
void InitInput()
[[nodiscard]] std::unique_ptr<InputHandlers> MakeInputHandlers()
{
g_Joystick.Initialise();
// register input handlers
// This stack is constructed so the first added, will be the last
// one called. This is important, because each of the handlers
// has the potential to block events to go further down
// in the chain. I.e. the last one in the list added, is the
// only handler that can block all messages before they are
// processed.
in_add_handler(game_view_handler);
std::unique_ptr<InputHandlers> handlers{std::make_unique<InputHandlers>()};
handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::GAME_VIEW, game_view_handler);
in_add_handler(CProfileViewer::InputThunk);
handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::PROFILE_VIEWER, CProfileViewer::InputThunk);
in_add_handler(HotkeyInputActualHandler);
handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::HOTKEY_INPUT, HotkeyInputActualHandler);
// gui_handler needs to be registered after (i.e. called before!) the
// hotkey handler so that input boxes can be typed in without
// setting off hotkeys.
in_add_handler(gui_handler);
handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::GUI, gui_handler);
// Likewise for the console.
in_add_handler(conInputHandler);
handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::CONSOLE, conInputHandler);
in_add_handler(touch_input_handler);
handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::TOUCH_INPUT, touch_input_handler);
// Should be called after scancode map update (i.e. after the global input, but before UI).
// This never blocks the event, but it does some processing necessary for hotkeys,
// which are triggered later down the input chain.
// (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI).
in_add_handler(HotkeyInputPrepHandler);
handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::HOTKEY_INPUT_PREPARATION,
HotkeyInputPrepHandler);
// These two must be called first (i.e. pushed last)
// GlobalsInputHandler deals with some important global state,
// such as which scancodes are being pressed, mouse buttons pressed, etc.
// while HotkeyStateChange updates the map of active hotkeys.
in_add_handler(GlobalsInputHandler);
in_add_handler(HotkeyStateChange);
handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::GLOBAL, GlobalsInputHandler);
handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::HOTKEY_STATE_CHANGE, HotkeyStateChange);
return handlers;
}
@ -635,8 +631,9 @@ bool Init(const CmdLineArgs& args, int flags)
return true;
}
void InitGraphics(const CmdLineArgs& args, int flags, const std::vector<CStr>& installedMods,
ScriptContext& scriptContext, ScriptInterface& scriptInterface)
[[nodiscard]] std::unique_ptr<InputHandlers> InitGraphics(const CmdLineArgs& args, int flags,
const std::vector<CStr>& installedMods, ScriptContext& scriptContext,
ScriptInterface& scriptInterface)
{
const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0;
@ -678,7 +675,7 @@ void InitGraphics(const CmdLineArgs& args, int flags, const std::vector<CStr>& i
// create renderer
new CRenderer(g_VideoMode.GetBackendDevice());
InitInput();
std::unique_ptr<InputHandlers> handlers{MakeInputHandlers()};
// TODO: Is this the best place for this?
if (VfsDirectoryExists(L"maps/"))
@ -709,6 +706,8 @@ void InitGraphics(const CmdLineArgs& args, int flags, const std::vector<CStr>& i
// (delete game data, switch GUI page, show error, etc.)
CancelLoad(CStr(e.what()).FromUTF8());
}
return handlers;
}
bool InitNonVisual(const CmdLineArgs& args)

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2024 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -19,7 +19,9 @@
#define INCLUDED_GAMESETUP
#include "ps/CStr.h"
#include "ps/Input.h"
#include <queue>
#include <vector>
class CmdLineArgs;
@ -70,13 +72,16 @@ void InitVfs(const CmdLineArgs& args);
* `ShutdownConfigAndSubsequent` has to be called later.
*/
extern bool Init(const CmdLineArgs& args, int flags);
extern void InitInput();
using InputHandlers = std::queue<Input::Handler<InReaction(&)(const SDL_Event&)>>;
[[nodiscard]] std::unique_ptr<InputHandlers> MakeInputHandlers();
/**
* `ShutdownNetworkAndUI` has to be called later.
*/
void InitGraphics(const CmdLineArgs& args, int flags, const std::vector<CStr>& installedMods,
ScriptContext& scriptContext, ScriptInterface& scriptInterface);
[[nodiscard]] std::unique_ptr<InputHandlers> InitGraphics(const CmdLineArgs& args, int flags,
const std::vector<CStr>& installedMods, ScriptContext& scriptContext,
ScriptInterface& scriptInterface);
/**
* `ShutdownNetworkAndUI` has to be called later.

View file

@ -26,6 +26,7 @@
#include "ps/Globals.h"
#include "ps/KeyName.h"
#include "ps/Profiler2.h"
#include "ps/VideoMode.h"
#include <SDL_events.h>
#include <SDL_mouse.h>
@ -429,7 +430,7 @@ InReaction HotkeyInputActualHandler(const SDL_Event& ev)
SDL_Event hotkeyPressNotification{};
hotkeyPressNotification.type = hotkey.retriggered ? SDL_HOTKEYPRESS_SILENT : SDL_HOTKEYPRESS;
hotkeyPressNotification.user.data1 = const_cast<char*>(hotkey.mapping->name.c_str());
in_push_priority_event(hotkeyPressNotification);
g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyPressNotification);
}
// Send a HotkeyDown event on every key, mouseButton and mouseWheel event.
@ -444,7 +445,7 @@ InReaction HotkeyInputActualHandler(const SDL_Event& ev)
SDL_Event hotkeyDownNotification{};
hotkeyDownNotification.type = SDL_HOTKEYDOWN;
hotkeyDownNotification.user.data1 = const_cast<char*>(hotkey.mapping->name.c_str());
in_push_priority_event(hotkeyDownNotification);
g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyDownNotification);
}
// Release instantaneous events (e.g. mouse wheel) right away.
@ -457,7 +458,7 @@ InReaction HotkeyInputActualHandler(const SDL_Event& ev)
SDL_Event hotkeyNotification{};
hotkeyNotification.type = hotkey.wasRetriggered ? SDL_HOTKEYUP_SILENT : SDL_HOTKEYUP;
hotkeyNotification.user.data1 = const_cast<char*>(hotkey.name);
in_push_priority_event(hotkeyNotification);
g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification);
}
return IN_PASS;
@ -481,7 +482,7 @@ void ResetActiveHotkeys()
SDL_Event hotkeyNotification;
hotkeyNotification.type = hotkey.retriggered ? SDL_HOTKEYUP_SILENT : SDL_HOTKEYUP;
hotkeyNotification.user.data1 = const_cast<char*>(hotkey.mapping->name.c_str());
in_push_priority_event(hotkeyNotification);
g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification);
}
pressedHotkeys.clear();
activeScancodes.clear();

130
source/ps/Input.cpp Normal file
View file

@ -0,0 +1,130 @@
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ps/Input.h"
#include "lib/debug.h"
#include "lib/external_libraries/libsdl.h"
#include "ps/Profile.h"
#include "ps/TouchInput.h"
#include "scriptinterface/JSON.h"
#include "scriptinterface/ScriptConversions.h"
#include "scriptinterface/ScriptRequest.h"
#include <utility>
namespace Input
{
Manager::Manager() :
m_PriorityEvents{std::make_unique<std::queue<SDL_Event>>()}
{
}
Manager::~Manager() = default;
void Manager::PushPriorityEvent(const SDL_Event& event)
{
m_PriorityEvents->push(event);
}
void Manager::DispatchEvent(const SDL_Event& event)
{
// Looks like std::find_if, but std::find_if does not guarantee the order of the handlers.
for (const auto handler : m_Handlers)
{
if (handler && (*handler)(event) == IN_HANDLED)
return;
}
}
Manager::PollEventsResult::EventIterator::EventIterator(PollEventsResult& range) :
m_Storage{&range}
{
++*this;
}
Manager::PollEventsResult::EventIterator::reference Manager::PollEventsResult::EventIterator::operator*()
{
return *m_Storage->m_Event;
}
Manager::PollEventsResult::EventIterator::pointer Manager::PollEventsResult::EventIterator::operator->()
{
return &**this;
}
Manager::PollEventsResult::EventIterator& Manager::PollEventsResult::EventIterator::operator++()
{
std::queue<SDL_Event>& priorityEvents{m_Storage->m_PriorityEvents};
if (!priorityEvents.empty())
{
*m_Storage->m_Event = priorityEvents.front();
priorityEvents.pop();
}
else if (!SDL_PollEvent(m_Storage->m_Event.get()))
m_Storage = nullptr;
return *this;
}
Manager::PollEventsResult::EventIterator& Manager::PollEventsResult::EventIterator::operator++(int)
{
++(*this);
return *this;
}
bool Manager::PollEventsResult::EventIterator::operator==(const EventIterator& other) const
{
return m_Storage == other.m_Storage;
}
Manager::PollEventsResult::PollEventsResult(std::queue<SDL_Event>& priorityEvents) :
m_PriorityEvents{priorityEvents},
m_Event{std::make_unique<SDL_Event>()}
{
}
Manager::PollEventsResult::EventIterator Manager::PollEventsResult::begin()
{
return EventIterator{*this};
}
Manager::PollEventsResult::EventIterator Manager::PollEventsResult::end()
{
return EventIterator{};
}
Manager::PollEventsResult Manager::PollEvents()
{
return PollEventsResult{*m_PriorityEvents};
}
HandlerBase::HandlerBase(HandlerBase*& pos) noexcept :
toReset{&pos}
{
const auto old = std::exchange(*toReset, this);
ENSURE(old == nullptr && "There is already a handler registered to this slot.");
}
HandlerBase::~HandlerBase()
{
const auto old = std::exchange(*toReset, nullptr);
ENSURE(old == this && "No handler is registered to this slot.");
}
} // namespace Input

187
source/ps/Input.h Normal file
View file

@ -0,0 +1,187 @@
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_INPUT_HANDLER
#define INCLUDED_INPUT_HANDLER
#include "lib/input.h"
#include <array>
#include <concepts>
#include <cstddef>
#include <queue>
#include <memory>
#include <optional>
#include <type_traits>
class ScriptRequest;
namespace Input
{
class HandlerBase;
// A slot for each handler. Numbers are in invocation order. A handler can discard events. The first handler
// is the only which gets all events.
namespace Slot
{
constexpr std::integral_constant<size_t, 0> PRIMARY;
// These two must be called first `globalsInput` deals with some important global state, such as which
// scancodes are being pressed, mouse buttons pressed, etc. while hotkeyStateChange updates the map of
// active hotkeys.
constexpr std::integral_constant<size_t, 1> HOTKEY_STATE_CHANGE;
constexpr std::integral_constant<size_t, 2> GLOBAL;
// Should be called after scancode map update (i.e. after the global input, but before UI). This never
// blocks the event, but it does some processing necessary for hotkeys, which are triggered later down the
// input chain. (by calling this before the UI, we can use `EventWouldTriggerHotkey` in the UI).
constexpr std::integral_constant<size_t, 3> HOTKEY_INPUT_PREPARATION;
constexpr std::integral_constant<size_t, 4> TOUCH_INPUT;
// The console handler needs to be called before the hotkey handler so that text can be typed in without
// setting off hotkeys.
constexpr std::integral_constant<size_t, 5> CONSOLE;
// Likewise for gui.
constexpr std::integral_constant<size_t, 6> GUI;
constexpr std::integral_constant<size_t, 7> HOTKEY_INPUT;
constexpr std::integral_constant<size_t, 8> PROFILE_VIEWER;
constexpr std::integral_constant<size_t, 9> GAME_VIEW;
}
/**
* Holds a pointer to all registered `HandlerBase`s.
* It does unregister all registered `Handler`s in the destructor.
*/
class Manager
{
public:
Manager();
// The `Manager` needs to have a constant memory location (the `Handler`s hold a pointer to it).
Manager(Manager&) = delete;
Manager& operator=(Manager&) = delete;
Manager(Manager&&) = delete;
Manager& operator=(Manager&&) = delete;
~Manager();
void PushPriorityEvent(const SDL_Event& event);
void DispatchEvent(const SDL_Event& event);
template<size_t slot>
HandlerBase*& Get() noexcept
{
return std::get<slot>(m_Handlers);
}
class PollEventsResult
{
public:
class EventIterator
{
public:
using difference_type = std::ptrdiff_t;
using value_type = SDL_Event;
using pointer = value_type*;
using reference = value_type&;
using iterator_category = std::input_iterator_tag;
EventIterator() = default;
explicit EventIterator(PollEventsResult& range);
reference operator*();
pointer operator->();
EventIterator& operator++();
EventIterator& operator++(int);
bool operator==(const EventIterator& other) const;
private:
PollEventsResult* m_Storage{nullptr};
};
explicit PollEventsResult(std::queue<SDL_Event>& priorityEvents);
EventIterator begin();
EventIterator end();
private:
std::queue<SDL_Event>& m_PriorityEvents;
const std::unique_ptr<SDL_Event> m_Event;
};
PollEventsResult PollEvents();
private:
std::array<HandlerBase*, 10> m_Handlers{{}};
const std::unique_ptr<std::queue<SDL_Event>> m_PriorityEvents;
};
/**
* Type-erased callable
*/
class HandlerBase
{
protected:
/**
* Can't not be constructed themself, use `Handler` instead.
* @param pos On construction the pointer is set to @c this. On
* destruction the pointer will be set to nullptr.
*/
explicit HandlerBase(HandlerBase*& pos) noexcept;
virtual ~HandlerBase();
// A `HandlerBase` needs to have a constant memory location (the `Manager` does hold a pointer to it).
HandlerBase(HandlerBase&) = delete;
HandlerBase& operator=(HandlerBase&) = delete;
HandlerBase(HandlerBase&&) = delete;
HandlerBase& operator=(HandlerBase&&) = delete;
public:
virtual InReaction operator()(const SDL_Event& event) = 0;
private:
HandlerBase** toReset;
};
/**
* Usable type to register a handler to the associated `Manager`.
*/
template<std::invocable<const SDL_Event&> Callback>
class Handler final : private HandlerBase
{
public:
// `slot` specifies when(in which order) the `callback` of this `Handler` is executed.
template<size_t slot>
explicit Handler(Manager& manager, std::integral_constant<size_t, slot>, Callback func) :
HandlerBase{manager.Get<slot>()},
callback{std::move(func)}
{
}
~Handler() final = default;
private:
InReaction operator()(const SDL_Event& event) final
{
return callback(event);
}
Callback callback;
};
}
#endif // INCLUDED_INPUT_HANDLER

View file

@ -18,6 +18,7 @@
#ifndef INCLUDED_VIDEOMODE
#define INCLUDED_VIDEOMODE
#include "ps/Input.h"
#include "renderer/backend/Backend.h"
#include <memory>
@ -144,7 +145,10 @@ private:
bool m_IsInitialised = false;
SDL_Window* m_Window = nullptr;
public:
Input::Manager m_InputManager;
private:
// Initial desktop settings.
// Frequency is in Hz, and BPP means bits per pixels (not bytes per pixels).
int m_PreferredW = 0;

View file

@ -32,6 +32,7 @@
#include "ps/Filesystem.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/VideoMode.h"
#include <SDL_events.h>
#include <SDL_keyboard.h>
@ -52,16 +53,18 @@ private:
void fakeInput(const char* key, bool keyDown)
{
SDL_Event ev;
ev.type = keyDown ? SDL_KEYDOWN : SDL_KEYUP;
ev.key.repeat = 0;
ev.key.keysym.scancode = SDL_GetScancodeFromName(key);
GlobalsInputHandler(ev);
HotkeyInputPrepHandler(ev);
HotkeyInputActualHandler(ev);
{
SDL_Event ev;
ev.type = keyDown ? SDL_KEYDOWN : SDL_KEYUP;
ev.key.repeat = 0;
ev.key.keysym.scancode = SDL_GetScancodeFromName(key);
GlobalsInputHandler(ev);
HotkeyInputPrepHandler(ev);
HotkeyInputActualHandler(ev);
}
hotkeyPress = false;
hotkeyUp = false;
while(in_poll_priority_event(ev))
for (const SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents())
{
hotkeyUp |= ev.type == SDL_HOTKEYUP;
hotkeyPress |= ev.type == SDL_HOTKEYPRESS;

View file

@ -0,0 +1,168 @@
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "lib/self_test.h"
#include "lib/external_libraries/libsdl.h"
#include "ps/Input.h"
#include <iterator>
class TestInput : public CxxTest::TestSuite
{
static std::uint32_t GetEventType(const int numevents)
{
std::uint32_t eventType{SDL_RegisterEvents(numevents)};
TS_ASSERT_DIFFERS(eventType, std::numeric_limits<std::uint32_t>::max());
TS_ASSERT_STR_EQUALS(SDL_GetError(), "");
return eventType;
}
static SDL_Event MakeEvent(const std::uint32_t eventType)
{
SDL_Event ev{};
ev.type = eventType;
return ev;
}
static void PushEvent(const std::uint32_t eventType)
{
SDL_Event ev{MakeEvent(eventType)};
TS_ASSERT_EQUALS(SDL_PushEvent(&ev), 1);
TS_ASSERT_STR_EQUALS(SDL_GetError(), "");
}
static void PushPriorityEvent(Input::Manager& manager, const std::uint32_t eventType)
{
const SDL_Event ev{MakeEvent(eventType)};
manager.PushPriorityEvent(ev);
}
public:
void setUp()
{
SDL_Init(SDL_INIT_EVENTS);
}
void tearDown()
{
SDL_Quit();
}
void test_NoEvent()
{
Input::Manager manager;
auto range = manager.PollEvents();
TS_ASSERT_EQUALS(std::distance(range.begin(), range.end()), 0);
}
void test_Event()
{
Input::Manager manager;
PushEvent(GetEventType(1));
auto range = manager.PollEvents();
TS_ASSERT_EQUALS(std::distance(range.begin(), range.end()), 1);
}
void test_PriorityEvent()
{
Input::Manager manager;
PushPriorityEvent(manager, GetEventType(1));
auto range = manager.PollEvents();
TS_ASSERT_EQUALS(std::distance(range.begin(), range.end()), 1);
}
void test_PriorityOrder()
{
Input::Manager manager;
const std::uint32_t eventTypeStart{SDL_RegisterEvents(2)};
const std::uint32_t priorityEventType{eventTypeStart + 1};
PushEvent(eventTypeStart);
PushPriorityEvent(manager, priorityEventType);
PushEvent(eventTypeStart);
auto range = manager.PollEvents();
auto iter = range.begin();
TS_ASSERT_DIFFERS(iter, range.end());
TS_ASSERT_EQUALS(iter->type, priorityEventType);
++iter;
TS_ASSERT_DIFFERS(iter, range.end());
TS_ASSERT_EQUALS(iter->type, eventTypeStart);
++iter;
TS_ASSERT_DIFFERS(iter, range.end());
TS_ASSERT_EQUALS(iter->type, eventTypeStart);
++iter;
TS_ASSERT_EQUALS(iter, range.end());
}
void test_Dispatch()
{
Input::Manager manager;
bool triggered{false};
Input::Handler _{manager, std::integral_constant<size_t, 0>{}, [&](const SDL_Event&){
triggered = true;
return IN_HANDLED;
}};
TS_ASSERT(!triggered);
SDL_Event ev{MakeEvent(GetEventType(1))};
manager.DispatchEvent(ev);
TS_ASSERT(triggered);
}
void test_DispatchFilter()
{
Input::Manager manager;
const std::uint32_t eventTypeStart{SDL_RegisterEvents(2)};
const std::uint32_t filteredEventType{eventTypeStart + 1};
[[maybe_unused]] Input::Handler filter{manager, std::integral_constant<size_t, 0>{},
[&](const SDL_Event& ev){
return ev.type == filteredEventType ? IN_HANDLED : IN_PASS;
}};
bool triggered{false};
[[maybe_unused]] Input::Handler test{manager, std::integral_constant<size_t, 1>{},
[&](const SDL_Event&){
triggered = true;
return IN_HANDLED;
}};
SDL_Event ev0{MakeEvent(filteredEventType)};
manager.DispatchEvent(ev0);
TS_ASSERT(!triggered);
SDL_Event ev1{MakeEvent(eventTypeStart)};
manager.DispatchEvent(ev1);
TS_ASSERT(triggered);
}
void test_Unsubscribe()
{
Input::Manager manager;
bool triggered{false};
{
Input::Handler _{manager, std::integral_constant<size_t, 0>{}, [&](const SDL_Event&){
triggered = true;
return IN_HANDLED;
}};
}
SDL_Event ev{MakeEvent(GetEventType(1))};
manager.DispatchEvent(ev);
TS_ASSERT(!triggered);
}
};

View file

@ -74,6 +74,7 @@ const int g_InitFlags = INIT_HAVE_VMODE | INIT_NO_GUI;
std::optional<FileLogger> g_FileLogger;
std::optional<ScriptInterface> g_ScriptInterface;
std::unique_ptr<InputHandlers> g_InputHandlers;
}
MESSAGEHANDLER(Init)
@ -131,7 +132,7 @@ MESSAGEHANDLER(InitGraphics)
g_VideoMode.CreateBackendDevice(false);
g_ScriptInterface.emplace("Engine", "GUIManager", *g_ScriptContext);
InitGraphics(g_AtlasGameLoop->args, g_InitFlags, {}, *g_ScriptContext, *g_ScriptInterface);
g_InputHandlers = InitGraphics(g_AtlasGameLoop->args, g_InitFlags, {}, *g_ScriptContext, *g_ScriptInterface);
}
@ -143,7 +144,7 @@ MESSAGEHANDLER(Shutdown)
AtlasView::DestroyViews();
g_AtlasGameLoop->view = AtlasView::GetView_None();
g_InputHandlers.reset();
ShutdownNetworkAndUI();
g_ScriptInterface.reset();
ShutdownConfigAndSubsequent();
@ -252,9 +253,8 @@ QUERYHANDLER(RenderLoop)
RendererIncrementalLoad();
// Pump SDL events (e.g. hotkeys)
SDL_Event ev{};
while (in_poll_priority_event(ev))
in_dispatch_event(ev);
for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents())
g_VideoMode.m_InputManager.DispatchEvent(ev);
if (g_GUI)
g_GUI->TickObjects();

View file

@ -26,6 +26,7 @@
#include "maths/MathUtil.h"
#include "ps/Game.h"
#include "ps/GameSetup/Config.h"
#include "ps/VideoMode.h"
#include "renderer/Renderer.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/components/ICmpSoundManager.h"
@ -118,7 +119,7 @@ MESSAGEHANDLER(GuiMouseButtonEvent)
msg->pos->GetScreenSpace(x, y);
ev.button.x = static_cast<u16>(Clamp<int>(x, 0, g_xres));
ev.button.y = static_cast<u16>(Clamp<int>(y, 0, g_yres));
in_dispatch_event(ev);
g_VideoMode.m_InputManager.DispatchEvent(ev);
}
MESSAGEHANDLER(GuiMouseMotionEvent)
@ -129,7 +130,7 @@ MESSAGEHANDLER(GuiMouseMotionEvent)
msg->pos->GetScreenSpace(x, y);
ev.motion.x = static_cast<u16>(Clamp<int>(x, 0, g_xres));
ev.motion.y = static_cast<u16>(Clamp<int>(y, 0, g_yres));
in_dispatch_event(ev);
g_VideoMode.m_InputManager.DispatchEvent(ev);
}
MESSAGEHANDLER(GuiKeyEvent)
@ -138,7 +139,7 @@ MESSAGEHANDLER(GuiKeyEvent)
ev.type = msg->pressed ? SDL_KEYDOWN : SDL_KEYUP;
ev.key.keysym.sym = static_cast<SDL_Keycode>(static_cast<int>(msg->sdlkey));
ev.key.keysym.scancode = SDL_GetScancodeFromKey(static_cast<SDL_Keycode>(static_cast<int>(msg->sdlkey)));
in_dispatch_event(ev);
g_VideoMode.m_InputManager.DispatchEvent(ev);
}
MESSAGEHANDLER(GuiCharEvent)
@ -151,13 +152,13 @@ MESSAGEHANDLER(GuiCharEvent)
ev.text.type = SDL_TEXTEDITING;
ev.text.text[0] = static_cast<char>(msg->sdlkey);
ev.text.text[1] = '\0';
in_dispatch_event(ev);
g_VideoMode.m_InputManager.DispatchEvent(ev);
ev.type = SDL_TEXTINPUT;
ev.text.type = SDL_TEXTINPUT;
ev.text.text[0] = static_cast<char>(msg->sdlkey);
ev.text.text[1] = '\0';
in_dispatch_event(ev);
g_VideoMode.m_InputManager.DispatchEvent(ev);
}
} // namespace AtlasMessage