Files
InstanceBrick--nightcap/include/mocktail/platform/platform_runtime.h
T
vii 66de662e5b fix(input): decide pointer capture from state, not coordinates
The router inferred relative mode from SDL reporting zero absolute
coordinates. X11 does; Wayland reports a position that accumulates the
deltas and leaves the window, so every camera-drag event took the
absolute branch and walked Roblox's cursor to the viewport edge.

Motion, button and wheel events now carry the window's relative mouse
mode, and the RMB camera-drag fallback only captures inside an
experience so the app surface keeps a free cursor.
2026-09-20 12:05:48 -05:00

267 lines
7.3 KiB
C++

#ifndef MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_
#define MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_
#include <cmath>
#include <cstdint>
#include <string>
#include <variant>
#include "mocktail/status.h"
namespace mocktail {
namespace platform {
enum class WindowSurfaceApi {
kAngleEgl = 0,
kDirectVulkan,
};
enum class NativeWindowSystem {
kUnknown = 0,
kX11,
kWayland,
};
struct WindowOptions {
int width = 1280;
int height = 720;
std::string title = "Roblox";
WindowSurfaceApi surface_api = WindowSurfaceApi::kAngleEgl;
bool high_pixel_density = false;
bool initially_hidden = true;
// Production callers should keep this true. Tests using SDL's dummy driver
// may opt out because that driver intentionally has no native WSI handle.
bool require_native_window = true;
};
// Non-owning handles captured from SDL window properties. backend_window is
// an opaque SDL_Window pointer used only by SDL-specific graphics adapters.
struct NativeWindowDescriptor {
NativeWindowSystem window_system = NativeWindowSystem::kUnknown;
WindowSurfaceApi surface_api = WindowSurfaceApi::kAngleEgl;
void* backend_window = nullptr;
void* native_display = nullptr;
std::uintptr_t native_window = 0;
void* native_surface = nullptr;
void* native_egl_window = nullptr;
bool HasPlatformHandle() const {
if (window_system == NativeWindowSystem::kX11) {
return native_display != nullptr && native_window != 0;
}
if (window_system == NativeWindowSystem::kWayland) {
return native_display != nullptr && native_surface != nullptr;
}
return false;
}
};
struct QuitEvent {};
// Android exposes pointer and TextBox geometry in density-independent surface
// coordinates. SDL exposes pointer positions in logical window coordinates,
// while Vulkan presents to the pixel-sized drawable. Keep all three spaces in
// one value so input, IME placement and same-surface overlays use exact inverse
// transforms.
struct SurfaceCoordinateTransform {
int32_t logical_width = 0;
int32_t logical_height = 0;
int32_t pixel_width = 0;
int32_t pixel_height = 0;
float density_scale = 1.0F;
bool valid() const {
return logical_width > 0 && logical_height > 0 && pixel_width > 0 &&
pixel_height > 0 && std::isfinite(density_scale) &&
density_scale > 0.0F;
}
float guest_width() const {
return valid() ? static_cast<float>(pixel_width) / density_scale : 0.0F;
}
float guest_height() const {
return valid() ? static_cast<float>(pixel_height) / density_scale : 0.0F;
}
int32_t rounded_guest_width() const {
return valid() ? static_cast<int32_t>(std::lround(guest_width())) : 0;
}
int32_t rounded_guest_height() const {
return valid() ? static_cast<int32_t>(std::lround(guest_height())) : 0;
}
float HostLogicalToGuestX(float coordinate) const {
return valid() ? coordinate * static_cast<float>(pixel_width) /
static_cast<float>(logical_width) / density_scale
: coordinate;
}
float HostLogicalToGuestY(float coordinate) const {
return valid() ? coordinate * static_cast<float>(pixel_height) /
static_cast<float>(logical_height) / density_scale
: coordinate;
}
float GuestToHostLogicalX(float coordinate) const {
return valid() ? coordinate * density_scale *
static_cast<float>(logical_width) /
static_cast<float>(pixel_width)
: coordinate;
}
float GuestToHostLogicalY(float coordinate) const {
return valid() ? coordinate * density_scale *
static_cast<float>(logical_height) /
static_cast<float>(pixel_height)
: coordinate;
}
};
struct WindowResizedEvent {
int logical_width = 0;
int logical_height = 0;
int pixel_width = 0;
int pixel_height = 0;
// Android DisplayMetrics.density equivalent. This is independent of the
// logical-to-pixel drawable ratio on X11/XWayland.
float density_scale = 1.0F;
};
struct WindowFocusEvent {
bool focused = false;
};
struct KeyEvent {
bool pressed = false;
bool repeat = false;
std::uint32_t scancode = 0;
std::uint32_t keycode = 0;
std::uint32_t modifiers = 0;
};
struct TextInputEvent {
std::string text;
};
struct TextEditingEvent {
std::string text;
int start = 0;
int length = 0;
};
struct MouseMotionEvent {
float x = 0.0f;
float y = 0.0f;
float delta_x = 0.0f;
float delta_y = 0.0f;
std::uint32_t buttons = 0;
// True while the window holds relative mouse mode. The absolute coordinates
// are meaningless then: X11 reports zeroes, Wayland reports a position that
// accumulates the deltas and can leave the window entirely.
bool relative = false;
};
struct MouseButtonEvent {
bool pressed = false;
std::uint8_t button = 0;
std::uint8_t clicks = 0;
float x = 0.0f;
float y = 0.0f;
// See MouseMotionEvent::relative.
bool relative = false;
};
struct MouseWheelEvent {
float delta_x = 0.0f;
float delta_y = 0.0f;
float mouse_x = 0.0f;
float mouse_y = 0.0f;
// See MouseMotionEvent::relative.
bool relative = false;
};
struct TouchEvent {
enum class Action {
kDown = 0,
kMove,
kUp,
kCancelled,
};
Action action = Action::kMove;
std::int64_t touch_id = 0;
std::int64_t finger_id = 0;
float x = 0.0f;
float y = 0.0f;
float delta_x = 0.0f;
float delta_y = 0.0f;
float pressure = 0.0f;
};
enum class GamepadFamily {
kUnknown,
kXbox,
kPlayStation4,
kPlayStation5,
};
struct GamepadDescriptor {
GamepadFamily family = GamepadFamily::kUnknown;
std::string name;
// Bit positions are SDL's standardized gamepad buttons and axes, not raw
// joystick indices or Android key codes.
std::uint32_t buttons = 0;
std::uint32_t axes = 0;
};
struct GamepadConnectionEvent {
std::int64_t instance_id = 0;
bool connected = false;
GamepadDescriptor descriptor{};
bool remapped = false;
};
struct GamepadAxisEvent {
std::int64_t instance_id = 0;
std::uint8_t axis = 0;
std::int16_t value = 0;
};
struct GamepadButtonEvent {
std::int64_t instance_id = 0;
std::uint8_t button = 0;
bool pressed = false;
};
using PlatformEventPayload =
std::variant<QuitEvent, WindowResizedEvent, WindowFocusEvent, KeyEvent,
TextInputEvent, TextEditingEvent, MouseMotionEvent,
MouseButtonEvent, MouseWheelEvent, TouchEvent,
GamepadConnectionEvent, GamepadAxisEvent, GamepadButtonEvent>;
struct PlatformEvent {
std::uint64_t timestamp_ns = 0;
PlatformEventPayload payload = QuitEvent{};
};
// Main-thread-owned platform boundary. Implementations never create an EGL or
// Vulkan device/context; those belong to the selected graphics backend.
class PlatformRuntime {
public:
virtual ~PlatformRuntime() = default;
virtual Status Initialize(const WindowOptions& options) = 0;
virtual void Shutdown() = 0;
virtual bool IsInitialized() const = 0;
// Returns OK with has_event=false when the queue currently contains no
// supported platform event.
virtual Status PollEvent(PlatformEvent* event, bool* has_event) = 0;
virtual Status SetTextInputEnabled(bool enabled) = 0;
virtual Status ShowWindow() = 0;
virtual NativeWindowDescriptor GetNativeWindow() const = 0;
};
} // namespace platform
} // namespace mocktail
#endif // MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_