Browse Source

- Fixed hidden OS cursor behaviour in Emscripten.
- Added a Pointer Lock API request in Emscripten when Input::SetMouseMode(MM_RELATIVE) is called.
- Added an event for mouse mode changing.
- Fixed Emscripten focus change detection.

hdunderscore 11 years ago
parent
commit
ee00a78d2b

+ 1 - 1
Source/Urho3D/Engine/Engine.cpp

@@ -83,7 +83,7 @@ Engine::Engine(Context* context) :
     timeStep_(0.0f),
     timeStepSmoothing_(2),
     minFps_(10),
-    #if defined(ANDROID) || defined(IOS) || defined(RPI)
+#if defined(ANDROID) || defined(IOS) || defined(RPI) || defined(EMSCRIPTEN)
     maxFps_(60),
     maxInactiveFps_(10),
     pauseMinimized_(true),

+ 232 - 44
Source/Urho3D/Input/Input.cpp

@@ -40,7 +40,9 @@
 #include <cstring>
 
 #include <SDL/SDL.h>
-
+#ifdef EMSCRIPTEN
+#include <emscripten/html5.h>
+#endif
 #include "../DebugNew.h"
 
 extern "C" int SDL_AddTouch(SDL_TouchID touchID, const char *name);
@@ -76,6 +78,100 @@ UIElement* TouchState::GetTouchedElement()
     return touchedElement_.Get();
 }
 
+#ifdef EMSCRIPTEN
+/// Glue between Urho Input and Emscripten HTML5
+/** HTML5 (Emscripten) is limited in the way it handles input. The EmscriptenInput class attempts to provide the glue between Urho3D Input behavior and HTML5, where SDL currently fails to do so.
+ *
+ * Mouse Input:
+ * - The OS mouse cursor position can't be set.
+ * - The mouse can be trapped within play area via 'PointerLock API', which requires a request and interaction between the user and browser.
+ *   - To request mouse lock, call SetMouseMode(MM_RELATIVE). The E_MOUSEMODECHANGED event will be sent if/when the user confirms the request.
+ *   - The user can press 'escape' key and browser will force user out of pointer lock. Urho will send the E_MOUSEMODECHANGED event.
+ *   - SetMouseMode(MM_ABSOLUTE) will leave pointer lock.
+ * - MM_WRAP is unsupported.
+ *
+ * Touch Input:
+ */
+/// % Emscripten Input glue. Intended to be used by the Input subsystem only.
+class EmscriptenInput
+{
+public:
+    /// Constructor, expecting pointer to constructing Input instance.
+    EmscriptenInput(Input* inputInst);
+
+    /// Static callback method for Pointer Lock API. Handles change in Pointer Lock state and sends events for mouse mode change.
+    static EM_BOOL PointerLockCallback(int eventType, const EmscriptenPointerlockChangeEvent* keyEvent, void* userData);
+    /// Static callback method for tracking lose focus event.
+    static EM_BOOL LoseFocus(int eventType, const EmscriptenFocusEvent* keyEvent, void* userData);
+    /// Static callback method for tracking gain focus event.
+    static EM_BOOL GainFocus(int eventType, const EmscriptenFocusEvent* keyEvent, void* userData);
+
+    /// Send request to user to gain pointer lock.
+    void RequestPointerLock();
+    void ExitPointerLock();
+
+private:
+    /// Instance of Input subsystem that constructed this instance.
+    Input* inputInst;
+};
+
+EmscriptenInput::EmscriptenInput(Input* inputInst)
+{
+    inputInst = inputInst;
+    emscripten_set_pointerlockchange_callback(
+        NULL, (void*)inputInst, false, EmscriptenInput::PointerLockCallback);
+
+    // Handle focus changes:
+    emscripten_set_blur_callback(NULL, (void*)inputInst, false, EmscriptenInput::LoseFocus);
+    emscripten_set_focus_callback(NULL, (void*)inputInst, false, EmscriptenInput::GainFocus);
+
+}
+
+void EmscriptenInput::RequestPointerLock()
+{
+    emscripten_request_pointerlock(NULL, true);
+}
+
+void EmscriptenInput::ExitPointerLock()
+{
+    emscripten_exit_pointerlock();
+}
+
+EM_BOOL EmscriptenInput::PointerLockCallback(int eventType, const EmscriptenPointerlockChangeEvent* keyEvent, void* userData)
+{
+    Input* inputInst = (Input*)userData;
+    if (keyEvent->isActive > 0)
+    {
+        // Pointer Lock is now active
+        inputInst->SetMouseVisibleEmscripten(false);
+        inputInst->SetMouseModeEmscripten(MM_RELATIVE);
+    }
+    else
+    {
+        // Pointer Lock is now inactive
+        inputInst->SetMouseVisibleEmscripten(true);
+        inputInst->SetMouseModeEmscripten(MM_ABSOLUTE);
+    }
+    return 1;
+}
+
+EM_BOOL EmscriptenInput::LoseFocus(int eventType, const EmscriptenFocusEvent* keyEvent, void* userData)
+{
+    Input* inputInst = (Input*)userData;
+    if (eventType == EMSCRIPTEN_EVENT_BLUR)
+        inputInst->LoseFocus();
+    return 1;
+}
+
+EM_BOOL EmscriptenInput::GainFocus(int eventType, const EmscriptenFocusEvent* keyEvent, void* userData)
+{
+    Input* inputInst = (Input*)userData;
+    if (eventType == EMSCRIPTEN_EVENT_FOCUS)
+        inputInst->GainFocus();
+    return 1;
+}
+#endif
+
 void JoystickState::Initialize(unsigned numButtons, unsigned numAxes, unsigned numHats)
 {
     buttons_.Resize(numButtons);
@@ -106,11 +202,7 @@ Input::Input(Context* context) :
     mouseMoveWheel_(0),
     windowID_(0),
     toggleFullscreen_(true),
-#if defined(EMSCRIPTEN) // TODO: properly implement relative mouse motion with emscripten
-    mouseVisible_(true),
-#else
     mouseVisible_(false),
-#endif
     lastMouseVisible_(false),
     mouseGrabbed_(false),
     mouseMode_(MM_ABSOLUTE),
@@ -127,12 +219,19 @@ Input::Input(Context* context) :
 
     SubscribeToEvent(E_SCREENMODE, HANDLER(Input, HandleScreenMode));
 
+    #ifdef EMSCRIPTEN
+    emscriptenInput = new EmscriptenInput(this);
+    #endif
+
     // Try to initialize right now, but skip if screen mode is not yet set
     Initialize();
 }
 
 Input::~Input()
 {
+    #ifdef EMSCRIPTEN
+    delete emscriptenInput;
+    #endif
 }
 
 void Input::Update()
@@ -166,12 +265,34 @@ void Input::Update()
     while (SDL_PollEvent(&evt))
         HandleSDLEvent(&evt);
 
+    SDL_Window* window = graphics_->GetImpl()->GetWindow();
+    unsigned flags = window ? SDL_GetWindowFlags(window) & (SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS) : 0;
+#ifndef EMSCRIPTEN
+    if (window)
+    {
+#if defined(REQUIRE_CLICK_TO_FOCUS)
+        if (!inputFocus_ && (graphics_->GetFullscreen() || mouseVisible_) && flags == (SDL_WINDOW_INPUT_FOCUS |
+            SDL_WINDOW_MOUSE_FOCUS))
+#else
+        if (!inputFocus_ && (flags & SDL_WINDOW_INPUT_FOCUS))
+#endif
+            focusedThisFrame_ = true;
+
+        if (focusedThisFrame_)
+            GainFocus();
+
+        if (inputFocus_ && (flags & SDL_WINDOW_INPUT_FOCUS) == 0)
+            LoseFocus();
+    }
+    else
+        return;
+
     if (mouseVisible_ && mouseMode_ == MM_WRAP)
     {
         IntVector2 mpos;
         SDL_GetMouseState(&mpos.x_, &mpos.y_);
 
-        int buffer = 5;
+        const int buffer = 5;
         int width = graphics_->GetWidth() - buffer * 2;
         int height = graphics_->GetHeight() - buffer * 2;
 
@@ -206,36 +327,17 @@ void Input::Update()
             SDL_FlushEvent(SDL_MOUSEMOTION);
         }
     }
-
-    // Check for activation and inactivation from SDL window flags. Must nullcheck the window pointer because it may have
-    // been closed due to input events
-    SDL_Window* window = graphics_->GetImpl()->GetWindow();
-    unsigned flags = window ? SDL_GetWindowFlags(window) & (SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS) : 0;
-    if (window)
-    {
-#ifdef REQUIRE_CLICK_TO_FOCUS
-        if (!inputFocus_ && (graphics_->GetFullscreen() || mouseVisible_) && flags == (SDL_WINDOW_INPUT_FOCUS |
-            SDL_WINDOW_MOUSE_FOCUS))
-#else
-        if (!inputFocus_ && (flags & SDL_WINDOW_INPUT_FOCUS))
-#endif
-            focusedThisFrame_ = true;
-
-        if (focusedThisFrame_)
-            GainFocus();
-
-        if (inputFocus_ && (flags & SDL_WINDOW_INPUT_FOCUS) == 0)
-            LoseFocus();
-    }
-    else
+    #else
+    if (!window)
         return;
+    #endif
 
     // Check for relative mode mouse move
-    if (!touchEmulation_ && (graphics_->GetExternalWindow() || (!mouseVisible_ && inputFocus_ && (flags & SDL_WINDOW_MOUSE_FOCUS))))
+    if (!touchEmulation_ && mouseMode_ != MM_RELATIVE && (graphics_->GetExternalWindow() || (!mouseVisible_ && inputFocus_ && (flags & SDL_WINDOW_MOUSE_FOCUS))))
     {
         IntVector2 mousePosition = GetMousePosition();
         mouseMove_ = mousePosition - lastMousePosition_;
-
+        #ifndef EMSCRIPTEN
         if (graphics_->GetExternalWindow())
             lastMousePosition_ = mousePosition;
         else
@@ -248,7 +350,12 @@ void Input::Update()
                 lastMousePosition_ = center;
             }
         }
-
+        #else
+        if (mouseMode_ == MM_ABSOLUTE)
+        {
+            lastMousePosition_ = mousePosition;
+        }
+        #endif
         // Send mouse move event if necessary
         if (mouseMove_ != IntVector2::ZERO)
         {
@@ -275,6 +382,17 @@ void Input::Update()
             }
         }
     }
+
+    if (mouseMode_ == MM_RELATIVE)
+    {
+        IntVector2 mousePosition = GetMousePosition();
+        IntVector2 center(graphics_->GetWidth() / 2, graphics_->GetHeight() / 2);
+        if (mousePosition != center)
+        {
+            SetMousePosition(center);
+            lastMousePosition_ = center;
+        }
+    }
 }
 
 void Input::SetMouseVisible(bool enable, bool suppressEvent)
@@ -307,17 +425,25 @@ void Input::SetMouseVisible(bool enable, bool suppressEvent)
             if (!mouseVisible_ && inputFocus_)
             {
                 SDL_ShowCursor(SDL_FALSE);
+                #ifndef EMSCRIPTEN
                 // Recenter the mouse cursor manually when hiding it to avoid erratic mouse move for one frame
                 lastVisibleMousePosition_ = GetMousePosition();
                 IntVector2 center(graphics_->GetWidth() / 2, graphics_->GetHeight() / 2);
                 SetMousePosition(center);
                 lastMousePosition_ = center;
+                #else
+                lastVisibleMousePosition_ = GetMousePosition();
+                lastMousePosition_ = lastVisibleMousePosition_;
+                #endif
             }
             else
             {
                 SDL_ShowCursor(SDL_TRUE);
+                #ifndef EMSCRIPTEN
                 if (lastVisibleMousePosition_.x_ != MOUSE_POSITION_OFFSCREEN.x_ && lastVisibleMousePosition_.y_ != MOUSE_POSITION_OFFSCREEN.y_)
                     SetMousePosition(lastVisibleMousePosition_);
+                lastMousePosition_ = lastVisibleMousePosition_;
+                #endif
             }
         }
 
@@ -337,6 +463,41 @@ void Input::SetMouseVisible(bool enable, bool suppressEvent)
     #endif
 }
 
+void Input::ResetMouseVisible()
+{
+    #ifndef EMSCRIPTEN
+    SetMouseVisible(lastMouseVisible_, true);
+    #else
+    SetMouseVisibleEmscripten(lastMouseVisible_);
+    #endif
+}
+
+#ifdef EMSCRIPTEN
+void Input::SetMouseVisibleEmscripten(bool enable)
+{
+    if (enable != mouseVisible_)
+    {
+        mouseVisible_ = enable;
+        SDL_ShowCursor(enable ? SDL_TRUE : SDL_FALSE);
+
+        if (!mouseVisible_)
+            lastVisibleMousePosition_ = GetMousePosition();
+
+        lastMousePosition_ = lastVisibleMousePosition_;
+    }
+}
+
+void Input::SetMouseModeEmscripten(MouseMode mode)
+{
+    mouseMode_ = mode;
+    suppressNextMouseMove_ = true;
+
+    VariantMap& eventData = GetEventDataMap();
+    eventData[MouseModeChanged::P_MODE] = mode;
+    SendEvent(E_MOUSEMODECHANGED, eventData);
+}
+#endif
+
 void Input::SetMouseGrabbed(bool grab)
 {
     mouseGrabbed_ = grab;
@@ -348,47 +509,70 @@ void Input::SetMouseMode(MouseMode mode)
     {
         MouseMode previousMode = mouseMode_;
         mouseMode_ = mode;
+        suppressNextMouseMove_ = true;
+        SDL_Window* window = graphics_->GetImpl()->GetWindow();
         // Handle changing away from previous mode
         if (previousMode == MM_RELATIVE)
         {
             /// \todo Use SDL_SetRelativeMouseMode() for MM_RELATIVE mode
             ResetMouseVisible();
 
-            // Send updated mouse position:
-            using namespace MouseMove;
+            #ifdef EMSCRIPTEN
+            emscriptenInput->ExitPointerLock();
+            #endif
 
-            VariantMap& eventData = GetEventDataMap();
-            eventData[P_X] = lastVisibleMousePosition_.x_;
-            eventData[P_Y] = lastVisibleMousePosition_.y_;
-            eventData[P_DX] = mouseMove_.x_;
-            eventData[P_DY] = mouseMove_.y_;
-            eventData[P_BUTTONS] = mouseButtonDown_;
-            eventData[P_QUALIFIERS] = GetQualifiers();
-            SendEvent(E_MOUSEMOVE, eventData);
+            SDL_SetWindowGrab(window, SDL_FALSE);
         }
+        #ifndef EMSCRIPTEN
         else if (previousMode == MM_WRAP)
         {
-            SDL_Window* window = graphics_->GetImpl()->GetWindow();
             SDL_SetWindowGrab(window, SDL_FALSE);
         }
+        #endif
 
         // Handle changing to new mode
         if (mode == MM_ABSOLUTE)
+        {
+            #ifndef EMSCRIPTEN
             SetMouseGrabbed(false);
+            VariantMap& eventData = GetEventDataMap();
+            eventData[MouseModeChanged::P_MODE] = mode;
+            SendEvent(E_MOUSEMODECHANGED, eventData);
+            #else
+            // Deferred mouse mode change to PointerLock callback
+            #endif
+        }
         else
         {
             SetMouseGrabbed(true);
 
             if (mode == MM_RELATIVE)
             {
+                SDL_SetWindowGrab(window, SDL_TRUE);
+                #ifndef EMSCRIPTEN
                 SetMouseVisible(false, true);
+
+                VariantMap& eventData = GetEventDataMap();
+                eventData[MouseModeChanged::P_MODE] = mode;
+                SendEvent(E_MOUSEMODECHANGED, eventData);
+                #else
+                // Defer mouse mode change to PointerLock callback
+                mouseMode_ = previousMode;
+                emscriptenInput->RequestPointerLock();
+                #endif
+
             }
+            #ifndef EMSCRIPTEN
             else if (mode == MM_WRAP)
             {
                 /// \todo When SDL 2.0.4 is integrated, use SDL_CaptureMouse() and global mouse functions for MM_WRAP mode.
-                SDL_Window* window = graphics_->GetImpl()->GetWindow();
                 SDL_SetWindowGrab(window, SDL_TRUE);
+
+                VariantMap& eventData = GetEventDataMap();
+                eventData[MouseModeChanged::P_MODE] = mode;
+                SendEvent(E_MOUSEMODECHANGED, eventData);
             }
+            #endif
         }
     }
 }
@@ -961,8 +1145,12 @@ void Input::Initialize()
         mouseVisible_ = true;
 
     // Set the initial activation
-    focusedThisFrame_ = true;
     initialized_ = true;
+#ifndef EMSCRIPTEN
+    focusedThisFrame_ = true;
+#else
+    GainFocus();
+#endif
 
     ResetJoysticks();
     ResetState();

+ 16 - 1
Source/Urho3D/Input/Input.h

@@ -120,9 +120,16 @@ struct JoystickState
     PODVector<int> hats_;
 };
 
+#ifdef EMSCRIPTEN
+class EmscriptenInput;
+#endif
+
 /// %Input subsystem. Converts operating system window messages to input state and events.
 class URHO3D_API Input : public Object
 {
+    #ifdef EMSCRIPTEN
+    friend class EmscriptenInput;
+    #endif
     OBJECT(Input);
 
 public:
@@ -138,7 +145,7 @@ public:
     /// Set whether the operating system mouse cursor is visible. When not visible (default), is kept centered to prevent leaving the window. Mouse visiblility event can be suppressed-- this also recalls any unsuppressed SetMouseVisible which can be returned by ResetMouseVisible().
     void SetMouseVisible(bool enable, bool suppressEvent = false);
     /// Reset last mouse visibilty that was not suppressed in SetMouseVisible.
-    void ResetMouseVisible() { SetMouseVisible(lastMouseVisible_, true); }
+    void ResetMouseVisible();
     /// Set whether the mouse is currently being grabbed by an operation.
     void SetMouseGrabbed(bool grab);
     /// Set the mouse mode.
@@ -288,6 +295,10 @@ private:
     void SetMouseButton(int button, bool newState);
     /// Handle a key change.
     void SetKey(int key, int scancode, unsigned raw, bool newState);
+    #ifdef EMSCRIPTEN
+    void SetMouseVisibleEmscripten(bool enable);
+    void SetMouseModeEmscripten(MouseMode mode);
+    #endif
     /// Handle mousewheel change.
     void SetMouseWheel(int delta);
     /// Internal function to set the mouse cursor position.
@@ -357,6 +368,10 @@ private:
     bool suppressNextMouseMove_;
     /// Initialized flag.
     bool initialized_;
+#ifdef EMSCRIPTEN
+    /// Emscripten Input glue instance
+    EmscriptenInput* emscriptenInput;
+#endif
 };
 
 }

+ 6 - 0
Source/Urho3D/Input/InputEvents.h

@@ -211,6 +211,12 @@ EVENT(E_MOUSEVISIBLECHANGED, MouseVisibleChanged)
     PARAM(P_VISIBLE, Visible);              // bool
 }
 
+/// Mouse mode changed.
+EVENT(E_MOUSEMODECHANGED, MouseModeChanged)
+{
+    PARAM(P_MODE, Mode);                    // MouseMode
+}
+
 /// Application exit requested.
 EVENT(E_EXITREQUESTED, ExitRequested)
 {