emscripten-ports / SDL2

Other
166 stars 64 forks source link

SDL_GRAB_KEYBOARD doesn't work and F5 triggers refresh #118

Closed zippoxer closed 4 years ago

zippoxer commented 4 years ago

Hi everyone,

I'm compiling an MMORPG (client, of course) with Emscripten. I've already rewritten the window management (from platform-native <-to-> SDL2) and networking (from boost::asio <-to-> C sockets).

I've spent north of 60 hours porting so far, and honestly had a blast. Emscripten is amazing! Thank you all so much for all this great work.

Everything works great, except for keyboards events. The game needs to capture function keys (F1, F2, ..) and also a few key combinations (like CTRL + L). I've tried to preventDefault keyboard events on the document and/or window, but then the game doesn't receive those events as well.

I've read a bit and found these two relevant options in SDL_hints.h:

/**
 *  \brief  A variable controlling whether grabbing input grabs the keyboard
 *
 *  This variable can be set to the following values:
 *    "0"       - Grab will affect only the mouse
 *    "1"       - Grab will affect mouse and keyboard
 *
 *  By default SDL will not grab the keyboard so system shortcuts still work.
 */
#define SDL_HINT_GRAB_KEYBOARD              "SDL_GRAB_KEYBOARD"

/**
 *  \brief override the binding element for keyboard inputs for Emscripten builds
 *
 * This hint only applies to the emscripten platform
 *
 * The variable can be one of
 *    "#window"      - The javascript window object (this is the default)
 *    "#document"    - The javascript document object
 *    "#screen"      - the javascript window.screen object
 *    "#canvas"      - the WebGL canvas element
 *    any other string without a leading # sign applies to the element on the page with that ID.
 */
#define SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT   "SDL_EMSCRIPTEN_KEYBOARD_ELEMENT"

I've set them up in the module's preRun:

Module['preRun'].push(function () {
    ENV.SDL_EMSCRIPTEN_KEYBOARD_ELEMENT = '#canvas';
    ENV.SDL_GRAB_KEYBOARD = '1';
});

SDL_EMSCRIPTEN_KEYBOARD_ELEMENT works well, and now the game only receives keyboard events if the canvas is in focus.

However, SDL_GRAB_KEYBOARD doesn't prevent keyboard events from bubbling up to the window, so F5 still refresh.

Here's the HTML & Module I'm using:

<html>

<head>
  <style>
    #canvas-container {
      position: absolute;
      top: 0px;
      left: 0px;
      margin: 0px;
      width: 100%;
      height: 100%;
      overflow: hidden;
      display: block;
    }

    #canvas {
      width: 100%;
      height: 100%;
    }
  </style>
</head>

<body>
  <div id="status">Downloading...</div>
  <div id="canvas-container">
    <canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
  </div>
  <pre id="output"></pre>

  <script>
    var statusElement = document.getElementById('status');
    var canvasElement = document.getElementById('canvas');

    canvasElement.focus();

    var Module = {
      doNotCaptureKeyboard: false,
      preRun: [],
      postRun: [],
      print: (function (text) {
        return function (text) {
          if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
          console.log(text);
        };
      })(),
      printErr: function (text) { console.error(text); },
      setStatus: function (text) {
        statusElement.textContent = text;
      },
      canvas: (function () {
        // As a default initial behavior, pop up an alert when webgl context is lost. To make your
        // application robust, you may want to override this behavior before shipping!
        // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
        canvasElement.addEventListener("webglcontextlost", function (e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);

        return canvasElement;
      })(),
      totalDependencies: 0,
      monitorRunDependencies: function (left) {
        this.totalDependencies = Math.max(this.totalDependencies, left);
        Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
      },
    };

    Module['preRun'].push(function () {
      ENV.SDL_EMSCRIPTEN_KEYBOARD_ELEMENT = '#canvas';
      ENV.SDL_GRAB_KEYBOARD = '1';
    });
  </script>

  <script src="/preload_assets.js"></script>
  <script src="/game.js"></script>
  <script src="/preload_things.js"></script>
</body>

</html>

Can anybody help me figure out what else can I try here?

Daft-Freak commented 4 years ago

Ah, we have a list of "navigation" keys that we always prevent default on, adding F5 (116?) there should help.

As a quick check, you can disable TEXTINPUT events (SDL_EventState(SDL_TEXTINPUT, SDL_DISABLE);), which prevents default on everything.

zippoxer commented 4 years ago

Ah, we have a list of "navigation" keys that we always prevent default on, adding F5 (116?) there should help.

As a quick check, you can disable TEXTINPUT events (SDL_EventState(SDL_TEXTINPUT, SDL_DISABLE);), which prevents default on everything.

Thanks for the quick response :-)

I'm now calling SDL_EventState(SDL_TEXTINPUT, SDL_DISABLE) right after SDL_CreateWindow (in the C), and it does prevent default on everything (F5 as well), but the issue now is that the game doesn't receive any text input as well.

I guess that's the expected behaviour with SDL_EventState, no?

Can't I preventDefault all keyboard events (as far as the browser allows obviously) and still have the game receive them through the canvas? Essentially, I want to "trap" keyboard events in the game from bubbling up to the browser.

zippoxer commented 4 years ago

Update

OK, I'm stupid. I can just do this:

    window.onkeydown = function (event) {
      // F1-F12
      if (event.keyCode >= 112 && event.keyCode <= 123) {
        event.preventDefault();
      }
    }

And the game still receives F1-F12 even with preventDefault. Yay!

For some reason, the game doesn't receive text keys when you preventDefault them in the same way. That's interesting, is this a limitation with Emscripten's SDL port or is it just how browsers work?

Code for disabling function keys & CTRL+key combinations

If anyone needs it:


function shouldPreventKey(event) {
  var code = event.which || event.keyCode;

  if (event.ctrlKey) {
    return true;
  }

  // F1-F12
  if (code >= 112 && code <= 123) {
    return true;
  }
}

window.onkeydown = function (event) {
  if (shouldPreventKey(event)) {
    event.preventDefault();
  }
}
Daft-Freak commented 4 years ago

It's a browser thing, preventing the default on a keydown event prevents the keypress event from being sent, which is what we use for SDL_TEXTINPUT.

Usually, any SDL event that is enabled passes it through to SDL and prevents the default action. keydown events have special handling to still allow keypress (unless SDL_TEXTINPUT is disabled) with that little blacklist of "bad keys". (Which I should add the F-keys to).

zippoxer commented 4 years ago

It's a browser thing, preventing the default on a keydown event prevents the keypress event from being sent, which is what we use for SDL_TEXTINPUT.

Usually, any SDL event that is enabled passes it through to SDL and prevents the default action. keydown events have special handling to still allow keypress (unless SDL_TEXTINPUT is disabled) with that little blacklist of "bad keys". (Which I should add the F-keys to).

Thanks, sounds great. I think your defaults are great for most applications. In my search, I haven't seen many issues like mine. Maybe an ENV to toggle "desktop mode" for applications like mine would be more approriate? If so, preventing CTRL + [S|L| and other known browser combinations would also make sense.

Daft-Freak commented 4 years ago

I think that's what the keyboard element hint is for (not taking over the entire window)... BTW, just noticed that the docs for that are a bit outdated (it takes a CSS selector now, '#canvas' is the only one that just happens to still work).