refactor(event): let modules translate their own platform events
The SDL pump did the translation itself, so it had to know about keyboard, joystick, touch, window, graphics, audio and ui: eight upward dependencies from one of the engine's lowest modules, and the source of every remaining dependency cycle. Each module now registers an IPlatformEventSink for the events it owns and the pump only dispatches. Key and text events move to keyboard/sdl, joystick and game-controller events (including the hotplug device table) to joystick/sdl, finger events to touch/sdl, resize and the mobile background/foreground surface watch to window/sdl, the ImGui observer to ui, and Audio::pump to an end-of-pump hook in audio. The pump keeps only quit and window-close, which are about the event loop rather than any one module. Sinks run in priority order -- observer, then surface, then input -- so ImGui still sees events first and the window is resized before anything reads its size. The mobile surface teardown keeps using SDL_AddEventWatch rather than the poll path, since by the time SDL_PollEvent returns the native surface is gone; the watch is now installed by the window module on its first pump. Also removes exceptionIfInRenderPass, whose body has been commented out since it was written and whose only remaining effect was a graphics include. The engine dependency graph now has zero cycles, down from 26, and the back-edge whitelist is down to 2 (window -> graphics, window -> image). Layering is also right way up: event sits at L0 with the input modules above it at L1/L2. Verified: unit tests for event/input/ui/audio/thread pass, and the example smoke suite gives the same 19 pass / 9 fail as the pre-change baseline (those 9 fail on main too, from script API drift in the examples themselves). Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>
This commit is contained in:
10 files changed
+511
-279
No files matched your search
@@ -25,25 +25,14 @@ MODULES_DIR = os.path.join(REPO, "src", "modules")
|
||||
INCLUDE_RE = re.compile(r'^\s*#\s*include\s*[<"]([^">]+)[">]', re.M)
|
||||
SOURCE_EXT = (".cpp", ".cc", ".h", ".hpp")
|
||||
|
||||
# Upward edges that break the layering. Each entry is a known debt item to be
|
||||
# removed by the capability-registry work; the --check mode fails on anything
|
||||
# outside this list so no new back-edge slips in.
|
||||
# Upward edges that break the layering. Each entry is a known debt item; --check
|
||||
# fails on anything outside this list so no new back-edge slips in, and reports
|
||||
# entries that are no longer needed so the list shrinks as debt is paid off.
|
||||
KNOWN_BACK_EDGES = {
|
||||
# event/sdl/Event.cpp: the SDL pump pushes into its consumers instead of
|
||||
# letting them subscribe.
|
||||
("event", "audio"),
|
||||
("event", "filesystem"),
|
||||
("event", "graphics"),
|
||||
("event", "joystick"),
|
||||
("event", "keyboard"),
|
||||
("event", "touch"),
|
||||
("event", "ui"),
|
||||
("event", "window"),
|
||||
# window/Window.cpp, window/sdl/Window.cpp
|
||||
# window/Window.cpp, window/sdl/Window.cpp: the window owns the Graphics
|
||||
# pointer and decodes its own icon image.
|
||||
("window", "graphics"),
|
||||
("window", "image"),
|
||||
# ui/EditorHost.cpp: the headless editor host pumps events itself.
|
||||
("ui", "event"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Drives streaming-source refills once per event pump.
|
||||
//
|
||||
// Audio::pump has nothing to do with platform events, but it does need a
|
||||
// per-frame tick and the event pump was the only one available -- so the event
|
||||
// module called into audio. Hanging it off the sink's end-of-pump hook keeps
|
||||
// the timing identical without the dependency.
|
||||
|
||||
#include "audio/Audio.h"
|
||||
#include "common/Capability.h"
|
||||
#include "common/Module.h"
|
||||
#include "event/PlatformEventSink.h"
|
||||
|
||||
namespace eve::audio {
|
||||
namespace {
|
||||
|
||||
class AudioPumpSink : public eve::event::IPlatformEventSink {
|
||||
public:
|
||||
void onPumpFinished() override {
|
||||
if (auto *audio = eve::ModuleManager::getInstance<Audio>("Audio")) audio->pump();
|
||||
}
|
||||
};
|
||||
|
||||
struct Register {
|
||||
Register() {
|
||||
static AudioPumpSink sink;
|
||||
eve::cap::addListener<eve::event::IPlatformEventSink>(&sink);
|
||||
}
|
||||
} g_register;
|
||||
|
||||
} // namespace
|
||||
} // namespace eve::audio
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
// A module's hook into the raw platform event stream.
|
||||
//
|
||||
// The SDL pump used to translate every event itself, which meant it had to know
|
||||
// about keyboard, joystick, touch, window, graphics, audio and ui -- eight
|
||||
// upward dependencies from what should be one of the lowest modules, and the
|
||||
// last dependency cycle in the engine.
|
||||
//
|
||||
// Now each module registers a sink for the events it owns. The pump drains the
|
||||
// platform queue and offers each event to the sinks in priority order; whoever
|
||||
// recognises it translates it. Modules the build does not contain simply have
|
||||
// no sink, so the pump keeps working with any subset linked in.
|
||||
//
|
||||
// Providers depend on event (downwards, which is fine); event depends on none
|
||||
// of them.
|
||||
//
|
||||
// `nativeEvent` is the backend's own event object -- SDL_Event* for the SDL
|
||||
// backend. Only sinks built against the active backend are ever registered, so
|
||||
// the cast is safe; the sinks in question are themselves backend code
|
||||
// (keyboard/sdl, touch/sdl, ...).
|
||||
|
||||
#include "common/Capability.h"
|
||||
#include "common/Export.h"
|
||||
|
||||
namespace eve::event {
|
||||
|
||||
class Message;
|
||||
|
||||
class EVENGINE_API IPlatformEventSink {
|
||||
public:
|
||||
/** Dispatch order; lower runs first. */
|
||||
enum Priority {
|
||||
kObserver = 0, // sees everything before anyone claims it (UI)
|
||||
kSurface = 10, // window / swapchain lifecycle
|
||||
kInput = 20, // keyboard, joystick, touch
|
||||
};
|
||||
|
||||
static constexpr const char* capabilityName = "eve.event.IPlatformEventSink";
|
||||
virtual ~IPlatformEventSink() = default;
|
||||
|
||||
/**
|
||||
* Inspect an event before translation. Return true to consume it, so no
|
||||
* later sink sees it and no Message is queued.
|
||||
*/
|
||||
virtual bool observePlatformEvent(const void* nativeEvent) { return false; }
|
||||
|
||||
/**
|
||||
* Translate an event into a queued Message, updating any module state it
|
||||
* implies. Return nullptr to leave the event to the next sink. Ownership of
|
||||
* a returned Message passes to the caller.
|
||||
*/
|
||||
virtual Message* translatePlatformEvent(const void* nativeEvent) { return nullptr; }
|
||||
|
||||
/** Called once per pump() after the platform queue drains. */
|
||||
virtual void onPumpFinished() {}
|
||||
};
|
||||
|
||||
} // namespace eve::event
|
||||
+20
-262
@@ -1,118 +1,17 @@
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
#include "filesystem/Filesystem.h"
|
||||
#include "graphics/Graphics.h"
|
||||
#include "window/Window.h"
|
||||
#include "window/sdl/Window.h"
|
||||
#ifndef EVENGINE_WEBGPU
|
||||
#include "audio/Audio.h"
|
||||
#endif
|
||||
#include "touch/Touch.h"
|
||||
#include "touch/sdl/Touch.h"
|
||||
#include "keyboard/Keyboard.h"
|
||||
#include "joystick/Joystick.h"
|
||||
#include "joystick/Pad.h"
|
||||
#include "ui/UI.h"
|
||||
#include "common/Capability.h"
|
||||
#include "common/Exception.h"
|
||||
#include "common/Module.h"
|
||||
#include "common/config.h"
|
||||
#include "event/PlatformEventSink.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_vulkan.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace eve::event::sdl
|
||||
{
|
||||
|
||||
// SDL reports mouse coordinates in the window coordinate system in OS X, but
|
||||
// we want them in pixel coordinates (may be different with high-DPI enabled.)
|
||||
static void windowToDPICoords(double *x, double *y)
|
||||
{
|
||||
auto window = getModInst(window,Window);
|
||||
// if (window)
|
||||
// window->windowToDPICoords(x, y);
|
||||
}
|
||||
|
||||
#ifndef EVENGINE_MACOSX
|
||||
static void normalizedToDPICoords(double *x, double *y)
|
||||
{
|
||||
double w = 1.0, h = 1.0;
|
||||
|
||||
auto window = getModInst(window,Window);
|
||||
if (window)
|
||||
{
|
||||
w = window->getWidth();
|
||||
h = window->getHeight();
|
||||
// window->windowToDPICoords(&w, &h);
|
||||
}
|
||||
|
||||
if (x)
|
||||
*x = ((*x) * w);
|
||||
if (y)
|
||||
*y = ((*y) * h);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void syncWindowPixelSize()
|
||||
{
|
||||
auto *win = getModInst(window, Window);
|
||||
auto *gfx = getModInst(graphics, Graphics);
|
||||
if (!win || !gfx)
|
||||
return;
|
||||
auto *sdlWin = dynamic_cast<eve::window::sdl::Window *>(win);
|
||||
if (!sdlWin)
|
||||
return;
|
||||
SDL_Window *native = static_cast<SDL_Window *>(sdlWin->getHandle());
|
||||
if (!native)
|
||||
return;
|
||||
|
||||
int lw = 0, lh = 0, pw = 0, ph = 0;
|
||||
SDL_GetWindowSize(native, &lw, &lh);
|
||||
SDL_Vulkan_GetDrawableSize(native, &pw, &ph);
|
||||
if (pw <= 0 || ph <= 0) {
|
||||
pw = lw;
|
||||
ph = lh;
|
||||
}
|
||||
window::WindowSettings s = win->getWindowSettings();
|
||||
s.width = static_cast<uint16_t>(std::max(lw, 1));
|
||||
s.height = static_cast<uint16_t>(std::max(lh, 1));
|
||||
// Keep settings + graphics viewport aligned after rotation / resume.
|
||||
sdlWin->updateSettings(s, true);
|
||||
}
|
||||
|
||||
// SDL's event watch callbacks trigger when the event is actually posted inside
|
||||
// SDL, unlike with SDL_PollEvents. This is useful for some events which require
|
||||
// handling inside the function which triggered them on some backends.
|
||||
static int SDLCALL watchAppEvents(void * /*udata*/, SDL_Event *event)
|
||||
{
|
||||
auto gfx = getModInst(graphics,Graphics);
|
||||
|
||||
switch (event->type)
|
||||
{
|
||||
case SDL_APP_DIDENTERBACKGROUND:
|
||||
// Stop presenting: the native surface is being torn down.
|
||||
if (gfx)
|
||||
gfx->setActive(false);
|
||||
break;
|
||||
case SDL_APP_WILLENTERFOREGROUND:
|
||||
case SDL_APP_DIDENTERFOREGROUND:
|
||||
// The native window is recreated on resume; rebuild the render surface
|
||||
// and swapchain, then resume presenting.
|
||||
if (gfx) {
|
||||
gfx->requestSurfaceRecreate();
|
||||
gfx->setActive(true);
|
||||
syncWindowPixelSize();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
Event::Event()
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_EVENTS) < 0)
|
||||
@@ -122,42 +21,31 @@ Event::Event()
|
||||
SDL_QuitSubSystem(SDL_INIT_EVENTS);
|
||||
throw eve::Exception("Could not reserve SDL wake event (%s)", SDL_GetError());
|
||||
}
|
||||
|
||||
SDL_AddEventWatch(watchAppEvents, this);
|
||||
}
|
||||
|
||||
Event::~Event()
|
||||
{
|
||||
SDL_DelEventWatch(watchAppEvents, this);
|
||||
SDL_QuitSubSystem(SDL_INIT_EVENTS);
|
||||
}
|
||||
|
||||
void Event::pump()
|
||||
{
|
||||
exceptionIfInRenderPass("eve.event.pump");
|
||||
|
||||
SDL_Event e;
|
||||
|
||||
while (SDL_PollEvent(&e))
|
||||
{
|
||||
if (auto *ui = eve::ModuleManager::getInstance<eve::ui::UI>("UI"))
|
||||
ui->processEvent(&e);
|
||||
Message *msg = convert(e);
|
||||
if (msg)
|
||||
push(msg);
|
||||
// Ownership stays in the queue until poll()/clear() — do not delete here.
|
||||
}
|
||||
|
||||
#ifndef EVENGINE_WEBGPU
|
||||
if (auto *audio = eve::ModuleManager::getInstance<eve::audio::Audio>("Audio"))
|
||||
audio->pump();
|
||||
#endif
|
||||
// Per-frame work that modules hang off the pump (audio streaming, ...).
|
||||
cap::forEach<IPlatformEventSink>([](IPlatformEventSink *sink) { sink->onPumpFinished(); });
|
||||
}
|
||||
|
||||
Message *Event::wait()
|
||||
{
|
||||
exceptionIfInRenderPass("eve.event.wait");
|
||||
|
||||
for (;;) {
|
||||
if (Message *queued = poll())
|
||||
return queued;
|
||||
@@ -184,8 +72,6 @@ void Event::wakeWaiters()
|
||||
|
||||
void Event::clear()
|
||||
{
|
||||
exceptionIfInRenderPass("eve.event.clear");
|
||||
|
||||
SDL_Event e;
|
||||
|
||||
while (SDL_PollEvent(&e))
|
||||
@@ -196,158 +82,30 @@ void Event::clear()
|
||||
eve::event::Event::clear();
|
||||
}
|
||||
|
||||
void Event::exceptionIfInRenderPass(const char *name)
|
||||
{
|
||||
// Some core OS graphics functionality (e.g. swap buffers on some platforms)
|
||||
// happens inside SDL_PumpEvents - which is called by SDL_PollEvent and
|
||||
// friends. It's probably a bad idea to call those functions while a Canvas
|
||||
// is active.
|
||||
auto gfx = getModInst(graphics,Graphics);
|
||||
// if (gfx != nullptr && gfx->isCanvasActive())
|
||||
// throw eve::Exception("%s cannot be called while a Canvas is active in eve.graphics.", name);
|
||||
}
|
||||
|
||||
Message *Event::convert(const SDL_Event &e)
|
||||
{
|
||||
// Modules that own an event family translate it themselves; see
|
||||
// event/PlatformEventSink.h. Only window-close and quit are handled here,
|
||||
// because they are about the event loop rather than any one module.
|
||||
Message *translated = nullptr;
|
||||
const bool consumed = cap::forEachUntil<IPlatformEventSink>([&](IPlatformEventSink *sink) {
|
||||
if (sink->observePlatformEvent(&e))
|
||||
return true;
|
||||
translated = sink->translatePlatformEvent(&e);
|
||||
return translated != nullptr;
|
||||
});
|
||||
if (translated)
|
||||
return translated;
|
||||
if (consumed)
|
||||
return nullptr;
|
||||
|
||||
switch (e.type) {
|
||||
case SDL_QUIT:
|
||||
return new Message("quit");
|
||||
case SDL_WINDOWEVENT:
|
||||
switch (e.window.event) {
|
||||
case SDL_WINDOWEVENT_CLOSE:
|
||||
if (e.window.event == SDL_WINDOWEVENT_CLOSE)
|
||||
return new Message("quit");
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
case SDL_WINDOWEVENT_RESIZED:
|
||||
syncWindowPixelSize();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SDL_KEYDOWN: {
|
||||
if (e.key.repeat) {
|
||||
auto *kb = getModInst(keyboard, Keyboard);
|
||||
if (kb && !kb->hasKeyRepeat())
|
||||
break;
|
||||
}
|
||||
const char *keyName = SDL_GetKeyName(e.key.keysym.sym);
|
||||
const char *scanName = SDL_GetScancodeName(e.key.keysym.scancode);
|
||||
return new Message("keypressed",
|
||||
{Variant::makeString(keyName ? keyName : ""),
|
||||
Variant::makeString(scanName ? scanName : ""),
|
||||
Variant::makeInt(e.key.repeat ? 1 : 0)});
|
||||
}
|
||||
case SDL_KEYUP: {
|
||||
const char *keyName = SDL_GetKeyName(e.key.keysym.sym);
|
||||
const char *scanName = SDL_GetScancodeName(e.key.keysym.scancode);
|
||||
return new Message("keyreleased",
|
||||
{Variant::makeString(keyName ? keyName : ""),
|
||||
Variant::makeString(scanName ? scanName : "")});
|
||||
}
|
||||
case SDL_TEXTINPUT:
|
||||
return new Message("textinput", {Variant::makeString(e.text.text)});
|
||||
case SDL_TEXTEDITING:
|
||||
return new Message("textedited",
|
||||
{Variant::makeString(e.edit.text),
|
||||
Variant::makeInt(e.edit.start),
|
||||
Variant::makeInt(e.edit.length)});
|
||||
case SDL_JOYDEVICEADDED: {
|
||||
auto *joy = getModInst(joystick, Joystick);
|
||||
if (!joy) break;
|
||||
auto *pad = joy->addJoystick(e.jdevice.which);
|
||||
if (!pad) break;
|
||||
return new Message("joystickadded",
|
||||
{Variant::makeInt(pad->getID()),
|
||||
Variant::makeString(pad->getName()),
|
||||
Variant::makeInt(pad->isGamepad() ? 1 : 0)});
|
||||
}
|
||||
case SDL_JOYDEVICEREMOVED: {
|
||||
auto *joy = getModInst(joystick, Joystick);
|
||||
if (!joy) break;
|
||||
auto *pad = joy->getJoystickFromID(e.jdevice.which);
|
||||
if (!pad) break;
|
||||
int id = pad->getID();
|
||||
std::string name = pad->getName();
|
||||
joy->removeJoystick(pad);
|
||||
return new Message("joystickremoved",
|
||||
{Variant::makeInt(id), Variant::makeString(name)});
|
||||
}
|
||||
case SDL_JOYBUTTONDOWN:
|
||||
case SDL_JOYBUTTONUP: {
|
||||
auto *joy = getModInst(joystick, Joystick);
|
||||
if (!joy) break;
|
||||
auto *pad = joy->getJoystickFromID(e.jbutton.which);
|
||||
if (!pad) break;
|
||||
return new Message(e.type == SDL_JOYBUTTONDOWN ? "joystickpressed" : "joystickreleased",
|
||||
{Variant::makeInt(pad->getID()), Variant::makeInt(e.jbutton.button)});
|
||||
}
|
||||
case SDL_JOYAXISMOTION: {
|
||||
auto *joy = getModInst(joystick, Joystick);
|
||||
if (!joy) break;
|
||||
auto *pad = joy->getJoystickFromID(e.jaxis.which);
|
||||
if (!pad) break;
|
||||
float value = eve::joystick::clampAxis(e.jaxis.value / 32768.0f);
|
||||
return new Message("joystickaxis",
|
||||
{Variant::makeInt(pad->getID()), Variant::makeInt(e.jaxis.axis),
|
||||
Variant::makeInt(static_cast<int64_t>(std::lround(value * 1000.0f)))});
|
||||
}
|
||||
case SDL_JOYHATMOTION: {
|
||||
auto *joy = getModInst(joystick, Joystick);
|
||||
if (!joy) break;
|
||||
auto *pad = joy->getJoystickFromID(e.jhat.which);
|
||||
if (!pad) break;
|
||||
std::string hat = pad->getHat(e.jhat.hat);
|
||||
// Prefer live hat from the event mask via re-query; getHat reads current state.
|
||||
return new Message("joystickhat",
|
||||
{Variant::makeInt(pad->getID()), Variant::makeInt(e.jhat.hat),
|
||||
Variant::makeString(hat)});
|
||||
}
|
||||
case SDL_CONTROLLERBUTTONDOWN:
|
||||
case SDL_CONTROLLERBUTTONUP: {
|
||||
auto *joy = getModInst(joystick, Joystick);
|
||||
if (!joy) break;
|
||||
auto *pad = joy->getJoystickFromID(e.cbutton.which);
|
||||
if (!pad) break;
|
||||
const char *btn =
|
||||
SDL_GameControllerGetStringForButton(static_cast<SDL_GameControllerButton>(e.cbutton.button));
|
||||
return new Message(e.type == SDL_CONTROLLERBUTTONDOWN ? "gamepadpressed" : "gamepadreleased",
|
||||
{Variant::makeInt(pad->getID()),
|
||||
Variant::makeString(btn ? btn : "")});
|
||||
}
|
||||
case SDL_CONTROLLERAXISMOTION: {
|
||||
auto *joy = getModInst(joystick, Joystick);
|
||||
if (!joy) break;
|
||||
auto *pad = joy->getJoystickFromID(e.caxis.which);
|
||||
if (!pad) break;
|
||||
const char *axis =
|
||||
SDL_GameControllerGetStringForAxis(static_cast<SDL_GameControllerAxis>(e.caxis.axis));
|
||||
float value = eve::joystick::clampAxis(e.caxis.value / 32768.0f);
|
||||
return new Message("gamepadaxis",
|
||||
{Variant::makeInt(pad->getID()),
|
||||
Variant::makeString(axis ? axis : ""),
|
||||
Variant::makeInt(static_cast<int64_t>(std::lround(value * 1000.0f)))});
|
||||
}
|
||||
case SDL_FINGERDOWN:
|
||||
case SDL_FINGERUP:
|
||||
case SDL_FINGERMOTION: {
|
||||
auto *touchMod = dynamic_cast<eve::touch::sdl::Touch *>(
|
||||
eve::ModuleManager::getInstance<eve::touch::Touch>("Touch"));
|
||||
if (touchMod) {
|
||||
eve::touch::Touch::TouchInfo info{};
|
||||
info.id = static_cast<int64_t>(e.tfinger.fingerId);
|
||||
info.x = e.tfinger.x;
|
||||
info.y = e.tfinger.y;
|
||||
info.dx = e.tfinger.dx;
|
||||
info.dy = e.tfinger.dy;
|
||||
info.pressure = e.tfinger.pressure;
|
||||
#ifndef EVENGINE_MACOSX
|
||||
normalizedToDPICoords(&info.x, &info.y);
|
||||
normalizedToDPICoords(&info.dx, &info.dy);
|
||||
#endif
|
||||
touchMod->onEvent(e.type, info);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public:
|
||||
|
||||
private:
|
||||
void wakeWaiters() override;
|
||||
void exceptionIfInRenderPass(const char *name);
|
||||
/** Offers the event to the registered sinks, then handles quit itself. */
|
||||
Message *convert(const SDL_Event &e);
|
||||
Uint32 wakeEventType_ = static_cast<Uint32>(-1);
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// Translates SDL joystick and game-controller events into engine messages, and
|
||||
// keeps the module's device table in sync on hotplug.
|
||||
//
|
||||
// This lived in the SDL event pump, which therefore had to know about the
|
||||
// Joystick module and its Pad type. Owning it here removes that edge.
|
||||
|
||||
#include "common/Capability.h"
|
||||
#include "common/Module.h"
|
||||
#include "event/Event.h"
|
||||
#include "event/PlatformEventSink.h"
|
||||
#include "joystick/Joystick.h"
|
||||
#include "joystick/Pad.h"
|
||||
|
||||
#include <SDL2/SDL_events.h>
|
||||
#include <SDL2/SDL_gamecontroller.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace eve::joystick::sdl {
|
||||
namespace {
|
||||
|
||||
using eve::event::Message;
|
||||
using eve::event::Variant;
|
||||
|
||||
/** Axis values travel as thousandths so the message stays integral. */
|
||||
int64_t axisMilli(int16_t raw) {
|
||||
return static_cast<int64_t>(std::lround(clampAxis(raw / 32768.0f) * 1000.0f));
|
||||
}
|
||||
|
||||
class JoystickEventSink : public eve::event::IPlatformEventSink {
|
||||
public:
|
||||
Message *translatePlatformEvent(const void *nativeEvent) override {
|
||||
const auto &e = *static_cast<const SDL_Event *>(nativeEvent);
|
||||
auto *joy = eve::ModuleManager::getInstance<Joystick>("Joystick");
|
||||
if (!joy) return nullptr;
|
||||
|
||||
switch (e.type) {
|
||||
case SDL_JOYDEVICEADDED: {
|
||||
auto *pad = joy->addJoystick(e.jdevice.which);
|
||||
if (!pad) return nullptr;
|
||||
return new Message("joystickadded",
|
||||
{Variant::makeInt(pad->getID()),
|
||||
Variant::makeString(pad->getName()),
|
||||
Variant::makeInt(pad->isGamepad() ? 1 : 0)});
|
||||
}
|
||||
case SDL_JOYDEVICEREMOVED: {
|
||||
auto *pad = joy->getJoystickFromID(e.jdevice.which);
|
||||
if (!pad) return nullptr;
|
||||
const int id = pad->getID();
|
||||
const std::string name = pad->getName();
|
||||
joy->removeJoystick(pad);
|
||||
return new Message("joystickremoved",
|
||||
{Variant::makeInt(id), Variant::makeString(name)});
|
||||
}
|
||||
case SDL_JOYBUTTONDOWN:
|
||||
case SDL_JOYBUTTONUP: {
|
||||
auto *pad = joy->getJoystickFromID(e.jbutton.which);
|
||||
if (!pad) return nullptr;
|
||||
return new Message(
|
||||
e.type == SDL_JOYBUTTONDOWN ? "joystickpressed" : "joystickreleased",
|
||||
{Variant::makeInt(pad->getID()), Variant::makeInt(e.jbutton.button)});
|
||||
}
|
||||
case SDL_JOYAXISMOTION: {
|
||||
auto *pad = joy->getJoystickFromID(e.jaxis.which);
|
||||
if (!pad) return nullptr;
|
||||
return new Message("joystickaxis",
|
||||
{Variant::makeInt(pad->getID()), Variant::makeInt(e.jaxis.axis),
|
||||
Variant::makeInt(axisMilli(e.jaxis.value))});
|
||||
}
|
||||
case SDL_JOYHATMOTION: {
|
||||
auto *pad = joy->getJoystickFromID(e.jhat.which);
|
||||
if (!pad) return nullptr;
|
||||
return new Message("joystickhat",
|
||||
{Variant::makeInt(pad->getID()), Variant::makeInt(e.jhat.hat),
|
||||
Variant::makeString(pad->getHat(e.jhat.hat))});
|
||||
}
|
||||
case SDL_CONTROLLERBUTTONDOWN:
|
||||
case SDL_CONTROLLERBUTTONUP: {
|
||||
auto *pad = joy->getJoystickFromID(e.cbutton.which);
|
||||
if (!pad) return nullptr;
|
||||
const char *btn = SDL_GameControllerGetStringForButton(
|
||||
static_cast<SDL_GameControllerButton>(e.cbutton.button));
|
||||
return new Message(
|
||||
e.type == SDL_CONTROLLERBUTTONDOWN ? "gamepadpressed" : "gamepadreleased",
|
||||
{Variant::makeInt(pad->getID()), Variant::makeString(btn ? btn : "")});
|
||||
}
|
||||
case SDL_CONTROLLERAXISMOTION: {
|
||||
auto *pad = joy->getJoystickFromID(e.caxis.which);
|
||||
if (!pad) return nullptr;
|
||||
const char *axis = SDL_GameControllerGetStringForAxis(
|
||||
static_cast<SDL_GameControllerAxis>(e.caxis.axis));
|
||||
return new Message("gamepadaxis", {Variant::makeInt(pad->getID()),
|
||||
Variant::makeString(axis ? axis : ""),
|
||||
Variant::makeInt(axisMilli(e.caxis.value))});
|
||||
}
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct Register {
|
||||
Register() {
|
||||
static JoystickEventSink sink;
|
||||
eve::cap::addListener<eve::event::IPlatformEventSink>(
|
||||
&sink, eve::event::IPlatformEventSink::kInput);
|
||||
}
|
||||
} g_register;
|
||||
|
||||
} // namespace
|
||||
} // namespace eve::joystick::sdl
|
||||
@@ -0,0 +1,67 @@
|
||||
// Translates SDL key and text events into engine messages.
|
||||
//
|
||||
// This lived in the SDL event pump, which had to consult the keyboard module
|
||||
// for the key-repeat setting. Owning the translation here removes that edge.
|
||||
|
||||
#include "common/Capability.h"
|
||||
#include "common/Module.h"
|
||||
#include "event/Event.h"
|
||||
#include "event/PlatformEventSink.h"
|
||||
#include "keyboard/Keyboard.h"
|
||||
|
||||
#include <SDL2/SDL_events.h>
|
||||
#include <SDL2/SDL_keyboard.h>
|
||||
|
||||
namespace eve::keyboard::sdl {
|
||||
namespace {
|
||||
|
||||
using eve::event::Message;
|
||||
using eve::event::Variant;
|
||||
|
||||
class KeyboardEventSink : public eve::event::IPlatformEventSink {
|
||||
public:
|
||||
Message *translatePlatformEvent(const void *nativeEvent) override {
|
||||
const auto &e = *static_cast<const SDL_Event *>(nativeEvent);
|
||||
switch (e.type) {
|
||||
case SDL_KEYDOWN: {
|
||||
// Repeats are dropped unless the module opted into them.
|
||||
if (e.key.repeat) {
|
||||
auto *kb = eve::ModuleManager::getInstance<Keyboard>("Keyboard");
|
||||
if (kb && !kb->hasKeyRepeat()) return nullptr;
|
||||
}
|
||||
const char *keyName = SDL_GetKeyName(e.key.keysym.sym);
|
||||
const char *scanName = SDL_GetScancodeName(e.key.keysym.scancode);
|
||||
return new Message("keypressed",
|
||||
{Variant::makeString(keyName ? keyName : ""),
|
||||
Variant::makeString(scanName ? scanName : ""),
|
||||
Variant::makeInt(e.key.repeat ? 1 : 0)});
|
||||
}
|
||||
case SDL_KEYUP: {
|
||||
const char *keyName = SDL_GetKeyName(e.key.keysym.sym);
|
||||
const char *scanName = SDL_GetScancodeName(e.key.keysym.scancode);
|
||||
return new Message("keyreleased",
|
||||
{Variant::makeString(keyName ? keyName : ""),
|
||||
Variant::makeString(scanName ? scanName : "")});
|
||||
}
|
||||
case SDL_TEXTINPUT:
|
||||
return new Message("textinput", {Variant::makeString(e.text.text)});
|
||||
case SDL_TEXTEDITING:
|
||||
return new Message("textedited", {Variant::makeString(e.edit.text),
|
||||
Variant::makeInt(e.edit.start),
|
||||
Variant::makeInt(e.edit.length)});
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct Register {
|
||||
Register() {
|
||||
static KeyboardEventSink sink;
|
||||
eve::cap::addListener<eve::event::IPlatformEventSink>(
|
||||
&sink, eve::event::IPlatformEventSink::kInput);
|
||||
}
|
||||
} g_register;
|
||||
|
||||
} // namespace
|
||||
} // namespace eve::keyboard::sdl
|
||||
@@ -0,0 +1,73 @@
|
||||
// Feeds SDL finger events into the touch module's press state.
|
||||
//
|
||||
// This lived in the SDL event pump, which therefore had to know about the touch
|
||||
// module and its SDL backend. Owning it here removes that edge.
|
||||
//
|
||||
// SDL also exposes touch state through query functions, but some backends
|
||||
// update those on another thread, so press state is only ever advanced from
|
||||
// here.
|
||||
|
||||
#include "common/Capability.h"
|
||||
#include "common/Module.h"
|
||||
#include "common/config.h"
|
||||
#include "event/PlatformEventSink.h"
|
||||
#include "touch/Touch.h"
|
||||
#include "touch/sdl/Touch.h"
|
||||
#include "window/Window.h"
|
||||
|
||||
#include <SDL2/SDL_events.h>
|
||||
|
||||
namespace eve::touch::sdl {
|
||||
namespace {
|
||||
|
||||
#ifndef EVENGINE_MACOSX
|
||||
/** SDL reports normalized finger coordinates; the engine works in pixels. */
|
||||
void normalizedToDPICoords(double *x, double *y) {
|
||||
double w = 1.0, h = 1.0;
|
||||
if (auto *win = eve::ModuleManager::getInstance<eve::window::Window>("Window")) {
|
||||
w = win->getWidth();
|
||||
h = win->getHeight();
|
||||
}
|
||||
if (x) *x = (*x) * w;
|
||||
if (y) *y = (*y) * h;
|
||||
}
|
||||
#endif
|
||||
|
||||
class TouchEventSink : public eve::event::IPlatformEventSink {
|
||||
public:
|
||||
bool observePlatformEvent(const void *nativeEvent) override {
|
||||
const auto &e = *static_cast<const SDL_Event *>(nativeEvent);
|
||||
if (e.type != SDL_FINGERDOWN && e.type != SDL_FINGERUP && e.type != SDL_FINGERMOTION)
|
||||
return false;
|
||||
|
||||
auto *touchMod = dynamic_cast<Touch *>(
|
||||
eve::ModuleManager::getInstance<eve::touch::Touch>("Touch"));
|
||||
if (!touchMod) return false;
|
||||
|
||||
eve::touch::Touch::TouchInfo info{};
|
||||
info.id = static_cast<int64_t>(e.tfinger.fingerId);
|
||||
info.x = e.tfinger.x;
|
||||
info.y = e.tfinger.y;
|
||||
info.dx = e.tfinger.dx;
|
||||
info.dy = e.tfinger.dy;
|
||||
info.pressure = e.tfinger.pressure;
|
||||
#ifndef EVENGINE_MACOSX
|
||||
normalizedToDPICoords(&info.x, &info.y);
|
||||
normalizedToDPICoords(&info.dx, &info.dy);
|
||||
#endif
|
||||
touchMod->onEvent(e.type, info);
|
||||
// Finger events update state only; they produce no queued Message.
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct Register {
|
||||
Register() {
|
||||
static TouchEventSink sink;
|
||||
eve::cap::addListener<eve::event::IPlatformEventSink>(
|
||||
&sink, eve::event::IPlatformEventSink::kInput);
|
||||
}
|
||||
} g_register;
|
||||
|
||||
} // namespace
|
||||
} // namespace eve::touch::sdl
|
||||
@@ -0,0 +1,36 @@
|
||||
// Lets ImGui see platform events before anything else claims them.
|
||||
//
|
||||
// The SDL event pump used to call UI::processEvent directly, which was one of
|
||||
// the eight modules it had to know about.
|
||||
|
||||
#include "common/Capability.h"
|
||||
#include "common/Module.h"
|
||||
#include "event/PlatformEventSink.h"
|
||||
#include "ui/UI.h"
|
||||
|
||||
#include <SDL2/SDL_events.h>
|
||||
|
||||
namespace eve::ui {
|
||||
namespace {
|
||||
|
||||
class UIEventSink : public eve::event::IPlatformEventSink {
|
||||
public:
|
||||
bool observePlatformEvent(const void *nativeEvent) override {
|
||||
if (auto *ui = eve::ModuleManager::getInstance<UI>("UI"))
|
||||
ui->processEvent(static_cast<const SDL_Event *>(nativeEvent));
|
||||
// ImGui records the event for its own state but does not claim it; the
|
||||
// game still receives the matching message.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct Register {
|
||||
Register() {
|
||||
static UIEventSink sink;
|
||||
eve::cap::addListener<eve::event::IPlatformEventSink>(
|
||||
&sink, eve::event::IPlatformEventSink::kObserver);
|
||||
}
|
||||
} g_register;
|
||||
|
||||
} // namespace
|
||||
} // namespace eve::ui
|
||||
@@ -0,0 +1,108 @@
|
||||
// Keeps the window settings and the render surface aligned with the platform.
|
||||
//
|
||||
// Two things used to live in the SDL event pump and forced it to depend on both
|
||||
// window and graphics: resize handling, and the app background/foreground
|
||||
// callback that tears down and rebuilds the Vulkan surface on mobile.
|
||||
|
||||
#include "common/Capability.h"
|
||||
#include "common/Module.h"
|
||||
#include "event/PlatformEventSink.h"
|
||||
#include "graphics/Graphics.h"
|
||||
#include "window/Window.h"
|
||||
#include "window/sdl/Window.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_vulkan.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace eve::window::sdl {
|
||||
namespace {
|
||||
|
||||
/** Re-reads the real window size after a resize, rotation or resume. */
|
||||
void syncWindowPixelSize() {
|
||||
auto *win = eve::ModuleManager::getInstance<eve::window::Window>("Window");
|
||||
auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>("Graphics");
|
||||
if (!win || !gfx) return;
|
||||
auto *sdlWin = dynamic_cast<Window *>(win);
|
||||
if (!sdlWin) return;
|
||||
SDL_Window *native = static_cast<SDL_Window *>(sdlWin->getHandle());
|
||||
if (!native) return;
|
||||
|
||||
int lw = 0, lh = 0, pw = 0, ph = 0;
|
||||
SDL_GetWindowSize(native, &lw, &lh);
|
||||
SDL_Vulkan_GetDrawableSize(native, &pw, &ph);
|
||||
if (pw <= 0 || ph <= 0) {
|
||||
pw = lw;
|
||||
ph = lh;
|
||||
}
|
||||
WindowSettings s = win->getWindowSettings();
|
||||
s.width = static_cast<uint16_t>(std::max(lw, 1));
|
||||
s.height = static_cast<uint16_t>(std::max(lh, 1));
|
||||
// Keep settings + graphics viewport aligned after rotation / resume.
|
||||
sdlWin->updateSettings(s, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* SDL event watches fire the moment the event is posted inside SDL, unlike
|
||||
* SDL_PollEvent. The mobile background/foreground transitions have to be
|
||||
* handled there: by the time the poll loop runs, the native surface is already
|
||||
* gone.
|
||||
*/
|
||||
int SDLCALL watchAppEvents(void * /*udata*/, SDL_Event *event) {
|
||||
auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>("Graphics");
|
||||
if (!gfx) return 1;
|
||||
|
||||
switch (event->type) {
|
||||
case SDL_APP_DIDENTERBACKGROUND:
|
||||
// Stop presenting: the native surface is being torn down.
|
||||
gfx->setActive(false);
|
||||
break;
|
||||
case SDL_APP_WILLENTERFOREGROUND:
|
||||
case SDL_APP_DIDENTERFOREGROUND:
|
||||
// The native window is recreated on resume; rebuild the render
|
||||
// surface and swapchain, then resume presenting.
|
||||
gfx->requestSurfaceRecreate();
|
||||
gfx->setActive(true);
|
||||
syncWindowPixelSize();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
class WindowEventSink : public eve::event::IPlatformEventSink {
|
||||
public:
|
||||
bool observePlatformEvent(const void *nativeEvent) override {
|
||||
const auto &e = *static_cast<const SDL_Event *>(nativeEvent);
|
||||
if (e.type == SDL_WINDOWEVENT && (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED ||
|
||||
e.window.event == SDL_WINDOWEVENT_RESIZED))
|
||||
syncWindowPixelSize();
|
||||
// Never consume: SDL_WINDOWEVENT_CLOSE still has to reach the pump,
|
||||
// which turns it into "quit".
|
||||
return false;
|
||||
}
|
||||
|
||||
void onPumpFinished() override {
|
||||
// Deferred to the first pump because the watch can only be added once
|
||||
// SDL is initialized, which is later than static registration.
|
||||
if (watchInstalled_) return;
|
||||
watchInstalled_ = true;
|
||||
SDL_AddEventWatch(watchAppEvents, nullptr);
|
||||
}
|
||||
|
||||
private:
|
||||
bool watchInstalled_ = false;
|
||||
};
|
||||
|
||||
struct Register {
|
||||
Register() {
|
||||
static WindowEventSink sink;
|
||||
eve::cap::addListener<eve::event::IPlatformEventSink>(
|
||||
&sink, eve::event::IPlatformEventSink::kSurface);
|
||||
}
|
||||
} g_register;
|
||||
|
||||
} // namespace
|
||||
} // namespace eve::window::sdl
|
||||
Reference in new issue
Block a user