pandemonium_engine/platform/javascript/os_javascript.cpp
Hugo Locurcio 3edb5e7f2e Fix window maximize error message appearing when starting the web editor
An error message is no longer printed when trying to maximize the window
in HTML5. This behavior is consistent with Android and iOS.
2022-07-29 16:33:04 +02:00

1091 lines
32 KiB
C++

/*************************************************************************/
/* os_javascript.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#include "os_javascript.h"
#include "core/io/json.h"
#include "drivers/gles2/rasterizer_gles2.h"
#include "drivers/unix/dir_access_unix.h"
#include "drivers/unix/file_access_unix.h"
#include "main/main.h"
#include "servers/visual/visual_server_raster.h"
#ifndef NO_THREADS
#include "servers/visual/visual_server_wrap_mt.h"
#endif
#include "scene/resources/texture.h"
#include <dlfcn.h>
#include <emscripten.h>
#include <png.h>
#include <stdlib.h>
#include "api/javascript_singleton.h"
#include "dom_keys.inc"
#include "pandemonium_js.h"
#define DOM_BUTTON_LEFT 0
#define DOM_BUTTON_MIDDLE 1
#define DOM_BUTTON_RIGHT 2
#define DOM_BUTTON_XBUTTON1 3
#define DOM_BUTTON_XBUTTON2 4
// Quit
void OS_JavaScript::request_quit_callback() {
OS_JavaScript *os = get_singleton();
if (os && os->get_main_loop()) {
os->get_main_loop()->notification(MainLoop::NOTIFICATION_WM_QUIT_REQUEST);
}
}
// Files drop (implemented in JS for now).
void OS_JavaScript::drop_files_callback(char **p_filev, int p_filec) {
OS_JavaScript *os = get_singleton();
if (!os || !os->get_main_loop()) {
return;
}
Vector<String> files;
for (int i = 0; i < p_filec; i++) {
files.push_back(String::utf8(p_filev[i]));
}
os->get_main_loop()->drop_files(files);
}
void OS_JavaScript::send_notification_callback(int p_notification) {
OS_JavaScript *os = get_singleton();
if (!os) {
return;
}
if (p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER || p_notification == MainLoop::NOTIFICATION_WM_MOUSE_EXIT) {
os->cursor_inside_canvas = p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER;
}
MainLoop *loop = os->get_main_loop();
if (loop) {
loop->notification(p_notification);
}
}
// Window (canvas)
bool OS_JavaScript::check_size_force_redraw() {
return pandemonium_js_display_size_update() != 0;
}
void OS_JavaScript::fullscreen_change_callback(int p_fullscreen) {
OS_JavaScript *os = get_singleton();
os->video_mode.fullscreen = p_fullscreen;
}
void OS_JavaScript::window_blur_callback() {
get_singleton()->input->release_pressed_events();
}
void OS_JavaScript::set_video_mode(const VideoMode &p_video_mode, int p_screen) {
video_mode = p_video_mode;
}
OS::VideoMode OS_JavaScript::get_video_mode(int p_screen) const {
return video_mode;
}
Size2 OS_JavaScript::get_screen_size(int p_screen) const {
int size[2];
pandemonium_js_display_screen_size_get(size, size + 1);
return Size2(size[0], size[1]);
}
void OS_JavaScript::set_window_size(const Size2 p_size) {
if (video_mode.fullscreen) {
set_window_fullscreen(false);
}
pandemonium_js_display_desired_size_set(p_size.x, p_size.y);
}
Size2 OS_JavaScript::get_window_size() const {
int size[2];
pandemonium_js_display_window_size_get(size, size + 1);
return Size2(size[0], size[1]);
}
void OS_JavaScript::set_window_maximized(bool p_enabled) {
}
bool OS_JavaScript::is_window_maximized() const {
return false;
}
void OS_JavaScript::set_window_fullscreen(bool p_enabled) {
if (p_enabled == video_mode.fullscreen) {
return;
}
// Just request changes here, if successful, logic continues in
// fullscreen_change_callback.
if (p_enabled) {
int result = pandemonium_js_display_fullscreen_request();
ERR_FAIL_COND_MSG(result, "The request was denied. Remember that enabling fullscreen is only possible from an input callback for the HTML5 platform.");
} else {
// No logic allowed here, since exiting w/ ESC key won't use this function.
ERR_FAIL_COND(pandemonium_js_display_fullscreen_exit());
}
}
bool OS_JavaScript::is_window_fullscreen() const {
return video_mode.fullscreen;
}
void OS_JavaScript::get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen) const {
Size2 screen = get_screen_size();
p_list->push_back(OS::VideoMode(screen.width, screen.height, true));
}
bool OS_JavaScript::get_window_per_pixel_transparency_enabled() const {
if (!is_layered_allowed()) {
return false;
}
return transparency_enabled;
}
void OS_JavaScript::set_window_per_pixel_transparency_enabled(bool p_enabled) {
if (!is_layered_allowed()) {
return;
}
transparency_enabled = p_enabled;
}
// Keys
static void dom2pandemonium_mod(Ref<InputEventWithModifiers> ev, int p_mod) {
ev->set_shift(p_mod & 1);
ev->set_alt(p_mod & 2);
ev->set_control(p_mod & 4);
ev->set_metakey(p_mod & 8);
}
void OS_JavaScript::key_callback(int p_pressed, int p_repeat, int p_modifiers) {
OS_JavaScript *os = get_singleton();
JSKeyEvent &key_event = os->key_event;
// Resume audio context after input in case autoplay was denied.
os->resume_audio();
Ref<InputEventKey> ev;
ev.instance();
ev->set_echo(p_repeat);
ev->set_scancode(dom_code2pandemonium_scancode(key_event.code, key_event.key, false));
ev->set_physical_scancode(dom_code2pandemonium_scancode(key_event.code, key_event.key, true));
ev->set_pressed(p_pressed);
dom2pandemonium_mod(ev, p_modifiers);
String unicode = String::utf8(key_event.key);
if (unicode.length() == 1) {
ev->set_unicode(unicode[0]);
}
os->input->parse_input_event(ev);
// Make sure to flush all events so we can call restricted APIs inside the event.
os->input->flush_buffered_events();
}
// Mouse
Point2 OS_JavaScript::get_mouse_position() const {
return input->get_mouse_position();
}
int OS_JavaScript::get_mouse_button_state() const {
return input->get_mouse_button_mask();
}
int OS_JavaScript::mouse_button_callback(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers) {
OS_JavaScript *os = get_singleton();
Ref<InputEventMouseButton> ev;
ev.instance();
ev->set_pressed(p_pressed);
ev->set_position(Point2(p_x, p_y));
ev->set_global_position(ev->get_position());
ev->set_pressed(p_pressed);
dom2pandemonium_mod(ev, p_modifiers);
switch (p_button) {
case DOM_BUTTON_LEFT:
ev->set_button_index(BUTTON_LEFT);
break;
case DOM_BUTTON_MIDDLE:
ev->set_button_index(BUTTON_MIDDLE);
break;
case DOM_BUTTON_RIGHT:
ev->set_button_index(BUTTON_RIGHT);
break;
case DOM_BUTTON_XBUTTON1:
ev->set_button_index(BUTTON_XBUTTON1);
break;
case DOM_BUTTON_XBUTTON2:
ev->set_button_index(BUTTON_XBUTTON2);
break;
default:
return false;
}
if (p_pressed) {
uint64_t diff = (OS::get_singleton()->get_ticks_usec() / 1000) - os->last_click_ms;
if (ev->get_button_index() == os->last_click_button_index) {
if (diff < 400 && Point2(os->last_click_pos).distance_to(ev->get_position()) < 5) {
os->last_click_ms = 0;
os->last_click_pos = Point2(-100, -100);
os->last_click_button_index = -1;
ev->set_doubleclick(true);
}
} else {
os->last_click_button_index = ev->get_button_index();
}
if (!ev->is_doubleclick()) {
os->last_click_ms += diff;
os->last_click_pos = ev->get_position();
}
}
int mask = os->input->get_mouse_button_mask();
int button_flag = 1 << (ev->get_button_index() - 1);
if (ev->is_pressed()) {
mask |= button_flag;
} else if (mask & button_flag) {
mask &= ~button_flag;
} else {
// Received release event, but press was outside the canvas, so ignore.
return false;
}
ev->set_button_mask(mask);
os->input->parse_input_event(ev);
// Resume audio context after input in case autoplay was denied.
os->resume_audio();
// Make sure to flush all events so we can call restricted APIs inside the event.
os->input->flush_buffered_events();
// Prevent multi-click text selection and wheel-click scrolling anchor.
// Context menu is prevented through contextmenu event.
return true;
}
void OS_JavaScript::mouse_move_callback(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers) {
OS_JavaScript *os = get_singleton();
int input_mask = os->input->get_mouse_button_mask();
// For motion outside the canvas, only read mouse movement if dragging
// started inside the canvas; imitating desktop app behaviour.
if (!os->cursor_inside_canvas && !input_mask)
return;
Ref<InputEventMouseMotion> ev;
ev.instance();
dom2pandemonium_mod(ev, p_modifiers);
ev->set_button_mask(input_mask);
ev->set_position(Point2(p_x, p_y));
ev->set_global_position(ev->get_position());
ev->set_relative(Vector2(p_rel_x, p_rel_y));
ev->set_speed(os->input->get_last_mouse_speed());
os->input->parse_input_event(ev);
}
static const char *pandemonium2dom_cursor(OS::CursorShape p_shape) {
switch (p_shape) {
case OS::CURSOR_ARROW:
default:
return "auto";
case OS::CURSOR_IBEAM:
return "text";
case OS::CURSOR_POINTING_HAND:
return "pointer";
case OS::CURSOR_CROSS:
return "crosshair";
case OS::CURSOR_WAIT:
return "wait";
case OS::CURSOR_BUSY:
return "progress";
case OS::CURSOR_DRAG:
return "grab";
case OS::CURSOR_CAN_DROP:
return "grabbing";
case OS::CURSOR_FORBIDDEN:
return "no-drop";
case OS::CURSOR_VSIZE:
return "ns-resize";
case OS::CURSOR_HSIZE:
return "ew-resize";
case OS::CURSOR_BDIAGSIZE:
return "nesw-resize";
case OS::CURSOR_FDIAGSIZE:
return "nwse-resize";
case OS::CURSOR_MOVE:
return "move";
case OS::CURSOR_VSPLIT:
return "row-resize";
case OS::CURSOR_HSPLIT:
return "col-resize";
case OS::CURSOR_HELP:
return "help";
}
}
void OS_JavaScript::set_cursor_shape(CursorShape p_shape) {
ERR_FAIL_INDEX(p_shape, CURSOR_MAX);
if (cursor_shape == p_shape) {
return;
}
cursor_shape = p_shape;
pandemonium_js_display_cursor_set_shape(pandemonium2dom_cursor(cursor_shape));
}
void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) {
if (p_cursor.is_valid()) {
Ref<Texture> texture = p_cursor;
Ref<AtlasTexture> atlas_texture = p_cursor;
Ref<Image> image;
Size2 texture_size;
Rect2 atlas_rect;
if (texture.is_valid()) {
image = texture->get_data();
if (image.is_valid()) {
image->duplicate();
}
}
if (!image.is_valid() && atlas_texture.is_valid()) {
texture = atlas_texture->get_atlas();
atlas_rect.size.width = texture->get_width();
atlas_rect.size.height = texture->get_height();
atlas_rect.position.x = atlas_texture->get_region().position.x;
atlas_rect.position.y = atlas_texture->get_region().position.y;
texture_size.width = atlas_texture->get_region().size.x;
texture_size.height = atlas_texture->get_region().size.y;
} else if (image.is_valid()) {
texture_size.width = texture->get_width();
texture_size.height = texture->get_height();
}
ERR_FAIL_COND(!texture.is_valid());
ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0);
ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256);
ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height);
image = texture->get_data();
ERR_FAIL_COND(!image.is_valid());
image = image->duplicate();
if (atlas_texture.is_valid())
image->crop_from_point(
atlas_rect.position.x,
atlas_rect.position.y,
texture_size.width,
texture_size.height);
if (image->get_format() != Image::FORMAT_RGBA8) {
image->convert(Image::FORMAT_RGBA8);
}
png_image png_meta;
memset(&png_meta, 0, sizeof png_meta);
png_meta.version = PNG_IMAGE_VERSION;
png_meta.width = texture_size.width;
png_meta.height = texture_size.height;
png_meta.format = PNG_FORMAT_RGBA;
PoolByteArray png;
size_t len;
PoolByteArray::Read r = image->get_data().read();
ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, r.ptr(), 0, NULL));
png.resize(len);
PoolByteArray::Write w = png.write();
ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, w.ptr(), &len, 0, r.ptr(), 0, NULL));
w = PoolByteArray::Write();
r = png.read();
pandemonium_js_display_cursor_set_custom_shape(pandemonium2dom_cursor(p_shape), r.ptr(), len, p_hotspot.x, p_hotspot.y);
r = PoolByteArray::Read();
} else {
pandemonium_js_display_cursor_set_custom_shape(pandemonium2dom_cursor(p_shape), NULL, 0, 0, 0);
}
}
void OS_JavaScript::set_mouse_mode(OS::MouseMode p_mode) {
ERR_FAIL_COND_MSG(p_mode == MOUSE_MODE_CONFINED, "MOUSE_MODE_CONFINED is not supported for the HTML5 platform.");
if (p_mode == get_mouse_mode())
return;
if (p_mode == MOUSE_MODE_VISIBLE) {
pandemonium_js_display_cursor_set_visible(1);
pandemonium_js_display_cursor_lock_set(false);
} else if (p_mode == MOUSE_MODE_HIDDEN) {
pandemonium_js_display_cursor_set_visible(0);
pandemonium_js_display_cursor_lock_set(false);
} else if (p_mode == MOUSE_MODE_CAPTURED) {
pandemonium_js_display_cursor_set_visible(1);
pandemonium_js_display_cursor_lock_set(true);
}
}
OS::MouseMode OS_JavaScript::get_mouse_mode() const {
if (pandemonium_js_display_cursor_is_hidden()) {
return MOUSE_MODE_HIDDEN;
}
if (pandemonium_js_display_cursor_is_locked()) {
return MOUSE_MODE_CAPTURED;
}
return MOUSE_MODE_VISIBLE;
}
// Wheel
int OS_JavaScript::mouse_wheel_callback(double p_delta_x, double p_delta_y) {
OS_JavaScript *os = get_singleton();
if (!pandemonium_js_display_canvas_is_focused()) {
if (os->cursor_inside_canvas) {
pandemonium_js_display_canvas_focus();
} else {
return false;
}
}
InputDefault *input = os->input;
Ref<InputEventMouseButton> ev;
ev.instance();
ev->set_position(input->get_mouse_position());
ev->set_global_position(ev->get_position());
ev->set_shift(input->is_key_pressed(KEY_SHIFT));
ev->set_alt(input->is_key_pressed(KEY_ALT));
ev->set_control(input->is_key_pressed(KEY_CONTROL));
ev->set_metakey(input->is_key_pressed(KEY_META));
if (p_delta_y < 0) {
ev->set_button_index(BUTTON_WHEEL_UP);
} else if (p_delta_y > 0) {
ev->set_button_index(BUTTON_WHEEL_DOWN);
} else if (p_delta_x > 0) {
ev->set_button_index(BUTTON_WHEEL_LEFT);
} else if (p_delta_x < 0) {
ev->set_button_index(BUTTON_WHEEL_RIGHT);
} else {
return false;
}
// Different browsers give wildly different delta values, and we can't
// interpret deltaMode, so use default value for wheel events' factor.
int button_flag = 1 << (ev->get_button_index() - 1);
ev->set_pressed(true);
ev->set_button_mask(input->get_mouse_button_mask() | button_flag);
input->parse_input_event(ev);
Ref<InputEventMouseButton> release = ev->duplicate();
release->set_pressed(false);
release->set_button_mask(input->get_mouse_button_mask() & ~button_flag);
input->parse_input_event(release);
return true;
}
// Touch
bool OS_JavaScript::has_touchscreen_ui_hint() const {
return pandemonium_js_display_touchscreen_is_available();
}
void OS_JavaScript::touch_callback(int p_type, int p_count) {
OS_JavaScript *os = get_singleton();
// Resume audio context after input in case autoplay was denied.
os->resume_audio();
const JSTouchEvent &touch_event = os->touch_event;
for (int i = 0; i < p_count; i++) {
Point2 point(touch_event.coords[i * 2], touch_event.coords[i * 2 + 1]);
if (p_type == 2) {
// touchmove
Ref<InputEventScreenDrag> ev;
ev.instance();
ev->set_index(touch_event.identifier[i]);
ev->set_position(point);
Point2 &prev = os->touches[i];
ev->set_relative(ev->get_position() - prev);
prev = ev->get_position();
os->input->parse_input_event(ev);
} else {
// touchstart/touchend
Ref<InputEventScreenTouch> ev;
ev.instance();
ev->set_index(touch_event.identifier[i]);
ev->set_position(point);
ev->set_pressed(p_type == 0);
os->touches[i] = point;
os->input->parse_input_event(ev);
// Make sure to flush all events so we can call restricted APIs inside the event.
os->input->flush_buffered_events();
}
}
}
// Gamepad
void OS_JavaScript::gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid) {
InputDefault *input = get_singleton()->input;
if (p_connected) {
input->joy_connection_changed(p_index, true, String::utf8(p_id), String::utf8(p_guid));
} else {
input->joy_connection_changed(p_index, false, "");
}
}
void OS_JavaScript::process_joypads() {
int32_t pads = pandemonium_js_input_gamepad_sample_count();
int32_t s_btns_num = 0;
int32_t s_axes_num = 0;
int32_t s_standard = 0;
float s_btns[16];
float s_axes[10];
for (int idx = 0; idx < pads; idx++) {
int err = pandemonium_js_input_gamepad_sample_get(idx, s_btns, &s_btns_num, s_axes, &s_axes_num, &s_standard);
if (err) {
continue;
}
for (int b = 0; b < s_btns_num; b++) {
// Buttons 6 and 7 in the standard mapping need to be
// axis to be handled as JOY_ANALOG by Pandemonium.
if (s_standard && (b == 6 || b == 7)) {
input->joy_axis(idx, b, s_btns[b]);
} else {
input->joy_button(idx, b, s_btns[b]);
}
}
for (int a = 0; a < s_axes_num; a++) {
input->joy_axis(idx, a, s_axes[a]);
}
}
}
bool OS_JavaScript::is_joy_known(int p_device) {
return input->is_joy_mapped(p_device);
}
String OS_JavaScript::get_joy_guid(int p_device) const {
return input->get_joy_guid_remapped(p_device);
}
// Video
int OS_JavaScript::get_video_driver_count() const {
return VIDEO_DRIVER_MAX;
}
const char *OS_JavaScript::get_video_driver_name(int p_driver) const {
switch (p_driver) {
case VIDEO_DRIVER_GLES2:
return "GLES2";
}
ERR_FAIL_V_MSG(NULL, "Invalid video driver index: " + itos(p_driver) + ".");
}
// Audio
int OS_JavaScript::get_audio_driver_count() const {
return audio_drivers.size();
}
const char *OS_JavaScript::get_audio_driver_name(int p_driver) const {
if (audio_drivers.size() <= p_driver) {
return "Unknown";
}
return audio_drivers[p_driver]->get_name();
}
// Clipboard
void OS_JavaScript::update_clipboard_callback(const char *p_text) {
// Only call set_clipboard from OS (sets local clipboard)
get_singleton()->OS::set_clipboard(String::utf8(p_text));
}
void OS_JavaScript::set_clipboard(const String &p_text) {
OS::set_clipboard(p_text);
int err = pandemonium_js_display_clipboard_set(p_text.utf8().get_data());
ERR_FAIL_COND_MSG(err, "Clipboard API is not supported.");
}
String OS_JavaScript::get_clipboard() const {
pandemonium_js_display_clipboard_get(update_clipboard_callback);
return this->OS::get_clipboard();
}
// Lifecycle
int OS_JavaScript::get_current_video_driver() const {
return video_driver_index;
}
void OS_JavaScript::initialize_core() {
OS_Unix::initialize_core();
}
Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) {
video_mode = p_desired;
// fullscreen_change_callback will correct this if the request is successful.
video_mode.fullscreen = false;
// Handle contextmenu, webglcontextlost, initial canvas setup.
pandemonium_js_display_setup_canvas(video_mode.width, video_mode.height, video_mode.fullscreen, is_hidpi_allowed() ? 1 : 0);
swap_ok_cancel = pandemonium_js_display_is_swap_ok_cancel() == 1;
EmscriptenWebGLContextAttributes attributes;
emscripten_webgl_init_context_attributes(&attributes);
attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed");
attributes.antialias = false;
attributes.explicitSwapControl = true;
ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER);
if (p_desired.layered) {
set_window_per_pixel_transparency_enabled(true);
}
bool gles2 = true;
//if (p_video_driver == VIDEO_DRIVER_GLES2) {
// gles3 = true;
//}
bool gl_initialization_error = false;
while (true) {
if (gles2) {
if (pandemonium_js_display_has_webgl(1) && RasterizerGLES2::is_viable() == OK) {
attributes.majorVersion = 1;
RasterizerGLES2::register_config();
RasterizerGLES2::make_current();
break;
} else {
gl_initialization_error = true;
break;
}
} else {
break;
}
}
webgl_ctx = emscripten_webgl_create_context(canvas_id, &attributes);
if (emscripten_webgl_make_context_current(webgl_ctx) != EMSCRIPTEN_RESULT_SUCCESS) {
gl_initialization_error = true;
}
if (gl_initialization_error) {
OS::get_singleton()->alert("Your browser does not support any of the supported WebGL versions.\n"
"Please update your browser version.",
"Unable to initialize Video driver");
return ERR_UNAVAILABLE;
}
video_driver_index = p_video_driver;
AudioDriverManager::initialize(p_audio_driver);
visual_server = memnew(VisualServerRaster());
#ifndef NO_THREADS
visual_server = memnew(VisualServerWrapMT(visual_server, false));
#endif
input = memnew(InputDefault);
// JS Input interface (js/libs/library_pandemonium_input.js)
pandemonium_js_input_mouse_button_cb(&OS_JavaScript::mouse_button_callback);
pandemonium_js_input_mouse_move_cb(&OS_JavaScript::mouse_move_callback);
pandemonium_js_input_mouse_wheel_cb(&OS_JavaScript::mouse_wheel_callback);
pandemonium_js_input_touch_cb(&OS_JavaScript::touch_callback, touch_event.identifier, touch_event.coords);
pandemonium_js_input_key_cb(&OS_JavaScript::key_callback, key_event.code, key_event.key);
pandemonium_js_input_gamepad_cb(&OS_JavaScript::gamepad_callback);
pandemonium_js_input_paste_cb(&OS_JavaScript::update_clipboard_callback);
pandemonium_js_input_drop_files_cb(&OS_JavaScript::drop_files_callback);
// JS Display interface (js/libs/library_pandemonium_display.js)
pandemonium_js_display_fullscreen_cb(&OS_JavaScript::fullscreen_change_callback);
pandemonium_js_display_window_blur_cb(&window_blur_callback);
pandemonium_js_display_notification_cb(&OS_JavaScript::send_notification_callback,
MainLoop::NOTIFICATION_WM_MOUSE_ENTER,
MainLoop::NOTIFICATION_WM_MOUSE_EXIT,
MainLoop::NOTIFICATION_WM_FOCUS_IN,
MainLoop::NOTIFICATION_WM_FOCUS_OUT);
pandemonium_js_display_vk_cb(&input_text_callback);
visual_server->init();
return OK;
}
void OS_JavaScript::input_text_callback(const char *p_text, int p_cursor) {
OS_JavaScript *os = OS_JavaScript::get_singleton();
if (!os || !os->get_main_loop()) {
return;
}
os->get_main_loop()->input_text(String::utf8(p_text));
Ref<InputEventKey> k;
for (int i = 0; i < p_cursor; i++) {
k.instance();
k->set_pressed(true);
k->set_echo(false);
k->set_scancode(KEY_RIGHT);
os->input->parse_input_event(k);
k.instance();
k->set_pressed(false);
k->set_echo(false);
k->set_scancode(KEY_RIGHT);
os->input->parse_input_event(k);
}
}
bool OS_JavaScript::has_virtual_keyboard() const {
return pandemonium_js_display_vk_available() != 0;
}
void OS_JavaScript::show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
pandemonium_js_display_vk_show(p_existing_text.utf8().get_data(), p_multiline, p_cursor_start, p_cursor_end);
}
void OS_JavaScript::hide_virtual_keyboard() {
pandemonium_js_display_vk_hide();
}
bool OS_JavaScript::get_swap_ok_cancel() {
return swap_ok_cancel;
}
void OS_JavaScript::swap_buffers() {
emscripten_webgl_commit_frame();
}
void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) {
main_loop = p_main_loop;
input->set_main_loop(p_main_loop);
}
MainLoop *OS_JavaScript::get_main_loop() const {
return main_loop;
}
void OS_JavaScript::resume_audio() {
AudioDriverJavaScript::resume();
}
void OS_JavaScript::fs_sync_callback() {
get_singleton()->idb_is_syncing = false;
}
bool OS_JavaScript::main_loop_iterate() {
if (is_userfs_persistent() && idb_needs_sync && !idb_is_syncing) {
idb_is_syncing = true;
idb_needs_sync = false;
pandemonium_js_os_fs_sync(&OS_JavaScript::fs_sync_callback);
}
input->flush_buffered_events();
if (pandemonium_js_input_gamepad_sample() == OK) {
process_joypads();
}
return Main::iteration();
}
int OS_JavaScript::get_screen_dpi(int p_screen) const {
return pandemonium_js_display_screen_dpi_get();
}
float OS_JavaScript::get_screen_scale(int p_screen) const {
return pandemonium_js_display_pixel_ratio_get();
}
float OS_JavaScript::get_screen_max_scale() const {
return get_screen_scale();
}
void OS_JavaScript::delete_main_loop() {
memdelete(main_loop);
main_loop = NULL;
}
void OS_JavaScript::finalize() {
memdelete(input);
visual_server->finish();
emscripten_webgl_commit_frame();
memdelete(visual_server);
emscripten_webgl_destroy_context(webgl_ctx);
for (int i = 0; i < audio_drivers.size(); i++) {
memdelete(audio_drivers[i]);
}
audio_drivers.clear();
}
// Miscellaneous
Error OS_JavaScript::execute(const String &p_path, const List<String> &p_arguments, bool p_blocking, ProcessID *r_child_id, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) {
Array args;
for (const List<String>::Element *E = p_arguments.front(); E; E = E->next()) {
args.push_back(E->get());
}
String json_args = JSON::print(args);
int failed = pandemonium_js_os_execute(json_args.utf8().get_data());
ERR_FAIL_COND_V_MSG(failed, ERR_UNAVAILABLE, "OS::execute() must be implemented in Javascript via 'engine.setOnExecute' if required.");
return OK;
}
Error OS_JavaScript::kill(const ProcessID &p_pid) {
ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "OS::kill() is not available on the HTML5 platform.");
}
int OS_JavaScript::get_process_id() const {
ERR_FAIL_V_MSG(0, "OS::get_process_id() is not available on the HTML5 platform.");
}
int OS_JavaScript::get_processor_count() const {
return pandemonium_js_os_hw_concurrency_get();
}
bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) {
if (p_feature == "HTML5" || p_feature == "web") {
return true;
}
#ifdef JAVASCRIPT_EVAL_ENABLED
if (p_feature == "JavaScript") {
return true;
}
#endif
#ifndef NO_THREADS
if (p_feature == "threads") {
return true;
}
#endif
#if WASM_GDNATIVE
if (p_feature == "wasm32") {
return true;
}
#endif
return false;
}
void OS_JavaScript::alert(const String &p_alert, const String &p_title) {
pandemonium_js_display_alert(p_alert.utf8().get_data());
}
void OS_JavaScript::set_window_title(const String &p_title) {
pandemonium_js_display_window_title_set(p_title.utf8().get_data());
}
void OS_JavaScript::set_icon(const Ref<Image> &p_icon) {
ERR_FAIL_COND(p_icon.is_null());
Ref<Image> icon = p_icon;
if (icon->is_compressed()) {
icon = icon->duplicate();
ERR_FAIL_COND(icon->decompress() != OK);
}
if (icon->get_format() != Image::FORMAT_RGBA8) {
if (icon == p_icon)
icon = icon->duplicate();
icon->convert(Image::FORMAT_RGBA8);
}
png_image png_meta;
memset(&png_meta, 0, sizeof png_meta);
png_meta.version = PNG_IMAGE_VERSION;
png_meta.width = icon->get_width();
png_meta.height = icon->get_height();
png_meta.format = PNG_FORMAT_RGBA;
PoolByteArray png;
size_t len;
PoolByteArray::Read r = icon->get_data().read();
ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, r.ptr(), 0, NULL));
png.resize(len);
PoolByteArray::Write w = png.write();
ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, w.ptr(), &len, 0, r.ptr(), 0, NULL));
w = PoolByteArray::Write();
r = png.read();
pandemonium_js_display_window_icon_set(r.ptr(), len);
}
String OS_JavaScript::get_executable_path() const {
return OS::get_executable_path();
}
Error OS_JavaScript::shell_open(String p_uri) {
// Open URI in a new tab, browser will deal with it by protocol.
pandemonium_js_os_shell_open(p_uri.utf8().get_data());
return OK;
}
String OS_JavaScript::get_name() const {
return "HTML5";
}
bool OS_JavaScript::can_draw() const {
return true; // Always?
}
String OS_JavaScript::get_user_data_dir() const {
return "/userfs";
};
String OS_JavaScript::get_cache_path() const {
return "/home/web_user/.cache";
}
String OS_JavaScript::get_config_path() const {
return "/home/web_user/.config";
}
String OS_JavaScript::get_data_path() const {
return "/home/web_user/.local/share";
}
OS::PowerState OS_JavaScript::get_power_state() {
WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to POWERSTATE_UNKNOWN");
return OS::POWERSTATE_UNKNOWN;
}
int OS_JavaScript::get_power_seconds_left() {
WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to -1");
return -1;
}
int OS_JavaScript::get_power_percent_left() {
WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to -1");
return -1;
}
void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) {
OS_JavaScript *os = get_singleton();
if (!(os->is_userfs_persistent() && p_flags & FileAccess::WRITE)) {
return; // FS persistence is not working or we are not writing.
}
bool is_file_persistent = p_file.begins_with("/userfs");
#ifdef TOOLS_ENABLED
// Hack for editor persistence (can we track).
is_file_persistent = is_file_persistent || p_file.begins_with("/home/web_user/");
#endif
if (is_file_persistent) {
os->idb_needs_sync = true;
}
}
void OS_JavaScript::update_pwa_state_callback() {
if (OS_JavaScript::get_singleton()) {
OS_JavaScript::get_singleton()->pwa_is_waiting = true;
}
if (JavaScript::get_singleton()) {
JavaScript::get_singleton()->emit_signal("pwa_update_available");
}
}
Error OS_JavaScript::pwa_update() {
return pandemonium_js_pwa_update() ? FAILED : OK;
}
bool OS_JavaScript::is_userfs_persistent() const {
return idb_available;
}
Error OS_JavaScript::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) {
String path = p_path.get_file();
p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);
ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ". Error: " + dlerror());
return OK;
}
OS_JavaScript *OS_JavaScript::get_singleton() {
return static_cast<OS_JavaScript *>(OS::get_singleton());
}
OS_JavaScript::OS_JavaScript() {
// Expose method for requesting quit.
pandemonium_js_os_request_quit_cb(&request_quit_callback);
// Set canvas ID
pandemonium_js_config_canvas_id_get(canvas_id, sizeof(canvas_id));
cursor_inside_canvas = true;
cursor_shape = OS::CURSOR_ARROW;
last_click_button_index = -1;
last_click_ms = 0;
last_click_pos = Point2(-100, -100);
transparency_enabled = false;
main_loop = NULL;
visual_server = NULL;
swap_ok_cancel = false;
idb_available = pandemonium_js_os_fs_is_persistent() != 0;
idb_needs_sync = false;
idb_is_syncing = false;
pwa_is_waiting = false;
pandemonium_js_pwa_cb(&OS_JavaScript::update_pwa_state_callback);
if (AudioDriverJavaScript::is_available()) {
#ifdef NO_THREADS
audio_drivers.push_back(memnew(AudioDriverScriptProcessor));
#endif
audio_drivers.push_back(memnew(AudioDriverWorklet));
}
for (int i = 0; i < audio_drivers.size(); i++) {
AudioDriverManager::add_driver(audio_drivers[i]);
}
Vector<Logger *> loggers;
loggers.push_back(memnew(StdLogger));
_set_logger(memnew(CompositeLogger(loggers)));
FileAccessUnix::close_notification_func = file_access_close_callback;
}