diff options
Diffstat (limited to 'platform')
43 files changed, 2982 insertions, 1155 deletions
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 5007b3f570..53b43ae0e8 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -29,7 +29,6 @@ /*************************************************************************/ #include "export.h" -#include "gradle_export_util.h" #include "core/config/project_settings.h" #include "core/io/image_loader.h" @@ -827,7 +826,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { int version_code = p_preset->get("version/code"); String package_name = p_preset->get("package/unique_name"); - int orientation = p_preset->get("screen/orientation"); + const int screen_orientation = _get_android_orientation_value(_get_screen_orientation()); bool screen_support_small = p_preset->get("screen/support_small"); bool screen_support_normal = p_preset->get("screen/support_normal"); @@ -937,7 +936,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } if (tname == "activity" && attrname == "screenOrientation") { - encode_uint32(orientation == 0 ? 0 : 1, &p_manifest.write[iofs + 16]); + encode_uint32(screen_orientation, &p_manifest.write[iofs + 16]); } if (tname == "supports-screens") { @@ -1636,7 +1635,6 @@ public: r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/signed"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/immersive_mode"), true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "screen/orientation", PROPERTY_HINT_ENUM, "Landscape,Portrait"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_small"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_normal"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_large"), true)); @@ -1645,10 +1643,10 @@ public: r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_icon_option, PROPERTY_HINT_FILE, "*.png"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_adaptive_icon_foreground_option, PROPERTY_HINT_FILE, "*.png"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_adaptive_icon_background_option, PROPERTY_HINT_FILE, "*.png"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "apk_expansion/enable"), false)); @@ -1968,9 +1966,21 @@ public: valid = false; } else { Error errn; + // Check for the platform-tools directory. DirAccessRef da = DirAccess::open(sdk_path.plus_file("platform-tools"), &errn); if (errn != OK) { - err += TTR("Invalid Android SDK path for custom build in Editor Settings.") + "\n"; + err += TTR("Invalid Android SDK path for custom build in Editor Settings."); + err += TTR("Missing 'platform-tools' directory!"); + err += "\n"; + valid = false; + } + + // Check for the build-tools directory. + DirAccessRef build_tools_da = DirAccess::open(sdk_path.plus_file("build-tools"), &errn); + if (errn != OK) { + err += TTR("Invalid Android SDK path for custom build in Editor Settings."); + err += TTR("Missing 'build-tools' directory!"); + err += "\n"; valid = false; } } @@ -2159,14 +2169,16 @@ public: } } - Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, String apk_path, EditorProgress ep) { + Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, String export_path, EditorProgress ep) { + int export_format = int(p_preset->get("custom_template/export_format")); + String export_label = export_format == 1 ? "AAB" : "APK"; String release_keystore = p_preset->get("keystore/release"); String release_username = p_preset->get("keystore/release_user"); String release_password = p_preset->get("keystore/release_password"); String jarsigner = EditorSettings::get_singleton()->get("export/android/jarsigner"); if (!FileAccess::exists(jarsigner)) { - EditorNode::add_io_error("'jarsigner' could not be found.\nPlease supply a path in the Editor Settings.\nThe resulting APK is unsigned."); + EditorNode::add_io_error("'jarsigner' could not be found.\nPlease supply a path in the Editor Settings.\nThe resulting " + export_label + " is unsigned."); return OK; } @@ -2184,7 +2196,7 @@ public: user = EditorSettings::get_singleton()->get("export/android/debug_keystore_user"); } - if (ep.step("Signing debug APK...", 103)) { + if (ep.step("Signing debug " + export_label + "...", 103)) { return ERR_SKIP; } @@ -2193,7 +2205,7 @@ public: password = release_password; user = release_username; - if (ep.step("Signing release APK...", 103)) { + if (ep.step("Signing release " + export_label + "...", 103)) { return ERR_SKIP; } } @@ -2218,7 +2230,7 @@ public: args.push_back(keystore); args.push_back("-storepass"); args.push_back(password); - args.push_back(apk_path); + args.push_back(export_path); args.push_back(user); int retval; OS::get_singleton()->execute(jarsigner, args, true, NULL, NULL, &retval); @@ -2227,7 +2239,7 @@ public: return ERR_CANT_CREATE; } - if (ep.step("Verifying APK...", 104)) { + if (ep.step("Verifying " + export_label + "...", 104)) { return ERR_SKIP; } @@ -2235,17 +2247,78 @@ public: args.push_back("-verify"); args.push_back("-keystore"); args.push_back(keystore); - args.push_back(apk_path); + args.push_back(export_path); args.push_back("-verbose"); OS::get_singleton()->execute(jarsigner, args, true, NULL, NULL, &retval); if (retval) { - EditorNode::add_io_error("'jarsigner' verification of APK failed. Make sure to use a jarsigner from OpenJDK 8."); + EditorNode::add_io_error("'jarsigner' verification of " + export_label + " failed. Make sure to use a jarsigner from OpenJDK 8."); return ERR_CANT_CREATE; } return OK; } + void _clear_assets_directory() { + DirAccessRef da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); + if (da_res->dir_exists("res://android/build/assets")) { + DirAccessRef da_assets = DirAccess::open("res://android/build/assets"); + da_assets->erase_contents_recursive(); + da_res->remove("res://android/build/assets"); + } + } + + Error _zip_align_project(const String &sdk_path, const String &unaligned_file_path, const String &aligned_file_path) { + // Look for the zipalign tool. + String zipalign_command; + Error errn; + String build_tools_dir = sdk_path.plus_file("build-tools"); + DirAccessRef da = DirAccess::open(build_tools_dir, &errn); + if (errn != OK) { + return errn; + } + + // There are additional versions directories we need to go through. + da->list_dir_begin(); + String sub_dir = da->get_next(); + while (!sub_dir.empty()) { + if (!sub_dir.begins_with(".") && da->current_is_dir()) { + // Check if the tool is here. + String tool_path = build_tools_dir.plus_file(sub_dir).plus_file("zipalign"); + if (FileAccess::exists(tool_path)) { + zipalign_command = tool_path; + break; + } + } + sub_dir = da->get_next(); + } + da->list_dir_end(); + + if (zipalign_command.empty()) { + EditorNode::get_singleton()->show_warning(TTR("Unable to find the zipalign tool.")); + return ERR_CANT_CREATE; + } + + List<String> zipalign_args; + zipalign_args.push_back("-f"); + zipalign_args.push_back("-v"); + zipalign_args.push_back("4"); + zipalign_args.push_back(unaligned_file_path); // source file + zipalign_args.push_back(aligned_file_path); // destination file + + int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Aligning APK..."), zipalign_command, zipalign_args); + if (result != 0) { + EditorNode::get_singleton()->show_warning(TTR("Unable to complete APK alignment.")); + return ERR_CANT_CREATE; + } + + // Delete the unaligned path. + errn = da->remove(unaligned_file_path); + if (errn != OK) { + EditorNode::get_singleton()->show_warning(TTR("Unable to delete unaligned APK.")); + } + return OK; + } + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); @@ -2325,13 +2398,8 @@ public: _write_tmp_manifest(p_preset, p_give_internet, p_debug); //stores all the project files inside the Gradle project directory. Also includes all ABIs + _clear_assets_directory(); if (!apk_expansion) { - DirAccess *da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); - if (da_res->dir_exists("res://android/build/assets")) { - DirAccess *da_assets = DirAccess::open("res://android/build/assets"); - da_assets->erase_contents_recursive(); - da_res->remove("res://android/build/assets"); - } err = export_project_files(p_preset, rename_and_store_file_in_gradle_project, NULL, ignore_so_file); if (err != OK) { EditorNode::add_io_error("Could not export project files to gradle project\n"); @@ -2419,7 +2487,16 @@ public: copy_args.push_back(build_path); // start directory. String export_filename = p_path.get_file(); + if (export_format == 0) { + // By default, generated apk are not aligned. + export_filename += ".unaligned"; + } String export_path = p_path.get_base_dir(); + if (export_path.is_rel_path()) { + export_path = OS::get_singleton()->get_resource_dir().plus_file(export_path); + } + export_path = ProjectSettings::get_singleton()->globalize_path(export_path).simplify_path(); + String export_file_path = export_path.plus_file(export_filename); copy_args.push_back("-Pexport_path=file:" + export_path); copy_args.push_back("-Pexport_filename=" + export_filename); @@ -2430,11 +2507,20 @@ public: return ERR_CANT_CREATE; } if (_signed) { - err = sign_apk(p_preset, p_debug, p_path, ep); + err = sign_apk(p_preset, p_debug, export_file_path, ep); + if (err != OK) { + return err; + } + } + + if (export_format == 0) { + // Perform zip alignment + err = _zip_align_project(sdk_path, export_file_path, export_path.plus_file(p_path.get_file())); if (err != OK) { return err; } } + return OK; } // This is the start of the Legacy build system @@ -2781,7 +2867,7 @@ void register_android_exporter() { EDITOR_DEF("export/android/jarsigner", ""); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/jarsigner", PROPERTY_HINT_GLOBAL_FILE, exe_ext)); EDITOR_DEF("export/android/debug_keystore", ""); - EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore")); + EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks")); EDITOR_DEF("export/android/debug_keystore_user", "androiddebugkey"); EDITOR_DEF("export/android/debug_keystore_pass", "android"); EDITOR_DEF("export/android/force_system_user", false); diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 95f870bc35..3bc3651712 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -44,6 +44,67 @@ const String godot_project_name_xml_string = R"(<?xml version="1.0" encoding="ut </resources> )"; +DisplayServer::ScreenOrientation _get_screen_orientation() { + String orientation_settings = ProjectSettings::get_singleton()->get("display/window/handheld/orientation"); + DisplayServer::ScreenOrientation screen_orientation; + if (orientation_settings == "portrait") + screen_orientation = DisplayServer::SCREEN_PORTRAIT; + else if (orientation_settings == "reverse_landscape") + screen_orientation = DisplayServer::SCREEN_REVERSE_LANDSCAPE; + else if (orientation_settings == "reverse_portrait") + screen_orientation = DisplayServer::SCREEN_REVERSE_PORTRAIT; + else if (orientation_settings == "sensor_landscape") + screen_orientation = DisplayServer::SCREEN_SENSOR_LANDSCAPE; + else if (orientation_settings == "sensor_portrait") + screen_orientation = DisplayServer::SCREEN_SENSOR_PORTRAIT; + else if (orientation_settings == "sensor") + screen_orientation = DisplayServer::SCREEN_SENSOR; + else + screen_orientation = DisplayServer::SCREEN_LANDSCAPE; + + return screen_orientation; +} + +int _get_android_orientation_value(DisplayServer::ScreenOrientation screen_orientation) { + switch (screen_orientation) { + case DisplayServer::SCREEN_PORTRAIT: + return 1; + case DisplayServer::SCREEN_REVERSE_LANDSCAPE: + return 8; + case DisplayServer::SCREEN_REVERSE_PORTRAIT: + return 9; + case DisplayServer::SCREEN_SENSOR_LANDSCAPE: + return 11; + case DisplayServer::SCREEN_SENSOR_PORTRAIT: + return 12; + case DisplayServer::SCREEN_SENSOR: + return 13; + case DisplayServer::SCREEN_LANDSCAPE: + default: + return 0; + } +} + +String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_orientation) { + switch (screen_orientation) { + case DisplayServer::SCREEN_PORTRAIT: + return "portrait"; + case DisplayServer::SCREEN_REVERSE_LANDSCAPE: + return "reverseLandscape"; + case DisplayServer::SCREEN_REVERSE_PORTRAIT: + return "reversePortrait"; + case DisplayServer::SCREEN_SENSOR_LANDSCAPE: + return "userLandscape"; + case DisplayServer::SCREEN_SENSOR_PORTRAIT: + return "userPortrait"; + case DisplayServer::SCREEN_SENSOR: + return "fullUser"; + case DisplayServer::SCREEN_LANDSCAPE: + default: + return "landscape"; + } +} + // Utility method used to create a directory. Error create_directory(const String &p_dir) { if (!DirAccess::exists(p_dir)) { @@ -209,7 +270,7 @@ String _get_plugins_tag(const String &plugins_names) { String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { bool uses_xr = (int)(p_preset->get("xr_features/xr_mode")) == 1; - String orientation = (int)(p_preset->get("screen/orientation")) == 1 ? "portrait" : "landscape"; + String orientation = _get_android_orientation_label(_get_screen_orientation()); String manifest_activity_text = vformat( " <activity android:name=\"com.godot.game.GodotApp\" " "tools:replace=\"android:screenOrientation\" " diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index 821a4dc584..73c136ed0e 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -22,8 +22,6 @@ allprojects { } ext { - sconsExt = org.gradle.internal.os.OperatingSystem.current().isWindows() ? ".bat" : "" - supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"] supportedTargets = ["release", "debug"] diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index e3c5a02203..89ce3d15e6 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -64,10 +64,42 @@ android { throw new GradleException("Invalid default abi: " + defaultAbi) } + // Find scons' executable path + File sconsExecutableFile = null + def sconsName = "scons" + def sconsExts = (org.gradle.internal.os.OperatingSystem.current().isWindows() + ? [".bat", ".exe"] + : [""]) + logger.lifecycle("Looking for $sconsName executable path") + for (ext in sconsExts) { + String sconsNameExt = sconsName + ext + logger.lifecycle("Checking $sconsNameExt") + + sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt) + if (sconsExecutableFile != null) { + // We're done! + break + } + + // Check all the options in path + List<File> allOptions = org.gradle.internal.os.OperatingSystem.current().findAllInPath(sconsNameExt) + if (!allOptions.isEmpty()) { + // Pick the first option and we're done! + sconsExecutableFile = allOptions.get(0) + break + } + } + + if (sconsExecutableFile == null) { + throw new GradleException("Unable to find executable path for the '$sconsName' command.") + } else { + logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}") + } + // Creating gradle task to generate the native libraries for the default abi. def taskName = getSconsTaskName(buildType) tasks.create(name: taskName, type: Exec) { - executable "scons" + sconsExt + executable sconsExecutableFile.absolutePath args "--directory=${pathToRootDir}", "platform=android", "target=${releaseTarget}", "android_arch=${defaultAbi}", "-j" + Runtime.runtime.availableProcessors() } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index 874fd88848..894009e30f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -515,13 +515,13 @@ public class GodotIO { activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); } break; case SCREEN_SENSOR_LANDSCAPE: { - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE); } break; case SCREEN_SENSOR_PORTRAIT: { - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT); } break; case SCREEN_SENSOR: { - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER); } break; } } diff --git a/platform/iphone/SCsub b/platform/iphone/SCsub index 0a558f8e3d..ee7b2f4ab5 100644 --- a/platform/iphone/SCsub +++ b/platform/iphone/SCsub @@ -14,9 +14,11 @@ iphone_lib = [ "joypad_iphone.mm", "godot_view.mm", "display_layer.mm", + "godot_app_delegate.m", "godot_view_renderer.mm", "godot_view_gesture_recognizer.mm", "device_metrics.m", + "keyboard_input_view.mm", "native_video_view.m", ] diff --git a/platform/iphone/display_server_iphone.mm b/platform/iphone/display_server_iphone.mm index 6fa5e6ee4a..d47d131719 100644 --- a/platform/iphone/display_server_iphone.mm +++ b/platform/iphone/display_server_iphone.mm @@ -35,6 +35,7 @@ #import "device_metrics.h" #import "godot_view.h" #include "ios.h" +#import "keyboard_input_view.h" #import "native_video_view.h" #include "os_iphone.h" #import "view_controller.h" @@ -529,11 +530,17 @@ bool DisplayServerIPhone::screen_is_touchscreen(int p_screen) const { } void DisplayServerIPhone::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_length, int p_cursor_start, int p_cursor_end) { - [AppDelegate.viewController.godotView becomeFirstResponderWithString:p_existing_text]; + NSString *existingString = [[NSString alloc] initWithUTF8String:p_existing_text.utf8().get_data()]; + + [AppDelegate.viewController.keyboardView + becomeFirstResponderWithString:existingString + multiline:p_multiline + cursorStart:p_cursor_start + cursorEnd:p_cursor_end]; } void DisplayServerIPhone::virtual_keyboard_hide() { - [AppDelegate.viewController.godotView resignFirstResponder]; + [AppDelegate.viewController.keyboardView resignFirstResponder]; } void DisplayServerIPhone::virtual_keyboard_set_height(int height) { diff --git a/platform/javascript/native/id_handler.js b/platform/iphone/godot_app_delegate.h index 67d29075b8..ebb21c499b 100644 --- a/platform/javascript/native/id_handler.js +++ b/platform/iphone/godot_app_delegate.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* id_handler.js */ +/* godot_app_delegate.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,36 +28,14 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var IDHandler = /** @constructor */ function() { +#import <UIKit/UIKit.h> - var ids = {}; - var size = 0; +typedef NSObject<UIApplicationDelegate> ApplicationDelegateService; - this.has = function(id) { - return ids.hasOwnProperty(id); - } +@interface GodotApplicalitionDelegate : NSObject <UIApplicationDelegate> - this.add = function(obj) { - size += 1; - var id = crypto.getRandomValues(new Int32Array(32))[0]; - ids[id] = obj; - return id; - } +@property(class, readonly, strong) NSArray<ApplicationDelegateService *> *services; - this.get = function(id) { - return ids[id]; - } ++ (void)addService:(ApplicationDelegateService *)service; - this.remove = function(id) { - size -= 1; - delete ids[id]; - } - - this.size = function() { - return size; - } - - this.ids = ids; -}; - -Module.IDHandler = new IDHandler; +@end diff --git a/platform/iphone/godot_app_delegate.m b/platform/iphone/godot_app_delegate.m new file mode 100644 index 0000000000..a5aad26bd5 --- /dev/null +++ b/platform/iphone/godot_app_delegate.m @@ -0,0 +1,497 @@ +/*************************************************************************/ +/* godot_app_delegate.m */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +#import "godot_app_delegate.h" + +#import "app_delegate.h" + +@interface GodotApplicalitionDelegate () + +@end + +@implementation GodotApplicalitionDelegate + +static NSMutableArray<ApplicationDelegateService *> *services = nil; + ++ (NSArray<ApplicationDelegateService *> *)services { + return services; +} + ++ (void)load { + services = [NSMutableArray new]; + [services addObject:[AppDelegate new]]; +} + ++ (void)addService:(ApplicationDelegateService *)service { + if (!services || !service) { + return; + } + [services addObject:service]; +} + +// UIApplicationDelegate documantation can be found here: https://developer.apple.com/documentation/uikit/uiapplicationdelegate + +// MARK: Window + +- (UIWindow *)window { + UIWindow *result = nil; + + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + UIWindow *value = [service window]; + + if (value) { + result = value; + } + } + + return result; +} + +// MARK: Initializing + +- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions { + BOOL result = NO; + + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + if ([service application:application willFinishLaunchingWithOptions:launchOptions]) { + result = YES; + } + } + + return result; +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions { + BOOL result = NO; + + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + if ([service application:application didFinishLaunchingWithOptions:launchOptions]) { + result = YES; + } + } + + return result; +} + +/* Can be handled by Info.plist. Not yet supported by Godot. + +// MARK: Scene + +- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {} + +- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {} + +*/ + +// MARK: Life-Cycle + +- (void)applicationDidBecomeActive:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationDidBecomeActive:application]; + } +} + +- (void)applicationWillResignActive:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationWillResignActive:application]; + } +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationDidEnterBackground:application]; + } +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationWillEnterForeground:application]; + } +} + +- (void)applicationWillTerminate:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationWillTerminate:application]; + } +} + +// MARK: Environment Changes + +- (void)applicationProtectedDataDidBecomeAvailable:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationProtectedDataDidBecomeAvailable:application]; + } +} + +- (void)applicationProtectedDataWillBecomeUnavailable:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationProtectedDataWillBecomeUnavailable:application]; + } +} + +- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationDidReceiveMemoryWarning:application]; + } +} + +- (void)applicationSignificantTimeChange:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationSignificantTimeChange:application]; + } +} + +// MARK: App State Restoration + +- (BOOL)application:(UIApplication *)application shouldSaveSecureApplicationState:(NSCoder *)coder API_AVAILABLE(ios(13.2)) { + BOOL result = NO; + + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + if ([service application:application shouldSaveSecureApplicationState:coder]) { + result = YES; + } + } + + return result; +} + +- (BOOL)application:(UIApplication *)application shouldRestoreSecureApplicationState:(NSCoder *)coder API_AVAILABLE(ios(13.2)) { + BOOL result = NO; + + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + if ([service application:application shouldRestoreSecureApplicationState:coder]) { + result = YES; + } + } + + return result; +} + +- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + UIViewController *controller = [service application:application viewControllerWithRestorationIdentifierPath:identifierComponents coder:coder]; + + if (controller) { + return controller; + } + } + + return nil; +} + +- (void)application:(UIApplication *)application willEncodeRestorableStateWithCoder:(NSCoder *)coder { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application willEncodeRestorableStateWithCoder:coder]; + } +} + +- (void)application:(UIApplication *)application didDecodeRestorableStateWithCoder:(NSCoder *)coder { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application didDecodeRestorableStateWithCoder:coder]; + } +} + +// MARK: Download Data in Background + +- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application handleEventsForBackgroundURLSession:identifier completionHandler:completionHandler]; + } + + completionHandler(); +} + +// MARK: Remote Notification + +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; + } +} + +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application didFailToRegisterForRemoteNotificationsWithError:error]; + } +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; + } + + completionHandler(UIBackgroundFetchResultNoData); +} + +// MARK: User Activity and Handling Quick Actions + +- (BOOL)application:(UIApplication *)application willContinueUserActivityWithType:(NSString *)userActivityType { + BOOL result = NO; + + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + if ([service application:application willContinueUserActivityWithType:userActivityType]) { + result = YES; + } + } + + return result; +} + +- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *restorableObjects))restorationHandler { + BOOL result = NO; + + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + if ([service application:application continueUserActivity:userActivity restorationHandler:restorationHandler]) { + result = YES; + } + } + + return result; +} + +- (void)application:(UIApplication *)application didUpdateUserActivity:(NSUserActivity *)userActivity { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application didUpdateUserActivity:userActivity]; + } +} + +- (void)application:(UIApplication *)application didFailToContinueUserActivityWithType:(NSString *)userActivityType error:(NSError *)error { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application didFailToContinueUserActivityWithType:userActivityType error:error]; + } +} + +- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application performActionForShortcutItem:shortcutItem completionHandler:completionHandler]; + } +} + +// MARK: WatchKit + +- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application handleWatchKitExtensionRequest:userInfo reply:reply]; + } +} + +// MARK: HealthKit + +- (void)applicationShouldRequestHealthAuthorization:(UIApplication *)application { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service applicationShouldRequestHealthAuthorization:application]; + } +} + +// MARK: Opening an URL + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + if ([service application:app openURL:url options:options]) { + return YES; + } + } + + return NO; +} + +// MARK: Disallowing Specified App Extension Types + +- (BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(UIApplicationExtensionPointIdentifier)extensionPointIdentifier { + BOOL result = NO; + + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + if ([service application:application shouldAllowExtensionPointIdentifier:extensionPointIdentifier]) { + result = YES; + } + } + + return result; +} + +// MARK: SiriKit + +- (id)application:(UIApplication *)application handlerForIntent:(INIntent *)intent API_AVAILABLE(ios(14.0)) { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + id result = [service application:application handlerForIntent:intent]; + + if (result) { + return result; + } + } + + return nil; +} + +// MARK: CloudKit + +- (void)application:(UIApplication *)application userDidAcceptCloudKitShareWithMetadata:(CKShareMetadata *)cloudKitShareMetadata { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application userDidAcceptCloudKitShareWithMetadata:cloudKitShareMetadata]; + } +} + +/* Handled By Info.plist file for now + +// MARK: Interface Geometry + +- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {} + +*/ + +@end diff --git a/platform/iphone/godot_view.h b/platform/iphone/godot_view.h index 62fa2f5a32..a8f4cb38d9 100644 --- a/platform/iphone/godot_view.h +++ b/platform/iphone/godot_view.h @@ -35,7 +35,7 @@ class String; @protocol DisplayLayer; @protocol GodotViewRendererProtocol; -@interface GodotView : UIView <UIKeyInput> +@interface GodotView : UIView @property(assign, nonatomic) id<GodotViewRendererProtocol> renderer; @@ -51,6 +51,4 @@ class String; - (void)stopRendering; - (void)startRendering; -- (BOOL)becomeFirstResponderWithString:(String)p_existing; - @end diff --git a/platform/iphone/godot_view.mm b/platform/iphone/godot_view.mm index eaf7e962ce..0c50842cfa 100644 --- a/platform/iphone/godot_view.mm +++ b/platform/iphone/godot_view.mm @@ -42,7 +42,6 @@ static const int max_touches = 8; @interface GodotView () { UITouch *godot_touches[max_touches]; - String keyboard_text; } @property(assign, nonatomic) BOOL isActive; @@ -278,40 +277,6 @@ static const int max_touches = 8; // MARK: - Input -// MARK: Keyboard - -- (BOOL)canBecomeFirstResponder { - return YES; -} - -- (BOOL)becomeFirstResponderWithString:(String)p_existing { - keyboard_text = p_existing; - return [self becomeFirstResponder]; -} - -- (BOOL)resignFirstResponder { - keyboard_text = String(); - return [super resignFirstResponder]; -} - -- (void)deleteBackward { - if (keyboard_text.length()) { - keyboard_text.erase(keyboard_text.length() - 1, 1); - } - DisplayServerIPhone::get_singleton()->key(KEY_BACKSPACE, true); -} - -- (BOOL)hasText { - return keyboard_text.length() > 0; -} - -- (void)insertText:(NSString *)p_text { - String character; - character.parse_utf8([p_text UTF8String]); - keyboard_text = keyboard_text + character; - DisplayServerIPhone::get_singleton()->key(character[0] == 10 ? KEY_ENTER : character[0], true); -} - // MARK: Touches - (void)initTouches { diff --git a/platform/iphone/keyboard_input_view.h b/platform/iphone/keyboard_input_view.h new file mode 100644 index 0000000000..5382a2e9a9 --- /dev/null +++ b/platform/iphone/keyboard_input_view.h @@ -0,0 +1,37 @@ +/*************************************************************************/ +/* keyboard_input_view.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +#import <UIKit/UIKit.h> + +@interface GodotKeyboardInputView : UITextView + +- (BOOL)becomeFirstResponderWithString:(NSString *)existingString multiline:(BOOL)flag cursorStart:(NSInteger)start cursorEnd:(NSInteger)end; + +@end diff --git a/platform/iphone/keyboard_input_view.mm b/platform/iphone/keyboard_input_view.mm new file mode 100644 index 0000000000..1a37403de7 --- /dev/null +++ b/platform/iphone/keyboard_input_view.mm @@ -0,0 +1,195 @@ +/*************************************************************************/ +/* keyboard_input_view.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +#import "keyboard_input_view.h" + +#include "core/os/keyboard.h" +#include "display_server_iphone.h" +#include "os_iphone.h" + +@interface GodotKeyboardInputView () <UITextViewDelegate> + +@property(nonatomic, copy) NSString *previousText; +@property(nonatomic, assign) NSRange previousSelectedRange; + +@end + +@implementation GodotKeyboardInputView + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer { + self = [super initWithFrame:frame textContainer:textContainer]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.hidden = YES; + self.delegate = self; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(observeTextChange:) + name:UITextViewTextDidChangeNotification + object:self]; +} + +- (void)dealloc { + self.delegate = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +// MARK: Keyboard + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +- (BOOL)becomeFirstResponderWithString:(NSString *)existingString multiline:(BOOL)flag cursorStart:(NSInteger)start cursorEnd:(NSInteger)end { + self.text = existingString; + self.previousText = existingString; + + NSRange textRange; + + // Either a simple cursor or a selection. + if (end > 0) { + textRange = NSMakeRange(start, end - start); + } else { + textRange = NSMakeRange(start, 0); + } + + self.selectedRange = textRange; + self.previousSelectedRange = textRange; + + return [self becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder { + self.text = nil; + self.previousText = nil; + return [super resignFirstResponder]; +} + +// MARK: OS Messages + +- (void)deleteText:(NSInteger)charactersToDelete { + for (int i = 0; i < charactersToDelete; i++) { + DisplayServerIPhone::get_singleton()->key(KEY_BACKSPACE, true); + DisplayServerIPhone::get_singleton()->key(KEY_BACKSPACE, false); + } +} + +- (void)enterText:(NSString *)substring { + String characters; + characters.parse_utf8([substring UTF8String]); + + for (int i = 0; i < characters.size(); i++) { + int character = characters[i]; + + switch (character) { + case 10: + character = KEY_ENTER; + break; + case 8198: + character = KEY_SPACE; + break; + default: + break; + } + + DisplayServerIPhone::get_singleton()->key(character, true); + DisplayServerIPhone::get_singleton()->key(character, false); + } +} + +// MARK: Observer + +- (void)observeTextChange:(NSNotification *)notification { + if (notification.object != self) { + return; + } + + if (self.previousSelectedRange.length == 0) { + // We are deleting all text before cursor if no range was selected. + // This way any inserted or changed text will be updated. + NSString *substringToDelete = [self.previousText substringToIndex:self.previousSelectedRange.location]; + [self deleteText:substringToDelete.length]; + } else { + // If text was previously selected + // we are sending only one `backspace`. + // It will remove all text from text input. + [self deleteText:1]; + } + + NSString *substringToEnter; + + if (self.selectedRange.length == 0) { + // If previous cursor had a selection + // we have to calculate an inserted text. + if (self.previousSelectedRange.length != 0) { + NSInteger rangeEnd = self.selectedRange.location + self.selectedRange.length; + NSInteger rangeStart = MIN(self.previousSelectedRange.location, self.selectedRange.location); + NSInteger rangeLength = MAX(0, rangeEnd - rangeStart); + + NSRange calculatedRange; + + if (rangeLength >= 0) { + calculatedRange = NSMakeRange(rangeStart, rangeLength); + } else { + calculatedRange = NSMakeRange(rangeStart, 0); + } + + substringToEnter = [self.text substringWithRange:calculatedRange]; + } else { + substringToEnter = [self.text substringToIndex:self.selectedRange.location]; + } + } else { + substringToEnter = [self.text substringWithRange:self.selectedRange]; + } + + [self enterText:substringToEnter]; + + self.previousText = self.text; + self.previousSelectedRange = self.selectedRange; +} + +@end diff --git a/platform/iphone/main.m b/platform/iphone/main.m index c292f02822..351b40a881 100644 --- a/platform/iphone/main.m +++ b/platform/iphone/main.m @@ -28,7 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#import "app_delegate.h" +#import "godot_app_delegate.h" #import <UIKit/UIKit.h> #include <stdio.h> @@ -49,7 +49,8 @@ int main(int argc, char *argv[]) { printf("running app main\n"); @autoreleasepool { - UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + NSString *className = NSStringFromClass([GodotApplicalitionDelegate class]); + UIApplicationMain(argc, argv, nil, className); } printf("main done\n"); return 0; diff --git a/platform/iphone/view_controller.h b/platform/iphone/view_controller.h index 2cc4e4c388..ff76359842 100644 --- a/platform/iphone/view_controller.h +++ b/platform/iphone/view_controller.h @@ -32,11 +32,13 @@ @class GodotView; @class GodotNativeVideoView; +@class GodotKeyboardInputView; @interface ViewController : UIViewController @property(nonatomic, readonly, strong) GodotView *godotView; @property(nonatomic, readonly, strong) GodotNativeVideoView *videoView; +@property(nonatomic, readonly, strong) GodotKeyboardInputView *keyboardView; // MARK: Native Video Player diff --git a/platform/iphone/view_controller.mm b/platform/iphone/view_controller.mm index 5d18a72be1..d3969e6b02 100644 --- a/platform/iphone/view_controller.mm +++ b/platform/iphone/view_controller.mm @@ -33,6 +33,7 @@ #include "display_server_iphone.h" #import "godot_view.h" #import "godot_view_renderer.h" +#import "keyboard_input_view.h" #import "native_video_view.h" #include "os_iphone.h" @@ -43,6 +44,7 @@ @property(strong, nonatomic) GodotViewRenderer *renderer; @property(strong, nonatomic) GodotNativeVideoView *videoView; +@property(strong, nonatomic) GodotKeyboardInputView *keyboardView; @end @@ -102,6 +104,10 @@ } - (void)observeKeyboard { + printf("******** setting up keyboard input view\n"); + self.keyboardView = [GodotKeyboardInputView new]; + [self.view addSubview:self.keyboardView]; + printf("******** adding observer for keyboard show/hide\n"); [[NSNotificationCenter defaultCenter] addObserver:self @@ -119,6 +125,9 @@ [self.videoView stopVideo]; self.videoView = nil; + + self.keyboardView = nil; + self.renderer = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 7381ea13b7..a0e6fa0e18 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -18,21 +18,22 @@ if env["threads_enabled"]: build = env.add_program(build_targets, javascript_files) -js_libraries = [ - "native/http_request.js", - "native/library_godot_audio.js", -] -for lib in js_libraries: - env.Append(LINKFLAGS=["--js-library", env.File(lib).path]) -env.Depends(build, js_libraries) +env.AddJSLibraries( + [ + "native/http_request.js", + "native/library_godot_audio.js", + "native/library_godot_display.js", + "native/library_godot_os.js", + ] +) -js_pre = [ - "native/id_handler.js", - "native/utils.js", -] -for js in js_pre: - env.Append(LINKFLAGS=["--pre-js", env.File(js).path]) -env.Depends(build, js_pre) +if env["tools"]: + env.AddJSLibraries(["native/library_godot_editor_tools.js"]) +if env["javascript_eval"]: + env.AddJSLibraries(["native/library_godot_eval.js"]) +for lib in env["JS_LIBS"]: + env.Append(LINKFLAGS=["--js-library", lib]) +env.Depends(build, env["JS_LIBS"]) engine = [ "engine/preloader.js", @@ -55,9 +56,10 @@ out_files = [ zip_dir.File(binary_name + ".js"), zip_dir.File(binary_name + ".wasm"), zip_dir.File(binary_name + ".html"), + zip_dir.File(binary_name + ".audio.worklet.js"), ] html_file = "#misc/dist/html/editor.html" if env["tools"] else "#misc/dist/html/full-size.html" -in_files = [js_wrapped, build[1], html_file] +in_files = [js_wrapped, build[1], html_file, "#platform/javascript/native/audio.worklet.js"] if env["threads_enabled"]: in_files.append(build[2]) out_files.append(zip_dir.File(binary_name + ".worker.js")) diff --git a/platform/javascript/api/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp index d44e8139a1..8d781703ed 100644 --- a/platform/javascript/api/javascript_tools_editor_plugin.cpp +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -39,6 +39,11 @@ #include <emscripten/emscripten.h> +// JavaScript functions defined in library_godot_editor_tools.js +extern "C" { +extern void godot_js_editor_download_file(const char *p_path, const char *p_name, const char *p_mime); +} + static void _javascript_editor_init_callback() { EditorNode::get_singleton()->add_editor_plugin(memnew(JavaScriptToolsEditorPlugin(EditorNode::get_singleton()))); } @@ -65,25 +70,7 @@ void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; _zip_recursive(resource_path, base_path, zip); zipClose(zip, NULL); - EM_ASM({ - const path = "/tmp/project.zip"; - const size = FS.stat(path)["size"]; - const buf = new Uint8Array(size); - const fd = FS.open(path, "r"); - FS.read(fd, buf, 0, size); - FS.close(fd); - FS.unlink(path); - const blob = new Blob([buf], { type: "application/zip" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "project.zip"; - a.style.display = "none"; - document.body.appendChild(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); - }); + godot_js_editor_download_file("/tmp/project.zip", "project.zip", "application/zip"); } void JavaScriptToolsEditorPlugin::_bind_methods() { diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index ab6ed54cc8..dd982bc3a8 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -31,7 +31,6 @@ #include "audio_driver_javascript.h" #include "core/config/project_settings.h" -#include "godot_audio.h" #include <emscripten.h> @@ -45,92 +44,109 @@ const char *AudioDriverJavaScript::get_name() const { return "JavaScript"; } -#ifndef NO_THREADS -void AudioDriverJavaScript::_audio_thread_func(void *p_data) { - AudioDriverJavaScript *obj = static_cast<AudioDriverJavaScript *>(p_data); - while (!obj->quit) { - obj->lock(); - if (!obj->needs_process) { - obj->unlock(); - OS::get_singleton()->delay_usec(1000); // Give the browser some slack. - continue; - } - obj->_js_driver_process(); - obj->needs_process = false; - obj->unlock(); - } +void AudioDriverJavaScript::_state_change_callback(int p_state) { + singleton->state = p_state; } -#endif -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_start() { -#ifndef NO_THREADS - AudioDriverJavaScript::singleton->lock(); -#else - AudioDriverJavaScript::singleton->_js_driver_process(); -#endif +void AudioDriverJavaScript::_latency_update_callback(float p_latency) { + singleton->output_latency = p_latency; } -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_end() { -#ifndef NO_THREADS - AudioDriverJavaScript::singleton->needs_process = true; - AudioDriverJavaScript::singleton->unlock(); -#endif -} +void AudioDriverJavaScript::_audio_driver_process(int p_from, int p_samples) { + int32_t *stream_buffer = reinterpret_cast<int32_t *>(output_rb); + const int max_samples = memarr_len(output_rb); -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) { - AudioDriverJavaScript::singleton->process_capture(sample); + int write_pos = p_from; + int to_write = p_samples; + if (to_write == 0) { + to_write = max_samples; + } + // High part + if (write_pos + to_write > max_samples) { + const int samples_high = max_samples - write_pos; + audio_server_process(samples_high / channel_count, &stream_buffer[write_pos]); + for (int i = write_pos; i < max_samples; i++) { + output_rb[i] = float(stream_buffer[i] >> 16) / 32768.f; + } + to_write -= samples_high; + write_pos = 0; + } + // Leftover + audio_server_process(to_write / channel_count, &stream_buffer[write_pos]); + for (int i = write_pos; i < write_pos + to_write; i++) { + output_rb[i] = float(stream_buffer[i] >> 16) / 32768.f; + } } -void AudioDriverJavaScript::_js_driver_process() { - int sample_count = memarr_len(internal_buffer) / channel_count; - int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer); - audio_server_process(sample_count, stream_buffer); - for (int i = 0; i < sample_count * channel_count; i++) { - internal_buffer[i] = float(stream_buffer[i] >> 16) / 32768.f; +void AudioDriverJavaScript::_audio_driver_capture(int p_from, int p_samples) { + if (get_input_buffer().size() == 0) { + return; // Input capture stopped. } -} + const int max_samples = memarr_len(input_rb); -void AudioDriverJavaScript::process_capture(float sample) { - int32_t sample32 = int32_t(sample * 32768.f) * (1U << 16); - input_buffer_write(sample32); + int read_pos = p_from; + int to_read = p_samples; + if (to_read == 0) { + to_read = max_samples; + } + // High part + if (read_pos + to_read > max_samples) { + const int samples_high = max_samples - read_pos; + for (int i = read_pos; i < max_samples; i++) { + input_buffer_write(int32_t(input_rb[i] * 32768.f) * (1U << 16)); + } + to_read -= samples_high; + read_pos = 0; + } + // Leftover + for (int i = read_pos; i < read_pos + to_read; i++) { + input_buffer_write(int32_t(input_rb[i] * 32768.f) * (1U << 16)); + } } Error AudioDriverJavaScript::init() { mix_rate = GLOBAL_GET("audio/mix_rate"); int latency = GLOBAL_GET("audio/output_latency"); - channel_count = godot_audio_init(mix_rate, latency); - buffer_length = closest_power_of_2(latency * mix_rate / 1000); - buffer_length = godot_audio_create_processor(buffer_length, channel_count); - if (!buffer_length) { - return FAILED; + channel_count = godot_audio_init(mix_rate, latency, &_state_change_callback, &_latency_update_callback); + buffer_length = closest_power_of_2((latency * mix_rate / 1000)); +#ifndef NO_THREADS + node = memnew(WorkletNode); +#else + node = memnew(ScriptProcessorNode); +#endif + buffer_length = node->create(buffer_length, channel_count); + if (output_rb) { + memdelete_arr(output_rb); } - - if (!internal_buffer || (int)memarr_len(internal_buffer) != buffer_length * channel_count) { - if (internal_buffer) - memdelete_arr(internal_buffer); - internal_buffer = memnew_arr(float, buffer_length *channel_count); + output_rb = memnew_arr(float, buffer_length *channel_count); + if (!output_rb) { + return ERR_OUT_OF_MEMORY; } - - if (!internal_buffer) { + if (input_rb) { + memdelete_arr(input_rb); + } + input_rb = memnew_arr(float, buffer_length *channel_count); + if (!input_rb) { return ERR_OUT_OF_MEMORY; } return OK; } void AudioDriverJavaScript::start() { -#ifndef NO_THREADS - thread = Thread::create(_audio_thread_func, this); -#endif - godot_audio_start(internal_buffer); + if (node) { + node->start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb)); + } } void AudioDriverJavaScript::resume() { - godot_audio_resume(); + if (state == 0) { // 'suspended' + godot_audio_resume(); + } } float AudioDriverJavaScript::get_latency() { - return godot_audio_get_latency(); + return output_latency + (float(buffer_length) / mix_rate); } int AudioDriverJavaScript::get_mix_rate() const { @@ -142,48 +158,128 @@ AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const { } void AudioDriverJavaScript::lock() { -#ifndef NO_THREADS - mutex.lock(); -#endif + if (node) { + node->unlock(); + } } void AudioDriverJavaScript::unlock() { -#ifndef NO_THREADS - mutex.unlock(); -#endif -} - -void AudioDriverJavaScript::finish_async() { -#ifndef NO_THREADS - quit = true; // Ask thread to quit. -#endif - godot_audio_finish_async(); + if (node) { + node->unlock(); + } } void AudioDriverJavaScript::finish() { -#ifndef NO_THREADS - Thread::wait_to_finish(thread); - memdelete(thread); - thread = NULL; -#endif - if (internal_buffer) { - memdelete_arr(internal_buffer); - internal_buffer = nullptr; + if (node) { + node->finish(); + memdelete(node); + node = nullptr; + } + if (output_rb) { + memdelete_arr(output_rb); + output_rb = nullptr; + } + if (input_rb) { + memdelete_arr(input_rb); + input_rb = nullptr; } } Error AudioDriverJavaScript::capture_start() { - godot_audio_capture_stop(); + lock(); input_buffer_init(buffer_length); + unlock(); godot_audio_capture_start(); return OK; } Error AudioDriverJavaScript::capture_stop() { + godot_audio_capture_stop(); + lock(); input_buffer.clear(); + unlock(); return OK; } AudioDriverJavaScript::AudioDriverJavaScript() { singleton = this; } + +#ifdef NO_THREADS +/// ScriptProcessorNode implementation +void AudioDriverJavaScript::ScriptProcessorNode::_process_callback() { + AudioDriverJavaScript::singleton->_audio_driver_capture(); + AudioDriverJavaScript::singleton->_audio_driver_process(); +} + +int AudioDriverJavaScript::ScriptProcessorNode::create(int p_buffer_samples, int p_channels) { + return godot_audio_script_create(p_buffer_samples, p_channels); +} + +void AudioDriverJavaScript::ScriptProcessorNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { + godot_audio_script_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, &_process_callback); +} +#else +/// AudioWorkletNode implementation +void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) { + AudioDriverJavaScript::WorkletNode *obj = static_cast<AudioDriverJavaScript::WorkletNode *>(p_data); + AudioDriverJavaScript *driver = AudioDriverJavaScript::singleton; + const int out_samples = memarr_len(driver->output_rb); + const int in_samples = memarr_len(driver->input_rb); + int wpos = 0; + int to_write = out_samples; + int rpos = 0; + int to_read = 0; + int32_t step = 0; + while (!obj->quit) { + if (to_read) { + driver->lock(); + driver->_audio_driver_capture(rpos, to_read); + godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_IN, -to_read); + driver->unlock(); + rpos += to_read; + if (rpos >= in_samples) { + rpos -= in_samples; + } + } + if (to_write) { + driver->lock(); + driver->_audio_driver_process(wpos, to_write); + godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_OUT, to_write); + driver->unlock(); + wpos += to_write; + if (wpos >= out_samples) { + wpos -= out_samples; + } + } + step = godot_audio_worklet_state_wait(obj->state, STATE_PROCESS, step, 1); + to_write = out_samples - godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_OUT); + to_read = godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_IN); + } +} + +int AudioDriverJavaScript::WorkletNode::create(int p_buffer_size, int p_channels) { + godot_audio_worklet_create(p_channels); + return p_buffer_size; +} + +void AudioDriverJavaScript::WorkletNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { + godot_audio_worklet_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, state); + thread = Thread::create(_audio_thread_func, this); +} + +void AudioDriverJavaScript::WorkletNode::lock() { + mutex.lock(); +} + +void AudioDriverJavaScript::WorkletNode::unlock() { + mutex.unlock(); +} + +void AudioDriverJavaScript::WorkletNode::finish() { + quit = true; // Ask thread to quit. + Thread::wait_to_finish(thread); + memdelete(thread); + thread = nullptr; +} +#endif diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index 56a7da0307..f112a1ede4 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -31,53 +31,96 @@ #ifndef AUDIO_DRIVER_JAVASCRIPT_H #define AUDIO_DRIVER_JAVASCRIPT_H -#include "servers/audio_server.h" - #include "core/os/mutex.h" #include "core/os/thread.h" +#include "servers/audio_server.h" + +#include "godot_audio.h" class AudioDriverJavaScript : public AudioDriver { +public: + class AudioNode { + public: + virtual int create(int p_buffer_size, int p_output_channels) = 0; + virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0; + virtual void finish() {} + virtual void lock() {} + virtual void unlock() {} + virtual ~AudioNode() {} + }; + + class WorkletNode : public AudioNode { + private: + enum { + STATE_LOCK, + STATE_PROCESS, + STATE_SAMPLES_IN, + STATE_SAMPLES_OUT, + STATE_MAX, + }; + Mutex mutex; + Thread *thread = nullptr; + bool quit = false; + int32_t state[STATE_MAX] = { 0 }; + + static void _audio_thread_func(void *p_data); + + public: + int create(int p_buffer_size, int p_output_channels) override; + void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; + void finish() override; + void lock() override; + void unlock() override; + }; + + class ScriptProcessorNode : public AudioNode { + private: + static void _process_callback(); + + public: + int create(int p_buffer_samples, int p_channels) override; + void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; + }; + private: - float *internal_buffer = nullptr; + AudioNode *node = nullptr; + + float *output_rb = nullptr; + float *input_rb = nullptr; int buffer_length = 0; int mix_rate = 0; int channel_count = 0; + int state = 0; + float output_latency = 0.0; -public: -#ifndef NO_THREADS - Mutex mutex; - Thread *thread = nullptr; - bool quit = false; - bool needs_process = true; - - static void _audio_thread_func(void *p_data); -#endif + static void _state_change_callback(int p_state); + static void _latency_update_callback(float p_latency); - void _js_driver_process(); +protected: + void _audio_driver_process(int p_from = 0, int p_samples = 0); + void _audio_driver_capture(int p_from = 0, int p_samples = 0); +public: static bool is_available(); - void process_capture(float sample); static AudioDriverJavaScript *singleton; - const char *get_name() const override; + virtual const char *get_name() const; - Error init() override; - void start() override; + virtual Error init(); + virtual void start(); void resume(); - float get_latency() override; - int get_mix_rate() const override; - SpeakerMode get_speaker_mode() const override; - void lock() override; - void unlock() override; - void finish() override; - void finish_async(); + virtual float get_latency(); + virtual int get_mix_rate() const; + virtual SpeakerMode get_speaker_mode() const; + virtual void lock(); + virtual void unlock(); + virtual void finish(); - Error capture_start() override; - Error capture_stop() override; + virtual Error capture_start(); + virtual Error capture_stop(); AudioDriverJavaScript(); }; - #endif diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 8f2961b33d..e6e35f6aa9 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -1,6 +1,6 @@ import os -from emscripten_helpers import run_closure_compiler, create_engine_file +from emscripten_helpers import run_closure_compiler, create_engine_file, add_js_libraries from SCons.Util import WhereIs @@ -85,7 +85,8 @@ def configure(env): if env["use_lto"]: env.Append(CCFLAGS=["-s", "WASM_OBJECT_FILES=0"]) env.Append(LINKFLAGS=["-s", "WASM_OBJECT_FILES=0"]) - env.Append(LINKFLAGS=["--llvm-lto", "1"]) + env.Append(CCFLAGS=["-flto"]) + env.Append(LINKFLAGS=["-flto"]) # Closure compiler if env["use_closure_compiler"]: @@ -95,6 +96,9 @@ def configure(env): jscc = env.Builder(generator=run_closure_compiler, suffix=".cc.js", src_suffix=".js") env.Append(BUILDERS={"BuildJS": jscc}) + # Add helper method for adding libraries. + env.AddMethod(add_js_libraries, "AddJSLibraries") + # Add method that joins/compiles our Engine files. env.AddMethod(create_engine_file, "CreateEngineFile") @@ -165,6 +169,6 @@ def configure(env): env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"]) # callMain for manual start, FS for preloading, PATH and ERRNO_CODES for BrowserFS. - env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain', 'FS', 'PATH']"]) + env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain']"]) # Add code that allow exiting runtime. env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"]) diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index 8dc33bdf64..768e326e80 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -37,6 +37,7 @@ #include <png.h> #include "dom_keys.inc" +#include "godot_js.h" #define DOM_BUTTON_LEFT 0 #define DOM_BUTTON_MIDDLE 1 @@ -44,28 +45,17 @@ #define DOM_BUTTON_XBUTTON1 3 #define DOM_BUTTON_XBUTTON2 4 -char DisplayServerJavaScript::canvas_id[256] = { 0 }; -static bool cursor_inside_canvas = true; - DisplayServerJavaScript *DisplayServerJavaScript::get_singleton() { return static_cast<DisplayServerJavaScript *>(DisplayServer::get_singleton()); } // Window (canvas) void DisplayServerJavaScript::focus_canvas() { - /* clang-format off */ - EM_ASM( - Module['canvas'].focus(); - ); - /* clang-format on */ + godot_js_display_canvas_focus(); } bool DisplayServerJavaScript::is_canvas_focused() { - /* clang-format off */ - return EM_ASM_INT_V( - return document.activeElement == Module['canvas']; - ); - /* clang-format on */ + return godot_js_display_canvas_is_focused() != 0; } bool DisplayServerJavaScript::check_size_force_redraw() { @@ -83,19 +73,17 @@ bool DisplayServerJavaScript::check_size_force_redraw() { } Point2 DisplayServerJavaScript::compute_position_in_canvas(int p_x, int p_y) { - int canvas_x = EM_ASM_INT({ - return Module['canvas'].getBoundingClientRect().x; - }); - int canvas_y = EM_ASM_INT({ - return Module['canvas'].getBoundingClientRect().y; - }); + DisplayServerJavaScript *display = get_singleton(); + int canvas_x; + int canvas_y; + godot_js_display_canvas_bounding_rect_position_get(&canvas_x, &canvas_y); int canvas_width; int canvas_height; - emscripten_get_canvas_element_size(canvas_id, &canvas_width, &canvas_height); + emscripten_get_canvas_element_size(display->canvas_id, &canvas_width, &canvas_height); double element_width; double element_height; - emscripten_get_element_css_size(canvas_id, &element_width, &element_height); + emscripten_get_element_css_size(display->canvas_id, &element_width, &element_height); return Point2((int)(canvas_width / element_width * (p_x - canvas_x)), (int)(canvas_height / element_height * (p_y - canvas_y))); @@ -105,8 +93,7 @@ EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, co DisplayServerJavaScript *display = get_singleton(); // Empty ID is canvas. String target_id = String::utf8(p_event->id); - String canvas_str_id = String::utf8(canvas_id); - if (target_id.empty() || target_id == canvas_str_id) { + if (target_id.empty() || target_id == String::utf8(display->canvas_id)) { // This event property is the only reliable data on // browser fullscreen state. if (p_event->isFullscreen) { @@ -118,14 +105,15 @@ EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, co return false; } -// Drag and drop callback (see native/utils.js). -extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p_filec) { - DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); +// Drag and drop callback. +void DisplayServerJavaScript::drop_files_js_callback(char **p_filev, int p_filec) { + DisplayServerJavaScript *ds = get_singleton(); if (!ds) { ERR_FAIL_MSG("Unable to drop files because the DisplayServer is not active"); } - if (ds->drop_files_callback.is_null()) + if (ds->drop_files_callback.is_null()) { return; + } Vector<String> files; for (int i = 0; i < p_filec; i++) { files.push_back(String::utf8(p_filev[i])); @@ -137,6 +125,18 @@ extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p ds->drop_files_callback.call((const Variant **)&vp, 1, ret, ce); } +// JavaScript quit request callback. +void DisplayServerJavaScript::request_quit_callback() { + DisplayServerJavaScript *ds = get_singleton(); + if (ds && !ds->window_event_callback.is_null()) { + Variant event = int(DisplayServer::WINDOW_EVENT_CLOSE_REQUEST); + Variant *eventp = &event; + Variant ret; + Callable::CallError ce; + ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce); + } +} + // Keys template <typename T> @@ -272,12 +272,13 @@ EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const E } EM_BOOL DisplayServerJavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { + DisplayServerJavaScript *ds = get_singleton(); Input *input = Input::get_singleton(); int input_mask = input->get_mouse_button_mask(); Point2 pos = compute_position_in_canvas(p_event->clientX, p_event->clientY); // For motion outside the canvas, only read mouse movement if dragging // started inside the canvas; imitating desktop app behaviour. - if (!cursor_inside_canvas && !input_mask) + if (!ds->cursor_inside_canvas && !input_mask) return false; Ref<InputEventMouseMotion> ev; @@ -339,35 +340,13 @@ const char *DisplayServerJavaScript::godot2dom_cursor(DisplayServer::CursorShape } } -void DisplayServerJavaScript::set_css_cursor(const char *p_cursor) { - /* clang-format off */ - EM_ASM_({ - Module['canvas'].style.cursor = UTF8ToString($0); - }, p_cursor); - /* clang-format on */ -} - -bool DisplayServerJavaScript::is_css_cursor_hidden() const { - /* clang-format off */ - return EM_ASM_INT({ - return Module['canvas'].style.cursor === 'none'; - }); - /* clang-format on */ -} - void DisplayServerJavaScript::cursor_set_shape(CursorShape p_shape) { ERR_FAIL_INDEX(p_shape, CURSOR_MAX); - - if (mouse_get_mode() == MOUSE_MODE_VISIBLE) { - if (cursors[p_shape] != "") { - Vector<String> url = cursors[p_shape].split("?"); - set_css_cursor(("url(\"" + url[0] + "\") " + url[1] + ", auto").utf8()); - } else { - set_css_cursor(godot2dom_cursor(p_shape)); - } + if (cursor_shape == p_shape) { + return; } - cursor_shape = p_shape; + godot_js_display_cursor_set_shape(godot2dom_cursor(cursor_shape)); } DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const { @@ -376,17 +355,6 @@ DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const { void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { if (p_cursor.is_valid()) { - Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); - - if (cursor_c) { - if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) { - cursor_set_shape(p_shape); - return; - } - - cursors_cache.erase(p_shape); - } - Ref<Texture2D> texture = p_cursor; Ref<AtlasTexture> atlas_texture = p_cursor; Ref<Image> image; @@ -449,53 +417,10 @@ void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, Curso png.resize(len); ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); - char *object_url; - /* clang-format off */ - EM_ASM({ - var PNG_PTR = $0; - var PNG_LEN = $1; - var PTR = $2; - - var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: 'image/png' }); - var url = URL.createObjectURL(png); - var length_bytes = lengthBytesUTF8(url) + 1; - var string_on_wasm_heap = _malloc(length_bytes); - setValue(PTR, string_on_wasm_heap, '*'); - stringToUTF8(url, string_on_wasm_heap, length_bytes); - }, png.ptr(), len, &object_url); - /* clang-format on */ - - String url = String::utf8(object_url) + "?" + itos(p_hotspot.x) + " " + itos(p_hotspot.y); - - /* clang-format off */ - EM_ASM({ _free($0); }, object_url); - /* clang-format on */ - - if (cursors[p_shape] != "") { - /* clang-format off */ - EM_ASM({ - URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); - }, cursors[p_shape].utf8().get_data()); - /* clang-format on */ - cursors[p_shape] = ""; - } - - cursors[p_shape] = url; + godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), png.ptr(), len, p_hotspot.x, p_hotspot.y); - Vector<Variant> params; - params.push_back(p_cursor); - params.push_back(p_hotspot); - cursors_cache.insert(p_shape, params); - - } else if (cursors[p_shape] != "") { - /* clang-format off */ - EM_ASM({ - URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); - }, cursors[p_shape].utf8().get_data()); - /* clang-format on */ - cursors[p_shape] = ""; - - cursors_cache.erase(p_shape); + } else { + godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), NULL, 0, 0, 0); } cursor_set_shape(cursor_shape); @@ -508,40 +433,37 @@ void DisplayServerJavaScript::mouse_set_mode(MouseMode p_mode) { return; if (p_mode == MOUSE_MODE_VISIBLE) { - // set_css_cursor must be called before set_cursor_shape to make the cursor visible - set_css_cursor(godot2dom_cursor(cursor_shape)); - cursor_set_shape(cursor_shape); + godot_js_display_cursor_set_visible(1); emscripten_exit_pointerlock(); } else if (p_mode == MOUSE_MODE_HIDDEN) { - set_css_cursor("none"); + godot_js_display_cursor_set_visible(0); emscripten_exit_pointerlock(); } else if (p_mode == MOUSE_MODE_CAPTURED) { - EMSCRIPTEN_RESULT result = emscripten_request_pointerlock("canvas", false); + godot_js_display_cursor_set_visible(1); + EMSCRIPTEN_RESULT result = emscripten_request_pointerlock(canvas_id, false); ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); - // set_css_cursor must be called before cursor_set_shape to make the cursor visible - set_css_cursor(godot2dom_cursor(cursor_shape)); - cursor_set_shape(cursor_shape); } } DisplayServer::MouseMode DisplayServerJavaScript::mouse_get_mode() const { - if (is_css_cursor_hidden()) + if (godot_js_display_cursor_is_hidden()) { return MOUSE_MODE_HIDDEN; + } EmscriptenPointerlockChangeEvent ev; emscripten_get_pointerlock_status(&ev); - return (ev.isActive && String::utf8(ev.id) == "canvas") ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; + return (ev.isActive && String::utf8(ev.id) == String::utf8(canvas_id)) ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; } // Wheel - EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data) { ERR_FAIL_COND_V(p_event_type != EMSCRIPTEN_EVENT_WHEEL, false); + DisplayServerJavaScript *ds = get_singleton(); if (!is_canvas_focused()) { - if (cursor_inside_canvas) { + if (ds->cursor_inside_canvas) { focus_canvas(); } else { return false; @@ -632,7 +554,7 @@ EM_BOOL DisplayServerJavaScript::touchmove_callback(int p_event_type, const Emsc } bool DisplayServerJavaScript::screen_is_touchscreen(int p_screen) const { - return EM_ASM_INT({ return 'ontouchstart' in window; }); + return godot_js_display_touchscreen_is_available(); } // Gamepad @@ -696,53 +618,30 @@ Vector<String> DisplayServerJavaScript::get_rendering_drivers_func() { } // Clipboard -extern "C" EMSCRIPTEN_KEEPALIVE void update_clipboard(const char *p_text) { - // Only call set_clipboard from OS (sets local clipboard) - DisplayServerJavaScript::get_singleton()->clipboard = p_text; +void DisplayServerJavaScript::update_clipboard_callback(const char *p_text) { + get_singleton()->clipboard = p_text; } void DisplayServerJavaScript::clipboard_set(const String &p_text) { - /* clang-format off */ - int err = EM_ASM_INT({ - var text = UTF8ToString($0); - if (!navigator.clipboard || !navigator.clipboard.writeText) - return 1; - navigator.clipboard.writeText(text).catch(function(e) { - // Setting OS clipboard is only possible from an input callback. - console.error("Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:", e); - }); - return 0; - }, p_text.utf8().get_data()); - /* clang-format on */ + clipboard = p_text; + int err = godot_js_display_clipboard_set(p_text.utf8().get_data()); ERR_FAIL_COND_MSG(err, "Clipboard API is not supported."); } String DisplayServerJavaScript::clipboard_get() const { - /* clang-format off */ - EM_ASM({ - try { - navigator.clipboard.readText().then(function (result) { - ccall('update_clipboard', 'void', ['string'], [result]); - }).catch(function (e) { - // Fail graciously. - }); - } catch (e) { - // Fail graciously. - } - }); - /* clang-format on */ + godot_js_display_clipboard_get(update_clipboard_callback); return clipboard; } -extern "C" EMSCRIPTEN_KEEPALIVE void send_window_event(int p_notification) { +void DisplayServerJavaScript::send_window_event_callback(int p_notification) { + DisplayServerJavaScript *ds = get_singleton(); + if (!ds) { + return; + } if (p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER || p_notification == DisplayServer::WINDOW_EVENT_MOUSE_EXIT) { - cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER; + ds->cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER; } - OS_JavaScript *os = OS_JavaScript::get_singleton(); - if (os->is_finalizing()) - return; // We don't want events anymore. - DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); - if (ds && !ds->window_event_callback.is_null()) { + if (!ds->window_event_callback.is_null()) { Variant event = int(p_notification); Variant *eventp = &event; Variant ret; @@ -752,11 +651,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE void send_window_event(int p_notification) { } void DisplayServerJavaScript::alert(const String &p_alert, const String &p_title) { - /* clang-format off */ - EM_ASM_({ - window.alert(UTF8ToString($0)); - }, p_alert.utf8().get_data()); - /* clang-format on */ + godot_js_display_alert(p_alert.utf8().get_data()); } void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) { @@ -787,29 +682,11 @@ void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) { png.resize(len); ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); - /* clang-format off */ - EM_ASM({ - var PNG_PTR = $0; - var PNG_LEN = $1; - - var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: "image/png" }); - var url = URL.createObjectURL(png); - var link = document.getElementById('-gd-engine-icon'); - if (link === null) { - link = document.createElement('link'); - link.rel = 'icon'; - link.id = '-gd-engine-icon'; - document.head.appendChild(link); - } - link.href = url; - }, png.ptr(), len); - /* clang-format on */ + godot_js_display_window_icon_set(png.ptr(), len); } void DisplayServerJavaScript::_dispatch_input_event(const Ref<InputEvent> &p_event) { OS_JavaScript *os = OS_JavaScript::get_singleton(); - if (os->is_finalizing()) - return; // We don't want events anymore. // Resume audio context after input in case autoplay was denied. os->resume_audio(); @@ -831,16 +708,14 @@ DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_dr DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { r_error = OK; // Always succeeds for now. - /* clang-format off */ - swap_cancel_ok = EM_ASM_INT({ - const win = (['Windows', 'Win64', 'Win32', 'WinCE']); - const plat = navigator.platform || ""; - if (win.indexOf(plat) !== -1) { - return 1; - } - return 0; - }) == 1; - /* clang-format on */ + // Ensure the canvas ID. + godot_js_config_canvas_id_get(canvas_id, 256); + + // Check if it's windows. + swap_cancel_ok = godot_js_display_is_swap_ok_cancel() == 1; + + // Expose method for requesting quit. + godot_js_os_request_quit_cb(request_quit_callback); RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? #if 0 @@ -878,10 +753,8 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive video_driver_index = p_video_driver; #endif - /* clang-format off */ window_set_mode(p_mode); - if (EM_ASM_INT_V({ return Module['resizeCanvasOnStart'] })) { - /* clang-format on */ + if (godot_js_config_is_resize_on_start()) { window_set_size(p_resolution); } @@ -899,8 +772,7 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive result = emscripten_set_##ev##_callback(nullptr, true, &cb); \ EM_CHECK(ev) // These callbacks from Emscripten's html5.h suffice to access most - // JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM - // is used below. + // JavaScript APIs. SET_EM_CALLBACK(canvas_id, mousedown, mouse_button_callback) SET_EM_WINDOW_CALLBACK(mousemove, mousemove_callback) SET_EM_WINDOW_CALLBACK(mouseup, mouse_button_callback) @@ -919,42 +791,20 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive #undef SET_EM_CALLBACK #undef EM_CHECK - /* clang-format off */ - EM_ASM_ARGS({ - // Bind native event listeners. - // Module.listeners, and Module.drop_handler are defined in native/utils.js - const canvas = Module['canvas']; - const send_window_event = cwrap('send_window_event', null, ['number']); - const notifications = arguments; - (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { - Module.listeners.add(canvas, event, send_window_event.bind(null, notifications[index]), true); - }); - // Clipboard - const update_clipboard = cwrap('update_clipboard', null, ['string']); - Module.listeners.add(window, 'paste', function(evt) { - update_clipboard(evt.clipboardData.getData('text')); - }, false); - // Drag an drop - Module.listeners.add(canvas, 'dragover', function(ev) { - // Prevent default behavior (which would try to open the file(s)) - ev.preventDefault(); - }, false); - Module.listeners.add(canvas, 'drop', Module.drop_handler, false); - }, - WINDOW_EVENT_MOUSE_ENTER, - WINDOW_EVENT_MOUSE_EXIT, - WINDOW_EVENT_FOCUS_IN, - WINDOW_EVENT_FOCUS_OUT - ); - /* clang-format on */ + // For APIs that are not (sufficiently) exposed, a + // library is used below (implemented in library_godot_display.js). + godot_js_display_notification_cb(&send_window_event_callback, + WINDOW_EVENT_MOUSE_ENTER, + WINDOW_EVENT_MOUSE_EXIT, + WINDOW_EVENT_FOCUS_IN, + WINDOW_EVENT_FOCUS_OUT); + godot_js_display_paste_cb(update_clipboard_callback); + godot_js_display_drop_files_cb(drop_files_js_callback); Input::get_singleton()->set_event_dispatch_function(_dispatch_input_event); } DisplayServerJavaScript::~DisplayServerJavaScript() { - EM_ASM({ - Module.listeners.clear(); - }); //emscripten_webgl_commit_frame(); //emscripten_webgl_destroy_context(webgl_ctx); } @@ -1057,11 +907,7 @@ void DisplayServerJavaScript::window_set_drop_files_callback(const Callable &p_c } void DisplayServerJavaScript::window_set_title(const String &p_title, WindowID p_window) { - /* clang-format off */ - EM_ASM_({ - document.title = UTF8ToString($0); - }, p_title.utf8().get_data()); - /* clang-format on */ + godot_js_display_window_title_set(p_title.utf8().get_data()); } int DisplayServerJavaScript::window_get_current_screen(WindowID p_window) const { @@ -1103,9 +949,7 @@ Size2i DisplayServerJavaScript::window_get_min_size(WindowID p_window) const { void DisplayServerJavaScript::window_set_size(const Size2i p_size, WindowID p_window) { last_width = p_size.x; last_height = p_size.y; - double scale = EM_ASM_DOUBLE({ - return window.devicePixelRatio || 1; - }); + double scale = godot_js_display_pixel_ratio_get(); emscripten_set_canvas_element_size(canvas_id, p_size.x * scale, p_size.y * scale); emscripten_set_element_css_size(canvas_id, p_size.x, p_size.y); } @@ -1130,7 +974,7 @@ void DisplayServerJavaScript::window_set_mode(WindowMode p_mode, WindowID p_wind emscripten_exit_fullscreen(); } window_mode = WINDOW_MODE_WINDOWED; - window_set_size(windowed_size); + window_set_size(Size2i(last_width, last_height)); } break; case WINDOW_MODE_FULLSCREEN: { EmscriptenFullscreenStrategy strategy; diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index d7116be36f..1f00295d48 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -37,18 +37,22 @@ #include <emscripten/html5.h> class DisplayServerJavaScript : public DisplayServer { - //int video_driver_index; - - Vector2 windowed_size; - +private: + WindowMode window_mode = WINDOW_MODE_WINDOWED; ObjectID window_attached_instance_id = {}; + Callable window_event_callback; + Callable input_event_callback; + Callable input_text_callback; + Callable drop_files_callback; + + String clipboard; Ref<InputEventKey> deferred_key_event; - CursorShape cursor_shape = CURSOR_ARROW; - String cursors[CURSOR_MAX]; - Map<CursorShape, Vector<Variant>> cursors_cache; Point2 touches[32]; + char canvas_id[256] = { 0 }; + bool cursor_inside_canvas = true; + CursorShape cursor_shape = CURSOR_ARROW; Point2i last_click_pos = Point2(-100, -100); // TODO check this again. double last_click_ms = 0; int last_click_button_index = -1; @@ -66,8 +70,6 @@ class DisplayServerJavaScript : public DisplayServer { static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event); static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event); static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape); - static void set_css_cursor(const char *p_cursor); - bool is_css_cursor_hidden() const; // events static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data); @@ -92,22 +94,17 @@ class DisplayServerJavaScript : public DisplayServer { static void _dispatch_input_event(const Ref<InputEvent> &p_event); + static void request_quit_callback(); + static void update_clipboard_callback(const char *p_text); + static void send_window_event_callback(int p_notification); + static void drop_files_js_callback(char **p_filev, int p_filec); + protected: int get_current_video_driver() const; public: // Override return type to make writing static callbacks less tedious. static DisplayServerJavaScript *get_singleton(); - static char canvas_id[256]; - - WindowMode window_mode = WINDOW_MODE_WINDOWED; - - String clipboard; - - Callable window_event_callback; - Callable input_event_callback; - Callable input_text_callback; - Callable drop_files_callback; // utilities bool check_size_force_redraw(); diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py index f6db10fbbd..cc874c432e 100644 --- a/platform/javascript/emscripten_helpers.py +++ b/platform/javascript/emscripten_helpers.py @@ -19,3 +19,9 @@ def create_engine_file(env, target, source, externs): if env["use_closure_compiler"]: return env.BuildJS(target, source, JSEXTERNS=externs) return env.Textfile(target, [env.File(s) for s in source]) + + +def add_js_libraries(env, libraries): + if "JS_LIBS" not in env: + env["JS_LIBS"] = [] + env.Append(JS_LIBS=env.File(libraries)) diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js index 05a11701c0..3745e04479 100644 --- a/platform/javascript/engine/engine.js +++ b/platform/javascript/engine/engine.js @@ -33,7 +33,7 @@ Function('return this')()['Engine'] = (function() { this.resizeCanvasOnStart = false; this.onExecute = null; this.onExit = null; - this.persistentPaths = []; + this.persistentPaths = ['/userfs']; }; Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { @@ -114,18 +114,30 @@ Function('return this')()['Engine'] = (function() { locale = navigator.languages ? navigator.languages[0] : navigator.language; locale = locale.split('.')[0]; } - me.rtenv['locale'] = locale; - me.rtenv['canvas'] = me.canvas; + // Emscripten configuration. me.rtenv['thisProgram'] = me.executableName; - me.rtenv['resizeCanvasOnStart'] = me.resizeCanvasOnStart; me.rtenv['noExitRuntime'] = true; - me.rtenv['onExecute'] = me.onExecute; - me.rtenv['onExit'] = function(code) { - me.rtenv['deinitFS'](); - if (me.onExit) - me.onExit(code); - me.rtenv = null; - }; + // Godot configuration. + me.rtenv['initConfig']({ + 'resizeCanvasOnStart': me.resizeCanvasOnStart, + 'canvas': me.canvas, + 'locale': locale, + 'onExecute': function(p_args) { + if (me.onExecute) { + me.onExecute(p_args); + return 0; + } + return 1; + }, + 'onExit': function(p_code) { + me.rtenv['deinitFS'](); + if (me.onExit) { + me.onExit(p_code); + } + me.rtenv = null; + }, + }); + return new Promise(function(resolve, reject) { preloader.preloadedFiles.forEach(function(file) { me.rtenv['copyToFS'](file.path, file.buffer); @@ -208,8 +220,6 @@ Function('return this')()['Engine'] = (function() { }; Engine.prototype.setOnExecute = function(onExecute) { - if (this.rtenv) - this.rtenv.onExecute = onExecute; this.onExecute = onExecute; }; diff --git a/platform/javascript/engine/utils.js b/platform/javascript/engine/utils.js index 0c97b38199..10e3abe91e 100644 --- a/platform/javascript/engine/utils.js +++ b/platform/javascript/engine/utils.js @@ -4,6 +4,8 @@ var Utils = { function rw(path) { if (path.endsWith('.worker.js')) { return execName + '.worker.js'; + } else if (path.endsWith('.audio.worklet.js')) { + return execName + '.audio.worklet.js'; } else if (path.endsWith('.js')) { return execName + '.js'; } else if (path.endsWith('.wasm')) { diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp index a83ff44d20..d520931067 100644 --- a/platform/javascript/export/export.cpp +++ b/platform/javascript/export/export.cpp @@ -94,6 +94,9 @@ public: } else if (req[1] == basereq + ".js") { filepath += ".js"; ctype = "application/javascript"; + } else if (req[1] == basereq + ".audio.worklet.js") { + filepath += ".audio.worklet.js"; + ctype = "application/javascript"; } else if (req[1] == basereq + ".worker.js") { filepath += ".worker.js"; ctype = "application/javascript"; @@ -440,6 +443,9 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese } else if (file == "godot.worker.js") { file = p_path.get_file().get_basename() + ".worker.js"; + } else if (file == "godot.audio.worklet.js") { + file = p_path.get_file().get_basename() + ".audio.worklet.js"; + } else if (file == "godot.wasm") { file = p_path.get_file().get_basename() + ".wasm"; } @@ -566,6 +572,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese DirAccess::remove_file_or_error(basepath + ".html"); DirAccess::remove_file_or_error(basepath + ".js"); DirAccess::remove_file_or_error(basepath + ".worker.js"); + DirAccess::remove_file_or_error(basepath + ".audio.worklet.js"); DirAccess::remove_file_or_error(basepath + ".pck"); DirAccess::remove_file_or_error(basepath + ".png"); DirAccess::remove_file_or_error(basepath + ".wasm"); diff --git a/platform/javascript/godot_audio.h b/platform/javascript/godot_audio.h index f7f26e5262..7ebda3ad39 100644 --- a/platform/javascript/godot_audio.h +++ b/platform/javascript/godot_audio.h @@ -38,19 +38,24 @@ extern "C" { #include "stddef.h" extern int godot_audio_is_available(); - -extern int godot_audio_init(int p_mix_rate, int p_latency); -extern int godot_audio_create_processor(int p_buffer_length, int p_channel_count); - -extern void godot_audio_start(float *r_buffer_ptr); +extern int godot_audio_init(int p_mix_rate, int p_latency, void (*_state_cb)(int), void (*_latency_cb)(float)); extern void godot_audio_resume(); -extern void godot_audio_finish_async(); - -extern float godot_audio_get_latency(); extern void godot_audio_capture_start(); extern void godot_audio_capture_stop(); +// Worklet +typedef int32_t GodotAudioState[4]; +extern void godot_audio_worklet_create(int p_channels); +extern void godot_audio_worklet_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, GodotAudioState p_state); +extern void godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value); +extern int godot_audio_worklet_state_get(GodotAudioState p_state, int p_idx); +extern int godot_audio_worklet_state_wait(int32_t *p_state, int p_idx, int32_t p_expected, int p_timeout); + +// Script +extern int godot_audio_script_create(int p_buffer_size, int p_channels); +extern void godot_audio_script_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, void (*p_cb)()); + #ifdef __cplusplus } #endif diff --git a/platform/javascript/godot_js.h b/platform/javascript/godot_js.h new file mode 100644 index 0000000000..23596a0897 --- /dev/null +++ b/platform/javascript/godot_js.h @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* godot_js.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +#ifndef GODOT_JS_H +#define GODOT_JS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "stddef.h" + +// Config +extern void godot_js_config_locale_get(char *p_ptr, int p_ptr_max); +extern void godot_js_config_canvas_id_get(char *p_ptr, int p_ptr_max); +extern int godot_js_config_is_resize_on_start(); + +// OS +extern void godot_js_os_finish_async(void (*p_callback)()); +extern void godot_js_os_request_quit_cb(void (*p_callback)()); +extern int godot_js_os_fs_is_persistent(); +extern void godot_js_os_fs_sync(void (*p_callback)()); +extern int godot_js_os_execute(const char *p_json); +extern void godot_js_os_shell_open(const char *p_uri); + +// Display +extern double godot_js_display_pixel_ratio_get(); +extern void godot_js_display_alert(const char *p_text); +extern int godot_js_display_touchscreen_is_available(); +extern int godot_js_display_is_swap_ok_cancel(); + +// Display canvas +extern void godot_js_display_canvas_focus(); +extern int godot_js_display_canvas_is_focused(); +extern void godot_js_display_canvas_bounding_rect_position_get(int32_t *p_x, int32_t *p_y); + +// Display window +extern void godot_js_display_window_request_fullscreen(); +extern void godot_js_display_window_title_set(const char *p_text); +extern void godot_js_display_window_icon_set(const uint8_t *p_ptr, int p_len); + +// Display clipboard +extern int godot_js_display_clipboard_set(const char *p_text); +extern int godot_js_display_clipboard_get(void (*p_callback)(const char *p_text)); + +// Display cursor +extern void godot_js_display_cursor_set_shape(const char *p_cursor); +extern int godot_js_display_cursor_is_hidden(); +extern void godot_js_display_cursor_set_custom_shape(const char *p_shape, const uint8_t *p_ptr, int p_len, int p_hotspot_x, int p_hotspot_y); +extern void godot_js_display_cursor_set_visible(int p_visible); + +// Display listeners +extern void godot_js_display_notification_cb(void (*p_callback)(int p_notification), int p_enter, int p_exit, int p_in, int p_out); +extern void godot_js_display_paste_cb(void (*p_callback)(const char *p_text)); +extern void godot_js_display_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); +#ifdef __cplusplus +} +#endif + +#endif /* GODOT_JS_H */ diff --git a/platform/javascript/javascript_eval.cpp b/platform/javascript/javascript_eval.cpp index 3a72b10dd4..b203253a39 100644 --- a/platform/javascript/javascript_eval.cpp +++ b/platform/javascript/javascript_eval.cpp @@ -33,95 +33,30 @@ #include "api/javascript_eval.h" #include "emscripten.h" -extern "C" EMSCRIPTEN_KEEPALIVE uint8_t *resize_PackedByteArray_and_open_write(PackedByteArray *p_arr, VectorWriteProxy<uint8_t> *r_write, int p_len) { - p_arr->resize(p_len); - *r_write = p_arr->write; - return p_arr->ptrw(); +extern "C" { +union js_eval_ret { + uint32_t b; + double d; + char *s; +}; + +extern int godot_js_eval(const char *p_js, int p_use_global_ctx, union js_eval_ret *p_union_ptr, void *p_byte_arr, void *p_byte_arr_write, void *(*p_callback)(void *p_ptr, void *p_ptr2, int p_len)); } -Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { - union { - bool b; - double d; - char *s; - } js_data; +void *resize_PackedByteArray_and_open_write(void *p_arr, void *r_write, int p_len) { + PackedByteArray *arr = (PackedByteArray *)p_arr; + VectorWriteProxy<uint8_t> *write = (VectorWriteProxy<uint8_t> *)r_write; + arr->resize(p_len); + *write = arr->write; + return arr->ptrw(); +} +Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { + union js_eval_ret js_data; PackedByteArray arr; VectorWriteProxy<uint8_t> arr_write; - /* clang-format off */ - Variant::Type return_type = static_cast<Variant::Type>(EM_ASM_INT({ - - const CODE = $0; - const USE_GLOBAL_EXEC_CONTEXT = $1; - const PTR = $2; - const BYTEARRAY_PTR = $3; - const BYTEARRAY_WRITE_PTR = $4; - var eval_ret; - try { - if (USE_GLOBAL_EXEC_CONTEXT) { - // indirect eval call grants global execution context - var global_eval = eval; - eval_ret = global_eval(UTF8ToString(CODE)); - } else { - eval_ret = eval(UTF8ToString(CODE)); - } - } catch (e) { - err(e); - eval_ret = null; - } - - switch (typeof eval_ret) { - - case 'boolean': - setValue(PTR, eval_ret, 'i32'); - return 1; // BOOL - - case 'number': - setValue(PTR, eval_ret, 'double'); - return 3; // FLOAT - - case 'string': - var array_len = lengthBytesUTF8(eval_ret)+1; - var array_ptr = _malloc(array_len); - try { - if (array_ptr===0) { - throw new Error('String allocation failed (probably out of memory)'); - } - setValue(PTR, array_ptr , '*'); - stringToUTF8(eval_ret, array_ptr, array_len); - return 4; // STRING - } catch (e) { - if (array_ptr!==0) { - _free(array_ptr) - } - err(e); - // fall through - } - break; - - case 'object': - if (eval_ret === null) { - break; - } - - if (ArrayBuffer.isView(eval_ret) && !(eval_ret instanceof Uint8Array)) { - eval_ret = new Uint8Array(eval_ret.buffer); - } - else if (eval_ret instanceof ArrayBuffer) { - eval_ret = new Uint8Array(eval_ret); - } - if (eval_ret instanceof Uint8Array) { - var bytes_ptr = ccall('resize_PackedByteArray_and_open_write', 'number', ['number', 'number' ,'number'], [BYTEARRAY_PTR, BYTEARRAY_WRITE_PTR, eval_ret.length]); - HEAPU8.set(eval_ret, bytes_ptr); - return 20; // PACKED_BYTE_ARRAY - } - break; - } - return 0; // NIL - - }, p_code.utf8().get_data(), p_use_global_exec_context, &js_data, &arr, &arr_write)); - /* clang-format on */ + Variant::Type return_type = static_cast<Variant::Type>(godot_js_eval(p_code.utf8().get_data(), p_use_global_exec_context, &js_data, &arr, &arr_write, resize_PackedByteArray_and_open_write)); switch (return_type) { case Variant::BOOL: @@ -130,9 +65,7 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { return js_data.d; case Variant::STRING: { String str = String::utf8(js_data.s); - /* clang-format off */ - EM_ASM_({ _free($0); }, js_data.s); - /* clang-format on */ + free(js_data.s); // Must free the string allocated in JS. return str; } case Variant::PACKED_BYTE_ARRAY: diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 01722c4bc8..2d28a63566 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -36,20 +36,11 @@ #include <emscripten/emscripten.h> #include <stdlib.h> +#include "godot_js.h" + static OS_JavaScript *os = nullptr; static uint64_t target_ticks = 0; -extern "C" EMSCRIPTEN_KEEPALIVE void _request_quit_callback(char *p_filev[], int p_filec) { - DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); - if (ds) { - Variant event = int(DisplayServer::WINDOW_EVENT_CLOSE_REQUEST); - Variant *eventp = &event; - Variant ret; - Callable::CallError ce; - ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce); - } -} - void exit_callback() { emscripten_cancel_main_loop(); // After this, we can exit! Main::cleanup(); @@ -59,6 +50,10 @@ void exit_callback() { emscripten_force_exit(exit_code); // No matter that we call cancel_main_loop, regular "exit" will not work, forcing. } +void cleanup_after_sync() { + emscripten_set_main_loop(exit_callback, -1, false); +} + void main_loop_callback() { uint64_t current_ticks = os->get_ticks_usec(); @@ -74,68 +69,14 @@ void main_loop_callback() { target_ticks += (uint64_t)(1000000 / target_fps); } if (os->main_loop_iterate()) { - emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async. - /* clang-format off */ - EM_ASM({ - // This will contain the list of operations that need to complete before cleanup. - Module.async_finish = [ - // Always contains at least one async promise, to avoid firing immediately if nothing is added. - new Promise(function(accept, reject) { - setTimeout(accept, 0); - }) - ]; - }); - /* clang-format on */ - os->get_main_loop()->finish(); - os->finalize_async(); // Will add all the async finish functions. - /* clang-format off */ - EM_ASM({ - Promise.all(Module.async_finish).then(function() { - Module.async_finish = []; - return new Promise(function(accept, reject) { - if (!Module.idbfs) { - accept(); - return; - } - FS.syncfs(function(error) { - if (error) { - err('Failed to save IDB file system: ' + error.message); - } - accept(); - }); - }); - }).then(function() { - ccall("cleanup_after_sync", null, []); - }); - }); - /* clang-format on */ + emscripten_cancel_main_loop(); // Cancel current loop and wait for cleanup_after_sync. + godot_js_os_finish_async(cleanup_after_sync); } } -extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() { - emscripten_set_main_loop(exit_callback, -1, false); -} - /// When calling main, it is assumed FS is setup and synced. int main(int argc, char *argv[]) { - // Configure locale. - char locale_ptr[16]; - /* clang-format off */ - EM_ASM({ - stringToUTF8(Module['locale'], $0, 16); - }, locale_ptr); - /* clang-format on */ - setenv("LANG", locale_ptr, true); - - // Ensure the canvas ID. - /* clang-format off */ - EM_ASM({ - stringToUTF8("#" + Module['canvas'].id, $0, 255); - }, DisplayServerJavaScript::canvas_id); - /* clang-format on */ - os = new OS_JavaScript(); - os->set_idb_available((bool)EM_ASM_INT({ return Module.idbfs })); // We must override main when testing is enabled TEST_MAIN_OVERRIDE @@ -147,14 +88,6 @@ int main(int argc, char *argv[]) { Main::start(); os->get_main_loop()->init(); - // Expose method for requesting quit. - /* clang-format off */ - EM_ASM({ - Module['request_quit'] = function() { - ccall("_request_quit_callback", null, []); - }; - }); - /* clang-format on */ emscripten_set_main_loop(main_loop_callback, -1, false); // Immediately run the first iteration. // We are inside an animation frame, we want to immediately draw on the newly setup canvas. diff --git a/platform/javascript/native/audio.worklet.js b/platform/javascript/native/audio.worklet.js new file mode 100644 index 0000000000..ad7957e45c --- /dev/null +++ b/platform/javascript/native/audio.worklet.js @@ -0,0 +1,186 @@ +/*************************************************************************/ +/* audio.worklet.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ +class RingBuffer { + + constructor(p_buffer, p_state) { + this.buffer = p_buffer; + this.avail = p_state; + this.rpos = 0; + this.wpos = 0; + } + + data_left() { + return Atomics.load(this.avail, 0); + } + + space_left() { + return this.buffer.length - this.data_left(); + } + + read(output) { + const size = this.buffer.length; + let from = 0 + let to_write = output.length; + if (this.rpos + to_write > size) { + const high = size - this.rpos; + output.set(this.buffer.subarray(this.rpos, size)); + from = high; + to_write -= high; + this.rpos = 0; + } + output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from); + this.rpos += to_write; + Atomics.add(this.avail, 0, -output.length); + Atomics.notify(this.avail, 0); + } + + write(p_buffer) { + const to_write = p_buffer.length; + const mw = this.buffer.length - this.wpos; + if (mw >= to_write) { + this.buffer.set(p_buffer, this.wpos); + } else { + const high = p_buffer.subarray(0, to_write - mw); + const low = p_buffer.subarray(to_write - mw); + this.buffer.set(high, this.wpos); + this.buffer.set(low); + } + let diff = to_write; + if (this.wpos + diff >= this.buffer.length) { + diff -= this.buffer.length; + } + this.wpos += diff; + Atomics.add(this.avail, 0, to_write); + Atomics.notify(this.avail, 0); + } +} + +class GodotProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.running = true; + this.lock = null; + this.notifier = null; + this.output = null; + this.output_buffer = new Float32Array(); + this.input = null; + this.input_buffer = new Float32Array(); + this.port.onmessage = (event) => { + const cmd = event.data['cmd']; + const data = event.data['data']; + this.parse_message(cmd, data); + }; + } + + process_notify() { + Atomics.add(this.notifier, 0, 1); + Atomics.notify(this.notifier, 0); + } + + parse_message(p_cmd, p_data) { + if (p_cmd == "start" && p_data) { + const state = p_data[0]; + let idx = 0; + this.lock = state.subarray(idx, ++idx); + this.notifier = state.subarray(idx, ++idx); + const avail_in = state.subarray(idx, ++idx); + const avail_out = state.subarray(idx, ++idx); + this.input = new RingBuffer(p_data[1], avail_in); + this.output = new RingBuffer(p_data[2], avail_out); + } else if (p_cmd == "stop") { + this.runing = false; + this.output = null; + this.input = null; + } + } + + array_has_data(arr) { + return arr.length && arr[0].length && arr[0][0].length; + } + + process(inputs, outputs, parameters) { + if (!this.running) { + return false; // Stop processing. + } + if (this.output === null) { + return true; // Not ready yet, keep processing. + } + const process_input = this.array_has_data(inputs); + if (process_input) { + const input = inputs[0]; + const chunk = input[0].length * input.length; + if (this.input_buffer.length != chunk) { + this.input_buffer = new Float32Array(chunk); + } + if (this.input.space_left() >= chunk) { + this.write_input(this.input_buffer, input); + this.input.write(this.input_buffer); + } else { + this.port.postMessage("Input buffer is full! Skipping input frame."); + } + } + const process_output = this.array_has_data(outputs); + if (process_output) { + const output = outputs[0]; + const chunk = output[0].length * output.length; + if (this.output_buffer.length != chunk) { + this.output_buffer = new Float32Array(chunk) + } + if (this.output.data_left() >= chunk) { + this.output.read(this.output_buffer); + this.write_output(output, this.output_buffer); + } else { + this.port.postMessage("Output buffer has not enough frames! Skipping output frame."); + } + } + this.process_notify(); + return true; + } + + write_output(dest, source) { + const channels = dest.length; + for (let ch = 0; ch < channels; ch++) { + for (let sample = 0; sample < dest[ch].length; sample++) { + dest[ch][sample] = source[sample * channels + ch]; + } + } + } + + write_input(dest, source) { + const channels = source.length; + for (let ch = 0; ch < channels; ch++) { + for (let sample = 0; sample < source[ch].length; sample++) { + dest[sample * channels + ch] = source[ch][sample]; + } + } + } +} + +registerProcessor('godot-processor', GodotProcessor); diff --git a/platform/javascript/native/library_godot_audio.js b/platform/javascript/native/library_godot_audio.js index 4e7f3e2af5..846359b8b2 100644 --- a/platform/javascript/native/library_godot_audio.js +++ b/platform/javascript/native/library_godot_audio.js @@ -27,13 +27,109 @@ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var GodotAudio = { - $GodotAudio: { +const GodotAudio = { + $GodotAudio__deps: ['$GodotOS'], + $GodotAudio: { ctx: null, input: null, - script: null, + driver: null, + interval: 0, + + init: function(mix_rate, latency, onstatechange, onlatencyupdate) { + const ctx = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: mix_rate, + // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. + }); + GodotAudio.ctx = ctx; + onstatechange(ctx.state); // Immeditately notify state. + ctx.onstatechange = function() { + let state = 0; + switch (ctx.state) { + case 'suspended': + state = 0; + break; + case 'running': + state = 1; + break; + case 'closed': + state = 2; + break; + } + onstatechange(state); + } + // Update computed latency + GodotAudio.interval = setInterval(function() { + let latency = 0; + if (ctx.baseLatency) { + latency += GodotAudio.ctx.baseLatency; + } + if (ctx.outputLatency) { + latency += GodotAudio.ctx.outputLatency; + } + onlatencyupdate(latency); + }, 1000); + GodotOS.atexit(GodotAudio.close_async); + return ctx.destination.channelCount; + }, + + create_input: function(callback) { + if (GodotAudio.input) { + return; // Already started. + } + function gotMediaInput(stream) { + GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); + callback(GodotAudio.input) + } + if (navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia({ + "audio": true + }).then(gotMediaInput, function(e) { out(e) }); + } else { + if (!navigator.getUserMedia) { + navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + } + navigator.getUserMedia({ + "audio": true + }, gotMediaInput, function(e) { out(e) }); + } + }, + + close_async: function(resolve, reject) { + const ctx = GodotAudio.ctx; + GodotAudio.ctx = null; + // Audio was not initialized. + if (!ctx) { + resolve(); + return; + } + // Remove latency callback + if (GodotAudio.interval) { + clearInterval(GodotAudio.interval); + GodotAudio.interval = 0; + } + // Disconnect input, if it was started. + if (GodotAudio.input) { + GodotAudio.input.disconnect(); + GodotAudio.input = null; + } + // Disconnect output + let closed = Promise.resolve(); + if (GodotAudio.driver) { + closed = GodotAudio.driver.close(); + } + closed.then(function() { + return ctx.close(); + }).then(function() { + ctx.onstatechange = null; + resolve(); + }).catch(function(e) { + ctx.onstatechange = null; + console.error("Error closing AudioContext", e); + resolve(); + }); + }, }, godot_audio_is_available__proxy: 'sync', @@ -44,50 +140,10 @@ var GodotAudio = { return 1; }, - godot_audio_init: function(mix_rate, latency) { - GodotAudio.ctx = new (window.AudioContext || window.webkitAudioContext)({ - sampleRate: mix_rate, - // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. - }); - return GodotAudio.ctx.destination.channelCount; - }, - - godot_audio_create_processor: function(buffer_length, channel_count) { - GodotAudio.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); - GodotAudio.script.connect(GodotAudio.ctx.destination); - return GodotAudio.script.bufferSize; - }, - - godot_audio_start: function(buffer_ptr) { - var audioDriverProcessStart = cwrap('audio_driver_process_start'); - var audioDriverProcessEnd = cwrap('audio_driver_process_end'); - var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']); - GodotAudio.script.onaudioprocess = function(audioProcessingEvent) { - audioDriverProcessStart(); - - var input = audioProcessingEvent.inputBuffer; - var output = audioProcessingEvent.outputBuffer; - var internalBuffer = HEAPF32.subarray( - buffer_ptr / HEAPF32.BYTES_PER_ELEMENT, - buffer_ptr / HEAPF32.BYTES_PER_ELEMENT + output.length * output.numberOfChannels); - for (var channel = 0; channel < output.numberOfChannels; channel++) { - var outputData = output.getChannelData(channel); - // Loop through samples. - for (var sample = 0; sample < outputData.length; sample++) { - outputData[sample] = internalBuffer[sample * output.numberOfChannels + channel]; - } - } - - if (GodotAudio.input) { - var inputDataL = input.getChannelData(0); - var inputDataR = input.getChannelData(1); - for (var i = 0; i < inputDataL.length; i++) { - audioDriverProcessCapture(inputDataL[i]); - audioDriverProcessCapture(inputDataR[i]); - } - } - audioDriverProcessEnd(); - }; + godot_audio_init: function(p_mix_rate, p_latency, p_state_change, p_latency_update) { + const statechange = GodotOS.get_func(p_state_change); + const latencyupdate = GodotOS.get_func(p_latency_update); + return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate); }, godot_audio_resume: function() { @@ -96,72 +152,22 @@ var GodotAudio = { } }, - godot_audio_finish_async: function() { - Module.async_finish.push(new Promise(function(accept, reject) { - if (!GodotAudio.ctx) { - setTimeout(accept, 0); - } else { - if (GodotAudio.script) { - GodotAudio.script.disconnect(); - GodotAudio.script = null; - } - if (GodotAudio.input) { - GodotAudio.input.disconnect(); - GodotAudio.input = null; - } - GodotAudio.ctx.close().then(function() { - accept(); - }).catch(function(e) { - accept(); - }); - GodotAudio.ctx = null; - } - })); - }, - - godot_audio_get_latency__proxy: 'sync', - godot_audio_get_latency: function() { - var latency = 0; - if (GodotAudio.ctx) { - if (GodotAudio.ctx.baseLatency) { - latency += GodotAudio.ctx.baseLatency; - } - if (GodotAudio.ctx.outputLatency) { - latency += GodotAudio.ctx.outputLatency; - } - } - return latency; - }, - godot_audio_capture_start__proxy: 'sync', godot_audio_capture_start: function() { if (GodotAudio.input) { return; // Already started. } - function gotMediaInput(stream) { - GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); - GodotAudio.input.connect(GodotAudio.script); - } - - function gotMediaInputError(e) { - out(e); - } - - if (navigator.mediaDevices.getUserMedia) { - navigator.mediaDevices.getUserMedia({"audio": true}).then(gotMediaInput, gotMediaInputError); - } else { - if (!navigator.getUserMedia) - navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError); - } + GodotAudio.create_input(function(input) { + input.connect(GodotAudio.driver.get_node()); + }); }, godot_audio_capture_stop__proxy: 'sync', godot_audio_capture_stop: function() { if (GodotAudio.input) { - const tracks = GodotAudio.input.mediaStream.getTracks(); - for (var i = 0; i < tracks.length; i++) { - tracks[i].stop(); + const tracks = GodotAudio.input['mediaStream']['getTracks'](); + for (let i = 0; i < tracks.length; i++) { + tracks[i]['stop'](); } GodotAudio.input.disconnect(); GodotAudio.input = null; @@ -171,3 +177,165 @@ var GodotAudio = { autoAddDeps(GodotAudio, "$GodotAudio"); mergeInto(LibraryManager.library, GodotAudio); + +/** + * The AudioWorklet API driver, used when threads are available. + */ +const GodotAudioWorklet = { + + $GodotAudioWorklet__deps: ['$GodotAudio'], + $GodotAudioWorklet: { + promise: null, + worklet: null, + + create: function(channels) { + const path = Module['locateFile']('godot.audio.worklet.js'); + GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet.addModule(path).then(function() { + GodotAudioWorklet.worklet = new AudioWorkletNode( + GodotAudio.ctx, + 'godot-processor', + { + 'outputChannelCount': [channels] + } + ); + return Promise.resolve(); + }); + GodotAudio.driver = GodotAudioWorklet; + }, + + start: function(in_buf, out_buf, state) { + GodotAudioWorklet.promise.then(function() { + const node = GodotAudioWorklet.worklet; + node.connect(GodotAudio.ctx.destination); + node.port.postMessage({ + 'cmd': 'start', + 'data': [state, in_buf, out_buf], + }); + node.port.onmessage = function(event) { + console.error(event.data); + }; + }); + }, + + get_node: function() { + return GodotAudioWorklet.worklet; + }, + + close: function() { + return new Promise(function(resolve, reject) { + GodotAudioWorklet.promise.then(function() { + GodotAudioWorklet.worklet.port.postMessage({ + 'cmd': 'stop', + 'data': null, + }); + GodotAudioWorklet.worklet.disconnect(); + GodotAudioWorklet.worklet = null; + GodotAudioWorklet.promise = null; + resolve(); + }); + }); + }, + }, + + godot_audio_worklet_create: function(channels) { + GodotAudioWorklet.create(channels); + }, + + godot_audio_worklet_start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) { + const out_buffer = GodotOS.heapSub(HEAPF32, p_out_buf, p_out_size); + const in_buffer = GodotOS.heapSub(HEAPF32, p_in_buf, p_in_size); + const state = GodotOS.heapSub(HEAP32, p_state, 4); + GodotAudioWorklet.start(in_buffer, out_buffer, state); + }, + + godot_audio_worklet_state_wait: function(p_state, p_idx, p_expected, p_timeout) { + Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout); + return Atomics.load(HEAP32, (p_state >> 2) + p_idx); + }, + + godot_audio_worklet_state_add: function(p_state, p_idx, p_value) { + return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value); + }, + + godot_audio_worklet_state_get: function(p_state, p_idx) { + return Atomics.load(HEAP32, (p_state >> 2) + p_idx); + }, +}; + +autoAddDeps(GodotAudioWorklet, "$GodotAudioWorklet"); +mergeInto(LibraryManager.library, GodotAudioWorklet); + +/* + * The deprecated ScriptProcessorNode API, used when threads are disabled. + */ +const GodotAudioScript = { + + $GodotAudioScript__deps: ['$GodotAudio'], + $GodotAudioScript: { + script: null, + + create: function(buffer_length, channel_count) { + GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); + GodotAudio.driver = GodotAudioScript; + return GodotAudioScript.script.bufferSize; + }, + + start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess) { + GodotAudioScript.script.onaudioprocess = function(event) { + // Read input + const inb = GodotOS.heapSub(HEAPF32, p_in_buf, p_in_size); + const input = event.inputBuffer; + if (GodotAudio.input) { + const inlen = input.getChannelData(0).length; + for (let ch = 0; ch < 2; ch++) { + const data = input.getChannelData(ch); + for (let s = 0; s < inlen; s++) { + inb[s * 2 + ch] = data[s]; + } + } + } + + // Let Godot process the input/output. + onprocess(); + + // Write the output. + const outb = GodotOS.heapSub(HEAPF32, p_out_buf, p_out_size); + const output = event.outputBuffer; + const channels = output.numberOfChannels; + for (let ch = 0; ch < channels; ch++) { + const data = output.getChannelData(ch); + // Loop through samples and assign computed values. + for (let sample = 0; sample < data.length; sample++) { + data[sample] = outb[sample * channels + ch]; + } + } + }; + GodotAudioScript.script.connect(GodotAudio.ctx.destination); + }, + + get_node: function() { + return GodotAudioScript.script; + }, + + close: function() { + return new Promise(function(resolve, reject) { + GodotAudioScript.script.disconnect(); + GodotAudioScript.script.onaudioprocess = null; + GodotAudioScript.script = null; + resolve(); + }); + }, + }, + + godot_audio_script_create: function(buffer_length, channel_count) { + return GodotAudioScript.create(buffer_length, channel_count); + }, + + godot_audio_script_start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) { + const onprocess = GodotOS.get_func(p_cb); + GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess); + }, +}; + +autoAddDeps(GodotAudioScript, "$GodotAudioScript"); +mergeInto(LibraryManager.library, GodotAudioScript); diff --git a/platform/javascript/native/library_godot_display.js b/platform/javascript/native/library_godot_display.js new file mode 100644 index 0000000000..490b9181d0 --- /dev/null +++ b/platform/javascript/native/library_godot_display.js @@ -0,0 +1,478 @@ +/*************************************************************************/ +/* library_godot_display.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +/* + * Display Server listeners. + * Keeps track of registered event listeners so it can remove them on shutdown. + */ +const GodotDisplayListeners = { + $GodotDisplayListeners__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayListeners.clear(); resolve(); });', + $GodotDisplayListeners: { + handlers: [], + + has: function(target, event, method, capture) { + return GodotDisplayListeners.handlers.findIndex(function(e) { + return e.target === target && e.event === event && e.method === method && e.capture == capture; + }) !== -1; + }, + + add: function(target, event, method, capture) { + if (GodotDisplayListeners.has(target, event, method, capture)) { + return; + } + function Handler(target, event, method, capture) { + this.target = target; + this.event = event; + this.method = method; + this.capture = capture; + }; + GodotDisplayListeners.handlers.push(new Handler(target, event, method, capture)); + target.addEventListener(event, method, capture); + }, + + clear: function() { + GodotDisplayListeners.handlers.forEach(function(h) { + h.target.removeEventListener(h.event, h.method, h.capture); + }); + GodotDisplayListeners.handlers.length = 0; + }, + }, +}; +mergeInto(LibraryManager.library, GodotDisplayListeners); + +/* + * Drag and drop handler. + * This is pretty big, but basically detect dropped files on GodotConfig.canvas, + * process them one by one (recursively for directories), and copies them to + * the temporary FS path '/tmp/drop-[random]/' so it can be emitted as a godot + * event (that requires a string array of paths). + * + * NOTE: The temporary files are removed after the callback. This means that + * deferred callbacks won't be able to access the files. + */ +const GodotDisplayDragDrop = { + + $GodotDisplayDragDrop__deps: ['$FS', '$GodotFS'], + $GodotDisplayDragDrop: { + promises: [], + pending_files: [], + + add_entry: function(entry) { + if (entry.isDirectory) { + GodotDisplayDragDrop.add_dir(entry); + } else if (entry.isFile) { + GodotDisplayDragDrop.add_file(entry); + } else { + console.error("Unrecognized entry...", entry); + } + }, + + add_dir: function(entry) { + GodotDisplayDragDrop.promises.push(new Promise(function(resolve, reject) { + const reader = entry.createReader(); + reader.readEntries(function(entries) { + for (let i = 0; i < entries.length; i++) { + GodotDisplayDragDrop.add_entry(entries[i]); + } + resolve(); + }); + })); + }, + + add_file: function(entry) { + GodotDisplayDragDrop.promises.push(new Promise(function(resolve, reject) { + entry.file(function(file) { + const reader = new FileReader(); + reader.onload = function() { + const f = { + "path": file.relativePath || file.webkitRelativePath, + "name": file.name, + "type": file.type, + "size": file.size, + "data": reader.result + }; + if (!f['path']) { + f['path'] = f['name']; + } + GodotDisplayDragDrop.pending_files.push(f); + resolve() + }; + reader.onerror = function() { + console.log("Error reading file"); + reject(); + } + reader.readAsArrayBuffer(file); + }, function(err) { + console.log("Error!"); + reject(); + }); + })); + }, + + process: function(resolve, reject) { + if (GodotDisplayDragDrop.promises.length == 0) { + resolve(); + return; + } + GodotDisplayDragDrop.promises.pop().then(function() { + setTimeout(function() { + GodotDisplayDragDrop.process(resolve, reject); + }, 0); + }); + }, + + _process_event: function(ev, callback) { + ev.preventDefault(); + if (ev.dataTransfer.items) { + // Use DataTransferItemList interface to access the file(s) + for (let i = 0; i < ev.dataTransfer.items.length; i++) { + const item = ev.dataTransfer.items[i]; + let entry = null; + if ("getAsEntry" in item) { + entry = item.getAsEntry(); + } else if ("webkitGetAsEntry" in item) { + entry = item.webkitGetAsEntry(); + } + if (entry) { + GodotDisplayDragDrop.add_entry(entry); + } + } + } else { + console.error("File upload not supported"); + } + new Promise(GodotDisplayDragDrop.process).then(function() { + const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/"; + const drops = []; + const files = []; + FS.mkdir(DROP); + GodotDisplayDragDrop.pending_files.forEach((elem) => { + const path = elem['path']; + GodotFS.copy_to_fs(DROP + path, elem['data']); + let idx = path.indexOf("/"); + if (idx == -1) { + // Root file + drops.push(DROP + path); + } else { + // Subdir + const sub = path.substr(0, idx); + idx = sub.indexOf("/"); + if (idx < 0 && drops.indexOf(DROP + sub) == -1) { + drops.push(DROP + sub); + } + } + files.push(DROP + path); + }); + GodotDisplayDragDrop.promises = []; + GodotDisplayDragDrop.pending_files = []; + callback(drops); + const dirs = [DROP.substr(0, DROP.length -1)]; + // Remove temporary files + files.forEach(function (file) { + FS.unlink(file); + let dir = file.replace(DROP, ""); + let idx = dir.lastIndexOf("/"); + while (idx > 0) { + dir = dir.substr(0, idx); + if (dirs.indexOf(DROP + dir) == -1) { + dirs.push(DROP + dir); + } + idx = dir.lastIndexOf("/"); + } + }); + // Remove dirs. + dirs.sort(function(a, b) { + const al = (a.match(/\//g) || []).length; + const bl = (b.match(/\//g) || []).length; + if (al > bl) + return -1; + else if (al < bl) + return 1; + return 0; + }).forEach(function(dir) { + FS.rmdir(dir); + }); + }); + }, + + handler: function(callback) { + return function(ev) { + GodotDisplayDragDrop._process_event(ev, callback); + }; + }, + }, +}; +mergeInto(LibraryManager.library, GodotDisplayDragDrop); + +/* + * Display server cursor helper. + * Keeps track of cursor status and custom shapes. + */ +const GodotDisplayCursor = { + $GodotDisplayCursor__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayCursor.clear(); resolve(); });', + $GodotDisplayCursor__deps: ['$GodotConfig', '$GodotOS'], + $GodotDisplayCursor: { + shape: 'auto', + visible: true, + cursors: {}, + set_style: function(style) { + GodotConfig.canvas.style.cursor = style; + }, + set_shape: function(shape) { + GodotDisplayCursor.shape = shape; + let css = shape; + if (shape in GodotDisplayCursor.cursors) { + const c = GodotDisplayCursor.cursors[shape]; + css = 'url("' + c.url + '") ' + c.x + ' ' + c.y + ', auto'; + } + if (GodotDisplayCursor.visible) { + GodotDisplayCursor.set_style(css); + } + }, + clear: function() { + GodotDisplayCursor.set_style(''); + GodotDisplayCursor.shape = 'auto'; + GodotDisplayCursor.visible = true; + Object.keys(GodotDisplayCursor.cursors).forEach(function(key) { + URL.revokeObjectURL(GodotDisplayCursor.cursors[key]); + delete GodotDisplayCursor.cursors[key]; + }); + }, + }, +}; +mergeInto(LibraryManager.library, GodotDisplayCursor); + +/** + * Display server interface. + * + * Exposes all the functions needed by DisplayServer implementation. + */ +const GodotDisplay = { + $GodotDisplay__deps: ['$GodotConfig', '$GodotOS', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop'], + $GodotDisplay: { + window_icon: '', + }, + + godot_js_display_is_swap_ok_cancel: function() { + const win = (['Windows', 'Win64', 'Win32', 'WinCE']); + const plat = navigator.platform || ""; + if (win.indexOf(plat) !== -1) { + return 1; + } + return 0; + }, + + godot_js_display_alert: function(p_text) { + window.alert(UTF8ToString(p_text)); + }, + + godot_js_display_pixel_ratio_get: function() { + return window.devicePixelRatio || 1; + }, + + /* + * Canvas + */ + godot_js_display_canvas_focus: function() { + GodotConfig.canvas.focus(); + }, + + godot_js_display_canvas_is_focused: function() { + return document.activeElement == GodotConfig.canvas; + }, + + godot_js_display_canvas_bounding_rect_position_get: function(r_x, r_y) { + const brect = GodotConfig.canvas.getBoundingClientRect(); + setValue(r_x, brect.x, 'i32'); + setValue(r_y, brect.y, 'i32'); + }, + + /* + * Touchscreen + */ + godot_js_display_touchscreen_is_available: function() { + return 'ontouchstart' in window; + }, + + /* + * Clipboard + */ + godot_js_display_clipboard_set: function(p_text) { + const text = UTF8ToString(p_text); + if (!navigator.clipboard || !navigator.clipboard.writeText) { + return 1; + } + navigator.clipboard.writeText(text).catch(function(e) { + // Setting OS clipboard is only possible from an input callback. + console.error("Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:", e); + }); + return 0; + }, + + godot_js_display_clipboard_get_deps: ['$GodotOS'], + godot_js_display_clipboard_get: function(callback) { + const func = GodotOS.get_func(callback); + try { + navigator.clipboard.readText().then(function (result) { + const ptr = allocate(intArrayFromString(result), ALLOC_NORMAL); + func(ptr); + _free(ptr); + }).catch(function (e) { + // Fail graciously. + }); + } catch (e) { + // Fail graciously. + } + }, + + /* + * Window + */ + godot_js_display_window_request_fullscreen: function() { + const canvas = GodotConfig.canvas; + (canvas.requestFullscreen || canvas.msRequestFullscreen || + canvas.mozRequestFullScreen || canvas.mozRequestFullscreen || + canvas.webkitRequestFullscreen + ).call(canvas); + }, + + godot_js_display_window_title_set: function(p_data) { + document.title = UTF8ToString(p_data); + }, + + godot_js_display_window_icon_set: function(p_ptr, p_len) { + let link = document.getElementById('-gd-engine-icon'); + if (link === null) { + link = document.createElement('link'); + link.rel = 'icon'; + link.id = '-gd-engine-icon'; + document.head.appendChild(link); + } + const old_icon = GodotDisplay.window_icon; + const png = new Blob([GodotOS.heapCopy(HEAPU8, p_ptr, p_len)], { type: "image/png" }); + GodotDisplay.window_icon = URL.createObjectURL(png); + link.href = GodotDisplay.window_icon; + if (old_icon) { + URL.revokeObjectURL(old_icon); + } + }, + + /* + * Cursor + */ + godot_js_display_cursor_set_visible: function(p_visible) { + const visible = p_visible != 0; + if (visible == GodotDisplayCursor.visible) { + return; + } + GodotDisplayCursor.visible = visible; + if (visible) { + GodotDisplayCursor.set_shape(GodotDisplayCursor.shape); + } else { + GodotDisplayCursor.set_style('none'); + } + }, + + godot_js_display_cursor_is_hidden: function() { + return !GodotDisplayCursor.visible; + }, + + godot_js_display_cursor_set_shape: function(p_string) { + GodotDisplayCursor.set_shape(UTF8ToString(p_string)); + }, + + godot_js_display_cursor_set_custom_shape: function(p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y) { + const shape = UTF8ToString(p_shape); + const old_shape = GodotDisplayCursor.cursors[shape]; + if (p_len > 0) { + const png = new Blob([GodotOS.heapCopy(HEAPU8, p_ptr, p_len)], { type: 'image/png' }); + const url = URL.createObjectURL(png); + GodotDisplayCursor.cursors[shape] = { + url: url, + x: p_hotspot_x, + y: p_hotspot_y, + }; + } else { + delete GodotDisplayCursor.cursors[shape]; + } + if (shape == GodotDisplayCursor.shape) { + GodotDisplayCursor.set_shape(GodotDisplayCursor.shape); + } + if (old_shape) { + URL.revokeObjectURL(old_shape.url); + } + }, + + /* + * Listeners + */ + godot_js_display_notification_cb: function(callback, p_enter, p_exit, p_in, p_out) { + const canvas = GodotConfig.canvas; + const func = GodotOS.get_func(callback); + const notif = [p_enter, p_exit, p_in, p_out]; + ['mouseover', 'mouseleave', 'focus', 'blur'].forEach(function(evt_name, idx) { + GodotDisplayListeners.add(canvas, evt_name, function() { + func.bind(null, notif[idx]); + }, true); + }); + }, + + godot_js_display_paste_cb: function(callback) { + const func = GodotOS.get_func(callback); + GodotDisplayListeners.add(window, 'paste', function(evt) { + const text = evt.clipboardData.getData('text'); + const ptr = allocate(intArrayFromString(text), ALLOC_NORMAL); + func(ptr); + _free(ptr); + }, false); + }, + + godot_js_display_drop_files_cb: function(callback) { + const func = GodotOS.get_func(callback) + const dropFiles = function(files) { + const args = files || []; + if (!args.length) { + return; + } + const argc = args.length; + const argv = GodotOS.allocStringArray(args); + func(argv, argc); + GodotOS.freeStringArray(argv, argc); + }; + const canvas = GodotConfig.canvas; + GodotDisplayListeners.add(canvas, 'dragover', function(ev) { + // Prevent default behavior (which would try to open the file(s)) + ev.preventDefault(); + }, false); + GodotDisplayListeners.add(canvas, 'drop', GodotDisplayDragDrop.handler(dropFiles)); + }, +}; + +autoAddDeps(GodotDisplay, '$GodotDisplay'); +mergeInto(LibraryManager.library, GodotDisplay); diff --git a/platform/javascript/native/library_godot_editor_tools.js b/platform/javascript/native/library_godot_editor_tools.js new file mode 100644 index 0000000000..bd62bbf4e1 --- /dev/null +++ b/platform/javascript/native/library_godot_editor_tools.js @@ -0,0 +1,57 @@ +/*************************************************************************/ +/* library_godot_editor_tools.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +const GodotEditorTools = { + + godot_js_editor_download_file__deps: ['$FS'], + godot_js_editor_download_file: function(p_path, p_name, p_mime) { + const path = UTF8ToString(p_path); + const name = UTF8ToString(p_name); + const mime = UTF8ToString(p_mime); + const size = FS.stat(path)['size']; + const buf = new Uint8Array(size); + const fd = FS.open(path, 'r'); + FS.read(fd, buf, 0, size); + FS.close(fd); + FS.unlink(path); + const blob = new Blob([buf], { type: mime }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = name; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + }, +}; + +mergeInto(LibraryManager.library, GodotEditorTools); diff --git a/platform/javascript/native/library_godot_eval.js b/platform/javascript/native/library_godot_eval.js new file mode 100644 index 0000000000..e83c61dd9d --- /dev/null +++ b/platform/javascript/native/library_godot_eval.js @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* library_godot_eval.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +const GodotEval = { + + godot_js_eval__deps: ['$GodotOS'], + godot_js_eval: function(p_js, p_use_global_ctx, p_union_ptr, p_byte_arr, p_byte_arr_write, p_callback) { + const js_code = UTF8ToString(p_js); + let eval_ret = null; + try { + if (p_use_global_ctx) { + // indirect eval call grants global execution context + const global_eval = eval; + eval_ret = global_eval(js_code); + } else { + eval_ret = eval(js_code); + } + } catch (e) { + err(e); + } + + switch (typeof eval_ret) { + + case 'boolean': + setValue(p_union_ptr, eval_ret, 'i32'); + return 1; // BOOL + + case 'number': + setValue(p_union_ptr, eval_ret, 'double'); + return 3; // REAL + + case 'string': + let array_ptr = GodotOS.allocString(eval_ret); + setValue(p_union_ptr, array_ptr , '*'); + return 4; // STRING + + case 'object': + if (eval_ret === null) { + break; + } + + if (ArrayBuffer.isView(eval_ret) && !(eval_ret instanceof Uint8Array)) { + eval_ret = new Uint8Array(eval_ret.buffer); + } + else if (eval_ret instanceof ArrayBuffer) { + eval_ret = new Uint8Array(eval_ret); + } + if (eval_ret instanceof Uint8Array) { + const func = GodotOS.get_func(p_callback); + const bytes_ptr = func(p_byte_arr, p_byte_arr_write, eval_ret.length); + HEAPU8.set(eval_ret, bytes_ptr); + return 20; // POOL_BYTE_ARRAY + } + break; + } + return 0; // NIL + }, +} + +mergeInto(LibraryManager.library, GodotEval); diff --git a/platform/javascript/native/library_godot_os.js b/platform/javascript/native/library_godot_os.js new file mode 100644 index 0000000000..ed48280674 --- /dev/null +++ b/platform/javascript/native/library_godot_os.js @@ -0,0 +1,313 @@ +/*************************************************************************/ +/* library_godot_os.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +const IDHandler = { + $IDHandler: { + _last_id: 0, + _references: {}, + + get: function(p_id) { + return IDHandler._references[p_id]; + }, + + add: function(p_data) { + const id = ++IDHandler._last_id; + IDHandler._references[id] = p_data; + return id; + }, + + remove: function(p_id) { + delete IDHandler._references[p_id]; + }, + }, +}; + +autoAddDeps(IDHandler, "$IDHandler"); +mergeInto(LibraryManager.library, IDHandler); + +const GodotConfig = { + + $GodotConfig__postset: 'Module["initConfig"] = GodotConfig.init_config;', + $GodotConfig: { + canvas: null, + locale: "en", + resize_on_start: false, + on_execute: null, + + init_config: function(p_opts) { + GodotConfig.resize_on_start = p_opts['resizeCanvasOnStart'] ? true : false; + GodotConfig.canvas = p_opts['canvas']; + GodotConfig.locale = p_opts['locale'] || GodotConfig.locale; + GodotConfig.on_execute = p_opts['onExecute']; + // This is called by emscripten, even if undocumented. + Module['onExit'] = p_opts['onExit']; + }, + }, + + godot_js_config_canvas_id_get: function(p_ptr, p_ptr_max) { + stringToUTF8('#' + GodotConfig.canvas.id, p_ptr, p_ptr_max); + }, + + godot_js_config_locale_get: function(p_ptr, p_ptr_max) { + stringToUTF8(GodotConfig.locale, p_ptr, p_ptr_max); + }, + + godot_js_config_is_resize_on_start: function() { + return GodotConfig.resize_on_start ? 1 : 0; + }, +}; + +autoAddDeps(GodotConfig, '$GodotConfig'); +mergeInto(LibraryManager.library, GodotConfig); + +const GodotFS = { + $GodotFS__deps: ['$FS', '$IDBFS'], + $GodotFS__postset: [ + 'Module["initFS"] = GodotFS.init;', + 'Module["deinitFS"] = GodotFS.deinit;', + 'Module["copyToFS"] = GodotFS.copy_to_fs;', + ].join(''), + $GodotFS: { + _idbfs: false, + _syncing: false, + _mount_points: [], + + is_persistent: function() { + return GodotFS._idbfs ? 1 : 0; + }, + + // Initialize godot file system, setting up persistent paths. + // Returns a promise that resolves when the FS is ready. + // We keep track of mount_points, so that we can properly close the IDBFS + // since emscripten is not doing it by itself. (emscripten GH#12516). + init: function(persistentPaths) { + GodotFS._idbfs = false; + if (!Array.isArray(persistentPaths)) { + return Promise.reject(new Error('Persistent paths must be an array')); + } + if (!persistentPaths.length) { + return Promise.resolve(); + } + GodotFS._mount_points = persistentPaths.slice(); + + function createRecursive(dir) { + try { + FS.stat(dir); + } catch (e) { + if (e.errno !== ERRNO_CODES.ENOENT) { + throw e; + } + FS.mkdirTree(dir); + } + } + + GodotFS._mount_points.forEach(function(path) { + createRecursive(path); + FS.mount(IDBFS, {}, path); + }); + return new Promise(function(resolve, reject) { + FS.syncfs(true, function(err) { + if (err) { + GodotFS._mount_points = []; + GodotFS._idbfs = false; + console.log("IndexedDB not available: " + err.message); + } else { + GodotFS._idbfs = true; + } + resolve(err); + }); + }); + }, + + // Deinit godot file system, making sure to unmount file systems, and close IDBFS(s). + deinit: function() { + GodotFS._mount_points.forEach(function(path) { + try { + FS.unmount(path); + } catch (e) { + console.log("Already unmounted", e); + } + if (GodotFS._idbfs && IDBFS.dbs[path]) { + IDBFS.dbs[path].close(); + delete IDBFS.dbs[path]; + } + }); + GodotFS._mount_points = []; + GodotFS._idbfs = false; + GodotFS._syncing = false; + }, + + sync: function() { + if (GodotFS._syncing) { + err('Already syncing!'); + return Promise.resolve(); + } + GodotFS._syncing = true; + return new Promise(function (resolve, reject) { + FS.syncfs(false, function(error) { + if (error) { + err('Failed to save IDB file system: ' + error.message); + } + GodotFS._syncing = false; + resolve(error); + }); + }); + }, + + // Copies a buffer to the internal file system. Creating directories recursively. + copy_to_fs: function(path, buffer) { + const idx = path.lastIndexOf("/"); + let dir = "/"; + if (idx > 0) { + dir = path.slice(0, idx); + } + try { + FS.stat(dir); + } catch (e) { + if (e.errno !== ERRNO_CODES.ENOENT) { + throw e; + } + FS.mkdirTree(dir); + } + FS.writeFile(path, new Uint8Array(buffer), {'flags': 'wx+'}); + }, + }, +}; +mergeInto(LibraryManager.library, GodotFS); + +const GodotOS = { + $GodotOS__deps: ['$GodotFS'], + $GodotOS__postset: [ + 'Module["request_quit"] = function() { GodotOS.request_quit() };', + 'GodotOS._fs_sync_promise = Promise.resolve();', + ].join(''), + $GodotOS: { + + request_quit: function() {}, + _async_cbs: [], + _fs_sync_promise: null, + + get_func: function(ptr) { + return wasmTable.get(ptr); + }, + + atexit: function(p_promise_cb) { + GodotOS._async_cbs.push(p_promise_cb); + }, + + finish_async: function(callback) { + GodotOS._fs_sync_promise.then(function(err) { + const promises = []; + GodotOS._async_cbs.forEach(function(cb) { + promises.push(new Promise(cb)); + }); + return Promise.all(promises); + }).then(function() { + return GodotFS.sync(); // Final FS sync. + }).then(function(err) { + // Always deferred. + setTimeout(function() { + callback(); + }, 0); + }); + }, + + allocString: function(p_str) { + const length = lengthBytesUTF8(p_str)+1; + const c_str = _malloc(length); + stringToUTF8(p_str, c_str, length); + return c_str; + }, + + allocStringArray: function(strings) { + const size = strings.length; + const c_ptr = _malloc(size * 4); + for (let i = 0; i < size; i++) { + HEAP32[(c_ptr >> 2) + i] = GodotOS.allocString(strings[i]); + } + return c_ptr; + }, + + freeStringArray: function(c_ptr, size) { + for (let i = 0; i < size; i++) { + _free(HEAP32[(c_ptr >> 2) + i]); + } + _free(c_ptr); + }, + + heapSub: function(heap, ptr, size) { + const bytes = heap.BYTES_PER_ELEMENT; + return heap.subarray(ptr / bytes, ptr / bytes + size); + }, + + heapCopy: function(heap, ptr, size) { + const bytes = heap.BYTES_PER_ELEMENT; + return heap.slice(ptr / bytes, ptr / bytes + size); + }, + }, + + godot_js_os_finish_async: function(p_callback) { + const func = GodotOS.get_func(p_callback); + GodotOS.finish_async(func); + }, + + godot_js_os_request_quit_cb: function(p_callback) { + GodotOS.request_quit = GodotOS.get_func(p_callback); + }, + + godot_js_os_fs_is_persistent: function() { + return GodotFS.is_persistent(); + }, + + godot_js_os_fs_sync: function(callback) { + const func = GodotOS.get_func(callback); + GodotOS._fs_sync_promise = GodotFS.sync(); + GodotOS._fs_sync_promise.then(function(err) { + func(); + }); + }, + + godot_js_os_execute: function(p_json) { + const json_args = UTF8ToString(p_json); + const args = JSON.parse(json_args); + if (GodotConfig.on_execute) { + GodotConfig.on_execute(args); + return 0; + } + return 1; + }, + + godot_js_os_shell_open: function(p_uri) { + window.open(UTF8ToString(p_uri), '_blank'); + }, +}; + +autoAddDeps(GodotOS, '$GodotOS'); +mergeInto(LibraryManager.library, GodotOS); diff --git a/platform/javascript/native/utils.js b/platform/javascript/native/utils.js deleted file mode 100644 index 8d0beba454..0000000000 --- a/platform/javascript/native/utils.js +++ /dev/null @@ -1,292 +0,0 @@ -/*************************************************************************/ -/* utils.js */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 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. */ -/*************************************************************************/ - -Module['initFS'] = function(persistentPaths) { - Module.mount_points = ['/userfs'].concat(persistentPaths); - - function createRecursive(dir) { - try { - FS.stat(dir); - } catch (e) { - if (e.errno !== ERRNO_CODES.ENOENT) { - throw e; - } - FS.mkdirTree(dir); - } - } - - Module.mount_points.forEach(function(path) { - createRecursive(path); - FS.mount(IDBFS, {}, path); - }); - return new Promise(function(resolve, reject) { - FS.syncfs(true, function(err) { - if (err) { - Module.mount_points = []; - Module.idbfs = false; - console.log("IndexedDB not available: " + err.message); - } else { - Module.idbfs = true; - } - resolve(err); - }); - }); -}; - -Module['deinitFS'] = function() { - Module.mount_points.forEach(function(path) { - try { - FS.unmount(path); - } catch (e) { - console.log("Already unmounted", e); - } - if (Module.idbfs && IDBFS.dbs[path]) { - IDBFS.dbs[path].close(); - delete IDBFS.dbs[path]; - } - }); - Module.mount_points = []; -}; - -Module['copyToFS'] = function(path, buffer) { - var p = path.lastIndexOf("/"); - var dir = "/"; - if (p > 0) { - dir = path.slice(0, path.lastIndexOf("/")); - } - try { - FS.stat(dir); - } catch (e) { - if (e.errno !== ERRNO_CODES.ENOENT) { - throw e; - } - FS.mkdirTree(dir); - } - // With memory growth, canOwn should be false. - FS.writeFile(path, new Uint8Array(buffer), {'flags': 'wx+'}); -} - -Module.drop_handler = (function() { - var upload = []; - var uploadPromises = []; - var uploadCallback = null; - - function readFilePromise(entry, path) { - return new Promise(function(resolve, reject) { - entry.file(function(file) { - var reader = new FileReader(); - reader.onload = function() { - var f = { - "path": file.relativePath || file.webkitRelativePath, - "name": file.name, - "type": file.type, - "size": file.size, - "data": reader.result - }; - if (!f['path']) - f['path'] = f['name']; - upload.push(f); - resolve() - }; - reader.onerror = function() { - console.log("Error reading file"); - reject(); - } - - reader.readAsArrayBuffer(file); - - }, function(err) { - console.log("Error!"); - reject(); - }); - }); - } - - function readDirectoryPromise(entry) { - return new Promise(function(resolve, reject) { - var reader = entry.createReader(); - reader.readEntries(function(entries) { - for (var i = 0; i < entries.length; i++) { - var ent = entries[i]; - if (ent.isDirectory) { - uploadPromises.push(readDirectoryPromise(ent)); - } else if (ent.isFile) { - uploadPromises.push(readFilePromise(ent)); - } - } - resolve(); - }); - }); - } - - function processUploadsPromises(resolve, reject) { - if (uploadPromises.length == 0) { - resolve(); - return; - } - uploadPromises.pop().then(function() { - setTimeout(function() { - processUploadsPromises(resolve, reject); - //processUploadsPromises.bind(null, resolve, reject) - }, 0); - }); - } - - function dropFiles(files) { - var args = files || []; - var argc = args.length; - var argv = stackAlloc((argc + 1) * 4); - for (var i = 0; i < argc; i++) { - HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i]); - } - HEAP32[(argv >> 2) + argc] = 0; - // Defined in display_server_javascript.cpp - ccall('_drop_files_callback', 'void', ['number', 'number'], [argv, argc]); - } - - return function(ev) { - ev.preventDefault(); - if (ev.dataTransfer.items) { - // Use DataTransferItemList interface to access the file(s) - for (var i = 0; i < ev.dataTransfer.items.length; i++) { - const item = ev.dataTransfer.items[i]; - var entry = null; - if ("getAsEntry" in item) { - entry = item.getAsEntry(); - } else if ("webkitGetAsEntry" in item) { - entry = item.webkitGetAsEntry(); - } - if (!entry) { - console.error("File upload not supported"); - } else if (entry.isDirectory) { - uploadPromises.push(readDirectoryPromise(entry)); - } else if (entry.isFile) { - uploadPromises.push(readFilePromise(entry)); - } else { - console.error("Unrecognized entry...", entry); - } - } - } else { - console.error("File upload not supported"); - } - uploadCallback = new Promise(processUploadsPromises).then(function() { - const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/"; - var drops = []; - var files = []; - upload.forEach((elem) => { - var path = elem['path']; - Module['copyToFS'](DROP + path, elem['data']); - var idx = path.indexOf("/"); - if (idx == -1) { - // Root file - drops.push(DROP + path); - } else { - // Subdir - var sub = path.substr(0, idx); - idx = sub.indexOf("/"); - if (idx < 0 && drops.indexOf(DROP + sub) == -1) { - drops.push(DROP + sub); - } - } - files.push(DROP + path); - }); - uploadPromises = []; - upload = []; - dropFiles(drops); - var dirs = [DROP.substr(0, DROP.length -1)]; - files.forEach(function (file) { - FS.unlink(file); - var dir = file.replace(DROP, ""); - var idx = dir.lastIndexOf("/"); - while (idx > 0) { - dir = dir.substr(0, idx); - if (dirs.indexOf(DROP + dir) == -1) { - dirs.push(DROP + dir); - } - idx = dir.lastIndexOf("/"); - } - }); - // Remove dirs. - dirs = dirs.sort(function(a, b) { - var al = (a.match(/\//g) || []).length; - var bl = (b.match(/\//g) || []).length; - if (al > bl) - return -1; - else if (al < bl) - return 1; - return 0; - }); - dirs.forEach(function(dir) { - FS.rmdir(dir); - }); - }); - } -})(); - -function EventHandlers() { - function Handler(target, event, method, capture) { - this.target = target; - this.event = event; - this.method = method; - this.capture = capture; - } - - var listeners = []; - - function has(target, event, method, capture) { - return listeners.findIndex(function(e) { - return e.target === target && e.event === event && e.method === method && e.capture == capture; - }) !== -1; - } - - this.add = function(target, event, method, capture) { - if (has(target, event, method, capture)) { - return; - } - listeners.push(new Handler(target, event, method, capture)); - target.addEventListener(event, method, capture); - }; - - this.remove = function(target, event, method, capture) { - if (!has(target, event, method, capture)) { - return; - } - target.removeEventListener(event, method, capture); - }; - - this.clear = function() { - listeners.forEach(function(h) { - h.target.removeEventListener(h.event, h.method, h.capture); - }); - listeners.length = 0; - }; -} - -Module.listeners = new EventHandlers(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index cf5751f384..80723d54fc 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -46,6 +46,8 @@ #include <emscripten.h> #include <stdlib.h> +#include "godot_js.h" + // Lifecycle void OS_JavaScript::initialize() { OS_Unix::initialize_core(); @@ -72,24 +74,15 @@ MainLoop *OS_JavaScript::get_main_loop() const { return main_loop; } -extern "C" EMSCRIPTEN_KEEPALIVE void _idb_synced() { - OS_JavaScript::get_singleton()->idb_is_syncing = false; +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; - /* clang-format off */ - EM_ASM({ - FS.syncfs(function(error) { - if (error) { - err('Failed to save IDB file system: ' + error.message); - } - ccall("_idb_synced", 'void', [], []); - }); - }); - /* clang-format on */ + godot_js_os_fs_sync(&fs_sync_callback); } DisplayServer::get_singleton()->process_events(); @@ -104,13 +97,6 @@ void OS_JavaScript::delete_main_loop() { main_loop = nullptr; } -void OS_JavaScript::finalize_async() { - finalizing = true; - if (audio_driver_javascript) { - audio_driver_javascript->finish_async(); - } -} - void OS_JavaScript::finalize() { delete_main_loop(); if (audio_driver_javascript) { @@ -127,17 +113,7 @@ Error OS_JavaScript::execute(const String &p_path, const List<String> &p_argumen args.push_back(E->get()); } String json_args = JSON::print(args); - /* clang-format off */ - int failed = EM_ASM_INT({ - const json_args = UTF8ToString($0); - const args = JSON.parse(json_args); - if (Module["onExecute"]) { - Module["onExecute"](args); - return 0; - } - return 1; - }, json_args.utf8().get_data()); - /* clang-format on */ + int failed = godot_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; } @@ -168,11 +144,7 @@ String OS_JavaScript::get_executable_path() const { Error OS_JavaScript::shell_open(String p_uri) { // Open URI in a new tab, browser will deal with it by protocol. - /* clang-format off */ - EM_ASM_({ - window.open(UTF8ToString($0), '_blank'); - }, p_uri.utf8().get_data()); - /* clang-format on */ + godot_js_os_shell_open(p_uri.utf8().get_data()); return OK; } @@ -211,10 +183,6 @@ void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags } } -void OS_JavaScript::set_idb_available(bool p_idb_available) { - idb_available = p_idb_available; -} - bool OS_JavaScript::is_userfs_persistent() const { return idb_available; } @@ -227,11 +195,17 @@ void OS_JavaScript::initialize_joypads() { } OS_JavaScript::OS_JavaScript() { + char locale_ptr[16]; + godot_js_config_locale_get(locale_ptr, 16); + setenv("LANG", locale_ptr, true); + if (AudioDriverJavaScript::is_available()) { audio_driver_javascript = memnew(AudioDriverJavaScript); AudioDriverManager::add_driver(audio_driver_javascript); } + idb_available = godot_js_os_fs_is_persistent(); + Vector<Logger *> loggers; loggers.push_back(memnew(StdLogger)); _set_logger(memnew(CompositeLogger(loggers))); diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 85551d708b..03a3053367 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -42,13 +42,14 @@ class OS_JavaScript : public OS_Unix { MainLoop *main_loop = nullptr; AudioDriverJavaScript *audio_driver_javascript = nullptr; - bool finalizing = false; + bool idb_is_syncing = false; bool idb_available = false; bool idb_needs_sync = false; static void main_loop_callback(); static void file_access_close_callback(const String &p_file, int p_flags); + static void fs_sync_callback(); protected: void initialize() override; @@ -61,15 +62,12 @@ protected: bool _check_internal_feature_support(const String &p_feature) override; public: - bool idb_is_syncing = false; - // Override return type to make writing static callbacks less tedious. static OS_JavaScript *get_singleton(); void initialize_joypads() override; MainLoop *get_main_loop() const override; - void finalize_async(); bool main_loop_iterate(); Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr) override; @@ -88,11 +86,9 @@ public: String get_data_path() const override; String get_user_data_dir() const override; - void set_idb_available(bool p_idb_available); bool is_userfs_persistent() const override; void resume_audio(); - bool is_finalizing() { return finalizing; } OS_JavaScript(); }; diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index 35418116b4..5fa737af8e 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -794,7 +794,9 @@ void DisplayServerX11::window_set_title(const String &p_title, WindowID p_window Atom _net_wm_name = XInternAtom(x11_display, "_NET_WM_NAME", false); Atom utf8_string = XInternAtom(x11_display, "UTF8_STRING", false); - XChangeProperty(x11_display, wd.x11_window, _net_wm_name, utf8_string, 8, PropModeReplace, (unsigned char *)p_title.utf8().get_data(), p_title.utf8().length()); + if (_net_wm_name != None && utf8_string != None) { + XChangeProperty(x11_display, wd.x11_window, _net_wm_name, utf8_string, 8, PropModeReplace, (unsigned char *)p_title.utf8().get_data(), p_title.utf8().length()); + } } void DisplayServerX11::window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window) { @@ -1184,6 +1186,10 @@ bool DisplayServerX11::_window_maximize_check(WindowID p_window, const char *p_a unsigned char *data = nullptr; bool retval = false; + if (property == None) { + return false; + } + int result = XGetWindowProperty( x11_display, wd.x11_window, @@ -1272,7 +1278,9 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled) { hints.flags = 2; hints.decorations = 0; property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True); - XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); + if (property != None) { + XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); + } } if (p_enabled) { @@ -1299,7 +1307,9 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled) { // set bypass compositor hint Atom bypass_compositor = XInternAtom(x11_display, "_NET_WM_BYPASS_COMPOSITOR", False); unsigned long compositing_disable_on = p_enabled ? 1 : 0; - XChangeProperty(x11_display, wd.x11_window, bypass_compositor, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&compositing_disable_on, 1); + if (bypass_compositor != None) { + XChangeProperty(x11_display, wd.x11_window, bypass_compositor, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&compositing_disable_on, 1); + } XFlush(x11_display); @@ -1313,7 +1323,9 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled) { hints.flags = 2; hints.decorations = window_get_flag(WINDOW_FLAG_BORDERLESS, p_window) ? 0 : 1; property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True); - XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); + if (property != None) { + XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); + } } } @@ -1448,6 +1460,10 @@ DisplayServer::WindowMode DisplayServerX11::window_get_mode(WindowID p_window) c { // Test minimized. // Using ICCCM -- Inter-Client Communication Conventions Manual Atom property = XInternAtom(x11_display, "WM_STATE", True); + if (property == None) { + return WINDOW_MODE_WINDOWED; + } + Atom type; int format; unsigned long len; @@ -1503,7 +1519,9 @@ void DisplayServerX11::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo hints.flags = 2; hints.decorations = p_enabled ? 0 : 1; property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True); - XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); + if (property != None) { + XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); + } // Preserve window size window_set_size(window_get_size(p_window), p_window); @@ -1943,28 +1961,29 @@ String DisplayServerX11::keyboard_get_layout_name(int p_index) const { } DisplayServerX11::Property DisplayServerX11::_read_property(Display *p_display, Window p_window, Atom p_property) { - Atom actual_type; - int actual_format; - unsigned long nitems; - unsigned long bytes_after; + Atom actual_type = None; + int actual_format = 0; + unsigned long nitems = 0; + unsigned long bytes_after = 0; unsigned char *ret = nullptr; int read_bytes = 1024; - //Keep trying to read the property until there are no - //bytes unread. - do { - if (ret != nullptr) { - XFree(ret); - } + // Keep trying to read the property until there are no bytes unread. + if (p_property != None) { + do { + if (ret != nullptr) { + XFree(ret); + } - XGetWindowProperty(p_display, p_window, p_property, 0, read_bytes, False, AnyPropertyType, - &actual_type, &actual_format, &nitems, &bytes_after, - &ret); + XGetWindowProperty(p_display, p_window, p_property, 0, read_bytes, False, AnyPropertyType, + &actual_type, &actual_format, &nitems, &bytes_after, + &ret); - read_bytes *= 2; + read_bytes *= 2; - } while (bytes_after != 0); + } while (bytes_after != 0); + } Property p = { ret, actual_format, (int)nitems, actual_type }; @@ -3376,7 +3395,9 @@ void DisplayServerX11::set_icon(const Ref<Image> &p_icon) { pr += 4; } - XChangeProperty(x11_display, wd.x11_window, net_wm_icon, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)pd.ptr(), pd.size()); + if (net_wm_icon != None) { + XChangeProperty(x11_display, wd.x11_window, net_wm_icon, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)pd.ptr(), pd.size()); + } if (!g_set_icon_error) { break; @@ -3469,7 +3490,9 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u { const long pid = OS::get_singleton()->get_process_id(); Atom net_wm_pid = XInternAtom(x11_display, "_NET_WM_PID", False); - XChangeProperty(x11_display, wd.x11_window, net_wm_pid, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&pid, 1); + if (net_wm_pid != None) { + XChangeProperty(x11_display, wd.x11_window, net_wm_pid, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&pid, 1); + } } long im_event_mask = 0; @@ -3517,7 +3540,9 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u /* set the titlebar name */ XStoreName(x11_display, wd.x11_window, "Godot"); XSetWMProtocols(x11_display, wd.x11_window, &wm_delete, 1); - XChangeProperty(x11_display, wd.x11_window, xdnd_aware, XA_ATOM, 32, PropModeReplace, (unsigned char *)&xdnd_version, 1); + if (xdnd_aware != None) { + XChangeProperty(x11_display, wd.x11_window, xdnd_aware, XA_ATOM, 32, PropModeReplace, (unsigned char *)&xdnd_version, 1); + } if (xim && xim_style) { // Block events polling while changing input focus @@ -3548,20 +3573,25 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u hints.flags = 2; hints.decorations = 0; property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True); - XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); + if (property != None) { + XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); + } } if (wd.menu_type) { // Set Utility type to disable fade animations. Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_UTILITY", False); Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - - XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); + if (wt_atom != None && type_atom != None) { + XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); + } } else { Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_NORMAL", False); Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); + if (wt_atom != None && type_atom != None) { + XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); + } } _update_size_hints(id); @@ -3843,6 +3873,10 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode (screen_get_size(0).width - p_resolution.width) / 2, (screen_get_size(0).height - p_resolution.height) / 2); WindowID main_window = _create_window(p_mode, p_flags, Rect2i(window_position, p_resolution)); + if (main_window == INVALID_WINDOW_ID) { + r_error = ERR_CANT_CREATE; + return; + } for (int i = 0; i < WINDOW_FLAG_MAX; i++) { if (p_flags & (1 << i)) { window_set_flag(WindowFlags(i), true, main_window); diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp index fda1358dfd..4a9d0a8181 100644 --- a/platform/linuxbsd/joypad_linux.cpp +++ b/platform/linuxbsd/joypad_linux.cpp @@ -459,9 +459,9 @@ void JoypadLinux::process_joypads() { case ABS_HAT0X: if (ev.value != 0) { if (ev.value < 0) { - joy->dpad |= Input::HAT_MASK_LEFT; + joy->dpad = (joy->dpad | Input::HAT_MASK_LEFT) & ~Input::HAT_MASK_RIGHT; } else { - joy->dpad |= Input::HAT_MASK_RIGHT; + joy->dpad = (joy->dpad | Input::HAT_MASK_RIGHT) & ~Input::HAT_MASK_LEFT; } } else { joy->dpad &= ~(Input::HAT_MASK_LEFT | Input::HAT_MASK_RIGHT); @@ -473,9 +473,9 @@ void JoypadLinux::process_joypads() { case ABS_HAT0Y: if (ev.value != 0) { if (ev.value < 0) { - joy->dpad |= Input::HAT_MASK_UP; + joy->dpad = (joy->dpad | Input::HAT_MASK_UP) & ~Input::HAT_MASK_DOWN; } else { - joy->dpad |= Input::HAT_MASK_DOWN; + joy->dpad = (joy->dpad | Input::HAT_MASK_DOWN) & ~Input::HAT_MASK_UP; } } else { joy->dpad &= ~(Input::HAT_MASK_UP | Input::HAT_MASK_DOWN); diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index b108d74b2e..646bc3aa4c 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -466,8 +466,10 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, ERR_FAIL_COND_V(ret == 0, ERR_CANT_FORK); if (p_blocking) { - DWORD ret2 = WaitForSingleObject(pi.pi.hProcess, INFINITE); + WaitForSingleObject(pi.pi.hProcess, INFINITE); if (r_exitcode) { + DWORD ret2; + GetExitCodeProcess(pi.pi.hProcess, &ret2); *r_exitcode = ret2; } |