#ifndef MOCKTAIL_RUNTIME_ROBLOX_INPUT_ROUTER_H_ #define MOCKTAIL_RUNTIME_ROBLOX_INPUT_ROUTER_H_ #include #include #include #include #include #include "mocktail/platform/platform_runtime.h" #include "mocktail/status.h" #include "runtime/roblox_text_editor.h" namespace mocktail { namespace runtime { struct RobloxInputViewport { 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; } }; struct AndroidKeyMapping { int32_t scan_code = 0; int32_t key_code = 0; bool valid() const { return scan_code > 0 && key_code > 0; } }; // Maps SDL's USB scancode to the Linux evdev scan code and Android KeyEvent // keycode observed by the APK. Unmapped keys are rejected rather than passed // through with an ABI-incompatible numeric value. AndroidKeyMapping MapSdlKeyToAndroid(uint32_t sdl_scancode, uint32_t sdl_key = 0); // Translates SDL button numbers to the nativePassMouseButton argument. // The first three are Roblox's zero-based MouseButton1/2/3 indices, not Android // MotionEvent button bits. int32_t MapSdlMouseButtonToRoblox(uint8_t sdl_button); int32_t MapSdlGamepadButtonToAndroid(uint8_t sdl_button); int32_t RobloxGamepadType(platform::GamepadFamily family); struct RobloxGamepadSink { using SupportedKeyFn = Status (*)(void*, int32_t, int32_t, bool, int32_t); using SupportedMotionFn = Status (*)(void*, int32_t, int32_t, int32_t, bool, int32_t); using ConnectFn = Status (*)(void*, int32_t, int32_t); using DisconnectFn = Status (*)(void*, int32_t); using ButtonFn = Status (*)(void*, int32_t, int32_t, bool); using AxisFn = Status (*)(void*, int32_t, int32_t, float, float, float); void* context = nullptr; SupportedKeyFn supported_key = nullptr; SupportedMotionFn supported_motion = nullptr; ConnectFn connect = nullptr; DisconnectFn disconnect = nullptr; ButtonFn button = nullptr; AxisFn axis = nullptr; bool valid() const { return supported_key != nullptr && supported_motion != nullptr && connect != nullptr && disconnect != nullptr && button != nullptr && axis != nullptr; } }; struct RobloxInputSink { using MouseMoveFn = Status (*)(void* context, float x, float y, float delta_x, float delta_y); using MouseButtonFn = Status (*)(void* context, float x, float y, bool pressed, int32_t button); using MouseWheelFn = Status (*)(void* context, float x, float y, float delta_y); using TouchFn = Status (*)(void* context, int32_t pointer_id, float x, float y, int32_t action, int32_t width, int32_t height); using KeyFn = Status (*)(void* context, bool pressed, int32_t scan_code, int32_t key_code, bool repeat); void* context = nullptr; MouseMoveFn mouse_move = nullptr; MouseButtonFn mouse_button = nullptr; MouseWheelFn mouse_wheel = nullptr; TouchFn touch = nullptr; KeyFn key = nullptr; RobloxTextSink text; RobloxGamepadSink gamepad{}; }; enum class RobloxInputEventKind { kNone = 0, kMouseMotion, kMouseButton, kMouseWheel, kTouch, kKeyboard, kText, kFocus, kViewport, kGamepadConnection, kGamepadButton, kGamepadAxis, }; enum class RobloxInputDispatchState { kDispatched = 0, kStateUpdated, kIgnoredInactive, kIgnoredUnfocused, kIgnoredUnsupported, kRejectedInvalid, kNativeError, }; struct RobloxInputDispatchResult { RobloxInputDispatchState state = RobloxInputDispatchState::kStateUpdated; RobloxInputEventKind kind = RobloxInputEventKind::kNone; Status status = Status::Ok(); bool dispatched() const { return state == RobloxInputDispatchState::kDispatched; } }; struct RobloxInputSnapshot { bool active = false; bool focused = false; RobloxInputViewport viewport; uint64_t mouse_events = 0; uint64_t touch_events = 0; uint64_t keyboard_events = 0; uint64_t ignored_text_events = 0; uint64_t text_events = 0; // Zero after the host editor finishes, even if Roblox sends no hide callback. uint64_t text_focus_generation = 0; uint64_t native_errors = 0; uint32_t active_mouse_buttons = 0; uint32_t active_touches = 0; uint32_t active_keys = 0; uint64_t gamepad_events = 0; uint32_t detected_gamepads = 0; uint32_t connected_gamepads = 0; }; // Stateful translation boundary from SDL platform events to the exact // Android/Roblox native input ABI. It owns no JNI or SDL objects. class RobloxInputRouter final { public: explicit RobloxInputRouter(RobloxInputSink sink, RobloxTextDisplaySink display_sink = {}, platform::TextClipboard* clipboard = nullptr); RobloxInputRouter(const RobloxInputRouter&) = delete; RobloxInputRouter& operator=(const RobloxInputRouter&) = delete; Status Activate(RobloxInputViewport viewport, bool initially_focused); Status Deactivate(); Status BeginTextFocusSession(RobloxTextFocusSession session); Status EndTextFocusSession(int64_t textbox_handle, uint64_t generation, bool notify_native); Status ReplaceFocusedTextFromEngine(uint64_t generation, std::string authoritative_utf8); Status UpdateTextFocusProperties(uint64_t generation, const RobloxTextFocusProperties& properties); RobloxInputDispatchResult HandleEvent(const platform::PlatformEvent& event); RobloxInputSnapshot Snapshot() const; // Raw mouse sends look deltas in host units instead of guest surface units. // The reported pointer position stays in guest units either way. void SetRawMouseEnabled(bool enabled); private: struct ActiveTouch { int64_t touch_id = 0; int64_t finger_id = 0; int32_t pointer_id = -1; float x = 0.0f; float y = 0.0f; }; struct ActiveKey { uint32_t sdl_scancode = 0; AndroidKeyMapping android; }; struct ActiveGamepad { int64_t instance_id = 0; int32_t device_id = 0; platform::GamepadDescriptor descriptor; bool connected = false; uint32_t pressed_buttons = 0; std::array raw_axes{}; std::array sent_axes{}; }; RobloxInputDispatchResult HandleMouseMotionLocked( const platform::MouseMotionEvent& event); RobloxInputDispatchResult HandleMouseButtonLocked( const platform::MouseButtonEvent& event); RobloxInputDispatchResult HandleMouseWheelLocked( const platform::MouseWheelEvent& event); RobloxInputDispatchResult HandleTouchLocked( const platform::TouchEvent& event); RobloxInputDispatchResult HandleKeyLocked(const platform::KeyEvent& event); RobloxInputDispatchResult HandleGamepadConnectionLocked( const platform::GamepadConnectionEvent& event); RobloxInputDispatchResult HandleGamepadButtonLocked( const platform::GamepadButtonEvent& event); RobloxInputDispatchResult HandleGamepadAxisLocked( const platform::GamepadAxisEvent& event); std::vector::iterator FindGamepadLocked(int64_t instance_id); Status ConfigureGamepadLocked(const ActiveGamepad& gamepad, bool available); Status ConnectGamepadLocked(ActiveGamepad& gamepad); Status DispatchGamepadAxesLocked(ActiveGamepad& gamepad, uint8_t axis, bool* dispatched); Status ReleaseGamepadInputsLocked(ActiveGamepad& gamepad); Status DisconnectGamepadLocked(ActiveGamepad& gamepad); Status DisconnectGamepadsLocked(); Status ReleasePressedInputsLocked(); // Keeps mouse_x_/mouse_y_ where they are for an event that carries no // usable position, clamped to the current viewport. Before any absolute // position has been seen, the held position is the viewport center. void HoldPointerLocked(const platform::SurfaceCoordinateTransform& transform); int32_t AllocatePointerIdLocked() const; std::vector::iterator FindTouchIteratorLocked(int64_t touch_id, int64_t finger_id); std::vector::iterator FindKeyIteratorLocked(uint32_t sdl_scancode); RobloxInputDispatchResult NativeResultLocked(Status status, RobloxInputEventKind kind); const RobloxInputSink sink_; RobloxTextEditor text_editor_; mutable std::mutex mutex_; RobloxInputSnapshot snapshot_; std::vector active_mouse_buttons_; std::vector active_touches_; std::vector active_keys_; std::vector gamepads_; // Last known pointer position in guest units. Relative-mode motion, and // wheel or button events with zero coordinates, do not move it: the pointer // stays where it was captured while the deltas pass through. float mouse_x_ = 0.0f; float mouse_y_ = 0.0f; // True once an absolute position has been received since Activate. bool pointer_anchored_ = false; bool raw_mouse_ = false; }; const char* RobloxInputDispatchStateName(RobloxInputDispatchState state); const char* RobloxInputEventKindName(RobloxInputEventKind kind); } // namespace runtime } // namespace mocktail #endif // MOCKTAIL_RUNTIME_ROBLOX_INPUT_ROUTER_H_