Skip to content

[rcore] Add ToggleFullscreen() implementation for PLATFORM_WEB#3634

Merged
raysan5 merged 1 commit intomasterfrom
unknown repository
Dec 15, 2023
Merged

[rcore] Add ToggleFullscreen() implementation for PLATFORM_WEB#3634
raysan5 merged 1 commit intomasterfrom
unknown repository

Conversation

@ghost
Copy link

@ghost ghost commented Dec 13, 2023

a. Preface

  • @raysan5 I'm sorry for the very long PR. I know this is going to be a real pain to review. Tried to simplify things as much as possible, but it's a complicated problem with a lot of background information. Please, take your time.

b. Context

  • Unfortunately "fullscreen" is very challenging to handle completely and reliably on web. So far I've identified 5 "types" of "fullscreen" that affects raylib with emscripten:

I. F11 fullscreen

  • Invoked by the user pressing the F11 key.
  • Will bring the entire page to fullscreen.
  • There's not much we do about this. The user will always be able to press F11, bringing the whole page to fullscreen.
  • Luckly, the F11 fullscreen won't break the other "types" of "fullscreen". So the user can invoke the F11 fullscreen and then, after, any other "type" of "fullscreen" and that other "type" will work without a problem, albeit inside the F11 fullscreen "context"/"space".

II. HTML5 fullscreen

  • Invoked by some variation of document.someElement.requestFullscreen().
  • Will create its own way of fullscreen.
  • Has very few options (doc), and none of which are particularly useful for us.
  • Appears to use the same inner "process" of creating a "fullscreen" like Emscripten fullscreen, although through different methods.

III. Emscripten fullscreen

  • Invoked by emscripten_request_fullscreen() or emscripten_request_fullscreen_strategy().
  • Will create its own way of fullscreen.
  • Has more options (doc) which are useful for us, but doesn't have some options we need (e.g.: "borderless windowed", aka "fullscreen desktop").
  • Appears to use the same inner "process" of creating a "fullscreen" like HTML5 fullscreen, although through different methods.

IV. Emscripten soft fullscreen

  • Invoked by emscripten_enter_soft_fullscreen().
  • Will create a "simulated"/"fake" fullscreen.
  • Won't cover web browser GUI, etc. And, since it's not a "fullscreen", won't trigger the default "Esc to leave fullscreen" browser message/dialog.
  • Requires a special emscripten_exit_soft_fullscreen() to leave and will expressly conflict with other "types" of "fullscreen" (specially Emscripten fullscreen).

V. Emscripten Module fullscreen

  • Invoked by Module.requestFullscreen().
  • Will create its own way of fullscreen.
  • Was the only method that provided the minimal options for the 2 "types" of "fullscreen" we need ("real fullscreen" and "borderless windowed").
  • However, conflicts with HTML5 fullscreen and Emscripten fullscreen, due to the inner "process" through which the "fullscreen" is created.

c. Conclusion

  • F11 fullscreen is no problem and can co-exist without issues. We can just forget it's there.
  • HTML5 fullscreen is insufficient. It's missing any sort of scale option (doc).
  • Emscripten fullscreen is, at this moment, insufficient. It's missing an option to not scale the content but scale the canvas. I've tried all combinations (test_html5_fullscreen.c L154-L161) but none provided that.
  • Emscripten soft fullscreen is not really an option for what we need.
  • Emscripten Module fullscreen is not perfect (doesn't have the fine-grained scale options from Emscripten fullscreen), but provides the minimal options we need.

d. Changes

  1. Since HTML5 fullscreen and Emscripten fullscreen don't have the options we need right now, had to go with the Emscripten Module fullscreen route.

  2. It's important to understand that there are 2 contexts for "fullscreen" for us:

    1. One where we call it from inside raylib.
      This we can track and control through CORE.Window.flags, ToggleFullscreen(), ToggleBorderlessWindowed(), SetWindowState() and ClearWindowState().

    2. And one where the user call it from outside raylib.
      This we have no control of and we have limited capacity to track.
      This happen, for example, when the user clicks the Fullscreen button from the shell.html file (L181).

  3. The problem of these 2 contexts is exactly not being able to track the one called from outside raylib. Which causes the Toggle*() and *WindowStates() to malfuction (e.g.: do nothing since it won't have the flags set or work inverted).

  4. To address that a new platform.ourFullscreen bool variable was added (R76). When it's true it means that "fullscreen" process is being operated by raylib calls. When it's false it means it wasn't us. We'll get in more detail later.

  5. To actually handle the "fullscreen" when ToggleFullscreen() or *WindowStates() are called, we:

    1. First check if the document is already on "fullscreen" (R147-R148).
      Note 1: EM_ASM_INT was used because we need to set flags inside raylib and outside JS (R155-R157).
      Note 2: document.fullscreenElement was used because, under my testing, it appeared to be the most reliable way to check if we were on some "type" of "fullscreen".

    2. If it is, we leave whatever "type" of "fullscreen" we were in (R150).
      Note: EM_ASM and document.exitFullscreen() were used because, under my testing, it appeared to be the most reliable way to leave whatever "type" of "fullscreen" was active.

    3. Since we left the whatever "type" of "fullscreen" we were in, we also reset the raylib flags (R155-R157).

    4. But, before that (otherwise the flag would have been reseted), we check if we where on a (our) FLAG_FULLSCREEN_MODE "fullscreen" (R152).

    5. If we were, it means this Toggle means leave fullscreen, so we stop there.

    6. If we weren't, it means this Toggle is coming after some other "type" of "fullscreen" other than FLAG_FULLSCREEN_MODE, so we will continue (R153) and proceed to enter a (our) FLAG_FULLSCREEN_MODE "fullscreen" (R161-R172).

    7. If we were not in any "type" of "fullscreen" to begin with, we continue to enter a (our) FLAG_FULLSCREEN_MODE "fullscreen" (R159-R161).

    8. To enter (our) FLAG_FULLSCREEN_MODE "fullscreen" we call Module.requestFullscreen(false, false); inside a short setTimeout (R164-R169) and them set the necessary flags (R170-R171).
      Note 1: the Module.requestFullscreen(); parameters are lockPointer that we keep as false and resizeCanvas that we keep as false to achieve the same type of fullscreen from the other platforms.
      Note 2: the setTimeout was necessary to handle the browser mode change delay.

    9. Side-note: the old notes were kept as a reference in case it's needed in the future (R174-R238).

  6. After this "fullscreen" call is done, it should trigger a "fullscreen change" event and the EmscriptenFullscreenChangeCallback() (R1440).

  7. But, to allow that, the emscripten_set_fullscreenchange_callback had to be changed to EMSCRIPTEN_EVENT_TARGET_WINDOW instead of #canvas (L1139-R1178), because #canvas won't be reliably tracked.

  8. Here is when platform.ourFullscreen (that is set to true right from the start of ToggleFullscreen R144) becomes important:

    1. If platform.ourFullscreen is true, we already cleaned and set the flags ourselves inside ToggleFullscreen() (R155-R157, R170-R171), as needed. So all we need to do is reset the platform.ourFullscreen to false (R1444).

    2. However, if platform.ourFullscreen is false, then this "fullscreen change" event if coming from outside raylib.

    3. If it's coming from outside raylib and is not on "fullscreen" (R1447-R1448), it means the user just left some "type" of "fullscreen" (e.g.: pressed the Esc key). So the assume we're no longer in some "type" of "fullscreen" and reset our raylib flags to match that non-fullscreen state (R1450-R1452).
      Note: this will handle the item 4 issue.

  9. Basically the same process happen for ToggleBorderlessWindowed(). The only difference being the variable names, the respective flags and entering the "borderless windowed" "fullscreen" (R242):

    1. For that, we use Module.requestFullscreen(false, true); instead, where true means resizeCanvas, achieving the FLAG_BORDERLESS_WINDOWED_MODE "fullscreen" (R268).

    2. We also need to use canvas.style.width="unset"; to handle the possibility of a width="value%" like on the default shell.html file (R271).

    3. There the setTimeouts are also used to handle the browser mode change delay (R266-R269).

e. Known issues

  1. With all this handling ToggleFullscreen() and ToggleBorderlessWindowed() and the *WindowState() become "hot swappable", meaning that the user can jump from FLAG_FULLSCREEN_MODE "fullscreen" to FLAG_BORDERLESS_WINDOWED_MODE "fullscreen" with just a single ToggleBorderlessWindowed() or SetWindowState(FLAG_BORDERLESS_WINDOWED_MODE); call. The same being true for the opposite (from FLAG_BORDERLESS_WINDOWED_MODE to FLAG_FULLSCREEN_MODE).

    1. There's one limitation tho: at the moment, I wasn't able to find a way to parse the Module.requestFullscreen() resizeCanvas parameter issued outside raylib. Getting that information would allow to find which "type" of "fullscreen" was issued from outside. And that would allow to better toggle it out.

    2. E.g.: if the user press the fullscreen button on the shell.html file, the canvas will go fullscreen as expected (that's outside raylib). Then if the user uses ToggleFullscreen() it will leave the fullscreen and then reenters the "fullscreen" (that's inside raylib). From there, everything works as expected. But, if we had that information, we could instead just leave the fullscreen shortcutting it.

    3. It's a minor issue, but if we ever figure out how to get that info, we can handle that exactly at R1453.
      Note: found a way to handle it using the following code inside ToggleFullscreen():
      const int cw = EM_ASM_INT( { return document.canvas.width; }, 0);
      const int csw = EM_ASM_INT( { return parseInt(document.canvas.style.width); }, 0);
      if (cw < csw) enterFullscreen = false;
      However, it causes SetWindowState() to break on that case. IMHO, this is such a minor issue that doesn't justify the overengineering, so better just leave it as is.

    4. IMPORTANT NOTE: @raysan5 I just realized that the other platforms don't have these toggles operating in "hot swap". Please let me know if I should revert this here so it matches the other platforms behavior for parity and consistency.

  2. As mentioned previously, Emscripten Module fullscreen conflicts with HTML5 fullscreen and Emscripten fullscreen:

    1. If the user enters "fullscreen" with ToggleFullscreen() or SetWindowState(), then leaves, then enters "fullscreen" with HTML5 fullscreen (e.g.: calling document.getElementById("canvas").requestFullscreen()) this will "break" the page content (but not the canvas, which will continue to work). By "break" I mean, for some reason, that document.getElementById("canvas").requestFullscreen() call will, at that moment, cause the <canvas> to replace <body>.

    2. The same will happen if the user enters "fullscreen" with HTML5 fullscreen (e.g.: document.getElementById("canvas").requestFullscreen()), then press Esc to leave, then enter "fullscreen" with ToggleFullscreen(), then leaves, then enter "fullscreen" again with HTML5 fullscreen (e.g.: document.getElementById("canvas").requestFullscreen()).

    3. Unfortunately I have no solution for this yet. But as long both methods are not mixed, everything should work fine.

f. Outro

  • Nevertheless, I think this implementation covers most use cases and works for both a minshell.html-style or a shell.html-style shell files.

  • Also, hopefully this PR documents enough detail to help for context or future updates of "fullscreen" for PLATFORM_WEB.

g. Notes

  • If Emscripten fullscreen eventually gets updated to provide an option to not scale the content but scale the canvas, we likely could replace the Module EM_ASM calls with something like:
// To enter emscripten fullscreen:
EmscriptenFullscreenStrategy strat = {
    .scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_SOMETHING,
    .canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_SOMETHING,
    .filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT,
    .canvasResizedCallback = NULL
};
emscripten_request_fullscreen_strategy("#canvas", 1, &strat);

// To leave emscripten fullscreen:
emscripten_exit_fullscreen();

h. Code example

  • This change can be tested for PLATFORM_WEB (requires ASYNCIFY) with:
#include "raylib.h"

int main(void) {
    InitWindow(800, 450, "test");
    SetTargetFPS(60);
    while (!WindowShouldClose()) {

        if (IsKeyPressed(KEY_F)) ToggleFullscreen();
        if (IsKeyPressed(KEY_G)) SetWindowState(FLAG_FULLSCREEN_MODE);
        if (IsKeyPressed(KEY_H)) ClearWindowState(FLAG_FULLSCREEN_MODE);

        if (IsKeyPressed(KEY_B)) ToggleBorderlessWindowed();
        if (IsKeyPressed(KEY_N)) SetWindowState(FLAG_BORDERLESS_WINDOWED_MODE);
        if (IsKeyPressed(KEY_M)) ClearWindowState(FLAG_BORDERLESS_WINDOWED_MODE);

        BeginDrawing();
        ClearBackground(RAYWHITE);

        DrawText("[F] ToggleFullscreen()", 20, 20, 20, BLACK);
        DrawText("[G] SetWindowState(FLAG_FULLSCREEN_MODE)", 20, 40, 20, BLACK);
        DrawText("[H] ClearWindowState(FLAG_FULLSCREEN_MODE)", 20, 60, 20, BLACK);

        DrawText("[B] ToggleBorderlessWindowed()", 20, 100, 20, BLACK);
        DrawText("[N] SetWindowState(FLAG_BORDERLESS_WINDOWED_MODE)", 20, 120, 20, BLACK);
        DrawText("[M] ClearWindowState(FLAG_BORDERLESS_WINDOWED_MODE)", 20, 140, 20, BLACK);

        EndDrawing();
    }
    CloseWindow();
    return 0;
}

i. Environment

  • Compiled on Linux (Ubuntu 22.04 64-bit) and tested on Firefox (115.3.1esr 64-bit) and Chromium (117.0.5938.149 64-bit) for both minshell.html and shell.html files.

j. Edits

  • 1: editing.
  • 2: typos, editing and formatting.
  • 3: editing, formatting.
  • 4: typos, correction.
  • 5: updated status on known issue 1.
  • 6: added item iv important note to known issues 1.

@raysan5
Copy link
Owner

raysan5 commented Dec 14, 2023

@ubkp Thank you very much for the super-detailed review and documentation of this issue! Just let me some days to review it carefully.

@raysan5 raysan5 merged commit e001f7e into raysan5:master Dec 15, 2023
@raysan5
Copy link
Owner

raysan5 commented Dec 15, 2023

@ubkp Ok, just reviewed it. Again, thanks for the super-detailed review of all fullscreen modes available on Web, I worked on that long time ago but I couldn't come up with a proper solution, actually my focus was mostly on emscripten_request_fullscreen_strategy() but most of my tests did not work as expected.

The proposed processes and assumptions look ok to me. About the known issues:

There's one limitation tho: at the moment, I wasn't able to find a way to parse the Module.requestFullscreen() resizeCanvas parameter issued outside raylib. Getting that information would allow to find which "type" of "fullscreen" was issued from outside. And that would allow to better toggle it out.

Isn't it possible to compare the CORE.Window.screen/CORE.Window.previousScreen with the new canvas size (queried using EM_ASM_INT()) to get that parameter?

IMPORTANT NOTE: @raysan5 I just realized that the other platforms don't have these toggles operating in "hot swap". Please let me know if I should revert this here so it matches the other platforms behavior for parity and consistency.

I think this is a minor detail, it's also highly dependant on users and I doubt many of them mix the toggle modes on their codebases.

In that regards, I see many of the potential conflicts/issues depend on users mixing modes so I think it's ok to leave that good-practices work to them. If many users hit those constrains and complain, we can review it in the future.

Thanks again for all the work put into this improvement.

@raysan5
Copy link
Owner

raysan5 commented Dec 15, 2023

An additional note: All my review was based on provided code and example but not on actual "production code". I got several tools relying on it, if I see some unexpected issue with this update I will just report it.

@ghost
Copy link
Author

ghost commented Dec 15, 2023

@raysan5 No problem, glad I could help. :)

Isn't it possible to compare the CORE.Window.screen/CORE.Window.previousScreen with the new canvas size (queried using EM_ASM_INT()) to get that parameter?

We could use CORE.Window.screen but I think we'd still need to EM_ASM_INT both document.canvas.width and parseInt(document.canvas.style.width) because:

  • For FLAG_FULLSCREEN_MODE, Module fullscreen will leave the canvas width intact and will stretch with CSS style width.
  • But for FLAG_BORDERLESS_WINDOWED_MODE, Module fullscreen will instead change canvas width itself and not touch CSS.

I have a partial solution for it, but I'll start working on a full fix. 👍

An additional note: All my review was based on provided code and example but not on actual "production code". I got several tools relying on it, if I see some unexpected issue with this update I will just report it.

Sure thing, if any issues arise, I'll promptly work on it! 👍

@ghost ghost deleted the add/web-fullscreen branch December 15, 2023 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant