diff options
489 files changed, 23221 insertions, 6334 deletions
diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml index 0ed7432833..9b8a86b8e7 100644 --- a/.github/workflows/static_checks.yml +++ b/.github/workflows/static_checks.yml @@ -48,6 +48,8 @@ jobs: - name: Style checks via pre-commit uses: pre-commit/action@v3.0.1 + with: + extra_args: --verbose --files ${{ env.CHANGED_FILES }} - name: File formatting checks (file_format.sh) run: | @@ -14,6 +14,7 @@ Ariel Manzur <ariel@godotengine.org> <punto@godotengine.org> Ariel Manzur <ariel@godotengine.org> <ariel@okamstudio.com> Ariel Manzur <ariel@godotengine.org> <punto@Ariels-Mac-mini.local> Ariel Manzur <ariel@godotengine.org> <punto@Ariels-Mac-mini-2.local> +Arman Elgudzhyan <48544263+puchik@users.noreply.github.com> A Thousand Ships <96648715+AThousandShips@users.noreply.github.com> <over999ships@gmail.com> Bastiaan Olij <mux213@gmail.com> Benjamin <mafortion.benjamin@gmail.com> @@ -77,6 +78,7 @@ Jason Knight <00jknight@gmail.com> <jason@winterpixel.com> Jean-Michel Bernard <jmb462@gmail.com> Jérôme Gully <jerome.gully0@gmail.com> JFonS <joan.fonssanchez@gmail.com> +jitspoe <jitspoe@yahoo.com> <jitspoeAyahoooDcom> Juan Linietsky <reduzio@gmail.com> Juan Linietsky <reduzio@gmail.com> <juan@godotengine.org> Juan Linietsky <reduzio@gmail.com> <juan@okamstudio.com> diff --git a/AUTHORS.md b/AUTHORS.md index f2fc58c1be..0f000a1a75 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -48,6 +48,7 @@ name is available. Anutrix Aren Villanueva (kurikaesu) Ariel Manzur (punto-) + Arman Elgudzhyan (puchik) AThousandShips aXu-AP Bartłomiej T. Listwon (Listwon) @@ -141,6 +142,7 @@ name is available. Jean-Michel Bernard (jmb462) Jérôme Gully (Nutriz) Jia Jun Chai (SkyLucilfer) + jitspoe Joan Fons Sanchez (JFonS) Johan Manuel (29jm) Johannes Witt (HaSa1002) @@ -217,6 +219,7 @@ name is available. Omar El Sheikh (The-O-King) Ovnuniarchos Pascal Richter (ShyRed) + passivestar Patrick Dawson (pkdawson) Patrick Exner (FlameLizard) Patrick (firefly2442) @@ -232,6 +235,7 @@ name is available. Poommetee Ketson (Noshyaar) Przemysław Gołąb (n-pigeon) Rafael M. G. (rafallus) + Raffaele Picca (RPicster) Rafał Mikrut (qarmin) Ralf Hölzemer (rollenrolm) Ramesh Ravone (RameshRavone) @@ -298,6 +302,7 @@ name is available. Zae Chao (zaevi) Zak Stam (zaksnet) Zher Huei Lee (leezh) + Zi Ye (MajorMcDoom) ZuBsPaCe 谢天 (jsjtxietian) 风青山 (Rindbee) diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index c1711a9e81..cbb0ad179d 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -293,6 +293,11 @@ Comment: jpeg-compressor Copyright: 2012, Rich Geldreich License: public-domain or Apache-2.0 +Files: ./thirdparty/libbacktrace/ +Comment: libbacktrace +Copyright: 2012-2021, Free Software Foundation, Inc. +License: BSD-3-clause + Files: ./thirdparty/libktx/ Comment: KTX Copyright: 2013-2020, Mark Callow @@ -406,6 +411,11 @@ Comment: PolyPartition / Triangulator Copyright: 2011-2021, Ivan Fratric and contributors License: Expat +Files: ./thirdparty/misc/qoa.h +Comment: Quite OK Audio Format +Copyright: 2023, Dominic Szablewski +License: Expat + Files: ./thirdparty/misc/r128.c ./thirdparty/misc/r128.h Comment: r128 library @@ -32,10 +32,8 @@ generous deed immortalized in the next stable release of Godot Engine. ## Silver sponsors - Affray Interactive <https://scp.games/pandemic/> Broken Rules <https://brokenrul.es/> Chasing Carrots <https://www.chasing-carrots.com/> - Gamblify <https://www.gamblify.com/> Indoor Astronaut <https://indoorastronaut.ch/> Null <https://null.com/> Orbital Knight <https://www.orbitalknight.com/> @@ -56,6 +54,7 @@ generous deed immortalized in the next stable release of Godot Engine. Garry Newman Isaiah Smith <https://www.isaiahsmith.dev/> Kenney <https://kenney.nl/> + Libretrend <https://libretrend.com/> Life Art Studios <https://lifeartstudios.net/> Lucid Silence Games Matthew Campbell @@ -99,19 +98,21 @@ generous deed immortalized in the next stable release of Godot Engine. Scott Pezza ShikadiGum Silver Creek Entertainment + SolarLabyrinth Stephan Kessler Stephan Lanfermann TigerJ Tim Yuen Violin Iliev Vladimír Chvátil - And 16 anonymous donors + And 15 anonymous donors ## Gold members @reilaos alMoo Games Alva Majo + Amadan Interactive (Cillian Clifford) Antti Vesanen Artur Ilkaev Asher Glick @@ -124,21 +125,31 @@ generous deed immortalized in the next stable release of Godot Engine. Brian Levinsen Brut Carlo del Mundo + Chickensoft ClarkThyLord Cosmin Munteanu + cowoder Coy Humphrey + Daniel Eichler David Chen Zhen David Coles David Hubber David Snopek + Deakcor Delton Ding + dfseifert + Don't You Know Who I Am? Inc. + Dono Dustuu + Edelweiss ElektroFox endaye Ends + Eren Öğrül Eric Phy Faisal Al-Kubaisi (QatariGameDev) FeralBytes + Garrus Vakarian GlassBrick Grau Guangzhou Lingchan @@ -153,6 +164,7 @@ generous deed immortalized in the next stable release of Godot Engine. John Gabriel Jon Woodward José Canepa + Justin Sasso KAR Games Karasu Studio korinVR @@ -167,6 +179,7 @@ generous deed immortalized in the next stable release of Godot Engine. Mara Huldra Martin Šenkeřík Megabit Interactive + Michael Gooch Modus Ponens nezticle Niklas Wahrman @@ -183,19 +196,25 @@ generous deed immortalized in the next stable release of Godot Engine. Robin Ward Saltlight Studio Samuel Judd + ScoreSpace Silverclad Studios Sofox Space Kraken Studios Spoony Panda + TANAKA Yu + TaraSophieDev ThatGamer ThePolyglotProgrammer Tim Nedvyga Tom Langwaldt Trevor Slocum tukon + Vagastella Vincent Foulon Weasel Games WuotanStudios.com + Yury K. + Zee Weasel Zhu Li zikes @@ -530,7 +549,7 @@ generous deed immortalized in the next stable release of Godot Engine. ケルベロス 貴宏 小松 - And 208 anonymous donors + And 201 anonymous donors ## Silver and bronze donors diff --git a/SConstruct b/SConstruct index 18511ff5ee..81ce4bca52 100644 --- a/SConstruct +++ b/SConstruct @@ -15,6 +15,17 @@ from collections import OrderedDict from importlib.util import spec_from_file_location, module_from_spec from SCons import __version__ as scons_raw_version +# Enable ANSI escape code support on Windows 10 and later (for colored console output). +# <https://github.com/python/cpython/issues/73245> +if sys.platform == "win32": + from ctypes import windll, c_int, byref + + stdout_handle = windll.kernel32.GetStdHandle(c_int(-11)) + mode = c_int(0) + windll.kernel32.GetConsoleMode(c_int(stdout_handle), byref(mode)) + mode = c_int(mode.value | 4) + windll.kernel32.SetConsoleMode(c_int(stdout_handle), mode) + # Explicitly resolve the helper modules, this is done to avoid clash with # modules of the same name that might be randomly added (e.g. someone adding # an `editor.py` file at the root of the module creates a clash with the editor @@ -57,6 +68,7 @@ import methods import glsl_builders import gles3_builders import scu_builders +from methods import print_warning, print_error from platform_methods import architectures, architecture_aliases, generate_export_icons if ARGUMENTS.get("target", "editor") == "editor": @@ -311,38 +323,41 @@ if selected_platform == "": selected_platform = "windows" if selected_platform != "": - print("Automatically detected platform: " + selected_platform) + print(f"Automatically detected platform: {selected_platform}") if selected_platform == "osx": # Deprecated alias kept for compatibility. - print('Platform "osx" has been renamed to "macos" in Godot 4. Building for platform "macos".') + print_warning('Platform "osx" has been renamed to "macos" in Godot 4. Building for platform "macos".') selected_platform = "macos" if selected_platform == "iphone": # Deprecated alias kept for compatibility. - print('Platform "iphone" has been renamed to "ios" in Godot 4. Building for platform "ios".') + print_warning('Platform "iphone" has been renamed to "ios" in Godot 4. Building for platform "ios".') selected_platform = "ios" if selected_platform in ["linux", "bsd", "x11"]: if selected_platform == "x11": # Deprecated alias kept for compatibility. - print('Platform "x11" has been renamed to "linuxbsd" in Godot 4. Building for platform "linuxbsd".') + print_warning('Platform "x11" has been renamed to "linuxbsd" in Godot 4. Building for platform "linuxbsd".') # Alias for convenience. selected_platform = "linuxbsd" if selected_platform == "javascript": # Deprecated alias kept for compatibility. - print('Platform "javascript" has been renamed to "web" in Godot 4. Building for platform "web".') + print_warning('Platform "javascript" has been renamed to "web" in Godot 4. Building for platform "web".') selected_platform = "web" if selected_platform not in platform_list: - if selected_platform == "": - print("Could not detect platform automatically.") - elif selected_platform != "list": - print(f'Invalid target platform "{selected_platform}".') + text = "The following platforms are available:\n\t{}\n".format("\n\t".join(platform_list)) + text += "Please run SCons again and select a valid platform: platform=<string>." + + if selected_platform == "list": + print(text) + elif selected_platform == "": + print_error("Could not detect platform automatically.\n" + text) + else: + print_error(f'Invalid target platform "{selected_platform}".\n' + text) - print("The following platforms are available:\n\t{}\n".format("\n\t".join(platform_list))) - print("Please run SCons again and select a valid platform: platform=<string>.") Exit(0 if selected_platform == "list" else 255) # Make sure to update this to the found, valid platform as it's used through the buildsystem as the reference. @@ -368,7 +383,7 @@ if env["custom_modules"]: try: module_search_paths.append(methods.convert_custom_modules_path(p)) except ValueError as e: - print(e) + print_error(e) Exit(255) for path in module_search_paths: @@ -507,7 +522,7 @@ env.SetOption("num_jobs", altered_num_jobs) if env.GetOption("num_jobs") == altered_num_jobs: cpu_count = os.cpu_count() if cpu_count is None: - print("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.") + print_warning("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.") else: safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1 print( @@ -531,7 +546,7 @@ env.Append(LINKFLAGS=env.get("linkflags", "").split()) # Feature build profile disabled_classes = [] if env["build_profile"] != "": - print("Using feature build profile: " + env["build_profile"]) + print('Using feature build profile: "{}"'.format(env["build_profile"])) import json try: @@ -543,7 +558,7 @@ if env["build_profile"] != "": for c in dbo: env[c] = dbo[c] except: - print("Error opening feature build profile: " + env["build_profile"]) + print_error('Failed to open feature build profile: "{}"'.format(env["build_profile"])) Exit(255) methods.write_disabled_classes(disabled_classes) @@ -605,14 +620,14 @@ cc_version_metadata1 = cc_version["metadata1"] or "" if methods.using_gcc(env): if cc_version_major == -1: - print( + print_warning( "Couldn't detect compiler version, skipping version checks. " "Build may fail if the compiler doesn't support C++17 fully." ) # GCC 8 before 8.4 has a regression in the support of guaranteed copy elision # which causes a build failure: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=86521 elif cc_version_major == 8 and cc_version_minor < 4: - print( + print_error( "Detected GCC 8 version < 8.4, which is not supported due to a " "regression in its C++17 guaranteed copy elision support. Use a " 'newer GCC version, or Clang 6 or later by passing "use_llvm=yes" ' @@ -620,7 +635,7 @@ if methods.using_gcc(env): ) Exit(255) elif cc_version_major < 7: - print( + print_error( "Detected GCC version older than 7, which does not fully support " "C++17. Supported versions are GCC 7, 9 and later. Use a newer GCC " 'version, or Clang 6 or later by passing "use_llvm=yes" to the ' @@ -628,7 +643,7 @@ if methods.using_gcc(env): ) Exit(255) elif cc_version_metadata1 == "win32": - print( + print_error( "Detected mingw version is not using posix threads. Only posix " "version of mingw is supported. " 'Use "update-alternatives --config x86_64-w64-mingw32-g++" ' @@ -636,11 +651,11 @@ if methods.using_gcc(env): ) Exit(255) if env["debug_paths_relative"] and cc_version_major < 8: - print("GCC < 8 doesn't support -ffile-prefix-map, disabling `debug_paths_relative` option.") + print_warning("GCC < 8 doesn't support -ffile-prefix-map, disabling `debug_paths_relative` option.") env["debug_paths_relative"] = False elif methods.using_clang(env): if cc_version_major == -1: - print( + print_warning( "Couldn't detect compiler version, skipping version checks. " "Build may fail if the compiler doesn't support C++17 fully." ) @@ -649,28 +664,30 @@ elif methods.using_clang(env): elif env["platform"] == "macos" or env["platform"] == "ios": vanilla = methods.is_vanilla_clang(env) if vanilla and cc_version_major < 6: - print( + print_warning( "Detected Clang version older than 6, which does not fully support " "C++17. Supported versions are Clang 6 and later." ) Exit(255) elif not vanilla and cc_version_major < 10: - print( + print_error( "Detected Apple Clang version older than 10, which does not fully " "support C++17. Supported versions are Apple Clang 10 and later." ) Exit(255) if env["debug_paths_relative"] and not vanilla and cc_version_major < 12: - print("Apple Clang < 12 doesn't support -ffile-prefix-map, disabling `debug_paths_relative` option.") + print_warning( + "Apple Clang < 12 doesn't support -ffile-prefix-map, disabling `debug_paths_relative` option." + ) env["debug_paths_relative"] = False elif cc_version_major < 6: - print( + print_error( "Detected Clang version older than 6, which does not fully support " "C++17. Supported versions are Clang 6 and later." ) Exit(255) if env["debug_paths_relative"] and cc_version_major < 10: - print("Clang < 10 doesn't support -ffile-prefix-map, disabling `debug_paths_relative` option.") + print_warning("Clang < 10 doesn't support -ffile-prefix-map, disabling `debug_paths_relative` option.") env["debug_paths_relative"] = False # Set optimize and debug_symbols flags. @@ -906,7 +923,7 @@ if env.editor_build: # And check if they are met. if not env.module_check_dependencies("editor"): - print("Not all modules required by editor builds are enabled.") + print_error("Not all modules required by editor builds are enabled.") Exit(255) methods.generate_version_header(env.module_version_string) @@ -932,14 +949,14 @@ env["SHOBJPREFIX"] = env["object_prefix"] if env["disable_3d"]: if env.editor_build: - print("Build option 'disable_3d=yes' cannot be used for editor builds, only for export template builds.") + print_error("Build option `disable_3d=yes` cannot be used for editor builds, only for export template builds.") Exit(255) else: env.Append(CPPDEFINES=["_3D_DISABLED"]) if env["disable_advanced_gui"]: if env.editor_build: - print( - "Build option 'disable_advanced_gui=yes' cannot be used for editor builds, " + print_error( + "Build option `disable_advanced_gui=yes` cannot be used for editor builds, " "only for export template builds." ) Exit(255) @@ -951,7 +968,7 @@ if env["brotli"]: env.Append(CPPDEFINES=["BROTLI_ENABLED"]) if not env["verbose"]: - methods.no_verbose(sys, env) + methods.no_verbose(env) GLSL_BUILDERS = { "RD_GLSL": env.Builder( @@ -983,7 +1000,7 @@ if env["vsproj"]: if env["compiledb"] and env.scons_version < (4, 0, 0): # Generating the compilation DB (`compile_commands.json`) requires SCons 4.0.0 or later. - print("The `compiledb=yes` option requires SCons 4.0 or later, but your version is %s." % scons_raw_version) + print_error("The `compiledb=yes` option requires SCons 4.0 or later, but your version is %s." % scons_raw_version) Exit(255) if env.scons_version >= (4, 0, 0): env.Tool("compilation_db") @@ -991,7 +1008,7 @@ if env.scons_version >= (4, 0, 0): if env["ninja"]: if env.scons_version < (4, 2, 0): - print("The `ninja=yes` option requires SCons 4.2 or later, but your version is %s." % scons_raw_version) + print_error("The `ninja=yes` option requires SCons 4.2 or later, but your version is %s." % scons_raw_version) Exit(255) SetOption("experimental", "ninja") @@ -1048,9 +1065,16 @@ methods.dump(env) def print_elapsed_time(): - elapsed_time_sec = round(time.time() - time_at_start, 3) - time_ms = round((elapsed_time_sec % 1) * 1000) - print("[Time elapsed: {}.{:03}]".format(time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)), time_ms)) + elapsed_time_sec = round(time.time() - time_at_start, 2) + time_centiseconds = round((elapsed_time_sec % 1) * 100) + print( + "{}[Time elapsed: {}.{:02}]{}".format( + methods.ANSI.GRAY, + time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)), + time_centiseconds, + methods.ANSI.RESET, + ) + ) atexit.register(print_elapsed_time) diff --git a/core/SCsub b/core/SCsub index ec4658e8ca..91620cb075 100644 --- a/core/SCsub +++ b/core/SCsub @@ -29,8 +29,8 @@ if "SCRIPT_AES256_ENCRYPTION_KEY" in os.environ: ec_valid = False txt += txts if not ec_valid: - print("Error: Invalid AES256 encryption key, not 64 hexadecimal characters: '" + key + "'.") - print( + methods.print_error( + f'Invalid AES256 encryption key, not 64 hexadecimal characters: "{key}".\n' "Unset 'SCRIPT_AES256_ENCRYPTION_KEY' in your environment " "or make sure that it contains exactly 64 hexadecimal characters." ) diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 104b17961d..ee20aea35d 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1017,7 +1017,7 @@ Error ProjectSettings::save_custom(const String &p_path, const CustomMap &p_cust } } // Check for the existence of a csproj file. - if (_csproj_exists(get_resource_path())) { + if (_csproj_exists(p_path.get_base_dir())) { // If there is a csproj file, add the C# feature if it doesn't already exist. if (!project_features.has("C#")) { project_features.append("C#"); @@ -1473,7 +1473,9 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF(PropertyInfo(Variant::INT, "display/window/size/window_height_override", PROPERTY_HINT_RANGE, "0,4320,1,or_greater"), 0); // 8K resolution GLOBAL_DEF("display/window/energy_saving/keep_screen_on", true); - GLOBAL_DEF("display/window/energy_saving/keep_screen_on.editor", false); +#ifdef TOOLS_ENABLED + GLOBAL_DEF("display/window/energy_saving/keep_screen_on.editor_hint", false); +#endif GLOBAL_DEF("animation/warnings/check_invalid_track_paths", true); GLOBAL_DEF("animation/warnings/check_angle_interpolation_type_conflicting", true); @@ -1531,6 +1533,10 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF_BASIC("internationalization/rendering/root_node_auto_translate", true); GLOBAL_DEF(PropertyInfo(Variant::INT, "gui/timers/incremental_search_max_interval_msec", PROPERTY_HINT_RANGE, "0,10000,1,or_greater"), 2000); + GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "gui/timers/tooltip_delay_sec", PROPERTY_HINT_RANGE, "0,5,0.01,or_greater"), 0.5); +#ifdef TOOLS_ENABLED + GLOBAL_DEF("gui/timers/tooltip_delay_sec.editor_hint", 0.5); +#endif GLOBAL_DEF_BASIC("gui/common/snap_controls_to_pixels", true); GLOBAL_DEF_BASIC("gui/fonts/dynamic_fonts/use_oversampling", true); @@ -1568,6 +1574,14 @@ ProjectSettings::ProjectSettings() { ProjectSettings::get_singleton()->add_hidden_prefix("input/"); } +ProjectSettings::ProjectSettings(const String &p_path) { + if (load_custom(p_path) == OK) { + project_loaded = true; + } +} + ProjectSettings::~ProjectSettings() { - singleton = nullptr; + if (singleton == this) { + singleton = nullptr; + } } diff --git a/core/config/project_settings.h b/core/config/project_settings.h index 1bad76acb1..922c88c151 100644 --- a/core/config/project_settings.h +++ b/core/config/project_settings.h @@ -224,6 +224,7 @@ public: #endif ProjectSettings(); + ProjectSettings(const String &p_path); ~ProjectSettings(); }; diff --git a/core/core_bind.cpp b/core/core_bind.cpp index 467b696eae..0996db9d89 100644 --- a/core/core_bind.cpp +++ b/core/core_bind.cpp @@ -1921,7 +1921,7 @@ void EngineDebugger::send_message(const String &p_msg, const Array &p_data) { Error EngineDebugger::call_capture(void *p_user, const String &p_cmd, const Array &p_data, bool &r_captured) { Callable &capture = *(Callable *)p_user; - if (capture.is_null()) { + if (!capture.is_valid()) { return FAILED; } Variant cmd = p_cmd, data = p_data; diff --git a/core/extension/gdextension.cpp b/core/extension/gdextension.cpp index 22a5df9935..b48ea97040 100644 --- a/core/extension/gdextension.cpp +++ b/core/extension/gdextension.cpp @@ -387,7 +387,7 @@ void GDExtension::_register_extension_class(GDExtensionClassLibraryPtr p_library p_extension_funcs->set_func, // GDExtensionClassSet set_func; p_extension_funcs->get_func, // GDExtensionClassGet get_func; p_extension_funcs->get_property_list_func, // GDExtensionClassGetPropertyList get_property_list_func; - p_extension_funcs->free_property_list_func, // GDExtensionClassFreePropertyList free_property_list_func; + nullptr, // GDExtensionClassFreePropertyList2 free_property_list_func; p_extension_funcs->property_can_revert_func, // GDExtensionClassPropertyCanRevert property_can_revert_func; p_extension_funcs->property_get_revert_func, // GDExtensionClassPropertyGetRevert property_get_revert_func; nullptr, // GDExtensionClassValidateProperty validate_property_func; @@ -406,7 +406,8 @@ void GDExtension::_register_extension_class(GDExtensionClassLibraryPtr p_library }; const ClassCreationDeprecatedInfo legacy = { - p_extension_funcs->notification_func, + p_extension_funcs->notification_func, // GDExtensionClassNotification notification_func; + p_extension_funcs->free_property_list_func, // GDExtensionClassFreePropertyList free_property_list_func; }; _register_extension_class_internal(p_library, p_class_name, p_parent_class_name, &class_info3, &legacy); } @@ -420,7 +421,7 @@ void GDExtension::_register_extension_class2(GDExtensionClassLibraryPtr p_librar p_extension_funcs->set_func, // GDExtensionClassSet set_func; p_extension_funcs->get_func, // GDExtensionClassGet get_func; p_extension_funcs->get_property_list_func, // GDExtensionClassGetPropertyList get_property_list_func; - p_extension_funcs->free_property_list_func, // GDExtensionClassFreePropertyList free_property_list_func; + nullptr, // GDExtensionClassFreePropertyList2 free_property_list_func; p_extension_funcs->property_can_revert_func, // GDExtensionClassPropertyCanRevert property_can_revert_func; p_extension_funcs->property_get_revert_func, // GDExtensionClassPropertyGetRevert property_get_revert_func; p_extension_funcs->validate_property_func, // GDExtensionClassValidateProperty validate_property_func; @@ -438,7 +439,11 @@ void GDExtension::_register_extension_class2(GDExtensionClassLibraryPtr p_librar p_extension_funcs->class_userdata, // void *class_userdata; }; - _register_extension_class_internal(p_library, p_class_name, p_parent_class_name, &class_info3); + const ClassCreationDeprecatedInfo legacy = { + nullptr, // GDExtensionClassNotification notification_func; + p_extension_funcs->free_property_list_func, // GDExtensionClassFreePropertyList free_property_list_func; + }; + _register_extension_class_internal(p_library, p_class_name, p_parent_class_name, &class_info3, &legacy); } #endif // DISABLE_DEPRECATED @@ -514,13 +519,14 @@ void GDExtension::_register_extension_class_internal(GDExtensionClassLibraryPtr extension->gdextension.set = p_extension_funcs->set_func; extension->gdextension.get = p_extension_funcs->get_func; extension->gdextension.get_property_list = p_extension_funcs->get_property_list_func; - extension->gdextension.free_property_list = p_extension_funcs->free_property_list_func; + extension->gdextension.free_property_list2 = p_extension_funcs->free_property_list_func; extension->gdextension.property_can_revert = p_extension_funcs->property_can_revert_func; extension->gdextension.property_get_revert = p_extension_funcs->property_get_revert_func; extension->gdextension.validate_property = p_extension_funcs->validate_property_func; #ifndef DISABLE_DEPRECATED if (p_deprecated_funcs) { extension->gdextension.notification = p_deprecated_funcs->notification_func; + extension->gdextension.free_property_list = p_deprecated_funcs->free_property_list_func; } #endif // DISABLE_DEPRECATED extension->gdextension.notification2 = p_extension_funcs->notification_func; diff --git a/core/extension/gdextension.h b/core/extension/gdextension.h index 23b1f51208..3b15639890 100644 --- a/core/extension/gdextension.h +++ b/core/extension/gdextension.h @@ -71,6 +71,7 @@ class GDExtension : public Resource { struct ClassCreationDeprecatedInfo { #ifndef DISABLE_DEPRECATED GDExtensionClassNotification notification_func = nullptr; + GDExtensionClassFreePropertyList free_property_list_func = nullptr; #endif // DISABLE_DEPRECATED }; diff --git a/core/extension/gdextension_interface.h b/core/extension/gdextension_interface.h index e9c570e994..00a98af4e2 100644 --- a/core/extension/gdextension_interface.h +++ b/core/extension/gdextension_interface.h @@ -256,6 +256,7 @@ typedef struct { typedef const GDExtensionPropertyInfo *(*GDExtensionClassGetPropertyList)(GDExtensionClassInstancePtr p_instance, uint32_t *r_count); typedef void (*GDExtensionClassFreePropertyList)(GDExtensionClassInstancePtr p_instance, const GDExtensionPropertyInfo *p_list); +typedef void (*GDExtensionClassFreePropertyList2)(GDExtensionClassInstancePtr p_instance, const GDExtensionPropertyInfo *p_list, uint32_t p_count); typedef GDExtensionBool (*GDExtensionClassPropertyCanRevert)(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name); typedef GDExtensionBool (*GDExtensionClassPropertyGetRevert)(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, GDExtensionVariantPtr r_ret); typedef GDExtensionBool (*GDExtensionClassValidateProperty)(GDExtensionClassInstancePtr p_instance, GDExtensionPropertyInfo *p_property); @@ -333,7 +334,7 @@ typedef struct { GDExtensionClassSet set_func; GDExtensionClassGet get_func; GDExtensionClassGetPropertyList get_property_list_func; - GDExtensionClassFreePropertyList free_property_list_func; + GDExtensionClassFreePropertyList2 free_property_list_func; GDExtensionClassPropertyCanRevert property_can_revert_func; GDExtensionClassPropertyGetRevert property_get_revert_func; GDExtensionClassValidateProperty validate_property_func; diff --git a/core/io/dir_access.cpp b/core/io/dir_access.cpp index e99885befa..5df67b1103 100644 --- a/core/io/dir_access.cpp +++ b/core/io/dir_access.cpp @@ -582,6 +582,10 @@ void DirAccess::_bind_methods() { ClassDB::bind_method(D_METHOD("remove", "path"), &DirAccess::remove); ClassDB::bind_static_method("DirAccess", D_METHOD("remove_absolute", "path"), &DirAccess::remove_absolute); + ClassDB::bind_method(D_METHOD("is_link", "path"), &DirAccess::is_link); + ClassDB::bind_method(D_METHOD("read_link", "path"), &DirAccess::read_link); + ClassDB::bind_method(D_METHOD("create_link", "source", "target"), &DirAccess::create_link); + ClassDB::bind_method(D_METHOD("set_include_navigational", "enable"), &DirAccess::set_include_navigational); ClassDB::bind_method(D_METHOD("get_include_navigational"), &DirAccess::get_include_navigational); ClassDB::bind_method(D_METHOD("set_include_hidden", "enable"), &DirAccess::set_include_hidden); diff --git a/core/io/file_access_zip.cpp b/core/io/file_access_zip.cpp index 3c7f67664d..c0d1afc8e1 100644 --- a/core/io/file_access_zip.cpp +++ b/core/io/file_access_zip.cpp @@ -277,7 +277,7 @@ void FileAccessZip::seek_end(int64_t p_position) { uint64_t FileAccessZip::get_position() const { ERR_FAIL_NULL_V(zfile, 0); - return unztell(zfile); + return unztell64(zfile); } uint64_t FileAccessZip::get_length() const { diff --git a/core/math/aabb.h b/core/math/aabb.h index 48a883e64c..c2945a3ef1 100644 --- a/core/math/aabb.h +++ b/core/math/aabb.h @@ -101,7 +101,7 @@ struct _NO_DISCARD_ AABB { _FORCE_INLINE_ void expand_to(const Vector3 &p_vector); /** expand to contain a point if necessary */ _FORCE_INLINE_ AABB abs() const { - return AABB(position + size.min(Vector3()), size.abs()); + return AABB(position + size.minf(0), size.abs()); } Variant intersects_segment_bind(const Vector3 &p_from, const Vector3 &p_to) const; diff --git a/core/math/delaunay_3d.h b/core/math/delaunay_3d.h index 25bd4e8d89..4f21a665de 100644 --- a/core/math/delaunay_3d.h +++ b/core/math/delaunay_3d.h @@ -278,7 +278,7 @@ public: } Vector3i grid_pos = Vector3i(points[i] * proportions * ACCEL_GRID_SIZE); - grid_pos = grid_pos.clamp(Vector3i(), Vector3i(ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1)); + grid_pos = grid_pos.clampi(0, ACCEL_GRID_SIZE - 1); for (List<Simplex *>::Element *E = acceleration_grid[grid_pos.x][grid_pos.y][grid_pos.z].front(); E;) { List<Simplex *>::Element *N = E->next(); //may be deleted @@ -335,8 +335,8 @@ public: Vector3 extents = Vector3(radius2, radius2, radius2); Vector3i from = Vector3i((center - extents) * proportions * ACCEL_GRID_SIZE); Vector3i to = Vector3i((center + extents) * proportions * ACCEL_GRID_SIZE); - from = from.clamp(Vector3i(), Vector3i(ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1)); - to = to.clamp(Vector3i(), Vector3i(ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1)); + from = from.clampi(0, ACCEL_GRID_SIZE - 1); + to = to.clampi(0, ACCEL_GRID_SIZE - 1); for (int32_t x = from.x; x <= to.x; x++) { for (int32_t y = from.y; y <= to.y; y++) { diff --git a/core/math/quick_hull.cpp b/core/math/quick_hull.cpp index 4483f61bc4..6a60a5925d 100644 --- a/core/math/quick_hull.cpp +++ b/core/math/quick_hull.cpp @@ -55,7 +55,7 @@ Error QuickHull::build(const Vector<Vector3> &p_points, Geometry3D::MeshData &r_ HashSet<Vector3> valid_cache; for (int i = 0; i < p_points.size(); i++) { - Vector3 sp = p_points[i].snapped(Vector3(0.0001, 0.0001, 0.0001)); + Vector3 sp = p_points[i].snappedf(0.0001); if (valid_cache.has(sp)) { valid_points.write[i] = false; } else { diff --git a/core/math/rect2.h b/core/math/rect2.h index 7f410feb1c..b4069ae86a 100644 --- a/core/math/rect2.h +++ b/core/math/rect2.h @@ -278,7 +278,7 @@ struct _NO_DISCARD_ Rect2 { } _FORCE_INLINE_ Rect2 abs() const { - return Rect2(position + size.min(Point2()), size.abs()); + return Rect2(position + size.minf(0), size.abs()); } _FORCE_INLINE_ Rect2 round() const { diff --git a/core/math/rect2i.h b/core/math/rect2i.h index 64806414c7..a1338da0bb 100644 --- a/core/math/rect2i.h +++ b/core/math/rect2i.h @@ -213,7 +213,7 @@ struct _NO_DISCARD_ Rect2i { } _FORCE_INLINE_ Rect2i abs() const { - return Rect2i(position + size.min(Point2i()), size.abs()); + return Rect2i(position + size.mini(0), size.abs()); } _FORCE_INLINE_ void set_end(const Vector2i &p_end) { diff --git a/core/math/triangle_mesh.cpp b/core/math/triangle_mesh.cpp index 0da1b8c7ad..01b733183d 100644 --- a/core/math/triangle_mesh.cpp +++ b/core/math/triangle_mesh.cpp @@ -133,7 +133,7 @@ void TriangleMesh::create(const Vector<Vector3> &p_faces, const Vector<int32_t> for (int j = 0; j < 3; j++) { int vidx = -1; - Vector3 vs = v[j].snapped(Vector3(0.0001, 0.0001, 0.0001)); + Vector3 vs = v[j].snappedf(0.0001); HashMap<Vector3, int>::Iterator E = db.find(vs); if (E) { vidx = E->value; diff --git a/core/math/vector2.cpp b/core/math/vector2.cpp index 198fd85d20..e86b97d6a8 100644 --- a/core/math/vector2.cpp +++ b/core/math/vector2.cpp @@ -135,12 +135,24 @@ Vector2 Vector2::clamp(const Vector2 &p_min, const Vector2 &p_max) const { CLAMP(y, p_min.y, p_max.y)); } +Vector2 Vector2::clampf(real_t p_min, real_t p_max) const { + return Vector2( + CLAMP(x, p_min, p_max), + CLAMP(y, p_min, p_max)); +} + Vector2 Vector2::snapped(const Vector2 &p_step) const { return Vector2( Math::snapped(x, p_step.x), Math::snapped(y, p_step.y)); } +Vector2 Vector2::snappedf(real_t p_step) const { + return Vector2( + Math::snapped(x, p_step), + Math::snapped(y, p_step)); +} + Vector2 Vector2::limit_length(real_t p_len) const { const real_t l = length(); Vector2 v = *this; diff --git a/core/math/vector2.h b/core/math/vector2.h index 6ad003edd1..8851942cdd 100644 --- a/core/math/vector2.h +++ b/core/math/vector2.h @@ -89,10 +89,18 @@ struct _NO_DISCARD_ Vector2 { return Vector2(MIN(x, p_vector2.x), MIN(y, p_vector2.y)); } + Vector2 minf(real_t p_scalar) const { + return Vector2(MIN(x, p_scalar), MIN(y, p_scalar)); + } + Vector2 max(const Vector2 &p_vector2) const { return Vector2(MAX(x, p_vector2.x), MAX(y, p_vector2.y)); } + Vector2 maxf(real_t p_scalar) const { + return Vector2(MAX(x, p_scalar), MAX(y, p_scalar)); + } + real_t distance_to(const Vector2 &p_vector2) const; real_t distance_squared_to(const Vector2 &p_vector2) const; real_t angle_to(const Vector2 &p_vector2) const; @@ -168,7 +176,9 @@ struct _NO_DISCARD_ Vector2 { Vector2 ceil() const; Vector2 round() const; Vector2 snapped(const Vector2 &p_by) const; + Vector2 snappedf(real_t p_by) const; Vector2 clamp(const Vector2 &p_min, const Vector2 &p_max) const; + Vector2 clampf(real_t p_min, real_t p_max) const; real_t aspect() const { return width / height; } operator String() const; diff --git a/core/math/vector2i.cpp b/core/math/vector2i.cpp index ba79d439dd..790f564734 100644 --- a/core/math/vector2i.cpp +++ b/core/math/vector2i.cpp @@ -39,12 +39,24 @@ Vector2i Vector2i::clamp(const Vector2i &p_min, const Vector2i &p_max) const { CLAMP(y, p_min.y, p_max.y)); } +Vector2i Vector2i::clampi(int32_t p_min, int32_t p_max) const { + return Vector2i( + CLAMP(x, p_min, p_max), + CLAMP(y, p_min, p_max)); +} + Vector2i Vector2i::snapped(const Vector2i &p_step) const { return Vector2i( Math::snapped(x, p_step.x), Math::snapped(y, p_step.y)); } +Vector2i Vector2i::snappedi(int32_t p_step) const { + return Vector2i( + Math::snapped(x, p_step), + Math::snapped(y, p_step)); +} + int64_t Vector2i::length_squared() const { return x * (int64_t)x + y * (int64_t)y; } diff --git a/core/math/vector2i.h b/core/math/vector2i.h index aa29263a65..aca9ae8272 100644 --- a/core/math/vector2i.h +++ b/core/math/vector2i.h @@ -81,10 +81,18 @@ struct _NO_DISCARD_ Vector2i { return Vector2i(MIN(x, p_vector2i.x), MIN(y, p_vector2i.y)); } + Vector2i mini(int32_t p_scalar) const { + return Vector2i(MIN(x, p_scalar), MIN(y, p_scalar)); + } + Vector2i max(const Vector2i &p_vector2i) const { return Vector2i(MAX(x, p_vector2i.x), MAX(y, p_vector2i.y)); } + Vector2i maxi(int32_t p_scalar) const { + return Vector2i(MAX(x, p_scalar), MAX(y, p_scalar)); + } + double distance_to(const Vector2i &p_to) const { return (p_to - *this).length(); } @@ -127,7 +135,9 @@ struct _NO_DISCARD_ Vector2i { Vector2i sign() const { return Vector2i(SIGN(x), SIGN(y)); } Vector2i abs() const { return Vector2i(Math::abs(x), Math::abs(y)); } Vector2i clamp(const Vector2i &p_min, const Vector2i &p_max) const; + Vector2i clampi(int32_t p_min, int32_t p_max) const; Vector2i snapped(const Vector2i &p_step) const; + Vector2i snappedi(int32_t p_step) const; operator String() const; operator Vector2() const; diff --git a/core/math/vector3.cpp b/core/math/vector3.cpp index fad5f2c0fb..1e90002665 100644 --- a/core/math/vector3.cpp +++ b/core/math/vector3.cpp @@ -52,6 +52,13 @@ Vector3 Vector3::clamp(const Vector3 &p_min, const Vector3 &p_max) const { CLAMP(z, p_min.z, p_max.z)); } +Vector3 Vector3::clampf(real_t p_min, real_t p_max) const { + return Vector3( + CLAMP(x, p_min, p_max), + CLAMP(y, p_min, p_max), + CLAMP(z, p_min, p_max)); +} + void Vector3::snap(const Vector3 &p_step) { x = Math::snapped(x, p_step.x); y = Math::snapped(y, p_step.y); @@ -64,6 +71,18 @@ Vector3 Vector3::snapped(const Vector3 &p_step) const { return v; } +void Vector3::snapf(real_t p_step) { + x = Math::snapped(x, p_step); + y = Math::snapped(y, p_step); + z = Math::snapped(z, p_step); +} + +Vector3 Vector3::snappedf(real_t p_step) const { + Vector3 v = *this; + v.snapf(p_step); + return v; +} + Vector3 Vector3::limit_length(real_t p_len) const { const real_t l = length(); Vector3 v = *this; diff --git a/core/math/vector3.h b/core/math/vector3.h index f5d16984d9..2313eb557a 100644 --- a/core/math/vector3.h +++ b/core/math/vector3.h @@ -80,10 +80,18 @@ struct _NO_DISCARD_ Vector3 { return Vector3(MIN(x, p_vector3.x), MIN(y, p_vector3.y), MIN(z, p_vector3.z)); } + Vector3 minf(real_t p_scalar) const { + return Vector3(MIN(x, p_scalar), MIN(y, p_scalar), MIN(z, p_scalar)); + } + Vector3 max(const Vector3 &p_vector3) const { return Vector3(MAX(x, p_vector3.x), MAX(y, p_vector3.y), MAX(z, p_vector3.z)); } + Vector3 maxf(real_t p_scalar) const { + return Vector3(MAX(x, p_scalar), MAX(y, p_scalar), MAX(z, p_scalar)); + } + _FORCE_INLINE_ real_t length() const; _FORCE_INLINE_ real_t length_squared() const; @@ -96,7 +104,9 @@ struct _NO_DISCARD_ Vector3 { _FORCE_INLINE_ void zero(); void snap(const Vector3 &p_step); + void snapf(real_t p_step); Vector3 snapped(const Vector3 &p_step) const; + Vector3 snappedf(real_t p_step) const; void rotate(const Vector3 &p_axis, real_t p_angle); Vector3 rotated(const Vector3 &p_axis, real_t p_angle) const; @@ -127,6 +137,7 @@ struct _NO_DISCARD_ Vector3 { _FORCE_INLINE_ Vector3 ceil() const; _FORCE_INLINE_ Vector3 round() const; Vector3 clamp(const Vector3 &p_min, const Vector3 &p_max) const; + Vector3 clampf(real_t p_min, real_t p_max) const; _FORCE_INLINE_ real_t distance_to(const Vector3 &p_to) const; _FORCE_INLINE_ real_t distance_squared_to(const Vector3 &p_to) const; diff --git a/core/math/vector3i.cpp b/core/math/vector3i.cpp index f41460e623..93f9d15ac1 100644 --- a/core/math/vector3i.cpp +++ b/core/math/vector3i.cpp @@ -48,6 +48,13 @@ Vector3i Vector3i::clamp(const Vector3i &p_min, const Vector3i &p_max) const { CLAMP(z, p_min.z, p_max.z)); } +Vector3i Vector3i::clampi(int32_t p_min, int32_t p_max) const { + return Vector3i( + CLAMP(x, p_min, p_max), + CLAMP(y, p_min, p_max), + CLAMP(z, p_min, p_max)); +} + Vector3i Vector3i::snapped(const Vector3i &p_step) const { return Vector3i( Math::snapped(x, p_step.x), @@ -55,6 +62,13 @@ Vector3i Vector3i::snapped(const Vector3i &p_step) const { Math::snapped(z, p_step.z)); } +Vector3i Vector3i::snappedi(int32_t p_step) const { + return Vector3i( + Math::snapped(x, p_step), + Math::snapped(y, p_step), + Math::snapped(z, p_step)); +} + Vector3i::operator String() const { return "(" + itos(x) + ", " + itos(y) + ", " + itos(z) + ")"; } diff --git a/core/math/vector3i.h b/core/math/vector3i.h index a9f298bff1..035cfcf9e2 100644 --- a/core/math/vector3i.h +++ b/core/math/vector3i.h @@ -73,10 +73,18 @@ struct _NO_DISCARD_ Vector3i { return Vector3i(MIN(x, p_vector3i.x), MIN(y, p_vector3i.y), MIN(z, p_vector3i.z)); } + Vector3i mini(int32_t p_scalar) const { + return Vector3i(MIN(x, p_scalar), MIN(y, p_scalar), MIN(z, p_scalar)); + } + Vector3i max(const Vector3i &p_vector3i) const { return Vector3i(MAX(x, p_vector3i.x), MAX(y, p_vector3i.y), MAX(z, p_vector3i.z)); } + Vector3i maxi(int32_t p_scalar) const { + return Vector3i(MAX(x, p_scalar), MAX(y, p_scalar), MAX(z, p_scalar)); + } + _FORCE_INLINE_ int64_t length_squared() const; _FORCE_INLINE_ double length() const; @@ -85,7 +93,9 @@ struct _NO_DISCARD_ Vector3i { _FORCE_INLINE_ Vector3i abs() const; _FORCE_INLINE_ Vector3i sign() const; Vector3i clamp(const Vector3i &p_min, const Vector3i &p_max) const; + Vector3i clampi(int32_t p_min, int32_t p_max) const; Vector3i snapped(const Vector3i &p_step) const; + Vector3i snappedi(int32_t p_step) const; _FORCE_INLINE_ double distance_to(const Vector3i &p_to) const; _FORCE_INLINE_ int64_t distance_squared_to(const Vector3i &p_to) const; diff --git a/core/math/vector4.cpp b/core/math/vector4.cpp index e6f6dee42c..555ca6c66c 100644 --- a/core/math/vector4.cpp +++ b/core/math/vector4.cpp @@ -171,12 +171,25 @@ void Vector4::snap(const Vector4 &p_step) { w = Math::snapped(w, p_step.w); } +void Vector4::snapf(real_t p_step) { + x = Math::snapped(x, p_step); + y = Math::snapped(y, p_step); + z = Math::snapped(z, p_step); + w = Math::snapped(w, p_step); +} + Vector4 Vector4::snapped(const Vector4 &p_step) const { Vector4 v = *this; v.snap(p_step); return v; } +Vector4 Vector4::snappedf(real_t p_step) const { + Vector4 v = *this; + v.snapf(p_step); + return v; +} + Vector4 Vector4::inverse() const { return Vector4(1.0f / x, 1.0f / y, 1.0f / z, 1.0f / w); } @@ -189,6 +202,14 @@ Vector4 Vector4::clamp(const Vector4 &p_min, const Vector4 &p_max) const { CLAMP(w, p_min.w, p_max.w)); } +Vector4 Vector4::clampf(real_t p_min, real_t p_max) const { + return Vector4( + CLAMP(x, p_min, p_max), + CLAMP(y, p_min, p_max), + CLAMP(z, p_min, p_max), + CLAMP(w, p_min, p_max)); +} + Vector4::operator String() const { return "(" + String::num_real(x, false) + ", " + String::num_real(y, false) + ", " + String::num_real(z, false) + ", " + String::num_real(w, false) + ")"; } diff --git a/core/math/vector4.h b/core/math/vector4.h index 4dba3126cb..52699c6281 100644 --- a/core/math/vector4.h +++ b/core/math/vector4.h @@ -72,10 +72,18 @@ struct _NO_DISCARD_ Vector4 { return Vector4(MIN(x, p_vector4.x), MIN(y, p_vector4.y), MIN(z, p_vector4.z), MIN(w, p_vector4.w)); } + Vector4 minf(real_t p_scalar) const { + return Vector4(MIN(x, p_scalar), MIN(y, p_scalar), MIN(z, p_scalar), MIN(w, p_scalar)); + } + Vector4 max(const Vector4 &p_vector4) const { return Vector4(MAX(x, p_vector4.x), MAX(y, p_vector4.y), MAX(z, p_vector4.z), MAX(w, p_vector4.w)); } + Vector4 maxf(real_t p_scalar) const { + return Vector4(MAX(x, p_scalar), MAX(y, p_scalar), MAX(z, p_scalar), MAX(w, p_scalar)); + } + _FORCE_INLINE_ real_t length_squared() const; bool is_equal_approx(const Vector4 &p_vec4) const; bool is_zero_approx() const; @@ -101,8 +109,11 @@ struct _NO_DISCARD_ Vector4 { Vector4 posmod(real_t p_mod) const; Vector4 posmodv(const Vector4 &p_modv) const; void snap(const Vector4 &p_step); + void snapf(real_t p_step); Vector4 snapped(const Vector4 &p_step) const; + Vector4 snappedf(real_t p_step) const; Vector4 clamp(const Vector4 &p_min, const Vector4 &p_max) const; + Vector4 clampf(real_t p_min, real_t p_max) const; Vector4 inverse() const; _FORCE_INLINE_ real_t dot(const Vector4 &p_vec4) const; diff --git a/core/math/vector4i.cpp b/core/math/vector4i.cpp index 8e36c6b534..afa77a4988 100644 --- a/core/math/vector4i.cpp +++ b/core/math/vector4i.cpp @@ -65,6 +65,14 @@ Vector4i Vector4i::clamp(const Vector4i &p_min, const Vector4i &p_max) const { CLAMP(w, p_min.w, p_max.w)); } +Vector4i Vector4i::clampi(int32_t p_min, int32_t p_max) const { + return Vector4i( + CLAMP(x, p_min, p_max), + CLAMP(y, p_min, p_max), + CLAMP(z, p_min, p_max), + CLAMP(w, p_min, p_max)); +} + Vector4i Vector4i::snapped(const Vector4i &p_step) const { return Vector4i( Math::snapped(x, p_step.x), @@ -73,6 +81,14 @@ Vector4i Vector4i::snapped(const Vector4i &p_step) const { Math::snapped(w, p_step.w)); } +Vector4i Vector4i::snappedi(int32_t p_step) const { + return Vector4i( + Math::snapped(x, p_step), + Math::snapped(y, p_step), + Math::snapped(z, p_step), + Math::snapped(w, p_step)); +} + Vector4i::operator String() const { return "(" + itos(x) + ", " + itos(y) + ", " + itos(z) + ", " + itos(w) + ")"; } diff --git a/core/math/vector4i.h b/core/math/vector4i.h index 5a96d98d18..8a9c580bc1 100644 --- a/core/math/vector4i.h +++ b/core/math/vector4i.h @@ -75,10 +75,18 @@ struct _NO_DISCARD_ Vector4i { return Vector4i(MIN(x, p_vector4i.x), MIN(y, p_vector4i.y), MIN(z, p_vector4i.z), MIN(w, p_vector4i.w)); } + Vector4i mini(int32_t p_scalar) const { + return Vector4i(MIN(x, p_scalar), MIN(y, p_scalar), MIN(z, p_scalar), MIN(w, p_scalar)); + } + Vector4i max(const Vector4i &p_vector4i) const { return Vector4i(MAX(x, p_vector4i.x), MAX(y, p_vector4i.y), MAX(z, p_vector4i.z), MAX(w, p_vector4i.w)); } + Vector4i maxi(int32_t p_scalar) const { + return Vector4i(MAX(x, p_scalar), MAX(y, p_scalar), MAX(z, p_scalar), MAX(w, p_scalar)); + } + _FORCE_INLINE_ int64_t length_squared() const; _FORCE_INLINE_ double length() const; @@ -90,7 +98,9 @@ struct _NO_DISCARD_ Vector4i { _FORCE_INLINE_ Vector4i abs() const; _FORCE_INLINE_ Vector4i sign() const; Vector4i clamp(const Vector4i &p_min, const Vector4i &p_max) const; + Vector4i clampi(int32_t p_min, int32_t p_max) const; Vector4i snapped(const Vector4i &p_step) const; + Vector4i snappedi(int32_t p_step) const; /* Operators */ diff --git a/core/object/class_db.cpp b/core/object/class_db.cpp index 7ef1ce74ed..876635529c 100644 --- a/core/object/class_db.cpp +++ b/core/object/class_db.cpp @@ -138,7 +138,7 @@ public: return nullptr; } - static void placeholder_instance_free_property_list(GDExtensionClassInstancePtr p_instance, const GDExtensionPropertyInfo *p_list) { + static void placeholder_instance_free_property_list(GDExtensionClassInstancePtr p_instance, const GDExtensionPropertyInfo *p_list, uint32_t p_count) { } static GDExtensionBool placeholder_instance_property_can_revert(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name) { @@ -600,12 +600,13 @@ ObjectGDExtension *ClassDB::get_placeholder_extension(const StringName &p_class) placeholder_extension->set = &PlaceholderExtensionInstance::placeholder_instance_set; placeholder_extension->get = &PlaceholderExtensionInstance::placeholder_instance_get; placeholder_extension->get_property_list = &PlaceholderExtensionInstance::placeholder_instance_get_property_list; - placeholder_extension->free_property_list = &PlaceholderExtensionInstance::placeholder_instance_free_property_list; + placeholder_extension->free_property_list2 = &PlaceholderExtensionInstance::placeholder_instance_free_property_list; placeholder_extension->property_can_revert = &PlaceholderExtensionInstance::placeholder_instance_property_can_revert; placeholder_extension->property_get_revert = &PlaceholderExtensionInstance::placeholder_instance_property_get_revert; placeholder_extension->validate_property = &PlaceholderExtensionInstance::placeholder_instance_validate_property; #ifndef DISABLE_DEPRECATED placeholder_extension->notification = nullptr; + placeholder_extension->free_property_list = nullptr; #endif // DISABLE_DEPRECATED placeholder_extension->notification2 = &PlaceholderExtensionInstance::placeholder_instance_notification; placeholder_extension->to_string = &PlaceholderExtensionInstance::placeholder_instance_to_string; diff --git a/core/object/object.cpp b/core/object/object.cpp index f8d2feb5a8..b6c8a87a22 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -503,9 +503,14 @@ void Object::get_property_list(List<PropertyInfo> *p_list, bool p_reversed) cons for (uint32_t i = 0; i < pcount; i++) { p_list->push_back(PropertyInfo(pinfo[i])); } - if (current_extension->free_property_list) { + if (current_extension->free_property_list2) { + current_extension->free_property_list2(_extension_instance, pinfo, pcount); + } +#ifndef DISABLE_DEPRECATED + else if (current_extension->free_property_list) { current_extension->free_property_list(_extension_instance, pinfo); } +#endif // DISABLE_DEPRECATED #ifdef TOOLS_ENABLED } #endif @@ -1455,7 +1460,7 @@ Error Object::connect(const StringName &p_signal, const Callable &p_callable, ui } bool Object::is_connected(const StringName &p_signal, const Callable &p_callable) const { - ERR_FAIL_COND_V_MSG(p_callable.is_null(), false, "Cannot determine if connected to '" + p_signal + "': the provided callable is null."); + ERR_FAIL_COND_V_MSG(p_callable.is_null(), false, "Cannot determine if connected to '" + p_signal + "': the provided callable is null."); // Should use `is_null`, see note in `connect` about the use of `is_valid`. const SignalData *s = signal_map.getptr(p_signal); if (!s) { bool signal_is_valid = ClassDB::has_signal(get_class_name(), p_signal); @@ -1478,7 +1483,7 @@ void Object::disconnect(const StringName &p_signal, const Callable &p_callable) } bool Object::_disconnect(const StringName &p_signal, const Callable &p_callable, bool p_force) { - ERR_FAIL_COND_V_MSG(p_callable.is_null(), false, "Cannot disconnect from '" + p_signal + "': the provided callable is null."); + ERR_FAIL_COND_V_MSG(p_callable.is_null(), false, "Cannot disconnect from '" + p_signal + "': the provided callable is null."); // Should use `is_null`, see note in `connect` about the use of `is_valid`. SignalData *s = signal_map.getptr(p_signal); if (!s) { diff --git a/core/object/object.h b/core/object/object.h index 915c3a8c25..adb50268d2 100644 --- a/core/object/object.h +++ b/core/object/object.h @@ -324,12 +324,13 @@ struct ObjectGDExtension { GDExtensionClassSet set; GDExtensionClassGet get; GDExtensionClassGetPropertyList get_property_list; - GDExtensionClassFreePropertyList free_property_list; + GDExtensionClassFreePropertyList2 free_property_list2; GDExtensionClassPropertyCanRevert property_can_revert; GDExtensionClassPropertyGetRevert property_get_revert; GDExtensionClassValidateProperty validate_property; #ifndef DISABLE_DEPRECATED GDExtensionClassNotification notification; + GDExtensionClassFreePropertyList free_property_list; #endif // DISABLE_DEPRECATED GDExtensionClassNotification2 notification2; GDExtensionClassToString to_string; diff --git a/core/object/undo_redo.cpp b/core/object/undo_redo.cpp index 6a1385e268..4d67cd930e 100644 --- a/core/object/undo_redo.cpp +++ b/core/object/undo_redo.cpp @@ -144,7 +144,7 @@ void UndoRedo::create_action(const String &p_name, MergeMode p_mode, bool p_back } void UndoRedo::add_do_method(const Callable &p_callable) { - ERR_FAIL_COND(p_callable.is_null()); + ERR_FAIL_COND(!p_callable.is_valid()); ERR_FAIL_COND(action_level <= 0); ERR_FAIL_COND((current_action + 1) >= actions.size()); @@ -169,7 +169,7 @@ void UndoRedo::add_do_method(const Callable &p_callable) { } void UndoRedo::add_undo_method(const Callable &p_callable) { - ERR_FAIL_COND(p_callable.is_null()); + ERR_FAIL_COND(!p_callable.is_valid()); ERR_FAIL_COND(action_level <= 0); ERR_FAIL_COND((current_action + 1) >= actions.size()); diff --git a/core/os/os.cpp b/core/os/os.cpp index 8582888740..fa7f23ded0 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -398,6 +398,11 @@ bool OS::has_feature(const String &p_feature) { if (p_feature == "editor") { return true; } + if (p_feature == "editor_hint") { + return _in_editor; + } else if (p_feature == "editor_runtime") { + return !_in_editor; + } #else if (p_feature == "template") { return true; diff --git a/core/os/os.h b/core/os/os.h index 069a3876af..d20f84b4ff 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -63,6 +63,7 @@ class OS { bool _stdout_enabled = true; bool _stderr_enabled = true; bool _writing_movie = false; + bool _in_editor = false; CompositeLogger *_logger = nullptr; diff --git a/core/templates/command_queue_mt.cpp b/core/templates/command_queue_mt.cpp index 0c5c6394a1..d9e5e0b217 100644 --- a/core/templates/command_queue_mt.cpp +++ b/core/templates/command_queue_mt.cpp @@ -41,35 +41,6 @@ void CommandQueueMT::unlock() { mutex.unlock(); } -void CommandQueueMT::wait_for_flush() { - // wait one millisecond for a flush to happen - OS::get_singleton()->delay_usec(1000); -} - -CommandQueueMT::SyncSemaphore *CommandQueueMT::_alloc_sync_sem() { - int idx = -1; - - while (true) { - lock(); - for (int i = 0; i < SYNC_SEMAPHORES; i++) { - if (!sync_sems[i].in_use) { - sync_sems[i].in_use = true; - idx = i; - break; - } - } - unlock(); - - if (idx == -1) { - wait_for_flush(); - } else { - break; - } - } - - return &sync_sems[idx]; -} - CommandQueueMT::CommandQueueMT() { } diff --git a/core/templates/command_queue_mt.h b/core/templates/command_queue_mt.h index a4ac338bed..c149861467 100644 --- a/core/templates/command_queue_mt.h +++ b/core/templates/command_queue_mt.h @@ -32,9 +32,9 @@ #define COMMAND_QUEUE_MT_H #include "core/object/worker_thread_pool.h" +#include "core/os/condition_variable.h" #include "core/os/memory.h" #include "core/os/mutex.h" -#include "core/os/semaphore.h" #include "core/string/print_string.h" #include "core/templates/local_vector.h" #include "core/templates/simple_type.h" @@ -251,14 +251,14 @@ #define DECL_PUSH(N) \ template <typename T, typename M COMMA(N) COMMA_SEP_LIST(TYPE_PARAM, N)> \ void push(T *p_instance, M p_method COMMA(N) COMMA_SEP_LIST(PARAM, N)) { \ - CMD_TYPE(N) *cmd = allocate_and_lock<CMD_TYPE(N)>(); \ + MutexLock mlock(mutex); \ + CMD_TYPE(N) *cmd = allocate<CMD_TYPE(N)>(); \ cmd->instance = p_instance; \ cmd->method = p_method; \ SEMIC_SEP_LIST(CMD_ASSIGN_PARAM, N); \ if (pump_task_id != WorkerThreadPool::INVALID_TASK_ID) { \ WorkerThreadPool::get_singleton()->notify_yield_over(pump_task_id); \ } \ - unlock(); \ } #define CMD_RET_TYPE(N) CommandRet##N<T, M, COMMA_SEP_LIST(TYPE_ARG, N) COMMA(N) R> @@ -266,19 +266,17 @@ #define DECL_PUSH_AND_RET(N) \ template <typename T, typename M, COMMA_SEP_LIST(TYPE_PARAM, N) COMMA(N) typename R> \ void push_and_ret(T *p_instance, M p_method, COMMA_SEP_LIST(PARAM, N) COMMA(N) R *r_ret) { \ - SyncSemaphore *ss = _alloc_sync_sem(); \ - CMD_RET_TYPE(N) *cmd = allocate_and_lock<CMD_RET_TYPE(N)>(); \ + MutexLock mlock(mutex); \ + CMD_RET_TYPE(N) *cmd = allocate<CMD_RET_TYPE(N)>(); \ cmd->instance = p_instance; \ cmd->method = p_method; \ SEMIC_SEP_LIST(CMD_ASSIGN_PARAM, N); \ cmd->ret = r_ret; \ - cmd->sync_sem = ss; \ if (pump_task_id != WorkerThreadPool::INVALID_TASK_ID) { \ WorkerThreadPool::get_singleton()->notify_yield_over(pump_task_id); \ } \ - unlock(); \ - ss->sem.wait(); \ - ss->in_use = false; \ + sync_tail++; \ + _wait_for_sync(mlock); \ } #define CMD_SYNC_TYPE(N) CommandSync##N<T, M COMMA(N) COMMA_SEP_LIST(TYPE_ARG, N)> @@ -286,39 +284,31 @@ #define DECL_PUSH_AND_SYNC(N) \ template <typename T, typename M COMMA(N) COMMA_SEP_LIST(TYPE_PARAM, N)> \ void push_and_sync(T *p_instance, M p_method COMMA(N) COMMA_SEP_LIST(PARAM, N)) { \ - SyncSemaphore *ss = _alloc_sync_sem(); \ - CMD_SYNC_TYPE(N) *cmd = allocate_and_lock<CMD_SYNC_TYPE(N)>(); \ + MutexLock mlock(mutex); \ + CMD_SYNC_TYPE(N) *cmd = allocate<CMD_SYNC_TYPE(N)>(); \ cmd->instance = p_instance; \ cmd->method = p_method; \ SEMIC_SEP_LIST(CMD_ASSIGN_PARAM, N); \ - cmd->sync_sem = ss; \ if (pump_task_id != WorkerThreadPool::INVALID_TASK_ID) { \ WorkerThreadPool::get_singleton()->notify_yield_over(pump_task_id); \ } \ - unlock(); \ - ss->sem.wait(); \ - ss->in_use = false; \ + sync_tail++; \ + _wait_for_sync(mlock); \ } #define MAX_CMD_PARAMS 15 class CommandQueueMT { - struct SyncSemaphore { - Semaphore sem; - bool in_use = false; - }; - struct CommandBase { + bool sync = false; virtual void call() = 0; - virtual SyncSemaphore *get_sync_semaphore() { return nullptr; } virtual ~CommandBase() = default; // Won't be called. }; struct SyncCommand : public CommandBase { - SyncSemaphore *sync_sem = nullptr; - - virtual SyncSemaphore *get_sync_semaphore() override { - return sync_sem; + virtual void call() override {} + SyncCommand() { + sync = true; } }; @@ -340,9 +330,11 @@ class CommandQueueMT { SYNC_SEMAPHORES = 8 }; + BinaryMutex mutex; LocalVector<uint8_t> command_mem; - SyncSemaphore sync_sems[SYNC_SEMAPHORES]; - Mutex mutex; + ConditionVariable sync_cond_var; + uint32_t sync_head = 0; + uint32_t sync_tail = 0; WorkerThreadPool::TaskID pump_task_id = WorkerThreadPool::INVALID_TASK_ID; uint64_t flush_read_ptr = 0; @@ -357,32 +349,23 @@ class CommandQueueMT { return cmd; } - template <typename T> - T *allocate_and_lock() { - lock(); - T *ret = allocate<T>(); - return ret; - } - void _flush() { - lock(); - if (unlikely(flush_read_ptr)) { // Re-entrant call. - unlock(); return; } + lock(); + WorkerThreadPool::thread_enter_command_queue_mt_flush(this); while (flush_read_ptr < command_mem.size()) { uint64_t size = *(uint64_t *)&command_mem[flush_read_ptr]; flush_read_ptr += 8; CommandBase *cmd = reinterpret_cast<CommandBase *>(&command_mem[flush_read_ptr]); - - SyncSemaphore *sync_sem = cmd->get_sync_semaphore(); cmd->call(); - if (sync_sem) { - sync_sem->sem.post(); // Release in case it needs sync/ret. + if (unlikely(cmd->sync)) { + sync_head++; + sync_cond_var.notify_all(); } flush_read_ptr += size; @@ -394,8 +377,12 @@ class CommandQueueMT { unlock(); } - void wait_for_flush(); - SyncSemaphore *_alloc_sync_sem(); + _FORCE_INLINE_ void _wait_for_sync(MutexLock<BinaryMutex> &p_lock) { + uint32_t sync_head_goal = sync_tail; + do { + sync_cond_var.wait(p_lock); + } while (sync_head != sync_head_goal); // Can't use lower-than because of wraparound. + } public: void lock(); diff --git a/core/templates/ring_buffer.h b/core/templates/ring_buffer.h index 54148a59bf..f5161cefa4 100644 --- a/core/templates/ring_buffer.h +++ b/core/templates/ring_buffer.h @@ -211,10 +211,10 @@ public: size_mask = mask; } - RingBuffer<T>(int p_power = 0) { + RingBuffer(int p_power = 0) { resize(p_power); } - ~RingBuffer<T>() {} + ~RingBuffer() {} }; #endif // RING_BUFFER_H diff --git a/core/templates/safe_refcount.h b/core/templates/safe_refcount.h index 637b068da9..16b605eaff 100644 --- a/core/templates/safe_refcount.h +++ b/core/templates/safe_refcount.h @@ -146,7 +146,7 @@ public: } } - _ALWAYS_INLINE_ explicit SafeNumeric<T>(T p_value = static_cast<T>(0)) { + _ALWAYS_INLINE_ explicit SafeNumeric(T p_value = static_cast<T>(0)) { set(p_value); } }; diff --git a/core/variant/method_ptrcall.h b/core/variant/method_ptrcall.h index 123f2067e2..c8d1241d3d 100644 --- a/core/variant/method_ptrcall.h +++ b/core/variant/method_ptrcall.h @@ -159,10 +159,7 @@ MAKE_PTRARG_BY_REFERENCE(Variant); template <typename T> struct PtrToArg<T *> { _FORCE_INLINE_ static T *convert(const void *p_ptr) { - if (p_ptr == nullptr) { - return nullptr; - } - return const_cast<T *>(*reinterpret_cast<T *const *>(p_ptr)); + return likely(p_ptr) ? const_cast<T *>(*reinterpret_cast<T *const *>(p_ptr)) : nullptr; } typedef Object *EncodeT; _FORCE_INLINE_ static void encode(T *p_var, void *p_ptr) { @@ -173,10 +170,7 @@ struct PtrToArg<T *> { template <typename T> struct PtrToArg<const T *> { _FORCE_INLINE_ static const T *convert(const void *p_ptr) { - if (p_ptr == nullptr) { - return nullptr; - } - return *reinterpret_cast<T *const *>(p_ptr); + return likely(p_ptr) ? *reinterpret_cast<T *const *>(p_ptr) : nullptr; } typedef const Object *EncodeT; _FORCE_INLINE_ static void encode(T *p_var, void *p_ptr) { diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp index 8e702ce8bb..37eb16f9b2 100644 --- a/core/variant/variant.cpp +++ b/core/variant/variant.cpp @@ -3495,50 +3495,6 @@ bool Variant::is_ref_counted() const { return type == OBJECT && _get_obj().id.is_ref_counted(); } -Vector<Variant> varray() { - return Vector<Variant>(); -} - -Vector<Variant> varray(const Variant &p_arg1) { - Vector<Variant> v; - v.push_back(p_arg1); - return v; -} - -Vector<Variant> varray(const Variant &p_arg1, const Variant &p_arg2) { - Vector<Variant> v; - v.push_back(p_arg1); - v.push_back(p_arg2); - return v; -} - -Vector<Variant> varray(const Variant &p_arg1, const Variant &p_arg2, const Variant &p_arg3) { - Vector<Variant> v; - v.push_back(p_arg1); - v.push_back(p_arg2); - v.push_back(p_arg3); - return v; -} - -Vector<Variant> varray(const Variant &p_arg1, const Variant &p_arg2, const Variant &p_arg3, const Variant &p_arg4) { - Vector<Variant> v; - v.push_back(p_arg1); - v.push_back(p_arg2); - v.push_back(p_arg3); - v.push_back(p_arg4); - return v; -} - -Vector<Variant> varray(const Variant &p_arg1, const Variant &p_arg2, const Variant &p_arg3, const Variant &p_arg4, const Variant &p_arg5) { - Vector<Variant> v; - v.push_back(p_arg1); - v.push_back(p_arg2); - v.push_back(p_arg3); - v.push_back(p_arg4); - v.push_back(p_arg5); - return v; -} - void Variant::static_assign(const Variant &p_variant) { } @@ -3559,6 +3515,17 @@ bool Variant::is_shared() const { return is_type_shared(type); } +bool Variant::is_read_only() const { + switch (type) { + case ARRAY: + return reinterpret_cast<const Array *>(_data._mem)->is_read_only(); + case DICTIONARY: + return reinterpret_cast<const Dictionary *>(_data._mem)->is_read_only(); + default: + return false; + } +} + void Variant::_variant_call_error(const String &p_method, Callable::CallError &error) { switch (error.error) { case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT: { diff --git a/core/variant/variant.h b/core/variant/variant.h index e40df3171f..93953c4e0e 100644 --- a/core/variant/variant.h +++ b/core/variant/variant.h @@ -349,6 +349,7 @@ public: bool is_zero() const; bool is_one() const; bool is_null() const; + bool is_read_only() const; // Make sure Variant is not implicitly cast when accessing it with bracket notation (GH-49469). Variant &operator[](const Variant &p_key) = delete; @@ -798,12 +799,23 @@ public: //typedef Dictionary Dictionary; no //typedef Array Array; -Vector<Variant> varray(); -Vector<Variant> varray(const Variant &p_arg1); -Vector<Variant> varray(const Variant &p_arg1, const Variant &p_arg2); -Vector<Variant> varray(const Variant &p_arg1, const Variant &p_arg2, const Variant &p_arg3); -Vector<Variant> varray(const Variant &p_arg1, const Variant &p_arg2, const Variant &p_arg3, const Variant &p_arg4); -Vector<Variant> varray(const Variant &p_arg1, const Variant &p_arg2, const Variant &p_arg3, const Variant &p_arg4, const Variant &p_arg5); +template <typename... VarArgs> +Vector<Variant> varray(VarArgs... p_args) { + Vector<Variant> v; + + Variant args[sizeof...(p_args) + 1] = { p_args..., Variant() }; // +1 makes sure zero sized arrays are also supported. + uint32_t argc = sizeof...(p_args); + + if (argc > 0) { + v.resize(argc); + Variant *vw = v.ptrw(); + + for (uint32_t i = 0; i < argc; i++) { + vw[i] = args[i]; + } + } + return v; +} struct VariantHasher { static _FORCE_INLINE_ uint32_t hash(const Variant &p_variant) { return p_variant.hash(); } diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index d0d940c47d..08fb2a9c91 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -1804,7 +1804,9 @@ static void _register_variant_builtin_methods() { bind_method(Vector2, abs, sarray(), varray()); bind_method(Vector2, sign, sarray(), varray()); bind_method(Vector2, clamp, sarray("min", "max"), varray()); + bind_method(Vector2, clampf, sarray("min", "max"), varray()); bind_method(Vector2, snapped, sarray("step"), varray()); + bind_method(Vector2, snappedf, sarray("step"), varray()); bind_static_method(Vector2, from_angle, sarray("angle"), varray()); @@ -1820,7 +1822,9 @@ static void _register_variant_builtin_methods() { bind_method(Vector2i, sign, sarray(), varray()); bind_method(Vector2i, abs, sarray(), varray()); bind_method(Vector2i, clamp, sarray("min", "max"), varray()); + bind_method(Vector2i, clampi, sarray("min", "max"), varray()); bind_method(Vector2i, snapped, sarray("step"), varray()); + bind_method(Vector2i, snappedi, sarray("step"), varray()); /* Rect2 */ @@ -1875,7 +1879,9 @@ static void _register_variant_builtin_methods() { bind_method(Vector3, is_finite, sarray(), varray()); bind_method(Vector3, inverse, sarray(), varray()); bind_method(Vector3, clamp, sarray("min", "max"), varray()); + bind_method(Vector3, clampf, sarray("min", "max"), varray()); bind_method(Vector3, snapped, sarray("step"), varray()); + bind_method(Vector3, snappedf, sarray("step"), varray()); bind_method(Vector3, rotated, sarray("axis", "angle"), varray()); bind_method(Vector3, lerp, sarray("to", "weight"), varray()); bind_method(Vector3, slerp, sarray("to", "weight"), varray()); @@ -1896,7 +1902,7 @@ static void _register_variant_builtin_methods() { bind_method(Vector3, project, sarray("b"), varray()); bind_method(Vector3, slide, sarray("n"), varray()); bind_method(Vector3, bounce, sarray("n"), varray()); - bind_method(Vector3, reflect, sarray("direction"), varray()); + bind_method(Vector3, reflect, sarray("n"), varray()); bind_method(Vector3, sign, sarray(), varray()); bind_method(Vector3, octahedron_encode, sarray(), varray()); bind_static_method(Vector3, octahedron_decode, sarray("uv"), varray()); @@ -1912,7 +1918,9 @@ static void _register_variant_builtin_methods() { bind_method(Vector3i, sign, sarray(), varray()); bind_method(Vector3i, abs, sarray(), varray()); bind_method(Vector3i, clamp, sarray("min", "max"), varray()); + bind_method(Vector3i, clampi, sarray("min", "max"), varray()); bind_method(Vector3i, snapped, sarray("step"), varray()); + bind_method(Vector3i, snappedi, sarray("step"), varray()); /* Vector4 */ @@ -1931,7 +1939,9 @@ static void _register_variant_builtin_methods() { bind_method(Vector4, posmod, sarray("mod"), varray()); bind_method(Vector4, posmodv, sarray("modv"), varray()); bind_method(Vector4, snapped, sarray("step"), varray()); + bind_method(Vector4, snappedf, sarray("step"), varray()); bind_method(Vector4, clamp, sarray("min", "max"), varray()); + bind_method(Vector4, clampf, sarray("min", "max"), varray()); bind_method(Vector4, normalized, sarray(), varray()); bind_method(Vector4, is_normalized, sarray(), varray()); bind_method(Vector4, direction_to, sarray("to"), varray()); @@ -1952,7 +1962,9 @@ static void _register_variant_builtin_methods() { bind_method(Vector4i, sign, sarray(), varray()); bind_method(Vector4i, abs, sarray(), varray()); bind_method(Vector4i, clamp, sarray("min", "max"), varray()); + bind_method(Vector4i, clampi, sarray("min", "max"), varray()); bind_method(Vector4i, snapped, sarray("step"), varray()); + bind_method(Vector4i, snappedi, sarray("step"), varray()); bind_method(Vector4i, distance_to, sarray("to"), varray()); bind_method(Vector4i, distance_squared_to, sarray("to"), varray()); diff --git a/core/variant/variant_setget.cpp b/core/variant/variant_setget.cpp index 9d5ed22b1a..f49e9e54b3 100644 --- a/core/variant/variant_setget.cpp +++ b/core/variant/variant_setget.cpp @@ -251,15 +251,21 @@ void Variant::set_named(const StringName &p_member, const Variant &p_value, bool return; } } else if (type == Variant::DICTIONARY) { - Variant *v = VariantGetInternalPtr<Dictionary>::get_ptr(this)->getptr(p_member); + Dictionary &dict = *VariantGetInternalPtr<Dictionary>::get_ptr(this); + + if (dict.is_read_only()) { + r_valid = false; + return; + } + + Variant *v = dict.getptr(p_member); if (v) { *v = p_value; - r_valid = true; } else { - VariantGetInternalPtr<Dictionary>::get_ptr(this)->operator[](p_member) = p_value; - r_valid = true; + dict[p_member] = p_value; } + r_valid = true; } else { r_valid = false; } diff --git a/doc/classes/AudioStreamWAV.xml b/doc/classes/AudioStreamWAV.xml index 206b6361cc..3df814cb7f 100644 --- a/doc/classes/AudioStreamWAV.xml +++ b/doc/classes/AudioStreamWAV.xml @@ -15,7 +15,7 @@ <return type="int" enum="Error" /> <param index="0" name="path" type="String" /> <description> - Saves the AudioStreamWAV as a WAV file to [param path]. Samples with IMA ADPCM format can't be saved. + Saves the AudioStreamWAV as a WAV file to [param path]. Samples with IMA ADPCM or QOA formats can't be saved. [b]Note:[/b] A [code].wav[/code] extension is automatically appended to [param path] if it is missing. </description> </method> @@ -56,6 +56,9 @@ <constant name="FORMAT_IMA_ADPCM" value="2" enum="Format"> Audio is compressed using IMA ADPCM. </constant> + <constant name="FORMAT_QOA" value="3" enum="Format"> + Audio is compressed as QOA ([url=https://qoaformat.org/]Quite OK Audio[/url]). + </constant> <constant name="LOOP_DISABLED" value="0" enum="LoopMode"> Audio does not loop. </constant> diff --git a/doc/classes/BaseMaterial3D.xml b/doc/classes/BaseMaterial3D.xml index 1bd4183b3c..fc8af02869 100644 --- a/doc/classes/BaseMaterial3D.xml +++ b/doc/classes/BaseMaterial3D.xml @@ -609,6 +609,9 @@ <constant name="BLEND_MODE_MUL" value="3" enum="BlendMode"> The color of the object is multiplied by the background. </constant> + <constant name="BLEND_MODE_PREMULT_ALPHA" value="4" enum="BlendMode"> + The color of the object is added to the background and the alpha channel is used to mask out the background. This is effectively a hybrid of the blend mix and add modes, useful for effects like fire where you want the flame to add but the smoke to mix. By default, this works with unshaded materials using premultiplied textures. For shaded materials, use the [code]PREMUL_ALPHA_FACTOR[/code] built-in so that lighting can be modulated as well. + </constant> <constant name="ALPHA_ANTIALIASING_OFF" value="0" enum="AlphaAntiAliasing"> Disables Alpha AntiAliasing for the material. </constant> diff --git a/doc/classes/CanvasItem.xml b/doc/classes/CanvasItem.xml index 18413c4be5..bf53cb2e14 100644 --- a/doc/classes/CanvasItem.xml +++ b/doc/classes/CanvasItem.xml @@ -77,8 +77,14 @@ <param index="0" name="position" type="Vector2" /> <param index="1" name="radius" type="float" /> <param index="2" name="color" type="Color" /> + <param index="3" name="filled" type="bool" default="true" /> + <param index="4" name="width" type="float" default="-1.0" /> + <param index="5" name="antialiased" type="bool" default="false" /> <description> - Draws a colored, filled circle. See also [method draw_arc], [method draw_polyline] and [method draw_polygon]. + Draws a circle. See also [method draw_arc], [method draw_polyline], and [method draw_polygon]. + If [param filled] is [code]true[/code], the circle will be filled with the [param color] specified. If [param filled] is [code]false[/code], the circle will be drawn as a stroke with the [param color] and [param width] specified. + If [param width] is negative, then two-point primitives will be drawn instead of a four-point ones. This means that when the CanvasItem is scaled, the lines will remain thin. If this behavior is not desired, then pass a positive [param width] like [code]1.0[/code]. + [b]Note:[/b] [param width] is only effective if [param filled] is [code]false[/code]. </description> </method> <method name="draw_colored_polygon"> diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index 7c6f1a51c4..d455799c29 100644 --- a/doc/classes/CodeEdit.xml +++ b/doc/classes/CodeEdit.xml @@ -14,7 +14,7 @@ <return type="void" /> <param index="0" name="replace" type="bool" /> <description> - Override this method to define how the selected entry should be inserted. If [param replace] is true, any existing text should be replaced. + Override this method to define how the selected entry should be inserted. If [param replace] is [code]true[/code], any existing text should be replaced. </description> </method> <method name="_filter_code_completion_candidates" qualifiers="virtual const"> @@ -29,7 +29,7 @@ <return type="void" /> <param index="0" name="force" type="bool" /> <description> - Override this method to define what happens when the user requests code completion. If [param force] is true, any checks should be bypassed. + Override this method to define what happens when the user requests code completion. If [param force] is [code]true[/code], any checks should be bypassed. </description> </method> <method name="add_auto_brace_completion_pair"> @@ -123,7 +123,7 @@ <return type="void" /> <param index="0" name="replace" type="bool" default="false" /> <description> - Inserts the selected entry into the text. If [param replace] is true, any existing text is replaced rather than merged. + Inserts the selected entry into the text. If [param replace] is [code]true[/code], any existing text is replaced rather than merged. </description> </method> <method name="convert_indent"> @@ -144,6 +144,12 @@ Code regions are delimited using start and end tags (respectively [code]region[/code] and [code]endregion[/code] by default) preceded by one line comment delimiter. (eg. [code]#region[/code] and [code]#endregion[/code]) </description> </method> + <method name="delete_lines"> + <return type="void" /> + <description> + Deletes all lines that are selected or have a caret on them. + </description> + </method> <method name="do_indent"> <return type="void" /> <description> @@ -156,6 +162,12 @@ Duplicates all lines currently selected with any caret. Duplicates the entire line beneath the current one no matter where the caret is within the line. </description> </method> + <method name="duplicate_selection"> + <return type="void" /> + <description> + Duplicates all selected text and duplicates all lines with a caret on them. + </description> + </method> <method name="fold_all_lines"> <return type="void" /> <description> @@ -379,6 +391,18 @@ Returns whether the line at the specified index is folded or not. </description> </method> + <method name="move_lines_down"> + <return type="void" /> + <description> + Moves all lines down that are selected or have a caret on them. + </description> + </method> + <method name="move_lines_up"> + <return type="void" /> + <description> + Moves all lines up that are selected or have a caret on them. + </description> + </method> <method name="remove_comment_delimiter"> <return type="void" /> <param index="0" name="start_key" type="String" /> @@ -397,7 +421,7 @@ <return type="void" /> <param index="0" name="force" type="bool" default="false" /> <description> - Emits [signal code_completion_requested], if [param force] is true will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path or signal. + Emits [signal code_completion_requested], if [param force] is [code]true[/code] will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path, or signal. </description> </method> <method name="set_code_completion_selected_index"> @@ -467,6 +491,12 @@ Toggle the folding of the code block at the given line. </description> </method> + <method name="toggle_foldable_lines_at_carets"> + <return type="void" /> + <description> + Toggle the folding of the code block on all lines with a caret on them. + </description> + </method> <method name="unfold_all_lines"> <return type="void" /> <description> diff --git a/doc/classes/DirAccess.xml b/doc/classes/DirAccess.xml index 03d7f68f43..9c71addf0c 100644 --- a/doc/classes/DirAccess.xml +++ b/doc/classes/DirAccess.xml @@ -94,6 +94,16 @@ Static version of [method copy]. Supports only absolute paths. </description> </method> + <method name="create_link"> + <return type="int" enum="Error" /> + <param index="0" name="source" type="String" /> + <param index="1" name="target" type="String" /> + <description> + Creates symbolic link between files or folders. + [b]Note:[/b] On Windows, this method works only if the application is running with elevated privileges or Developer Mode is enabled. + [b]Note:[/b] This method is implemented on macOS, Linux, and Windows. + </description> + </method> <method name="current_is_dir" qualifiers="const"> <return type="bool" /> <description> @@ -212,6 +222,14 @@ [b]Note:[/b] This method is implemented on macOS, Linux (for EXT4 and F2FS filesystems only) and Windows. On other platforms, it always returns [code]true[/code]. </description> </method> + <method name="is_link"> + <return type="bool" /> + <param index="0" name="path" type="String" /> + <description> + Returns [code]true[/code] if the file or directory is a symbolic link, directory junction, or other reparse point. + [b]Note:[/b] This method is implemented on macOS, Linux, and Windows. + </description> + </method> <method name="list_dir_begin"> <return type="int" enum="Error" /> <description> @@ -264,6 +282,14 @@ Returns [code]null[/code] if opening the directory failed. You can use [method get_open_error] to check the error that occurred. </description> </method> + <method name="read_link"> + <return type="String" /> + <param index="0" name="path" type="String" /> + <description> + Returns target of the symbolic link. + [b]Note:[/b] This method is implemented on macOS, Linux, and Windows. + </description> + </method> <method name="remove"> <return type="int" enum="Error" /> <param index="0" name="path" type="String" /> diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 42ca336a25..fe67c2a38e 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -58,7 +58,7 @@ </method> <method name="create_status_indicator"> <return type="int" /> - <param index="0" name="icon" type="Image" /> + <param index="0" name="icon" type="Texture2D" /> <param index="1" name="tooltip" type="String" /> <param index="2" name="callback" type="Callable" /> <description> @@ -1178,11 +1178,22 @@ <method name="status_indicator_set_icon"> <return type="void" /> <param index="0" name="id" type="int" /> - <param index="1" name="icon" type="Image" /> + <param index="1" name="icon" type="Texture2D" /> <description> Sets the application status indicator icon. </description> </method> + <method name="status_indicator_set_menu"> + <return type="void" /> + <param index="0" name="id" type="int" /> + <param index="1" name="menu_rid" type="RID" /> + <description> + Sets the application status indicator native popup menu. + [b]Note:[/b] On macOS, the menu is activated by any mouse button. Its activation callback is [i]not[/i] triggered. + [b]Note:[/b] On Windows, the menu is activated by the right mouse button, selecting the status icon and pressing [kbd]Shift + F10[/kbd], or the applications key. The menu's activation callback for the other mouse buttons is still triggered. + [b]Note:[/b] Native popup is only supported if [NativeMenu] supports the [constant NativeMenu.FEATURE_POPUP_MENU] feature. + </description> + </method> <method name="status_indicator_set_tooltip"> <return type="void" /> <param index="0" name="id" type="int" /> @@ -1602,7 +1613,7 @@ <param index="0" name="max_size" type="Vector2i" /> <param index="1" name="window_id" type="int" default="0" /> <description> - Sets the maximum size of the window specified by [param window_id] in pixels. Normally, the user will not be able to drag the window to make it smaller than the specified size. See also [method window_get_max_size]. + Sets the maximum size of the window specified by [param window_id] in pixels. Normally, the user will not be able to drag the window to make it larger than the specified size. See also [method window_get_max_size]. [b]Note:[/b] It's recommended to change this value using [member Window.max_size] instead. [b]Note:[/b] Using third-party tools, it is possible for users to disable window geometry restrictions and therefore bypass this limit. </description> @@ -1612,7 +1623,7 @@ <param index="0" name="min_size" type="Vector2i" /> <param index="1" name="window_id" type="int" default="0" /> <description> - Sets the minimum size for the given window to [param min_size] (in pixels). Normally, the user will not be able to drag the window to make it larger than the specified size. See also [method window_get_min_size]. + Sets the minimum size for the given window to [param min_size] in pixels. Normally, the user will not be able to drag the window to make it smaller than the specified size. See also [method window_get_min_size]. [b]Note:[/b] It's recommended to change this value using [member Window.min_size] instead. [b]Note:[/b] By default, the main window has a minimum size of [code]Vector2i(64, 64)[/code]. This prevents issues that can arise when the window is resized to a near-zero size. [b]Note:[/b] Using third-party tools, it is possible for users to disable window geometry restrictions and therefore bypass this limit. diff --git a/doc/classes/FileAccess.xml b/doc/classes/FileAccess.xml index fafc02734a..99574da808 100644 --- a/doc/classes/FileAccess.xml +++ b/doc/classes/FileAccess.xml @@ -8,23 +8,23 @@ Here's a sample on how to write and read from a file: [codeblocks] [gdscript] - func save(content): + func save_to_file(content): var file = FileAccess.open("user://save_game.dat", FileAccess.WRITE) file.store_string(content) - func load(): + func load_from_file(): var file = FileAccess.open("user://save_game.dat", FileAccess.READ) var content = file.get_as_text() return content [/gdscript] [csharp] - public void Save(string content) + public void SaveToFile(string content) { using var file = FileAccess.Open("user://save_game.dat", FileAccess.ModeFlags.Write); file.StoreString(content); } - public string Load() + public string LoadFromFile() { using var file = FileAccess.Open("user://save_game.dat", FileAccess.ModeFlags.Read); string content = file.GetAsText(); diff --git a/doc/classes/HingeJoint3D.xml b/doc/classes/HingeJoint3D.xml index d150c79b78..f794853caf 100644 --- a/doc/classes/HingeJoint3D.xml +++ b/doc/classes/HingeJoint3D.xml @@ -53,7 +53,7 @@ <member name="angular_limit/relaxation" type="float" setter="set_param" getter="get_param" default="1.0"> The lower this value, the more the rotation gets slowed down. </member> - <member name="angular_limit/softness" type="float" setter="set_param" getter="get_param" default="0.9"> + <member name="angular_limit/softness" type="float" setter="set_param" getter="get_param" default="0.9" deprecated="This property is never set by the engine and is kept for compatibility purposes."> </member> <member name="angular_limit/upper" type="float" setter="set_param" getter="get_param" default="1.5708"> The maximum rotation. Only active if [member angular_limit/enable] is [code]true[/code]. @@ -84,7 +84,7 @@ <constant name="PARAM_LIMIT_BIAS" value="3" enum="Param"> The speed with which the rotation across the axis perpendicular to the hinge gets corrected. </constant> - <constant name="PARAM_LIMIT_SOFTNESS" value="4" enum="Param"> + <constant name="PARAM_LIMIT_SOFTNESS" value="4" enum="Param" deprecated="This property is never used by the engine and is kept for compatibility purpose."> </constant> <constant name="PARAM_LIMIT_RELAXATION" value="5" enum="Param"> The lower this value, the more the rotation gets slowed down. diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml index 37e64da8c8..3342e99ab6 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -619,6 +619,12 @@ [method request_ready] resets it back to [code]false[/code]. </description> </method> + <method name="is_part_of_edited_scene" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if the node is part of the scene currently opened in the editor. + </description> + </method> <method name="is_physics_interpolated" qualifiers="const"> <return type="bool" /> <description> diff --git a/doc/classes/Node2D.xml b/doc/classes/Node2D.xml index 091acdf6f2..851290de7b 100644 --- a/doc/classes/Node2D.xml +++ b/doc/classes/Node2D.xml @@ -44,7 +44,8 @@ <return type="void" /> <param index="0" name="point" type="Vector2" /> <description> - Rotates the node so it points towards the [param point], which is expected to use global coordinates. + Rotates the node so that its local +X axis points towards the [param point], which is expected to use global coordinates. + [param point] should not be the same as the node's position, otherwise the node always looks to the right. </description> </method> <method name="move_local_x"> diff --git a/doc/classes/PackedScene.xml b/doc/classes/PackedScene.xml index 579b4c5b9f..26d8fa8d5f 100644 --- a/doc/classes/PackedScene.xml +++ b/doc/classes/PackedScene.xml @@ -99,14 +99,14 @@ <return type="int" enum="Error" /> <param index="0" name="path" type="Node" /> <description> - Pack will ignore any sub-nodes not owned by given node. See [member Node.owner]. + Packs the [param path] node, and all owned sub-nodes, into this [PackedScene]. Any existing data will be cleared. See [member Node.owner]. </description> </method> </methods> <members> <member name="_bundled" type="Dictionary" setter="_set_bundled_scene" getter="_get_bundled_scene" default="{ "conn_count": 0, "conns": PackedInt32Array(), "editable_instances": [], "names": PackedStringArray(), "node_count": 0, "node_paths": [], "nodes": PackedInt32Array(), "variants": [], "version": 3 }"> A dictionary representation of the scene contents. - Available keys include "rnames" and "variants" for resources, "node_count", "nodes", "node_paths" for nodes, "editable_instances" for paths to overridden nodes, "conn_count" and "conns" for signal connections, and "version" for the format style of the PackedScene. + Available keys include "names" and "variants" for resources, "node_count", "nodes", "node_paths" for nodes, "editable_instances" for paths to overridden nodes, "conn_count" and "conns" for signal connections, and "version" for the format style of the PackedScene. </member> </members> <constants> diff --git a/doc/classes/ParticleProcessMaterial.xml b/doc/classes/ParticleProcessMaterial.xml index 8d0ae317b9..1502690b45 100644 --- a/doc/classes/ParticleProcessMaterial.xml +++ b/doc/classes/ParticleProcessMaterial.xml @@ -185,6 +185,7 @@ </member> <member name="emission_box_extents" type="Vector3" setter="set_emission_box_extents" getter="get_emission_box_extents"> The box's extents if [member emission_shape] is set to [constant EMISSION_SHAPE_BOX]. + [b]Note:[/b] [member emission_box_extents] starts from the center point and applies the X, Y, and Z values in both directions. The size is twice the area of the extents. </member> <member name="emission_color_texture" type="Texture2D" setter="set_emission_color_texture" getter="get_emission_color_texture"> Particle color will be modulated by color determined by sampling this texture at the same point as the [member emission_point_texture]. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 1daa1b04e4..e8d7dfb913 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -254,6 +254,7 @@ Path to an image used as the boot splash. If left empty, the default Godot Engine splash will be displayed instead. [b]Note:[/b] Only effective if [member application/boot_splash/show_image] is [code]true[/code]. [b]Note:[/b] The only supported format is PNG. Using another image format will result in an error. + [b]Note:[/b] The image will also show when opening the project in the editor. If you want to display the default splash image in the editor, add an empty override for [code]editor_hint[/code] feature. </member> <member name="application/boot_splash/minimum_display_time" type="int" setter="" getter="" default="0"> Minimum boot splash display time (in milliseconds). It is not recommended to set too high values for this setting. @@ -343,7 +344,7 @@ This setting can be overridden using the [code]--frame-delay <ms;>[/code] command line argument. </member> <member name="application/run/low_processor_mode" type="bool" setter="" getter="" default="false"> - If [code]true[/code], enables low-processor usage mode. This setting only works on desktop platforms. The screen is not redrawn if nothing changes visually. This is meant for writing applications and editors, but is pretty useless (and can hurt performance) in most games. + If [code]true[/code], enables low-processor usage mode. The screen is not redrawn if nothing changes visually. This is meant for writing applications and editors, but is pretty useless (and can hurt performance) in most games. </member> <member name="application/run/low_processor_mode_sleep_usec" type="int" setter="" getter="" default="6900"> Amount of sleeping between frames when the low-processor usage mode is enabled (in microseconds). Higher values will result in lower CPU usage. @@ -797,8 +798,8 @@ <member name="display/window/energy_saving/keep_screen_on" type="bool" setter="" getter="" default="true"> If [code]true[/code], keeps the screen on (even in case of inactivity), so the screensaver does not take over. Works on desktop and mobile platforms. </member> - <member name="display/window/energy_saving/keep_screen_on.editor" type="bool" setter="" getter="" default="false"> - Editor-only override for [member display/window/energy_saving/keep_screen_on]. Does not affect exported projects in debug or release mode. + <member name="display/window/energy_saving/keep_screen_on.editor_hint" type="bool" setter="" getter="" default="false"> + Editor-only override for [member display/window/energy_saving/keep_screen_on]. Does not affect running project. </member> <member name="display/window/handheld/orientation" type="int" setter="" getter="" default="0"> The default screen orientation to use on mobile devices. See [enum DisplayServer.ScreenOrientation] for possible values. @@ -1071,6 +1072,9 @@ <member name="gui/timers/tooltip_delay_sec" type="float" setter="" getter="" default="0.5"> Default delay for tooltips (in seconds). </member> + <member name="gui/timers/tooltip_delay_sec.editor_hint" type="float" setter="" getter="" default="0.5"> + Delay for tooltips in the editor. + </member> <member name="input/ui_accept" type="Dictionary" setter="" getter=""> Default [InputEventAction] to confirm a focused button, menu or list item, or validate input. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. @@ -2896,6 +2900,9 @@ <member name="xr/openxr/extensions/eye_gaze_interaction" type="bool" setter="" getter="" default="false"> Specify whether to enable eye tracking for this project. Depending on the platform, additional export configuration may be needed. </member> + <member name="xr/openxr/extensions/hand_interaction_profile" type="bool" setter="" getter="" default="false"> + If true the hand interaction profile extension will be activated if supported by the platform. + </member> <member name="xr/openxr/extensions/hand_tracking" type="bool" setter="" getter="" default="true"> If true we enable the hand tracking extension if available. </member> diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index 5d90cd6b92..519bba4e7c 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -1979,6 +1979,12 @@ [b]Warning:[/b] This function is primarily intended for editor usage. For in-game use cases, prefer physics collision. </description> </method> + <method name="is_on_render_thread"> + <return type="bool" /> + <description> + Returns [code]true[/code] if our code is currently executing on the rendering thread. + </description> + </method> <method name="light_directional_set_blend_splits"> <return type="void" /> <param index="0" name="light" type="RID" /> diff --git a/doc/classes/ResourceImporterWAV.xml b/doc/classes/ResourceImporterWAV.xml index 5336c98d0f..d3dafb03b6 100644 --- a/doc/classes/ResourceImporterWAV.xml +++ b/doc/classes/ResourceImporterWAV.xml @@ -14,6 +14,7 @@ The compression mode to use on import. [b]Disabled:[/b] Imports audio data without any compression. This results in the highest possible quality. [b]RAM (Ima-ADPCM):[/b] Performs fast lossy compression on import. Low CPU cost, but quality is noticeably decreased compared to Ogg Vorbis or even MP3. + [b]QOA ([url=https://qoaformat.org/]Quite OK Audio[/url]):[/b] Performs lossy compression on import. CPU cost is slightly higher than IMA-ADPCM, but quality is much higher. </member> <member name="edit/loop_begin" type="int" setter="" getter="" default="0"> The begin loop point to use when [member edit/loop_mode] is [b]Forward[/b], [b]Ping-Pong[/b] or [b]Backward[/b]. This is set in seconds after the beginning of the audio file. diff --git a/doc/classes/ScriptEditorBase.xml b/doc/classes/ScriptEditorBase.xml index dca4fe9276..403608355a 100644 --- a/doc/classes/ScriptEditorBase.xml +++ b/doc/classes/ScriptEditorBase.xml @@ -71,6 +71,12 @@ Emitted when the user contextual goto and the item is in the same script. </description> </signal> + <signal name="request_save_previous_state"> + <param index="0" name="line" type="int" /> + <description> + Emitted when the user changes current script or moves caret by 10 or more columns within the same script. + </description> + </signal> <signal name="search_in_files_requested"> <param index="0" name="text" type="String" /> <description> diff --git a/doc/classes/StatusIndicator.xml b/doc/classes/StatusIndicator.xml index e1fcc35ad7..fb156b3c9f 100644 --- a/doc/classes/StatusIndicator.xml +++ b/doc/classes/StatusIndicator.xml @@ -9,9 +9,13 @@ <tutorials> </tutorials> <members> - <member name="icon" type="Image" setter="set_icon" getter="get_icon"> + <member name="icon" type="Texture2D" setter="set_icon" getter="get_icon"> Status indicator icon. </member> + <member name="menu" type="NodePath" setter="set_menu" getter="get_menu" default="NodePath("")"> + Status indicator native popup menu. If this is set, the [signal pressed] signal is not emitted. + [b]Note:[/b] Native popup is only supported if [NativeMenu] supports [constant NativeMenu.FEATURE_POPUP_MENU] feature. + </member> <member name="tooltip" type="String" setter="set_tooltip" getter="get_tooltip" default=""""> Status indicator tooltip. </member> diff --git a/doc/classes/String.xml b/doc/classes/String.xml index a33a1aea41..59733e9696 100644 --- a/doc/classes/String.xml +++ b/doc/classes/String.xml @@ -747,7 +747,7 @@ <method name="reverse" qualifiers="const"> <return type="String" /> <description> - Returns the copy of this string in reverse order. + Returns the copy of this string in reverse order. This operation works on unicode codepoints, rather than sequences of codepoints, and may break things like compound letters or emojis. </description> </method> <method name="rfind" qualifiers="const"> diff --git a/doc/classes/StringName.xml b/doc/classes/StringName.xml index e837b65199..e3c254fb48 100644 --- a/doc/classes/StringName.xml +++ b/doc/classes/StringName.xml @@ -648,7 +648,7 @@ <method name="reverse" qualifiers="const"> <return type="String" /> <description> - Returns the copy of this string in reverse order. + Returns the copy of this string in reverse order. This operation works on unicode codepoints, rather than sequences of codepoints, and may break things like compound letters or emojis. </description> </method> <method name="rfind" qualifiers="const"> diff --git a/doc/classes/TabContainer.xml b/doc/classes/TabContainer.xml index f4d69c3076..090afa0220 100644 --- a/doc/classes/TabContainer.xml +++ b/doc/classes/TabContainer.xml @@ -64,6 +64,13 @@ Returns the [Texture2D] for the tab at index [param tab_idx] or [code]null[/code] if the tab has no [Texture2D]. </description> </method> + <method name="get_tab_icon_max_width" qualifiers="const"> + <return type="int" /> + <param index="0" name="tab_idx" type="int" /> + <description> + Returns the maximum allowed width of the icon for the tab at index [param tab_idx]. + </description> + </method> <method name="get_tab_idx_at_point" qualifiers="const"> <return type="int" /> <param index="0" name="point" type="Vector2" /> @@ -164,6 +171,14 @@ Sets an icon for the tab at index [param tab_idx]. </description> </method> + <method name="set_tab_icon_max_width"> + <return type="void" /> + <param index="0" name="tab_idx" type="int" /> + <param index="1" name="width" type="int" /> + <description> + Sets the maximum allowed width of the icon for the tab at index [param tab_idx]. This limit is applied on top of the default size of the icon and on top of [theme_item icon_max_width]. The height is adjusted according to the icon's ratio. + </description> + </method> <method name="set_tab_metadata"> <return type="void" /> <param index="0" name="tab_idx" type="int" /> diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index db0c1f17b0..2959ec4cfa 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -5,7 +5,7 @@ </brief_description> <description> A multiline text editor. It also has limited facilities for editing code, such as syntax highlighting support. For more advanced facilities for editing code, see [CodeEdit]. - [b]Note:[/b] Most viewport, caret and edit methods contain a [code]caret_index[/code] argument for [member caret_multiple] support. The argument should be one of the following: [code]-1[/code] for all carets, [code]0[/code] for the main caret, or greater than [code]0[/code] for secondary carets. + [b]Note:[/b] Most viewport, caret, and edit methods contain a [code]caret_index[/code] argument for [member caret_multiple] support. The argument should be one of the following: [code]-1[/code] for all carets, [code]0[/code] for the main caret, or greater than [code]0[/code] for secondary carets in the order they were created. [b]Note:[/b] When holding down [kbd]Alt[/kbd], the vertical scroll wheel will scroll 5 times as fast as it would normally do. This also works in the Godot script editor. </description> <tutorials> @@ -58,7 +58,7 @@ <method name="add_caret"> <return type="int" /> <param index="0" name="line" type="int" /> - <param index="1" name="col" type="int" /> + <param index="1" name="column" type="int" /> <description> Adds a new caret at the given location. Returns the index of the new caret, or [code]-1[/code] if the location is invalid. </description> @@ -67,7 +67,7 @@ <return type="void" /> <param index="0" name="below" type="bool" /> <description> - Adds an additional caret above or below every caret. If [param below] is true the new caret will be added below and above otherwise. + Adds an additional caret above or below every caret. If [param below] is [code]true[/code] the new caret will be added below and above otherwise. </description> </method> <method name="add_gutter"> @@ -83,7 +83,7 @@ Adds a selection and a caret for the next occurrence of the current selection. If there is no active selection, selects word under caret. </description> </method> - <method name="adjust_carets_after_edit"> + <method name="adjust_carets_after_edit" deprecated="No longer necessary since methods now adjust carets themselves."> <return type="void" /> <param index="0" name="caret" type="int" /> <param index="1" name="from_line" type="int" /> @@ -91,7 +91,7 @@ <param index="3" name="to_line" type="int" /> <param index="4" name="to_col" type="int" /> <description> - Reposition the carets affected by the edit. This assumes edits are applied in edit order, see [method get_caret_index_edit_order]. + This method does nothing. </description> </method> <method name="adjust_viewport_to_caret"> @@ -120,6 +120,23 @@ Starts a multipart edit. All edits will be treated as one action until [method end_complex_operation] is called. </description> </method> + <method name="begin_multicaret_edit"> + <return type="void" /> + <description> + Starts an edit for multiple carets. The edit must be ended with [method end_multicaret_edit]. Multicaret edits can be used to edit text at multiple carets and delay merging the carets until the end, so the caret indexes aren't affected immediately. [method begin_multicaret_edit] and [method end_multicaret_edit] can be nested, and the merge will happen at the last [method end_multicaret_edit]. + Example usage: + [codeblock] + begin_complex_operation() + begin_multicaret_edit() + for i in range(get_caret_count()): + if multicaret_edit_ignore_caret(i): + continue + # Logic here. + end_multicaret_edit() + end_complex_operation() + [/codeblock] + </description> + </method> <method name="cancel_ime"> <return type="void" /> <description> @@ -145,6 +162,20 @@ Clears the undo history. </description> </method> + <method name="collapse_carets"> + <return type="void" /> + <param index="0" name="from_line" type="int" /> + <param index="1" name="from_column" type="int" /> + <param index="2" name="to_line" type="int" /> + <param index="3" name="to_column" type="int" /> + <param index="4" name="inclusive" type="bool" default="false" /> + <description> + Collapse all carets in the given range to the [param from_line] and [param from_column] position. + [param inclusive] applies to both ends. + If [method is_in_mulitcaret_edit] is [code]true[/code], carets that are collapsed will be [code]true[/code] for [method multicaret_edit_ignore_caret]. + [method merge_overlapping_carets] will be called if any carets were collapsed. + </description> + </method> <method name="copy"> <return type="void" /> <param index="0" name="caret_index" type="int" default="-1" /> @@ -185,6 +216,12 @@ Ends a multipart edit, started with [method begin_complex_operation]. If called outside a complex operation, the current operation is pushed onto the undo/redo stack. </description> </method> + <method name="end_multicaret_edit"> + <return type="void" /> + <description> + Ends an edit for multiple carets, that was started with [method begin_multicaret_edit]. If this was the last [method end_multicaret_edit] and [method merge_overlapping_carets] was called, carets will be merged. + </description> + </method> <method name="get_caret_column" qualifiers="const"> <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> @@ -205,7 +242,7 @@ Returns the caret pixel draw position. </description> </method> - <method name="get_caret_index_edit_order"> + <method name="get_caret_index_edit_order" deprecated="Carets no longer need to be edited in any specific order. If the carets need to be sorted, use [method get_sorted_carets] instead."> <return type="PackedInt32Array" /> <description> Returns a list of caret indexes in their edit order, this done from bottom to top. Edit order refers to the way actions such as [method insert_text_at_caret] are applied. @@ -363,6 +400,15 @@ [b]Note:[/b] The return value is influenced by [theme_item line_spacing] and [theme_item font_size]. And it will not be less than [code]1[/code]. </description> </method> + <method name="get_line_ranges_from_carets" qualifiers="const"> + <return type="Vector2i[]" /> + <param index="0" name="only_selections" type="bool" default="false" /> + <param index="1" name="merge_adjacent" type="bool" default="true" /> + <description> + Returns an [Array] of line ranges where [code]x[/code] is the first line and [code]y[/code] is the last line. All lines within these ranges will have a caret on them or be part of a selection. Each line will only be part of one line range, even if it has multiple carets on it. + If a selection's end column ([method get_selection_to_column]) is at column [code]0[/code], that line will not be included. If a selection begins on the line after another selection ends and [param merge_adjacent] is [code]true[/code], or they begin and end on the same line, one line range will include both selections. + </description> + </method> <method name="get_line_width" qualifiers="const"> <return type="int" /> <param index="0" name="line" type="int" /> @@ -514,7 +560,18 @@ Returns the text inside the selection of a caret, or all the carets if [param caret_index] is its default value [code]-1[/code]. </description> </method> - <method name="get_selection_column" qualifiers="const"> + <method name="get_selection_at_line_column" qualifiers="const"> + <return type="int" /> + <param index="0" name="line" type="int" /> + <param index="1" name="column" type="int" /> + <param index="2" name="include_edges" type="bool" default="true" /> + <param index="3" name="only_selections" type="bool" default="true" /> + <description> + Returns the caret index of the selection at the given [param line] and [param column], or [code]-1[/code] if there is none. + If [param include_edges] is [code]false[/code], the position must be inside the selection and not at either end. If [param only_selections] is [code]false[/code], carets without a selection will also be considered. + </description> + </method> + <method name="get_selection_column" qualifiers="const" deprecated="Use [method get_selection_origin_column] instead."> <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> <description> @@ -525,17 +582,17 @@ <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> <description> - Returns the selection begin column. + Returns the selection begin column. Returns the caret column if there is no selection. </description> </method> <method name="get_selection_from_line" qualifiers="const"> <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> <description> - Returns the selection begin line. + Returns the selection begin line. Returns the caret line if there is no selection. </description> </method> - <method name="get_selection_line" qualifiers="const"> + <method name="get_selection_line" qualifiers="const" deprecated="Use [method get_selection_origin_line] instead."> <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> <description> @@ -548,18 +605,40 @@ Returns the current selection mode. </description> </method> + <method name="get_selection_origin_column" qualifiers="const"> + <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> + <description> + Returns the origin column of the selection. This is the opposite end from the caret. + </description> + </method> + <method name="get_selection_origin_line" qualifiers="const"> + <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> + <description> + Returns the origin line of the selection. This is the opposite end from the caret. + </description> + </method> <method name="get_selection_to_column" qualifiers="const"> <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> <description> - Returns the selection end column. + Returns the selection end column. Returns the caret column if there is no selection. </description> </method> <method name="get_selection_to_line" qualifiers="const"> <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> <description> - Returns the selection end line. + Returns the selection end line. Returns the caret line if there is no selection. + </description> + </method> + <method name="get_sorted_carets" qualifiers="const"> + <return type="PackedInt32Array" /> + <param index="0" name="include_ignored_carets" type="bool" default="false" /> + <description> + Returns the carets sorted by selection beginning from lowest line and column to highest (from top to bottom of text). + If [param include_ignored_carets] is [code]false[/code], carets from [method multicaret_edit_ignore_caret] will be ignored. </description> </method> <method name="get_tab_size" qualifiers="const"> @@ -653,6 +732,19 @@ Inserts a new line with [param text] at [param line]. </description> </method> + <method name="insert_text"> + <return type="void" /> + <param index="0" name="text" type="String" /> + <param index="1" name="line" type="int" /> + <param index="2" name="column" type="int" /> + <param index="3" name="before_selection_begin" type="bool" default="true" /> + <param index="4" name="before_selection_end" type="bool" default="false" /> + <description> + Inserts the [param text] at [param line] and [param column]. + If [param before_selection_begin] is [code]true[/code], carets and selections that begin at [param line] and [param column] will moved to the end of the inserted text, along with all carets after it. + If [param before_selection_end] is [code]true[/code], selections that end at [param line] and [param column] will be extended to the end of the inserted text. These parameters can be used to insert text inside of or outside of selections. + </description> + </method> <method name="insert_text_at_caret"> <return type="void" /> <param index="0" name="text" type="String" /> @@ -661,6 +753,13 @@ Insert the specified text at the caret position. </description> </method> + <method name="is_caret_after_selection_origin" qualifiers="const"> + <return type="bool" /> + <param index="0" name="caret_index" type="int" default="0" /> + <description> + Returns [code]true[/code] if the caret of the selection is after the selection origin. This can be used to determine the direction of the selection. + </description> + </method> <method name="is_caret_visible" qualifiers="const"> <return type="bool" /> <param index="0" name="caret_index" type="int" default="0" /> @@ -671,7 +770,7 @@ <method name="is_dragging_cursor" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the user is dragging their mouse for scrolling or selecting. + Returns [code]true[/code] if the user is dragging their mouse for scrolling, selecting, or text dragging. </description> </method> <method name="is_gutter_clickable" qualifiers="const"> @@ -695,6 +794,12 @@ Returns whether the gutter is overwritable. </description> </method> + <method name="is_in_mulitcaret_edit" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if a [method begin_multicaret_edit] has been called and [method end_multicaret_edit] has not yet been called. + </description> + </method> <method name="is_line_gutter_clickable" qualifiers="const"> <return type="bool" /> <param index="0" name="line" type="int" /> @@ -749,9 +854,18 @@ <return type="void" /> <description> Merges any overlapping carets. Will favor the newest caret, or the caret with a selection. + If [method is_in_mulitcaret_edit] is [code]true[/code], the merge will be queued to happen at the end of the multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit]. [b]Note:[/b] This is not called when a caret changes position but after certain actions, so it is possible to get into a state where carets overlap. </description> </method> + <method name="multicaret_edit_ignore_caret" qualifiers="const"> + <return type="bool" /> + <param index="0" name="caret_index" type="int" /> + <description> + Returns [code]true[/code] if the given [param caret_index] should be ignored as part of a multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit]. Carets that should be ignored are ones that were part of removed text and will likely be merged at the end of the edit, or carets that were added during the edit. + It is recommended to [code]continue[/code] within a loop iterating on multiple carets if a caret should be ignored. + </description> + </method> <method name="paste"> <return type="void" /> <param index="0" name="caret_index" type="int" default="-1" /> @@ -787,6 +901,15 @@ Removes the gutter from this [TextEdit]. </description> </method> + <method name="remove_line_at"> + <return type="void" /> + <param index="0" name="line" type="int" /> + <param index="1" name="move_carets_down" type="bool" default="true" /> + <description> + Removes the line of text at [param line]. Carets on this line will attempt to match their previous visual x position. + If [param move_carets_down] is [code]true[/code] carets will move to the next line down, otherwise carets will move up. + </description> + </method> <method name="remove_secondary_carets"> <return type="void" /> <description> @@ -801,7 +924,6 @@ <param index="3" name="to_column" type="int" /> <description> Removes text between the given positions. - [b]Note:[/b] This does not adjust the caret or selection, which as a result it can end up in an invalid position. </description> </method> <method name="search" qualifiers="const"> @@ -809,7 +931,7 @@ <param index="0" name="text" type="String" /> <param index="1" name="flags" type="int" /> <param index="2" name="from_line" type="int" /> - <param index="3" name="from_colum" type="int" /> + <param index="3" name="from_column" type="int" /> <description> Perform a search inside the text. Search flags can be specified in the [enum SearchFlags] enum. In the returned vector, [code]x[/code] is the column, [code]y[/code] is the line. If no results are found, both are equal to [code]-1[/code]. @@ -835,14 +957,15 @@ </method> <method name="select"> <return type="void" /> - <param index="0" name="from_line" type="int" /> - <param index="1" name="from_column" type="int" /> - <param index="2" name="to_line" type="int" /> - <param index="3" name="to_column" type="int" /> + <param index="0" name="origin_line" type="int" /> + <param index="1" name="origin_column" type="int" /> + <param index="2" name="caret_line" type="int" /> + <param index="3" name="caret_column" type="int" /> <param index="4" name="caret_index" type="int" default="0" /> <description> - Perform selection, from line/column to line/column. + Selects text from [param origin_line] and [param origin_column] to [param caret_line] and [param caret_column] for the given [param caret_index]. This moves the selection origin and the caret. If the positions are the same, the selection will be deselected. If [member selecting_enabled] is [code]false[/code], no selection will occur. + [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. </description> </method> <method name="select_all"> @@ -878,9 +1001,10 @@ <param index="3" name="wrap_index" type="int" default="0" /> <param index="4" name="caret_index" type="int" default="0" /> <description> - Moves the caret to the specified [param line] index. + Moves the caret to the specified [param line] index. The caret column will be moved to the same visual position it was at the last time [method set_caret_column] was called, or clamped to the end of the line. If [param adjust_viewport] is [code]true[/code], the viewport will center at the caret position after the move occurs. If [param can_be_hidden] is [code]true[/code], the specified [param line] can be hidden. + If [param wrap_index] is [code]-1[/code], the caret column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_caret_column] was called. [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. </description> </method> @@ -945,7 +1069,8 @@ <param index="0" name="line" type="int" /> <param index="1" name="new_text" type="String" /> <description> - Sets the text for a specific line. + Sets the text for a specific [param line]. + Carets on the line will attempt to keep their visual x position. </description> </method> <method name="set_line_as_center_visible"> @@ -1049,13 +1174,30 @@ <method name="set_selection_mode"> <return type="void" /> <param index="0" name="mode" type="int" enum="TextEdit.SelectionMode" /> - <param index="1" name="line" type="int" default="-1" /> - <param index="2" name="column" type="int" default="-1" /> - <param index="3" name="caret_index" type="int" default="0" /> <description> Sets the current selection mode. </description> </method> + <method name="set_selection_origin_column"> + <return type="void" /> + <param index="0" name="column" type="int" /> + <param index="1" name="caret_index" type="int" default="0" /> + <description> + Sets the selection origin column to the [param column] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect. + </description> + </method> + <method name="set_selection_origin_line"> + <return type="void" /> + <param index="0" name="line" type="int" /> + <param index="1" name="can_be_hidden" type="bool" default="true" /> + <param index="2" name="wrap_index" type="int" default="-1" /> + <param index="3" name="caret_index" type="int" default="0" /> + <description> + Sets the selection origin line to the [param line] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect. + If [param can_be_hidden] is [code]false[/code], The line will be set to the nearest unhidden line below or above. + If [param wrap_index] is [code]-1[/code], the selection origin column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_selection_origin_column] or [method select] was called. + </description> + </method> <method name="set_tab_size"> <return type="void" /> <param index="0" name="size" type="int" /> @@ -1089,7 +1231,7 @@ <param index="0" name="from_line" type="int" /> <param index="1" name="to_line" type="int" /> <description> - Swaps the two lines. + Swaps the two lines. Carets will be swapped with the lines. </description> </method> <method name="tag_saved_version"> @@ -1140,7 +1282,7 @@ If [code]true[/code], the selected text will be deselected when focus is lost. </member> <member name="drag_and_drop_selection_enabled" type="bool" setter="set_drag_and_drop_selection_enabled" getter="is_drag_and_drop_selection_enabled" default="true"> - If [code]true[/code], allow drag and drop of selected text. + If [code]true[/code], allow drag and drop of selected text. Text can still be dropped from other sources. </member> <member name="draw_control_chars" type="bool" setter="set_draw_control_chars" getter="get_draw_control_chars" default="false"> If [code]true[/code], control characters are displayed. @@ -1231,7 +1373,7 @@ <signals> <signal name="caret_changed"> <description> - Emitted when the caret changes position. + Emitted when any caret changes position. </description> </signal> <signal name="gutter_added"> diff --git a/doc/classes/TileSetAtlasSource.xml b/doc/classes/TileSetAtlasSource.xml index 6f212274f8..a34ca0ce91 100644 --- a/doc/classes/TileSetAtlasSource.xml +++ b/doc/classes/TileSetAtlasSource.xml @@ -295,6 +295,15 @@ # If tile is not already flipped, flip it. $TileMap.set_cell(0, Vector2i(2, 2), source_id, atlas_coords, alternate_id | TileSetAtlasSource.TRANSFORM_FLIP_H) [/codeblock] + [b]Note:[/b] These transformations can be combined to do the equivalent of 0, 90, 180, and 270 degree rotations, as shown below: + [codeblock] + enum TileTransform { + ROTATE_0 = 0, + ROTATE_90 = TileSetAtlasSource.TRANSFORM_TRANSPOSE | TileSetAtlasSource.TRANSFORM_FLIP_H, + ROTATE_180 = TileSetAtlasSource.TRANSFORM_FLIP_H | TileSetAtlasSource.TRANSFORM_FLIP_V, + ROTATE_270 = TileSetAtlasSource.TRANSFORM_TRANSPOSE | TileSetAtlasSource.TRANSFORM_FLIP_V, + } + [/codeblock] </constant> <constant name="TRANSFORM_FLIP_V" value="8192"> Represents cell's vertical flip flag. See [constant TRANSFORM_FLIP_H] for usage. diff --git a/doc/classes/Variant.xml b/doc/classes/Variant.xml index eb837a4643..b420933285 100644 --- a/doc/classes/Variant.xml +++ b/doc/classes/Variant.xml @@ -42,8 +42,8 @@ # Note that Objects are their own special category. # To get the name of the underlying Object type, you need the `get_class()` method. print("foo is a(n) %s" % foo.get_class()) # inject the class name into a formatted string. - # Note also that there is not yet any way to get a script's `class_name` string easily. - # To fetch that value, you can use ProjectSettings.get_global_class_list(). + # Note that this does not get the script's `class_name` global identifier. + # If the `class_name` is needed, use `foo.get_script().get_global_name()` instead. [/gdscript] [csharp] Variant foo = 2; diff --git a/doc/classes/Vector2.xml b/doc/classes/Vector2.xml index 7b166a4fb0..961a60060a 100644 --- a/doc/classes/Vector2.xml +++ b/doc/classes/Vector2.xml @@ -128,6 +128,14 @@ Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component. </description> </method> + <method name="clampf" qualifiers="const"> + <return type="Vector2" /> + <param index="0" name="min" type="float" /> + <param index="1" name="max" type="float" /> + <description> + Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component. + </description> + </method> <method name="cross" qualifiers="const"> <return type="float" /> <param index="0" name="with" type="Vector2" /> @@ -371,6 +379,13 @@ Returns a new vector with each component snapped to the nearest multiple of the corresponding component in [param step]. This can also be used to round the components to an arbitrary number of decimals. </description> </method> + <method name="snappedf" qualifiers="const"> + <return type="Vector2" /> + <param index="0" name="step" type="float" /> + <description> + Returns a new vector with each component snapped to the nearest multiple of [param step]. This can also be used to round the components to an arbitrary number of decimals. + </description> + </method> </methods> <members> <member name="x" type="float" setter="" getter="" default="0.0"> diff --git a/doc/classes/Vector2i.xml b/doc/classes/Vector2i.xml index 18291e06a9..db848e3186 100644 --- a/doc/classes/Vector2i.xml +++ b/doc/classes/Vector2i.xml @@ -64,6 +64,14 @@ Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component. </description> </method> + <method name="clampi" qualifiers="const"> + <return type="Vector2i" /> + <param index="0" name="min" type="int" /> + <param index="1" name="max" type="int" /> + <description> + Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component. + </description> + </method> <method name="distance_squared_to" qualifiers="const"> <return type="int" /> <param index="0" name="to" type="Vector2i" /> @@ -117,6 +125,13 @@ Returns a new vector with each component snapped to the closest multiple of the corresponding component in [param step]. </description> </method> + <method name="snappedi" qualifiers="const"> + <return type="Vector2i" /> + <param index="0" name="step" type="int" /> + <description> + Returns a new vector with each component snapped to the closest multiple of [param step]. + </description> + </method> </methods> <members> <member name="x" type="int" setter="" getter="" default="0"> diff --git a/doc/classes/Vector3.xml b/doc/classes/Vector3.xml index 031d91af78..a98ed479d4 100644 --- a/doc/classes/Vector3.xml +++ b/doc/classes/Vector3.xml @@ -104,6 +104,14 @@ Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component. </description> </method> + <method name="clampf" qualifiers="const"> + <return type="Vector3" /> + <param index="0" name="min" type="float" /> + <param index="1" name="max" type="float" /> + <description> + Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component. + </description> + </method> <method name="cross" qualifiers="const"> <return type="Vector3" /> <param index="0" name="with" type="Vector3" /> @@ -307,10 +315,10 @@ </method> <method name="reflect" qualifiers="const"> <return type="Vector3" /> - <param index="0" name="direction" type="Vector3" /> + <param index="0" name="n" type="Vector3" /> <description> - Returns the result of reflecting the vector from a plane defined by the given direction vector [param direction]. - [b]Note:[/b] [method reflect] differs from what other engines and frameworks call [code skip-lint]reflect()[/code]. In other engines, [code skip-lint]reflect()[/code] takes a normal direction which is a direction perpendicular to the plane. In Godot, you specify a direction parallel to the plane. See also [method bounce] which does what most engines call [code skip-lint]reflect()[/code]. + Returns the result of reflecting the vector through a plane defined by the given normal vector [param n]. + [b]Note:[/b] [method reflect] differs from what other engines and frameworks call [code skip-lint]reflect()[/code]. In other engines, [code skip-lint]reflect()[/code] returns the result of the vector reflected by the given plane. The reflection thus passes through the given normal. While in Godot the reflection passes through the plane and can be thought of as bouncing off the normal. See also [method bounce] which does what most engines call [code skip-lint]reflect()[/code]. </description> </method> <method name="rotated" qualifiers="const"> @@ -365,6 +373,13 @@ Returns a new vector with each component snapped to the nearest multiple of the corresponding component in [param step]. This can also be used to round the components to an arbitrary number of decimals. </description> </method> + <method name="snappedf" qualifiers="const"> + <return type="Vector3" /> + <param index="0" name="step" type="float" /> + <description> + Returns a new vector with each component snapped to the nearest multiple of [param step]. This can also be used to round the components to an arbitrary number of decimals. + </description> + </method> </methods> <members> <member name="x" type="float" setter="" getter="" default="0.0"> diff --git a/doc/classes/Vector3i.xml b/doc/classes/Vector3i.xml index ffebd3e1f3..8fc0309685 100644 --- a/doc/classes/Vector3i.xml +++ b/doc/classes/Vector3i.xml @@ -59,6 +59,14 @@ Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component. </description> </method> + <method name="clampi" qualifiers="const"> + <return type="Vector3i" /> + <param index="0" name="min" type="int" /> + <param index="1" name="max" type="int" /> + <description> + Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component. + </description> + </method> <method name="distance_squared_to" qualifiers="const"> <return type="int" /> <param index="0" name="to" type="Vector3i" /> @@ -112,6 +120,13 @@ Returns a new vector with each component snapped to the closest multiple of the corresponding component in [param step]. </description> </method> + <method name="snappedi" qualifiers="const"> + <return type="Vector3i" /> + <param index="0" name="step" type="int" /> + <description> + Returns a new vector with each component snapped to the closest multiple of [param step]. + </description> + </method> </methods> <members> <member name="x" type="int" setter="" getter="" default="0"> diff --git a/doc/classes/Vector4.xml b/doc/classes/Vector4.xml index b31cdb01c9..49cf726086 100644 --- a/doc/classes/Vector4.xml +++ b/doc/classes/Vector4.xml @@ -64,6 +64,14 @@ Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component. </description> </method> + <method name="clampf" qualifiers="const"> + <return type="Vector4" /> + <param index="0" name="min" type="float" /> + <param index="1" name="max" type="float" /> + <description> + Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component. + </description> + </method> <method name="cubic_interpolate" qualifiers="const"> <return type="Vector4" /> <param index="0" name="b" type="Vector4" /> @@ -228,6 +236,13 @@ Returns a new vector with each component snapped to the nearest multiple of the corresponding component in [param step]. This can also be used to round the components to an arbitrary number of decimals. </description> </method> + <method name="snappedf" qualifiers="const"> + <return type="Vector4" /> + <param index="0" name="step" type="float" /> + <description> + Returns a new vector with each component snapped to the nearest multiple of [param step]. This can also be used to round the components to an arbitrary number of decimals. + </description> + </method> </methods> <members> <member name="w" type="float" setter="" getter="" default="0.0"> diff --git a/doc/classes/Vector4i.xml b/doc/classes/Vector4i.xml index f8a0026066..ade558dd06 100644 --- a/doc/classes/Vector4i.xml +++ b/doc/classes/Vector4i.xml @@ -57,6 +57,14 @@ Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component. </description> </method> + <method name="clampi" qualifiers="const"> + <return type="Vector4i" /> + <param index="0" name="min" type="int" /> + <param index="1" name="max" type="int" /> + <description> + Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component. + </description> + </method> <method name="distance_squared_to" qualifiers="const"> <return type="int" /> <param index="0" name="to" type="Vector4i" /> @@ -110,6 +118,13 @@ Returns a new vector with each component snapped to the closest multiple of the corresponding component in [param step]. </description> </method> + <method name="snappedi" qualifiers="const"> + <return type="Vector4i" /> + <param index="0" name="step" type="int" /> + <description> + Returns a new vector with each component snapped to the closest multiple of [param step]. + </description> + </method> </methods> <members> <member name="w" type="int" setter="" getter="" default="0"> diff --git a/doc/classes/XRHandTracker.xml b/doc/classes/XRHandTracker.xml index 69390df696..636af6625b 100644 --- a/doc/classes/XRHandTracker.xml +++ b/doc/classes/XRHandTracker.xml @@ -11,12 +11,6 @@ <link title="XR documentation index">$DOCS_URL/tutorials/xr/index.html</link> </tutorials> <methods> - <method name="get_hand" qualifiers="const"> - <return type="int" enum="XRHandTracker.Hand" /> - <description> - Returns the type of hand. - </description> - </method> <method name="get_hand_joint_angular_velocity" qualifiers="const"> <return type="Vector3" /> <param index="0" name="joint" type="int" enum="XRHandTracker.HandJoint" /> @@ -52,13 +46,6 @@ Returns the transform for the given hand joint. </description> </method> - <method name="set_hand"> - <return type="void" /> - <param index="0" name="hand" type="int" enum="XRHandTracker.Hand" /> - <description> - Sets the type of hand. - </description> - </method> <method name="set_hand_joint_angular_velocity"> <return type="void" /> <param index="0" name="joint" type="int" enum="XRHandTracker.HandJoint" /> @@ -101,6 +88,7 @@ </method> </methods> <members> + <member name="hand" type="int" setter="set_tracker_hand" getter="get_tracker_hand" overrides="XRPositionalTracker" enum="XRPositionalTracker.TrackerHand" default="1" /> <member name="hand_tracking_source" type="int" setter="set_hand_tracking_source" getter="get_hand_tracking_source" enum="XRHandTracker.HandTrackingSource" default="0"> The source of the hand tracking data. </member> @@ -110,15 +98,6 @@ <member name="type" type="int" setter="set_tracker_type" getter="get_tracker_type" overrides="XRTracker" enum="XRServer.TrackerType" default="16" /> </members> <constants> - <constant name="HAND_LEFT" value="0" enum="Hand"> - A left hand. - </constant> - <constant name="HAND_RIGHT" value="1" enum="Hand"> - A right hand. - </constant> - <constant name="HAND_MAX" value="2" enum="Hand"> - Represents the size of the [enum Hand] enum. - </constant> <constant name="HAND_TRACKING_SOURCE_UNKNOWN" value="0" enum="HandTrackingSource"> The source of hand tracking data is unknown. </constant> diff --git a/doc/classes/XRServer.xml b/doc/classes/XRServer.xml index d5714980c3..4179ba821c 100644 --- a/doc/classes/XRServer.xml +++ b/doc/classes/XRServer.xml @@ -37,8 +37,8 @@ You should call this method after a few seconds have passed. For example, when the user requests a realignment of the display holding a designated button on a controller for a short period of time, or when implementing a teleport mechanism. </description> </method> - <method name="clear_reference_frame" qualifiers="const"> - <return type="Transform3D" /> + <method name="clear_reference_frame"> + <return type="void" /> <description> Clears the reference frame that was set by previous calls to [method center_on_hmd]. </description> diff --git a/drivers/SCsub b/drivers/SCsub index 9c8b16d3e5..e77b96cc87 100644 --- a/drivers/SCsub +++ b/drivers/SCsub @@ -14,6 +14,8 @@ SConscript("coreaudio/SCsub") SConscript("pulseaudio/SCsub") if env["platform"] == "windows": SConscript("wasapi/SCsub") + if not env.msvc: + SConscript("backtrace/SCsub") if env["xaudio2"]: SConscript("xaudio2/SCsub") diff --git a/drivers/backtrace/SCsub b/drivers/backtrace/SCsub new file mode 100644 index 0000000000..f61fb21581 --- /dev/null +++ b/drivers/backtrace/SCsub @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +Import("env") + +env_backtrace = env.Clone() + +# Thirdparty source files + +thirdparty_obj = [] + +thirdparty_dir = "#thirdparty/libbacktrace/" +thirdparty_sources = [ + "atomic.c", + "dwarf.c", + "fileline.c", + "posix.c", + "print.c", + "sort.c", + "state.c", + "backtrace.c", + "simple.c", + "pecoff.c", + "read.c", + "alloc.c", +] +thirdparty_sources = [thirdparty_dir + file for file in thirdparty_sources] + +env_backtrace.Prepend(CPPPATH=[thirdparty_dir]) + +env_thirdparty = env_backtrace.Clone() +env_thirdparty.disable_warnings() +env_thirdparty.add_source_files(thirdparty_obj, thirdparty_sources) + +env.drivers_sources += thirdparty_obj + +# Godot source files + +driver_obj = [] + +env_backtrace.add_source_files(driver_obj, "*.cpp") +env.drivers_sources += driver_obj + +# Needed to force rebuilding the driver files when the thirdparty library is updated. +env.Depends(driver_obj, thirdparty_obj) diff --git a/drivers/gles3/effects/copy_effects.cpp b/drivers/gles3/effects/copy_effects.cpp index 6e64652982..47ca832bd7 100644 --- a/drivers/gles3/effects/copy_effects.cpp +++ b/drivers/gles3/effects/copy_effects.cpp @@ -198,8 +198,7 @@ void CopyEffects::bilinear_blur(GLuint p_source_texture, int p_mipmap_count, con for (int i = 1; i < p_mipmap_count; i++) { dest_region.position.x >>= 1; dest_region.position.y >>= 1; - dest_region.size.x = MAX(1, dest_region.size.x >> 1); - dest_region.size.y = MAX(1, dest_region.size.y >> 1); + dest_region.size = Size2i(dest_region.size.x >> 1, dest_region.size.y >> 1).maxi(1); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffers[i % 2]); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, p_source_texture, i); glBlitFramebuffer(source_region.position.x, source_region.position.y, source_region.position.x + source_region.size.x, source_region.position.y + source_region.size.y, @@ -238,8 +237,7 @@ void CopyEffects::gaussian_blur(GLuint p_source_texture, int p_mipmap_count, con for (int i = 1; i < p_mipmap_count; i++) { dest_region.position.x >>= 1; dest_region.position.y >>= 1; - dest_region.size.x = MAX(1, dest_region.size.x >> 1); - dest_region.size.y = MAX(1, dest_region.size.y >> 1); + dest_region.size = Size2i(dest_region.size.x >> 1, dest_region.size.y >> 1).maxi(1); base_size.x >>= 1; base_size.y >>= 1; diff --git a/drivers/gles3/effects/post_effects.cpp b/drivers/gles3/effects/post_effects.cpp index 8ad872f319..105c8f6b71 100644 --- a/drivers/gles3/effects/post_effects.cpp +++ b/drivers/gles3/effects/post_effects.cpp @@ -87,7 +87,7 @@ void PostEffects::_draw_screen_triangle() { glBindVertexArray(0); } -void PostEffects::post_copy(GLuint p_dest_framebuffer, Size2i p_dest_size, GLuint p_source_color, Size2i p_source_size, float p_luminance_multiplier, const Glow::GLOWLEVEL *p_glow_buffers, float p_glow_intensity, uint32_t p_view, bool p_use_multiview) { +void PostEffects::post_copy(GLuint p_dest_framebuffer, Size2i p_dest_size, GLuint p_source_color, Size2i p_source_size, float p_luminance_multiplier, const Glow::GLOWLEVEL *p_glow_buffers, float p_glow_intensity, uint32_t p_view, bool p_use_multiview, uint64_t p_spec_constants) { glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); glDisable(GL_BLEND); @@ -96,7 +96,7 @@ void PostEffects::post_copy(GLuint p_dest_framebuffer, Size2i p_dest_size, GLuin glViewport(0, 0, p_dest_size.x, p_dest_size.y); PostShaderGLES3::ShaderVariant mode = PostShaderGLES3::MODE_DEFAULT; - uint64_t flags = 0; + uint64_t flags = p_spec_constants; if (p_use_multiview) { flags |= PostShaderGLES3::USE_MULTIVIEW; } diff --git a/drivers/gles3/effects/post_effects.h b/drivers/gles3/effects/post_effects.h index b90c77d6c7..916d29a052 100644 --- a/drivers/gles3/effects/post_effects.h +++ b/drivers/gles3/effects/post_effects.h @@ -59,7 +59,7 @@ public: PostEffects(); ~PostEffects(); - void post_copy(GLuint p_dest_framebuffer, Size2i p_dest_size, GLuint p_source_color, Size2i p_source_size, float p_luminance_multiplier, const Glow::GLOWLEVEL *p_glow_buffers, float p_glow_intensity, uint32_t p_view = 0, bool p_use_multiview = false); + void post_copy(GLuint p_dest_framebuffer, Size2i p_dest_size, GLuint p_source_color, Size2i p_source_size, float p_luminance_multiplier, const Glow::GLOWLEVEL *p_glow_buffers, float p_glow_intensity, uint32_t p_view = 0, bool p_use_multiview = false, uint64_t p_spec_constants = 0); }; } //namespace GLES3 diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp index bc1af86938..ee770be3da 100644 --- a/drivers/gles3/rasterizer_scene_gles3.cpp +++ b/drivers/gles3/rasterizer_scene_gles3.cpp @@ -788,7 +788,6 @@ void RasterizerSceneGLES3::_draw_sky(RID p_env, const Projection &p_projection, } if (!p_apply_color_adjustments_in_post) { spec_constants |= SkyShaderGLES3::APPLY_TONEMAPPING; - // TODO add BCS and color corrections once supported. } RS::EnvironmentBG background = environment_get_background(p_env); @@ -2336,9 +2335,18 @@ void RasterizerSceneGLES3::render_scene(const Ref<RenderSceneBuffers> &p_render_ SceneState::TonemapUBO tonemap_ubo; if (render_data.environment.is_valid()) { + bool use_bcs = environment_get_adjustments_enabled(render_data.environment); + if (use_bcs) { + apply_color_adjustments_in_post = true; + } + tonemap_ubo.exposure = environment_get_exposure(render_data.environment); tonemap_ubo.white = environment_get_white(render_data.environment); tonemap_ubo.tonemapper = int32_t(environment_get_tone_mapper(render_data.environment)); + + tonemap_ubo.brightness = environment_get_adjustments_brightness(render_data.environment); + tonemap_ubo.contrast = environment_get_adjustments_contrast(render_data.environment); + tonemap_ubo.saturation = environment_get_adjustments_saturation(render_data.environment); } if (scene_state.tonemap_buffer == 0) { @@ -2558,8 +2566,6 @@ void RasterizerSceneGLES3::render_scene(const Ref<RenderSceneBuffers> &p_render_ if (!apply_color_adjustments_in_post) { spec_constant_base_flags |= SceneShaderGLES3::APPLY_TONEMAPPING; - - // TODO add BCS and Color corrections here once supported. } } // Render Opaque Objects. @@ -2700,6 +2706,29 @@ void RasterizerSceneGLES3::_render_post_processing(const RenderDataGLES3 *p_rend rb->check_glow_buffers(); } + bool use_bcs = environment_get_adjustments_enabled(p_render_data->environment); + uint64_t bcs_spec_constants = 0; + RID color_correction_texture = environment_get_color_correction(p_render_data->environment); + if (use_bcs && color_correction_texture.is_valid()) { + bcs_spec_constants |= PostShaderGLES3::USE_BCS; + bcs_spec_constants |= PostShaderGLES3::USE_COLOR_CORRECTION; + + bool use_1d_lut = environment_get_use_1d_color_correction(p_render_data->environment); + GLenum texture_target = GL_TEXTURE_3D; + if (use_1d_lut) { + bcs_spec_constants |= PostShaderGLES3::USE_1D_LUT; + texture_target = GL_TEXTURE_2D; + } + + glActiveTexture(GL_TEXTURE2); + glBindTexture(texture_target, texture_storage->texture_get_texid(color_correction_texture)); + glTexParameteri(texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(texture_target, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + } + if (view_count == 1) { // Resolve if needed. if (fbo_msaa_3d != 0 && msaa3d_needs_resolve) { @@ -2735,7 +2764,7 @@ void RasterizerSceneGLES3::_render_post_processing(const RenderDataGLES3 *p_rend } // Copy color buffer - post_effects->post_copy(fbo_rt, target_size, color, internal_size, p_render_data->luminance_multiplier, glow_buffers, glow_intensity); + post_effects->post_copy(fbo_rt, target_size, color, internal_size, p_render_data->luminance_multiplier, glow_buffers, glow_intensity, 0, false, bcs_spec_constants); // Copy depth buffer glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo_int); @@ -2803,7 +2832,7 @@ void RasterizerSceneGLES3::_render_post_processing(const RenderDataGLES3 *p_rend glBindFramebuffer(GL_FRAMEBUFFER, fbos[2]); glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, write_color, 0, v); - post_effects->post_copy(fbos[2], target_size, source_color, internal_size, p_render_data->luminance_multiplier, glow_buffers, glow_intensity, v, true); + post_effects->post_copy(fbos[2], target_size, source_color, internal_size, p_render_data->luminance_multiplier, glow_buffers, glow_intensity, v, true, bcs_spec_constants); } // Copy depth @@ -2824,6 +2853,9 @@ void RasterizerSceneGLES3::_render_post_processing(const RenderDataGLES3 *p_rend glBindFramebuffer(GL_FRAMEBUFFER, fbo_rt); glDeleteFramebuffers(3, fbos); } + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, 0); } template <PassMode p_pass_mode> @@ -3009,6 +3041,11 @@ void RasterizerSceneGLES3::_render_list_template(RenderListParameters *p_params, } } break; + case GLES3::SceneShaderData::BLEND_MODE_PREMULT_ALPHA: { + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + } break; case GLES3::SceneShaderData::BLEND_MODE_ALPHA_TO_COVERAGE: { // Do nothing for now. } break; diff --git a/drivers/gles3/rasterizer_scene_gles3.h b/drivers/gles3/rasterizer_scene_gles3.h index cc479bd4e9..c656ee3cc7 100644 --- a/drivers/gles3/rasterizer_scene_gles3.h +++ b/drivers/gles3/rasterizer_scene_gles3.h @@ -434,6 +434,11 @@ private: float white = 1.0; int32_t tonemapper = 0; int32_t pad = 0; + + int32_t pad2 = 0; + float brightness = 1.0; + float contrast = 1.0; + float saturation = 1.0; }; static_assert(sizeof(TonemapUBO) % 16 == 0, "Tonemap UBO size must be a multiple of 16 bytes"); diff --git a/drivers/gles3/shaders/canvas.glsl b/drivers/gles3/shaders/canvas.glsl index efddbe9ad2..65332c06be 100644 --- a/drivers/gles3/shaders/canvas.glsl +++ b/drivers/gles3/shaders/canvas.glsl @@ -776,6 +776,12 @@ void main() { vec2 tex_uv = (vec4(vertex, 0.0, 1.0) * mat4(light_array[light_base].texture_matrix[0], light_array[light_base].texture_matrix[1], vec4(0.0, 0.0, 1.0, 0.0), vec4(0.0, 0.0, 0.0, 1.0))).xy; //multiply inverse given its transposed. Optimizer removes useless operations. vec2 tex_uv_atlas = tex_uv * light_array[light_base].atlas_rect.zw + light_array[light_base].atlas_rect.xy; + + if (any(lessThan(tex_uv, vec2(0.0, 0.0))) || any(greaterThanEqual(tex_uv, vec2(1.0, 1.0)))) { + //if outside the light texture, light color is zero + continue; + } + vec4 light_color = textureLod(atlas_texture, tex_uv_atlas, 0.0); vec4 light_base_color = light_array[light_base].color; @@ -800,10 +806,6 @@ void main() { light_color.rgb *= base_color.rgb; } #endif - if (any(lessThan(tex_uv, vec2(0.0, 0.0))) || any(greaterThanEqual(tex_uv, vec2(1.0, 1.0)))) { - //if outside the light texture, light color is zero - light_color.a = 0.0; - } if (bool(light_array[light_base].flags & LIGHT_FLAGS_HAS_SHADOW)) { vec2 shadow_pos = (vec4(shadow_vertex, 0.0, 1.0) * mat4(light_array[light_base].shadow_matrix[0], light_array[light_base].shadow_matrix[1], vec4(0.0, 0.0, 1.0, 0.0), vec4(0.0, 0.0, 0.0, 1.0))).xy; //multiply inverse given its transposed. Optimizer removes useless operations. diff --git a/drivers/gles3/shaders/effects/post.glsl b/drivers/gles3/shaders/effects/post.glsl index e61171c92a..1d17510c52 100644 --- a/drivers/gles3/shaders/effects/post.glsl +++ b/drivers/gles3/shaders/effects/post.glsl @@ -1,13 +1,15 @@ /* clang-format off */ #[modes] -mode_default = #define MODE_DEFAULT -// mode_glow = #define MODE_GLOW +mode_default = #[specializations] USE_MULTIVIEW = false USE_GLOW = false USE_LUMINANCE_MULTIPLIER = false +USE_BCS = false +USE_COLOR_CORRECTION = false +USE_1D_LUT = false #[vertex] layout(location = 0) in vec2 vertex_attrib; @@ -25,6 +27,9 @@ void main() { #[fragment] /* clang-format on */ +// If we reach this code, we always tonemap. +#define APPLY_TONEMAPPING + #include "../tonemap_inc.glsl" #ifdef USE_MULTIVIEW @@ -57,6 +62,35 @@ vec4 get_glow_color(vec2 uv) { } #endif // USE_GLOW +#ifdef USE_COLOR_CORRECTION +#ifdef USE_1D_LUT +uniform sampler2D source_color_correction; //texunit:2 + +vec3 apply_color_correction(vec3 color) { + color.r = texture(source_color_correction, vec2(color.r, 0.0f)).r; + color.g = texture(source_color_correction, vec2(color.g, 0.0f)).g; + color.b = texture(source_color_correction, vec2(color.b, 0.0f)).b; + return color; +} +#else +uniform sampler3D source_color_correction; //texunit:2 + +vec3 apply_color_correction(vec3 color) { + return textureLod(source_color_correction, color, 0.0).rgb; +} +#endif // USE_1D_LUT +#endif // USE_COLOR_CORRECTION + +#ifdef USE_BCS +vec3 apply_bcs(vec3 color) { + color = mix(vec3(0.0), color, brightness); + color = mix(vec3(0.5), color, contrast); + color = mix(vec3(dot(vec3(1.0), color) * 0.33333), color, saturation); + + return color; +} +#endif + in vec2 uv_interp; layout(location = 0) out vec4 frag_color; @@ -85,11 +119,11 @@ void main() { color.rgb = linear_to_srgb(color.rgb); #ifdef USE_BCS - color.rgb = apply_bcs(color.rgb, bcs); + color.rgb = apply_bcs(color.rgb); #endif #ifdef USE_COLOR_CORRECTION - color.rgb = apply_color_correction(color.rgb, color_correction); + color.rgb = apply_color_correction(color.rgb); #endif frag_color = color; diff --git a/drivers/gles3/shaders/scene.glsl b/drivers/gles3/shaders/scene.glsl index 1973eb56c2..2b372cb88d 100644 --- a/drivers/gles3/shaders/scene.glsl +++ b/drivers/gles3/shaders/scene.glsl @@ -1442,6 +1442,9 @@ void main() { float clearcoat_roughness = 0.0; float anisotropy = 0.0; vec2 anisotropy_flow = vec2(1.0, 0.0); +#ifdef PREMUL_ALPHA_USED + float premul_alpha = 1.0; +#endif // PREMUL_ALPHA_USED #ifndef FOG_DISABLED vec4 fog = vec4(0.0); #endif // !FOG_DISABLED @@ -1535,6 +1538,7 @@ void main() { if (alpha < alpha_scissor_threshold) { discard; } + alpha = 1.0; #else #ifdef MODE_RENDER_DEPTH #ifdef USE_OPAQUE_PREPASS @@ -1858,23 +1862,16 @@ void main() { #endif // !MODE_RENDER_DEPTH #if defined(USE_SHADOW_TO_OPACITY) +#ifndef MODE_RENDER_DEPTH alpha = min(alpha, clamp(length(ambient_light), 0.0, 1.0)); #if defined(ALPHA_SCISSOR_USED) if (alpha < alpha_scissor) { discard; } -#else -#ifdef MODE_RENDER_DEPTH -#ifdef USE_OPAQUE_PREPASS - - if (alpha < opaque_prepass_threshold) { - discard; - } -#endif // USE_OPAQUE_PREPASS -#endif // MODE_RENDER_DEPTH #endif // !ALPHA_SCISSOR_USED +#endif // !MODE_RENDER_DEPTH #endif // USE_SHADOW_TO_OPACITY #ifdef MODE_RENDER_DEPTH @@ -1934,13 +1931,6 @@ void main() { #endif frag_color.rgb = linear_to_srgb(frag_color.rgb); -#ifdef USE_BCS - frag_color.rgb = apply_bcs(frag_color.rgb, bcs); -#endif - -#ifdef USE_COLOR_CORRECTION - frag_color.rgb = apply_color_correction(frag_color.rgb, color_correction); -#endif #else // !BASE_PASS frag_color = vec4(0.0, 0.0, 0.0, alpha); #endif // !BASE_PASS @@ -2152,19 +2142,14 @@ void main() { #endif additive_light_color = linear_to_srgb(additive_light_color); -#ifdef USE_BCS - additive_light_color = apply_bcs(additive_light_color, bcs); -#endif - -#ifdef USE_COLOR_CORRECTION - additive_light_color = apply_color_correction(additive_light_color, color_correction); -#endif - frag_color.rgb += additive_light_color; #endif // USE_ADDITIVE_LIGHTING - frag_color.rgb *= scene_data.luminance_multiplier; #endif // !RENDER_MATERIAL #endif // !MODE_RENDER_DEPTH + +#ifdef PREMUL_ALPHA_USED + frag_color.rgb *= premul_alpha; +#endif // PREMUL_ALPHA_USED } diff --git a/drivers/gles3/shaders/sky.glsl b/drivers/gles3/shaders/sky.glsl index 6c33bf7123..26549901a6 100644 --- a/drivers/gles3/shaders/sky.glsl +++ b/drivers/gles3/shaders/sky.glsl @@ -209,14 +209,6 @@ void main() { #endif color = linear_to_srgb(color); -#ifdef USE_BCS - color = apply_bcs(color, bcs); -#endif - -#ifdef USE_COLOR_CORRECTION - color = apply_color_correction(color, color_correction); -#endif - frag_color.rgb = color * luminance_multiplier; frag_color.a = alpha; diff --git a/drivers/gles3/shaders/tonemap.glsl b/drivers/gles3/shaders/tonemap.glsl deleted file mode 100644 index 0b769e77f2..0000000000 --- a/drivers/gles3/shaders/tonemap.glsl +++ /dev/null @@ -1,333 +0,0 @@ -/* clang-format off */ -[vertex] - -#ifdef USE_GLES_OVER_GL -#define lowp -#define mediump -#define highp -#else -precision highp float; -precision highp int; -#endif - -layout(location = 0) in vec2 vertex_attrib; -/* clang-format on */ -layout(location = 4) in vec2 uv_in; - -out vec2 uv_interp; - -void main() { - gl_Position = vec4(vertex_attrib, 0.0, 1.0); - - uv_interp = uv_in; -} - -/* clang-format off */ -[fragment] - -#ifdef USE_GLES_OVER_GL -#define lowp -#define mediump -#define highp -#else -#if defined(USE_HIGHP_PRECISION) -precision highp float; -precision highp int; -#else -precision mediump float; -precision mediump int; -#endif -#endif - -in vec2 uv_interp; -/* clang-format on */ - -layout(location = 0) out vec4 frag_color; - -#ifdef USE_MULTIVIEW -uniform highp sampler2DArray source; //texunit:0 -#else -uniform highp sampler2D source; //texunit:0 -#endif - -#if defined(USE_GLOW_LEVEL1) || defined(USE_GLOW_LEVEL2) || defined(USE_GLOW_LEVEL3) || defined(USE_GLOW_LEVEL4) || defined(USE_GLOW_LEVEL5) || defined(USE_GLOW_LEVEL6) || defined(USE_GLOW_LEVEL7) -#define USING_GLOW // only use glow when at least one glow level is selected - -#ifdef USE_MULTI_TEXTURE_GLOW -uniform highp sampler2D source_glow1; //texunit:2 -uniform highp sampler2D source_glow2; //texunit:3 -uniform highp sampler2D source_glow3; //texunit:4 -uniform highp sampler2D source_glow4; //texunit:5 -uniform highp sampler2D source_glow5; //texunit:6 -uniform highp sampler2D source_glow6; //texunit:7 -#ifdef USE_GLOW_LEVEL7 -uniform highp sampler2D source_glow7; //texunit:8 -#endif -#else -uniform highp sampler2D source_glow; //texunit:2 -#endif -uniform highp float glow_intensity; -#endif - -#ifdef USE_BCS -uniform vec3 bcs; -#endif - -#ifdef USE_FXAA -uniform vec2 pixel_size; -#endif - -#ifdef USE_COLOR_CORRECTION -uniform sampler2D color_correction; //texunit:1 -#endif - -#ifdef USE_GLOW_FILTER_BICUBIC -// w0, w1, w2, and w3 are the four cubic B-spline basis functions -float w0(float a) { - return (1.0 / 6.0) * (a * (a * (-a + 3.0) - 3.0) + 1.0); -} - -float w1(float a) { - return (1.0 / 6.0) * (a * a * (3.0 * a - 6.0) + 4.0); -} - -float w2(float a) { - return (1.0 / 6.0) * (a * (a * (-3.0 * a + 3.0) + 3.0) + 1.0); -} - -float w3(float a) { - return (1.0 / 6.0) * (a * a * a); -} - -// g0 and g1 are the two amplitude functions -float g0(float a) { - return w0(a) + w1(a); -} - -float g1(float a) { - return w2(a) + w3(a); -} - -// h0 and h1 are the two offset functions -float h0(float a) { - return -1.0 + w1(a) / (w0(a) + w1(a)); -} - -float h1(float a) { - return 1.0 + w3(a) / (w2(a) + w3(a)); -} - -uniform ivec2 glow_texture_size; - -vec4 texture_bicubic(sampler2D tex, vec2 uv, int p_lod) { - float lod = float(p_lod); - vec2 tex_size = vec2(glow_texture_size >> p_lod); - vec2 texel_size = vec2(1.0) / tex_size; - - uv = uv * tex_size + vec2(0.5); - - vec2 iuv = floor(uv); - vec2 fuv = fract(uv); - - float g0x = g0(fuv.x); - float g1x = g1(fuv.x); - float h0x = h0(fuv.x); - float h1x = h1(fuv.x); - float h0y = h0(fuv.y); - float h1y = h1(fuv.y); - - vec2 p0 = (vec2(iuv.x + h0x, iuv.y + h0y) - vec2(0.5)) * texel_size; - vec2 p1 = (vec2(iuv.x + h1x, iuv.y + h0y) - vec2(0.5)) * texel_size; - vec2 p2 = (vec2(iuv.x + h0x, iuv.y + h1y) - vec2(0.5)) * texel_size; - vec2 p3 = (vec2(iuv.x + h1x, iuv.y + h1y) - vec2(0.5)) * texel_size; - - return (g0(fuv.y) * (g0x * textureLod(tex, p0, lod) + g1x * textureLod(tex, p1, lod))) + - (g1(fuv.y) * (g0x * textureLod(tex, p2, lod) + g1x * textureLod(tex, p3, lod))); -} - -#define GLOW_TEXTURE_SAMPLE(m_tex, m_uv, m_lod) texture_bicubic(m_tex, m_uv, m_lod) -#else //!USE_GLOW_FILTER_BICUBIC -#define GLOW_TEXTURE_SAMPLE(m_tex, m_uv, m_lod) textureLod(m_tex, m_uv, float(m_lod)) -#endif //USE_GLOW_FILTER_BICUBIC - -vec3 apply_glow(vec3 color, vec3 glow) { // apply glow using the selected blending mode -#ifdef USE_GLOW_REPLACE - color = glow; -#endif - -#ifdef USE_GLOW_SCREEN - color = max((color + glow) - (color * glow), vec3(0.0)); -#endif - -#ifdef USE_GLOW_SOFTLIGHT - glow = glow * vec3(0.5) + vec3(0.5); - - color.r = (glow.r <= 0.5) ? (color.r - (1.0 - 2.0 * glow.r) * color.r * (1.0 - color.r)) : (((glow.r > 0.5) && (color.r <= 0.25)) ? (color.r + (2.0 * glow.r - 1.0) * (4.0 * color.r * (4.0 * color.r + 1.0) * (color.r - 1.0) + 7.0 * color.r)) : (color.r + (2.0 * glow.r - 1.0) * (sqrt(color.r) - color.r))); - color.g = (glow.g <= 0.5) ? (color.g - (1.0 - 2.0 * glow.g) * color.g * (1.0 - color.g)) : (((glow.g > 0.5) && (color.g <= 0.25)) ? (color.g + (2.0 * glow.g - 1.0) * (4.0 * color.g * (4.0 * color.g + 1.0) * (color.g - 1.0) + 7.0 * color.g)) : (color.g + (2.0 * glow.g - 1.0) * (sqrt(color.g) - color.g))); - color.b = (glow.b <= 0.5) ? (color.b - (1.0 - 2.0 * glow.b) * color.b * (1.0 - color.b)) : (((glow.b > 0.5) && (color.b <= 0.25)) ? (color.b + (2.0 * glow.b - 1.0) * (4.0 * color.b * (4.0 * color.b + 1.0) * (color.b - 1.0) + 7.0 * color.b)) : (color.b + (2.0 * glow.b - 1.0) * (sqrt(color.b) - color.b))); -#endif - -#if !defined(USE_GLOW_SCREEN) && !defined(USE_GLOW_SOFTLIGHT) && !defined(USE_GLOW_REPLACE) // no other selected -> additive - color += glow; -#endif - - return color; -} - -vec3 apply_bcs(vec3 color, vec3 bcs) { - color = mix(vec3(0.0), color, bcs.x); - color = mix(vec3(0.5), color, bcs.y); - color = mix(vec3(dot(vec3(1.0), color) * 0.33333), color, bcs.z); - - return color; -} - -vec3 apply_color_correction(vec3 color, sampler2D correction_tex) { - color.r = texture(correction_tex, vec2(color.r, 0.0)).r; - color.g = texture(correction_tex, vec2(color.g, 0.0)).g; - color.b = texture(correction_tex, vec2(color.b, 0.0)).b; - - return color; -} - -vec3 apply_fxaa(vec3 color, vec2 uv_interp, vec2 pixel_size) { - const float FXAA_REDUCE_MIN = (1.0 / 128.0); - const float FXAA_REDUCE_MUL = (1.0 / 8.0); - const float FXAA_SPAN_MAX = 8.0; - -#ifdef USE_MULTIVIEW - vec3 rgbNW = textureLod(source, vec3(uv_interp + vec2(-1.0, -1.0) * pixel_size, ViewIndex), 0.0).xyz; - vec3 rgbNE = textureLod(source, vec3(uv_interp + vec2(1.0, -1.0) * pixel_size, ViewIndex), 0.0).xyz; - vec3 rgbSW = textureLod(source, vec3(uv_interp + vec2(-1.0, 1.0) * pixel_size, ViewIndex), 0.0).xyz; - vec3 rgbSE = textureLod(source, vec3(uv_interp + vec2(1.0, 1.0) * pixel_size, ViewIndex), 0.0).xyz; -#else - vec3 rgbNW = textureLod(source, uv_interp + vec2(-1.0, -1.0) * pixel_size, 0.0).xyz; - vec3 rgbNE = textureLod(source, uv_interp + vec2(1.0, -1.0) * pixel_size, 0.0).xyz; - vec3 rgbSW = textureLod(source, uv_interp + vec2(-1.0, 1.0) * pixel_size, 0.0).xyz; - vec3 rgbSE = textureLod(source, uv_interp + vec2(1.0, 1.0) * pixel_size, 0.0).xyz; -#endif - vec3 rgbM = color; - vec3 luma = vec3(0.299, 0.587, 0.114); - float lumaNW = dot(rgbNW, luma); - float lumaNE = dot(rgbNE, luma); - float lumaSW = dot(rgbSW, luma); - float lumaSE = dot(rgbSE, luma); - float lumaM = dot(rgbM, luma); - float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE))); - float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE))); - - vec2 dir; - dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE)); - dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE)); - - float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * - (0.25 * FXAA_REDUCE_MUL), - FXAA_REDUCE_MIN); - - float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce); - dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX), - max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), - dir * rcpDirMin)) * - pixel_size; - -#ifdef USE_MULTIVIEW - vec3 rgbA = 0.5 * (textureLod(source, vec3(uv_interp + dir * (1.0 / 3.0 - 0.5), ViewIndex), 0.0).xyz + textureLod(source, vec3(uv_interp + dir * (2.0 / 3.0 - 0.5), ViewIndex), 0.0).xyz); - vec3 rgbB = rgbA * 0.5 + 0.25 * (textureLod(source, vec3(uv_interp + dir * -0.5, ViewIndex), 0.0).xyz + textureLod(source, vec3(uv_interp + dir * 0.5, ViewIndex), 0.0).xyz); -#else - vec3 rgbA = 0.5 * (textureLod(source, uv_interp + dir * (1.0 / 3.0 - 0.5), 0.0).xyz + textureLod(source, uv_interp + dir * (2.0 / 3.0 - 0.5), 0.0).xyz); - vec3 rgbB = rgbA * 0.5 + 0.25 * (textureLod(source, uv_interp + dir * -0.5, 0.0).xyz + textureLod(source, uv_interp + dir * 0.5, 0.0).xyz); -#endif - - float lumaB = dot(rgbB, luma); - if ((lumaB < lumaMin) || (lumaB > lumaMax)) { - return rgbA; - } else { - return rgbB; - } -} - -void main() { -#ifdef USE_MULTIVIEW - vec4 color = textureLod(source, vec3(uv_interp, ViewIndex), 0.0); -#else - vec4 color = textureLod(source, uv_interp, 0.0); -#endif - -#ifdef USE_FXAA - color.rgb = apply_fxaa(color.rgb, uv_interp, pixel_size); -#endif - - // Glow - -#ifdef USING_GLOW - vec3 glow = vec3(0.0); -#ifdef USE_MULTI_TEXTURE_GLOW -#ifdef USE_GLOW_LEVEL1 - glow += GLOW_TEXTURE_SAMPLE(source_glow1, uv_interp, 0).rgb; -#ifdef USE_GLOW_LEVEL2 - glow += GLOW_TEXTURE_SAMPLE(source_glow2, uv_interp, 0).rgb; -#ifdef USE_GLOW_LEVEL3 - glow += GLOW_TEXTURE_SAMPLE(source_glow3, uv_interp, 0).rgb; -#ifdef USE_GLOW_LEVEL4 - glow += GLOW_TEXTURE_SAMPLE(source_glow4, uv_interp, 0).rgb; -#ifdef USE_GLOW_LEVEL5 - glow += GLOW_TEXTURE_SAMPLE(source_glow5, uv_interp, 0).rgb; -#ifdef USE_GLOW_LEVEL6 - glow += GLOW_TEXTURE_SAMPLE(source_glow6, uv_interp, 0).rgb; -#ifdef USE_GLOW_LEVEL7 - glow += GLOW_TEXTURE_SAMPLE(source_glow7, uv_interp, 0).rgb; -#endif -#endif -#endif -#endif -#endif -#endif -#endif - -#else - -#ifdef USE_GLOW_LEVEL1 - glow += GLOW_TEXTURE_SAMPLE(source_glow, uv_interp, 1).rgb; -#endif - -#ifdef USE_GLOW_LEVEL2 - glow += GLOW_TEXTURE_SAMPLE(source_glow, uv_interp, 2).rgb; -#endif - -#ifdef USE_GLOW_LEVEL3 - glow += GLOW_TEXTURE_SAMPLE(source_glow, uv_interp, 3).rgb; -#endif - -#ifdef USE_GLOW_LEVEL4 - glow += GLOW_TEXTURE_SAMPLE(source_glow, uv_interp, 4).rgb; -#endif - -#ifdef USE_GLOW_LEVEL5 - glow += GLOW_TEXTURE_SAMPLE(source_glow, uv_interp, 5).rgb; -#endif - -#ifdef USE_GLOW_LEVEL6 - glow += GLOW_TEXTURE_SAMPLE(source_glow, uv_interp, 6).rgb; -#endif - -#ifdef USE_GLOW_LEVEL7 - glow += GLOW_TEXTURE_SAMPLE(source_glow, uv_interp, 7).rgb; -#endif -#endif //USE_MULTI_TEXTURE_GLOW - - glow *= glow_intensity; - color.rgb = apply_glow(color.rgb, glow); -#endif - - // Additional effects - -#ifdef USE_BCS - color.rgb = apply_bcs(color.rgb, bcs); -#endif - -#ifdef USE_COLOR_CORRECTION - color.rgb = apply_color_correction(color.rgb, color_correction); -#endif - - frag_color = color; -} diff --git a/drivers/gles3/shaders/tonemap_inc.glsl b/drivers/gles3/shaders/tonemap_inc.glsl index f8f12760ec..fb915aeb38 100644 --- a/drivers/gles3/shaders/tonemap_inc.glsl +++ b/drivers/gles3/shaders/tonemap_inc.glsl @@ -1,43 +1,31 @@ -#ifdef USE_BCS -uniform vec3 bcs; -#endif - -#ifdef USE_COLOR_CORRECTION -#ifdef USE_1D_LUT -uniform sampler2D source_color_correction; //texunit:-1 -#else -uniform sampler3D source_color_correction; //texunit:-1 -#endif -#endif - layout(std140) uniform TonemapData { //ubo:0 float exposure; float white; int tonemapper; int pad; -}; -vec3 apply_bcs(vec3 color, vec3 bcs) { - color = mix(vec3(0.0), color, bcs.x); - color = mix(vec3(0.5), color, bcs.y); - color = mix(vec3(dot(vec3(1.0), color) * 0.33333), color, bcs.z); + int pad2; + float brightness; + float contrast; + float saturation; +}; - return color; -} -#ifdef USE_COLOR_CORRECTION -#ifdef USE_1D_LUT -vec3 apply_color_correction(vec3 color) { - color.r = texture(source_color_correction, vec2(color.r, 0.0f)).r; - color.g = texture(source_color_correction, vec2(color.g, 0.0f)).g; - color.b = texture(source_color_correction, vec2(color.b, 0.0f)).b; - return color; +// This expects 0-1 range input. +vec3 linear_to_srgb(vec3 color) { + //color = clamp(color, vec3(0.0), vec3(1.0)); + //const vec3 a = vec3(0.055f); + //return mix((vec3(1.0f) + a) * pow(color.rgb, vec3(1.0f / 2.4f)) - a, 12.92f * color.rgb, lessThan(color.rgb, vec3(0.0031308f))); + // Approximation from http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html + return max(vec3(1.055) * pow(color, vec3(0.416666667)) - vec3(0.055), vec3(0.0)); } -#else -vec3 apply_color_correction(vec3 color) { - return textureLod(source_color_correction, color, 0.0).rgb; + +// This expects 0-1 range input, outside that range it behaves poorly. +vec3 srgb_to_linear(vec3 color) { + // Approximation from http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html + return color * (color * (color * 0.305306011 + 0.682171111) + 0.012522878); } -#endif -#endif + +#ifdef APPLY_TONEMAPPING vec3 tonemap_filmic(vec3 color, float p_white) { // exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers @@ -92,21 +80,6 @@ vec3 tonemap_reinhard(vec3 color, float p_white) { return (p_white * color + color) / (color * p_white + p_white); } -// This expects 0-1 range input. -vec3 linear_to_srgb(vec3 color) { - //color = clamp(color, vec3(0.0), vec3(1.0)); - //const vec3 a = vec3(0.055f); - //return mix((vec3(1.0f) + a) * pow(color.rgb, vec3(1.0f / 2.4f)) - a, 12.92f * color.rgb, lessThan(color.rgb, vec3(0.0031308f))); - // Approximation from http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html - return max(vec3(1.055) * pow(color, vec3(0.416666667)) - vec3(0.055), vec3(0.0)); -} - -// This expects 0-1 range input, outside that range it behaves poorly. -vec3 srgb_to_linear(vec3 color) { - // Approximation from http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html - return color * (color * (color * 0.305306011 + 0.682171111) + 0.012522878); -} - #define TONEMAPPER_LINEAR 0 #define TONEMAPPER_REINHARD 1 #define TONEMAPPER_FILMIC 2 @@ -125,3 +98,5 @@ vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR, always return tonemap_aces(max(vec3(0.0f), color), p_white); } } + +#endif // APPLY_TONEMAPPING diff --git a/drivers/gles3/storage/material_storage.cpp b/drivers/gles3/storage/material_storage.cpp index 62d22dac4d..996c205042 100644 --- a/drivers/gles3/storage/material_storage.cpp +++ b/drivers/gles3/storage/material_storage.cpp @@ -1243,6 +1243,7 @@ MaterialStorage::MaterialStorage() { actions.renames["NORMAL_MAP_DEPTH"] = "normal_map_depth"; actions.renames["ALBEDO"] = "albedo"; actions.renames["ALPHA"] = "alpha"; + actions.renames["PREMUL_ALPHA_FACTOR"] = "premul_alpha"; actions.renames["METALLIC"] = "metallic"; actions.renames["SPECULAR"] = "specular"; actions.renames["ROUGHNESS"] = "roughness"; @@ -1327,6 +1328,7 @@ MaterialStorage::MaterialStorage() { actions.usage_defines["ALPHA_HASH_SCALE"] = "#define ALPHA_HASH_USED\n"; actions.usage_defines["ALPHA_ANTIALIASING_EDGE"] = "#define ALPHA_ANTIALIASING_EDGE_USED\n"; actions.usage_defines["ALPHA_TEXTURE_COORDINATE"] = "@ALPHA_ANTIALIASING_EDGE"; + actions.usage_defines["PREMULT_ALPHA_FACTOR"] = "#define PREMULT_ALPHA_USED"; actions.usage_defines["SSS_STRENGTH"] = "#define ENABLE_SSS\n"; actions.usage_defines["SSS_TRANSMITTANCE_DEPTH"] = "#define ENABLE_TRANSMITTANCE\n"; @@ -1964,13 +1966,9 @@ void MaterialStorage::global_shader_parameters_load_settings(bool p_load_texture Variant value = d["value"]; if (gvtype >= RS::GLOBAL_VAR_TYPE_SAMPLER2D) { - //textire - if (!p_load_textures) { - continue; - } - String path = value; - if (path.is_empty()) { + // Don't load the textures, but still add the parameter so shaders compile correctly while loading. + if (!p_load_textures || path.is_empty()) { value = RID(); } else { Ref<Resource> resource = ResourceLoader::load(path); @@ -2908,6 +2906,7 @@ void SceneShaderData::set_code(const String &p_code) { actions.render_mode_values["blend_mix"] = Pair<int *, int>(&blend_modei, BLEND_MODE_MIX); actions.render_mode_values["blend_sub"] = Pair<int *, int>(&blend_modei, BLEND_MODE_SUB); actions.render_mode_values["blend_mul"] = Pair<int *, int>(&blend_modei, BLEND_MODE_MUL); + actions.render_mode_values["blend_premul_alpha"] = Pair<int *, int>(&blend_modei, BLEND_MODE_PREMULT_ALPHA); actions.render_mode_values["alpha_to_coverage"] = Pair<int *, int>(&alpha_antialiasing_modei, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); actions.render_mode_values["alpha_to_coverage_and_one"] = Pair<int *, int>(&alpha_antialiasing_modei, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE_AND_TO_ONE); diff --git a/drivers/gles3/storage/material_storage.h b/drivers/gles3/storage/material_storage.h index 02aecf33d6..392ebcc570 100644 --- a/drivers/gles3/storage/material_storage.h +++ b/drivers/gles3/storage/material_storage.h @@ -248,6 +248,7 @@ struct SceneShaderData : public ShaderData { BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, + BLEND_MODE_PREMULT_ALPHA, BLEND_MODE_ALPHA_TO_COVERAGE }; diff --git a/drivers/gles3/storage/render_scene_buffers_gles3.cpp b/drivers/gles3/storage/render_scene_buffers_gles3.cpp index 6803c92dc9..cb194933ed 100644 --- a/drivers/gles3/storage/render_scene_buffers_gles3.cpp +++ b/drivers/gles3/storage/render_scene_buffers_gles3.cpp @@ -577,8 +577,7 @@ void RenderSceneBuffersGLES3::check_glow_buffers() { GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton(); Size2i level_size = internal_size; for (int i = 0; i < 4; i++) { - level_size.x = MAX(level_size.x >> 1, 4); - level_size.y = MAX(level_size.y >> 1, 4); + level_size = Size2i(level_size.x >> 1, level_size.y >> 1).maxi(4); glow.levels[i].size = level_size; diff --git a/drivers/gles3/storage/texture_storage.cpp b/drivers/gles3/storage/texture_storage.cpp index 6f32e4d49d..373df8d8de 100644 --- a/drivers/gles3/storage/texture_storage.cpp +++ b/drivers/gles3/storage/texture_storage.cpp @@ -2812,7 +2812,7 @@ void TextureStorage::_render_target_allocate_sdf(RenderTarget *rt) { } rt->process_size = size * scale / 100; - rt->process_size = rt->process_size.max(Size2i(1, 1)); + rt->process_size = rt->process_size.maxi(1); glGenTextures(2, rt->sdf_texture_process); glBindTexture(GL_TEXTURE_2D, rt->sdf_texture_process[0]); diff --git a/drivers/gles3/storage/utilities.cpp b/drivers/gles3/storage/utilities.cpp index 7e2e3dfa2b..356dc06733 100644 --- a/drivers/gles3/storage/utilities.cpp +++ b/drivers/gles3/storage/utilities.cpp @@ -299,7 +299,7 @@ void Utilities::visibility_notifier_call(RID p_notifier, bool p_enter, bool p_de ERR_FAIL_NULL(vn); if (p_enter) { - if (!vn->enter_callback.is_null()) { + if (vn->enter_callback.is_valid()) { if (p_deferred) { vn->enter_callback.call_deferred(); } else { @@ -307,7 +307,7 @@ void Utilities::visibility_notifier_call(RID p_notifier, bool p_enter, bool p_de } } } else { - if (!vn->exit_callback.is_null()) { + if (vn->exit_callback.is_valid()) { if (p_deferred) { vn->exit_callback.call_deferred(); } else { diff --git a/drivers/png/SCsub b/drivers/png/SCsub index dd4777a19b..e38f3c4760 100644 --- a/drivers/png/SCsub +++ b/drivers/png/SCsub @@ -39,7 +39,7 @@ if env["builtin_libpng"]: if env["arch"].startswith("arm"): if env.msvc: # Can't compile assembly files with MSVC. - env_thirdparty.Append(CPPDEFINES=[("PNG_ARM_NEON_OPT"), 0]) + env_thirdparty.Append(CPPDEFINES=[("PNG_ARM_NEON_OPT", 0)]) else: env_neon = env_thirdparty.Clone() if "S_compiler" in env: diff --git a/drivers/vulkan/rendering_device_driver_vulkan.cpp b/drivers/vulkan/rendering_device_driver_vulkan.cpp index 1906d168fe..803555cb07 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.cpp +++ b/drivers/vulkan/rendering_device_driver_vulkan.cpp @@ -1008,7 +1008,7 @@ VkResult RenderingDeviceDriverVulkan::_create_render_pass(VkDevice p_device, con const uint32_t depth_attachment_index = vector_base_index + 3; _convert_subpass_attachments(p_create_info->pSubpasses[i].pInputAttachments, p_create_info->pSubpasses[i].inputAttachmentCount, subpasses_attachments[input_attachments_index]); _convert_subpass_attachments(p_create_info->pSubpasses[i].pColorAttachments, p_create_info->pSubpasses[i].colorAttachmentCount, subpasses_attachments[color_attachments_index]); - _convert_subpass_attachments(p_create_info->pSubpasses[i].pResolveAttachments, p_create_info->pSubpasses[i].colorAttachmentCount, subpasses_attachments[resolve_attachments_index]); + _convert_subpass_attachments(p_create_info->pSubpasses[i].pResolveAttachments, (p_create_info->pSubpasses[i].pResolveAttachments != nullptr) ? p_create_info->pSubpasses[i].colorAttachmentCount : 0, subpasses_attachments[resolve_attachments_index]); _convert_subpass_attachments(p_create_info->pSubpasses[i].pDepthStencilAttachment, (p_create_info->pSubpasses[i].pDepthStencilAttachment != nullptr) ? 1 : 0, subpasses_attachments[depth_attachment_index]); // Ignores sType and pNext from the subpass. diff --git a/drivers/windows/dir_access_windows.cpp b/drivers/windows/dir_access_windows.cpp index 43dd62cdf6..63ba6a6c96 100644 --- a/drivers/windows/dir_access_windows.cpp +++ b/drivers/windows/dir_access_windows.cpp @@ -396,6 +396,66 @@ bool DirAccessWindows::is_case_sensitive(const String &p_path) const { } } +bool DirAccessWindows::is_link(String p_file) { + String f = p_file; + + if (!f.is_absolute_path()) { + f = get_current_dir().path_join(f); + } + f = fix_path(f); + + DWORD attr = GetFileAttributesW((LPCWSTR)(f.utf16().get_data())); + if (attr == INVALID_FILE_ATTRIBUTES) { + return false; + } + + return (attr & FILE_ATTRIBUTE_REPARSE_POINT); +} + +String DirAccessWindows::read_link(String p_file) { + String f = p_file; + + if (!f.is_absolute_path()) { + f = get_current_dir().path_join(f); + } + f = fix_path(f); + + HANDLE hfile = CreateFileW((LPCWSTR)(f.utf16().get_data()), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (hfile == INVALID_HANDLE_VALUE) { + return f; + } + + DWORD ret = GetFinalPathNameByHandleW(hfile, nullptr, 0, VOLUME_NAME_DOS | FILE_NAME_NORMALIZED); + if (ret == 0) { + return f; + } + Char16String cs; + cs.resize(ret + 1); + GetFinalPathNameByHandleW(hfile, (LPWSTR)cs.ptrw(), ret, VOLUME_NAME_DOS | FILE_NAME_NORMALIZED); + CloseHandle(hfile); + + return String::utf16((const char16_t *)cs.ptr(), ret).trim_prefix(R"(\\?\)"); +} + +Error DirAccessWindows::create_link(String p_source, String p_target) { + if (p_target.is_relative_path()) { + p_target = get_current_dir().path_join(p_target); + } + + p_source = fix_path(p_source); + p_target = fix_path(p_target); + + DWORD file_attr = GetFileAttributesW((LPCWSTR)(p_source.utf16().get_data())); + bool is_dir = (file_attr & FILE_ATTRIBUTE_DIRECTORY); + + DWORD flags = ((is_dir) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0) | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + if (CreateSymbolicLinkW((LPCWSTR)p_target.utf16().get_data(), (LPCWSTR)p_source.utf16().get_data(), flags) != 0) { + return OK; + } else { + return FAILED; + } +} + DirAccessWindows::DirAccessWindows() { p = memnew(DirAccessWindowsPrivate); p->h = INVALID_HANDLE_VALUE; diff --git a/drivers/windows/dir_access_windows.h b/drivers/windows/dir_access_windows.h index 576ba18d9a..46755cbf33 100644 --- a/drivers/windows/dir_access_windows.h +++ b/drivers/windows/dir_access_windows.h @@ -77,9 +77,9 @@ public: virtual Error rename(String p_path, String p_new_path) override; virtual Error remove(String p_path) override; - virtual bool is_link(String p_file) override { return false; }; - virtual String read_link(String p_file) override { return p_file; }; - virtual Error create_link(String p_source, String p_target) override { return FAILED; }; + virtual bool is_link(String p_file) override; + virtual String read_link(String p_file) override; + virtual Error create_link(String p_source, String p_target) override; uint64_t get_space_left() override; diff --git a/drivers/windows/file_access_windows.cpp b/drivers/windows/file_access_windows.cpp index 726e0fdc5a..9885d9d7ee 100644 --- a/drivers/windows/file_access_windows.cpp +++ b/drivers/windows/file_access_windows.cpp @@ -32,6 +32,7 @@ #include "file_access_windows.h" +#include "core/config/project_settings.h" #include "core/os/os.h" #include "core/string/print_string.h" @@ -121,19 +122,63 @@ Error FileAccessWindows::open_internal(const String &p_path, int p_mode_flags) { // Windows is case insensitive, but all other platforms are sensitive to it // To ease cross-platform development, we issue a warning if users try to access // a file using the wrong case (which *works* on Windows, but won't on other - // platforms). - if (p_mode_flags == READ) { - WIN32_FIND_DATAW d; - HANDLE fnd = FindFirstFileW((LPCWSTR)(path.utf16().get_data()), &d); - if (fnd != INVALID_HANDLE_VALUE) { - String fname = String::utf16((const char16_t *)(d.cFileName)); - if (!fname.is_empty()) { - String base_file = path.get_file(); - if (base_file != fname && base_file.findn(fname) == 0) { - WARN_PRINT("Case mismatch opening requested file '" + base_file + "', stored as '" + fname + "' in the filesystem. This file will not open when exported to other case-sensitive platforms."); + // platforms), we only check for relative paths, or paths in res:// or user://, + // other paths aren't likely to be portable anyway. + if (p_mode_flags == READ && (p_path.is_relative_path() || get_access_type() != ACCESS_FILESYSTEM)) { + String base_path = path; + String working_path; + String proper_path; + + if (get_access_type() == ACCESS_RESOURCES) { + if (ProjectSettings::get_singleton()) { + working_path = ProjectSettings::get_singleton()->get_resource_path(); + if (!working_path.is_empty()) { + base_path = working_path.path_to_file(base_path); } } + proper_path = "res://"; + } else if (get_access_type() == ACCESS_USERDATA) { + working_path = OS::get_singleton()->get_user_data_dir(); + if (!working_path.is_empty()) { + base_path = working_path.path_to_file(base_path); + } + proper_path = "user://"; + } + + WIN32_FIND_DATAW d; + Vector<String> parts = base_path.split("/"); + + bool mismatch = false; + + for (const String &part : parts) { + working_path = working_path.path_join(part); + + // Skip if relative. + if (part == "." || part == "..") { + proper_path = proper_path.path_join(part); + continue; + } + + HANDLE fnd = FindFirstFileW((LPCWSTR)(working_path.utf16().get_data()), &d); + + if (fnd == INVALID_HANDLE_VALUE) { + mismatch = false; + break; + } + + const String fname = String::utf16((const char16_t *)(d.cFileName)); + FindClose(fnd); + + if (!mismatch) { + mismatch = (part != fname && part.findn(fname) == 0); + } + + proper_path = proper_path.path_join(fname); + } + + if (mismatch) { + WARN_PRINT("Case mismatch opening requested file '" + p_path + "', stored as '" + proper_path + "' in the filesystem. This file will not open when exported to other case-sensitive platforms."); } } #endif diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index bec95d40c6..86fe9dd1fb 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -4485,6 +4485,7 @@ AnimationTrackEditor::TrackIndices AnimationTrackEditor::_confirm_insert(InsertD if (p_id.type == Animation::TYPE_VALUE) { undo_redo->add_do_method(reset_anim, "value_track_set_update_mode", p_next_tracks.reset, update_mode); } + undo_redo->add_do_method(reset_anim, "track_set_interpolation_type", p_next_tracks.reset, interp_type); undo_redo->add_do_method(reset_anim, "track_insert_key", p_next_tracks.reset, 0.0f, value); undo_redo->add_undo_method(reset_anim, "remove_track", reset_anim->get_track_count()); p_next_tracks.reset++; diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 779080786a..cfeb495690 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -33,7 +33,6 @@ #include "core/input/input.h" #include "core/os/keyboard.h" #include "core/string/string_builder.h" -#include "core/templates/pair.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/plugins/script_editor_plugin.h" @@ -173,10 +172,8 @@ bool FindReplaceBar::_search(uint32_t p_flags, int p_from_line, int p_from_col) if (pos.x != -1) { if (!preserve_cursor && !is_selection_only()) { text_editor->unfold_line(pos.y); - text_editor->set_caret_line(pos.y, false); - text_editor->set_caret_column(pos.x + text.length(), false); - text_editor->center_viewport_to_caret(0); text_editor->select(pos.y, pos.x, pos.y, pos.x + text.length()); + text_editor->center_viewport_to_caret(0); line_col_changed_for_result = true; } @@ -216,7 +213,7 @@ void FindReplaceBar::_replace() { text_editor->begin_complex_operation(); if (selection_enabled && is_selection_only()) { // Restrict search_current() to selected region. - text_editor->set_caret_line(selection_begin.width, false, true, 0, 0); + text_editor->set_caret_line(selection_begin.width, false, true, -1, 0); text_editor->set_caret_column(selection_begin.height, true, 0); } @@ -285,10 +282,10 @@ void FindReplaceBar::_replace_all() { text_editor->begin_complex_operation(); if (selection_enabled && is_selection_only()) { - text_editor->set_caret_line(selection_begin.width, false, true, 0, 0); + text_editor->set_caret_line(selection_begin.width, false, true, -1, 0); text_editor->set_caret_column(selection_begin.height, true, 0); } else { - text_editor->set_caret_line(0, false, true, 0, 0); + text_editor->set_caret_line(0, false, true, -1, 0); text_editor->set_caret_column(0, true, 0); } @@ -812,22 +809,22 @@ void CodeTextEditor::input(const Ref<InputEvent> &event) { } if (ED_IS_SHORTCUT("script_text_editor/move_up", key_event)) { - move_lines_up(); + text_editor->move_lines_up(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/move_down", key_event)) { - move_lines_down(); + text_editor->move_lines_down(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/delete_line", key_event)) { - delete_lines(); + text_editor->delete_lines(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/duplicate_selection", key_event)) { - duplicate_selection(); + text_editor->duplicate_selection(); accept_event(); return; } @@ -1116,31 +1113,23 @@ void CodeTextEditor::trim_trailing_whitespace() { break; } } - text_editor->set_line(i, line.substr(0, end)); + text_editor->remove_text(i, end, i, line.length()); } } if (trimmed_whitespace) { text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); - text_editor->queue_redraw(); } } void CodeTextEditor::insert_final_newline() { int final_line = text_editor->get_line_count() - 1; - String line = text_editor->get_line(final_line); // Length 0 means it's already an empty line, no need to add a newline. if (line.length() > 0 && !line.ends_with("\n")) { - text_editor->begin_complex_operation(); - - line += "\n"; - text_editor->set_line(final_line, line); - - text_editor->end_complex_operation(); - text_editor->queue_redraw(); + text_editor->insert_text("\n", final_line, line.length(), false); } } @@ -1149,9 +1138,12 @@ void CodeTextEditor::convert_case(CaseStyle p_case) { return; } text_editor->begin_complex_operation(); + text_editor->begin_multicaret_edit(); - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { + for (int c = 0; c < text_editor->get_caret_count(); c++) { + if (text_editor->multicaret_edit_ignore_caret(c)) { + continue; + } if (!text_editor->has_selection(c)) { continue; } @@ -1192,6 +1184,7 @@ void CodeTextEditor::convert_case(CaseStyle p_case) { text_editor->set_line(i, new_line); } } + text_editor->end_multicaret_edit(); text_editor->end_complex_operation(); } @@ -1200,308 +1193,24 @@ void CodeTextEditor::set_indent_using_spaces(bool p_use_spaces) { indentation_txt->set_text(p_use_spaces ? TTR("Spaces", "Indentation") : TTR("Tabs", "Indentation")); } -void CodeTextEditor::move_lines_up() { - text_editor->begin_complex_operation(); - - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - - // Lists of carets representing each group. - Vector<Vector<int>> caret_groups; - Vector<Pair<int, int>> group_borders; - - // Search for groups of carets and their selections residing on the same lines. - for (int i = 0; i < caret_edit_order.size(); i++) { - int c = caret_edit_order[i]; - - Vector<int> new_group{ c }; - Pair<int, int> group_border; - group_border.first = _get_affected_lines_from(c); - group_border.second = _get_affected_lines_to(c); - - for (int j = i; j < caret_edit_order.size() - 1; j++) { - int c_current = caret_edit_order[j]; - int c_next = caret_edit_order[j + 1]; - - int next_start_pos = _get_affected_lines_from(c_next); - int next_end_pos = _get_affected_lines_to(c_next); - - int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current); - - i = j; - if (next_end_pos != current_start_pos && next_end_pos + 1 != current_start_pos) { - break; - } - group_border.first = next_start_pos; - new_group.push_back(c_next); - // If the last caret is added to the current group there is no need to process it again. - if (j + 1 == caret_edit_order.size() - 1) { - i++; - } - } - group_borders.push_back(group_border); - caret_groups.push_back(new_group); - } - - for (int i = group_borders.size() - 1; i >= 0; i--) { - if (group_borders[i].first - 1 < 0) { - continue; - } - - // If the group starts overlapping with the upper group don't move it. - if (i < group_borders.size() - 1 && group_borders[i].first - 1 <= group_borders[i + 1].second) { - continue; - } - - // We have to remember caret positions and selections prior to line swapping. - Vector<Vector<int>> caret_group_parameters; - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - int cursor_line = text_editor->get_caret_line(c); - int cursor_column = text_editor->get_caret_column(c); - - if (!text_editor->has_selection(c)) { - caret_group_parameters.push_back(Vector<int>{ -1, -1, -1, -1, cursor_line, cursor_column }); - continue; - } - int from_line = text_editor->get_selection_from_line(c); - int from_col = text_editor->get_selection_from_column(c); - int to_line = text_editor->get_selection_to_line(c); - int to_column = text_editor->get_selection_to_column(c); - caret_group_parameters.push_back(Vector<int>{ from_line, from_col, to_line, to_column, cursor_line, cursor_column }); - } - - for (int line_id = group_borders[i].first; line_id <= group_borders[i].second; line_id++) { - text_editor->unfold_line(line_id); - text_editor->unfold_line(line_id - 1); - - text_editor->swap_lines(line_id - 1, line_id); - } - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - const Vector<int> &caret_parameters = caret_group_parameters[j]; - text_editor->set_caret_line(caret_parameters[4] - 1, c == 0, true, 0, c); - text_editor->set_caret_column(caret_parameters[5], c == 0, c); - - if (caret_parameters[0] >= 0) { - text_editor->select(caret_parameters[0] - 1, caret_parameters[1], caret_parameters[2] - 1, caret_parameters[3], c); - } - } - } - - text_editor->end_complex_operation(); - text_editor->merge_overlapping_carets(); - text_editor->queue_redraw(); -} - -void CodeTextEditor::move_lines_down() { - text_editor->begin_complex_operation(); - - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - - // Lists of carets representing each group. - Vector<Vector<int>> caret_groups; - Vector<Pair<int, int>> group_borders; - Vector<int> group_border_ends; - // Search for groups of carets and their selections residing on the same lines. - for (int i = 0; i < caret_edit_order.size(); i++) { - int c = caret_edit_order[i]; - - Vector<int> new_group{ c }; - Pair<int, int> group_border; - group_border.first = _get_affected_lines_from(c); - group_border.second = _get_affected_lines_to(c); - - for (int j = i; j < caret_edit_order.size() - 1; j++) { - int c_current = caret_edit_order[j]; - int c_next = caret_edit_order[j + 1]; - - int next_start_pos = _get_affected_lines_from(c_next); - int next_end_pos = _get_affected_lines_to(c_next); - - int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current); - - i = j; - if (next_end_pos == current_start_pos || next_end_pos + 1 == current_start_pos) { - group_border.first = next_start_pos; - new_group.push_back(c_next); - // If the last caret is added to the current group there is no need to process it again. - if (j + 1 == caret_edit_order.size() - 1) { - i++; - } - } else { - break; - } - } - group_borders.push_back(group_border); - group_border_ends.push_back(text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c)); - caret_groups.push_back(new_group); - } - - for (int i = 0; i < group_borders.size(); i++) { - if (group_border_ends[i] + 1 > text_editor->get_line_count() - 1) { - continue; - } - - // If the group starts overlapping with the upper group don't move it. - if (i > 0 && group_border_ends[i] + 1 >= group_borders[i - 1].first) { - continue; - } - - // We have to remember caret positions and selections prior to line swapping. - Vector<Vector<int>> caret_group_parameters; - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - int cursor_line = text_editor->get_caret_line(c); - int cursor_column = text_editor->get_caret_column(c); - - if (!text_editor->has_selection(c)) { - caret_group_parameters.push_back(Vector<int>{ -1, -1, -1, -1, cursor_line, cursor_column }); - continue; - } - int from_line = text_editor->get_selection_from_line(c); - int from_col = text_editor->get_selection_from_column(c); - int to_line = text_editor->get_selection_to_line(c); - int to_column = text_editor->get_selection_to_column(c); - caret_group_parameters.push_back(Vector<int>{ from_line, from_col, to_line, to_column, cursor_line, cursor_column }); - } - - for (int line_id = group_borders[i].second; line_id >= group_borders[i].first; line_id--) { - text_editor->unfold_line(line_id); - text_editor->unfold_line(line_id + 1); - - text_editor->swap_lines(line_id + 1, line_id); - } - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - const Vector<int> &caret_parameters = caret_group_parameters[j]; - text_editor->set_caret_line(caret_parameters[4] + 1, c == 0, true, 0, c); - text_editor->set_caret_column(caret_parameters[5], c == 0, c); - - if (caret_parameters[0] >= 0) { - text_editor->select(caret_parameters[0] + 1, caret_parameters[1], caret_parameters[2] + 1, caret_parameters[3], c); - } - } - } - - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); - text_editor->queue_redraw(); -} - -void CodeTextEditor::delete_lines() { - text_editor->begin_complex_operation(); - - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - Vector<int> lines; - int last_line = INT_MAX; - for (const int &c : caret_edit_order) { - for (int line = _get_affected_lines_to(c); line >= _get_affected_lines_from(c); line--) { - if (line >= last_line) { - continue; - } - last_line = line; - lines.append(line); - } - } - - for (const int &line : lines) { - if (line != text_editor->get_line_count() - 1) { - text_editor->remove_text(line, 0, line + 1, 0); - } else { - text_editor->remove_text(line - 1, text_editor->get_line(line - 1).length(), line, text_editor->get_line(line).length()); - } - // Readjust carets. - int new_line = MIN(line, text_editor->get_line_count() - 1); - text_editor->unfold_line(new_line); - for (const int &c : caret_edit_order) { - if (text_editor->get_caret_line(c) == line || (text_editor->get_caret_line(c) == line + 1 && text_editor->get_caret_column(c) == 0)) { - text_editor->deselect(c); - text_editor->set_caret_line(new_line, c == 0, true, 0, c); - continue; - } - if (text_editor->get_caret_line(c) > line) { - text_editor->set_caret_line(text_editor->get_caret_line(c) - 1, c == 0, true, 0, c); - continue; - } - break; - } - } - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); -} - -void CodeTextEditor::duplicate_selection() { - text_editor->begin_complex_operation(); - - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { - const int cursor_column = text_editor->get_caret_column(c); - int from_line = text_editor->get_caret_line(c); - int to_line = text_editor->get_caret_line(c); - int from_column = 0; - int to_column = 0; - int cursor_new_line = to_line + 1; - int cursor_new_column = text_editor->get_caret_column(c); - String new_text = "\n" + text_editor->get_line(from_line); - bool selection_active = false; - - text_editor->set_caret_column(text_editor->get_line(from_line).length(), c == 0, c); - if (text_editor->has_selection(c)) { - from_column = text_editor->get_selection_from_column(c); - to_column = text_editor->get_selection_to_column(c); - - from_line = text_editor->get_selection_from_line(c); - to_line = text_editor->get_selection_to_line(c); - cursor_new_line = to_line + text_editor->get_caret_line(c) - from_line; - cursor_new_column = to_column == cursor_column ? 2 * to_column - from_column : to_column; - new_text = text_editor->get_selected_text(c); - selection_active = true; - - text_editor->set_caret_line(to_line, c == 0, true, 0, c); - text_editor->set_caret_column(to_column, c == 0, c); - } - - for (int i = from_line; i <= to_line; i++) { - text_editor->unfold_line(i); - } - text_editor->deselect(c); - text_editor->insert_text_at_caret(new_text, c); - text_editor->set_caret_line(cursor_new_line, c == 0, true, 0, c); - text_editor->set_caret_column(cursor_new_column, c == 0, c); - if (selection_active) { - text_editor->select(to_line, to_column, 2 * to_line - from_line, to_line == from_line ? 2 * to_column - from_column : to_column, c); - } - } - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); - text_editor->queue_redraw(); -} - void CodeTextEditor::toggle_inline_comment(const String &delimiter) { text_editor->begin_complex_operation(); + text_editor->begin_multicaret_edit(); - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - caret_edit_order.reverse(); - int last_line = -1; + Vector<Point2i> line_ranges = text_editor->get_line_ranges_from_carets(); int folded_to = 0; - for (const int &c1 : caret_edit_order) { - int from = _get_affected_lines_from(c1); - from += from == last_line ? 1 + folded_to : 0; - int to = _get_affected_lines_to(c1); - last_line = to; + for (Point2i line_range : line_ranges) { + int from_line = line_range.x; + int to_line = line_range.y; // If last line is folded, extends to the end of the folded section - if (text_editor->is_line_folded(to)) { - folded_to = text_editor->get_next_visible_line_offset_from(to + 1, 1) - 1; - to += folded_to; + if (text_editor->is_line_folded(to_line)) { + folded_to = text_editor->get_next_visible_line_offset_from(to_line + 1, 1) - 1; + to_line += folded_to; } // Check first if there's any uncommented lines in selection. bool is_commented = true; bool is_all_empty = true; - for (int line = from; line <= to; line++) { + for (int line = from_line; line <= to_line; line++) { // `+ delimiter.length()` here because comment delimiter is not actually `in comment` so we check first character after it int delimiter_idx = text_editor->is_in_comment(line, text_editor->get_first_non_whitespace_column(line) + delimiter.length()); // Empty lines should not be counted. @@ -1517,58 +1226,24 @@ void CodeTextEditor::toggle_inline_comment(const String &delimiter) { // Special case for commenting empty lines, treat it/them as uncommented lines. is_commented = is_commented && !is_all_empty; - // Caret positions need to be saved since they could be moved at the eol. - Vector<int> caret_cols; - Vector<int> selection_to_cols; - for (const int &c2 : caret_edit_order) { - if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) { - caret_cols.append(text_editor->get_caret_column(c2)); - } - if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) { - selection_to_cols.append(text_editor->get_selection_to_column(c2)); - } - } - // Comment/uncomment. - for (int line = from; line <= to; line++) { - String line_text = text_editor->get_line(line); + for (int line = from_line; line <= to_line; line++) { if (is_all_empty) { - text_editor->set_line(line, delimiter); + text_editor->insert_text(delimiter, line, 0); continue; } if (is_commented) { - text_editor->set_line(line, line_text.replace_first(delimiter, "")); + int delimiter_column = text_editor->get_line(line).find(delimiter); + text_editor->remove_text(line, delimiter_column, line, delimiter_column + delimiter.length()); } else { - text_editor->set_line(line, line_text.insert(text_editor->get_first_non_whitespace_column(line), delimiter)); - } - } - - // Readjust carets and selections. - int caret_i = 0; - int selection_i = 0; - int offset = (is_commented ? -1 : 1) * delimiter.length(); - for (const int &c2 : caret_edit_order) { - bool is_line_selection = text_editor->has_selection(c2) && text_editor->get_selection_from_line(c2) < text_editor->get_selection_to_line(c2); - if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) { - int caret_col = caret_cols[caret_i++]; - caret_col += (is_line_selection && caret_col == 0) ? 0 : offset; - text_editor->set_caret_column(caret_col, c2 == 0, c2); - } - if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) { - int from_col = text_editor->get_selection_from_column(c2); - from_col += (is_line_selection && from_col == 0) ? 0 : offset; - int to_col = selection_to_cols[selection_i++]; - to_col += (to_col == 0) ? 0 : offset; - text_editor->select( - text_editor->get_selection_from_line(c2), from_col, - text_editor->get_selection_to_line(c2), to_col, c2); + text_editor->insert_text(delimiter, line, text_editor->get_first_non_whitespace_column(line)); } } } - text_editor->merge_overlapping_carets(); + + text_editor->end_multicaret_edit(); text_editor->end_complex_operation(); - text_editor->queue_redraw(); } void CodeTextEditor::goto_line(int p_line) { @@ -1613,13 +1288,26 @@ Variant CodeTextEditor::get_edit_state() { return state; } +Variant CodeTextEditor::get_previous_state() { + return previous_state; +} + +void CodeTextEditor::store_previous_state() { + previous_state = get_navigation_state(); +} + void CodeTextEditor::set_edit_state(const Variant &p_state) { Dictionary state = p_state; /* update the row first as it sets the column to 0 */ text_editor->set_caret_line(state["row"]); text_editor->set_caret_column(state["column"]); - text_editor->set_v_scroll(state["scroll_position"]); + if (int(state["scroll_position"]) == -1) { + // Special case for previous state. + text_editor->center_viewport_to_caret(); + } else { + text_editor->set_v_scroll(state["scroll_position"]); + } text_editor->set_h_scroll(state["h_scroll_position"]); if (state.get("selection", false)) { @@ -1648,6 +1336,10 @@ void CodeTextEditor::set_edit_state(const Variant &p_state) { text_editor->set_line_as_bookmarked(bookmarks[i], true); } } + + if (previous_state.is_empty()) { + previous_state = p_state; + } } Variant CodeTextEditor::get_navigation_state() { @@ -1798,22 +1490,6 @@ void CodeTextEditor::_toggle_scripts_pressed() { update_toggle_scripts_button(); } -int CodeTextEditor::_get_affected_lines_from(int p_caret) { - return text_editor->has_selection(p_caret) ? text_editor->get_selection_from_line(p_caret) : text_editor->get_caret_line(p_caret); -} - -int CodeTextEditor::_get_affected_lines_to(int p_caret) { - if (!text_editor->has_selection(p_caret)) { - return text_editor->get_caret_line(p_caret); - } - int line = text_editor->get_selection_to_line(p_caret); - // Don't affect a line with no selected characters. - if (text_editor->get_selection_to_column(p_caret) == 0) { - line--; - } - return line; -} - void CodeTextEditor::_error_pressed(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> mb = p_event; if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { @@ -1862,13 +1538,12 @@ void CodeTextEditor::set_warning_count(int p_warning_count) { } void CodeTextEditor::toggle_bookmark() { - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - caret_edit_order.reverse(); + Vector<int> sorted_carets = text_editor->get_sorted_carets(); int last_line = -1; - for (const int &c : caret_edit_order) { - int from = text_editor->has_selection(c) ? text_editor->get_selection_from_line(c) : text_editor->get_caret_line(c); + for (const int &c : sorted_carets) { + int from = text_editor->get_selection_from_line(c); from += from == last_line ? 1 : 0; - int to = text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c); + int to = text_editor->get_selection_to_line(c); if (to < from) { continue; } diff --git a/editor/code_editor.h b/editor/code_editor.h index 64b13b9006..75a2a68d58 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -174,6 +174,8 @@ class CodeTextEditor : public VBoxContainer { int error_line; int error_column; + Dictionary previous_state; + void _update_text_editor_theme(); void _update_font_ligatures(); void _complete_request(); @@ -205,9 +207,6 @@ class CodeTextEditor : public VBoxContainer { void _toggle_scripts_pressed(); - int _get_affected_lines_from(int p_caret); - int _get_affected_lines_to(int p_caret); - protected: virtual void _load_theme_settings() {} virtual void _validate_script() {} @@ -236,11 +235,6 @@ public: void set_indent_using_spaces(bool p_use_spaces); - void move_lines_up(); - void move_lines_down(); - void delete_lines(); - void duplicate_selection(); - /// Toggle inline comment on currently selected lines, or on current line if nothing is selected, /// by adding or removing comment delimiter void toggle_inline_comment(const String &delimiter); @@ -254,6 +248,8 @@ public: Variant get_edit_state(); void set_edit_state(const Variant &p_state); Variant get_navigation_state(); + Variant get_previous_state(); + void store_previous_state(); void set_error_count(int p_error_count); void set_warning_count(int p_warning_count); diff --git a/editor/debugger/debug_adapter/debug_adapter_server.h b/editor/debugger/debug_adapter/debug_adapter_server.h index c834ab2182..310bac1b6c 100644 --- a/editor/debugger/debug_adapter/debug_adapter_server.h +++ b/editor/debugger/debug_adapter/debug_adapter_server.h @@ -32,7 +32,7 @@ #define DEBUG_ADAPTER_SERVER_H #include "debug_adapter_protocol.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class DebugAdapterServer : public EditorPlugin { GDCLASS(DebugAdapterServer, EditorPlugin); diff --git a/editor/editor_about.cpp b/editor/editor_about.cpp index 61c4eed669..af0419a81a 100644 --- a/editor/editor_about.cpp +++ b/editor/editor_about.cpp @@ -63,7 +63,6 @@ void EditorAbout::_notification(int p_what) { _logo->set_texture(get_editor_theme_icon(SNAME("Logo"))); - Ref<StyleBoxEmpty> empty_stylebox = memnew(StyleBoxEmpty); for (ItemList *il : name_lists) { for (int i = 0; i < il->get_item_count(); i++) { if (il->get_item_metadata(i)) { diff --git a/editor/editor_audio_buses.cpp b/editor/editor_audio_buses.cpp index b4e9faa4fd..658bc33ddc 100644 --- a/editor/editor_audio_buses.cpp +++ b/editor/editor_audio_buses.cpp @@ -87,9 +87,14 @@ void EditorAudioBus::_notification(int p_what) { disabled_vu = get_editor_theme_icon(SNAME("BusVuFrozen")); - Color solo_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.0, 0.92, 0.44); - Color mute_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(1.0, 0.44, 0.44); - Color bypass_color = EditorThemeManager::is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(0.44, 0.87, 1.0); + Color solo_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.9, 1.74, 0.83); + Color mute_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(2.35, 1.03, 1.03); + Color bypass_color = EditorThemeManager::is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(1.03, 2.04, 2.35); + float darkening_factor = EditorThemeManager::is_dark_theme() ? 0.15 : 0.65; + + Ref<StyleBoxFlat>(solo->get_theme_stylebox("pressed"))->set_border_color(solo_color.darkened(darkening_factor)); + Ref<StyleBoxFlat>(mute->get_theme_stylebox("pressed"))->set_border_color(mute_color.darkened(darkening_factor)); + Ref<StyleBoxFlat>(bypass->get_theme_stylebox("pressed"))->set_border_color(bypass_color.darkened(darkening_factor)); solo->set_icon(get_editor_theme_icon(SNAME("AudioBusSolo"))); solo->add_theme_color_override("icon_pressed_color", solo_color); @@ -835,7 +840,13 @@ EditorAudioBus::EditorAudioBus(EditorAudioBuses *p_buses, bool p_is_master) { child->add_theme_style_override("normal", sbempty); child->add_theme_style_override("hover", sbempty); child->add_theme_style_override("focus", sbempty); - child->add_theme_style_override("pressed", sbempty); + + Ref<StyleBoxFlat> sbflat = memnew(StyleBoxFlat); + sbflat->set_content_margin_all(0); + sbflat->set_bg_color(Color(1, 1, 1, 0)); + sbflat->set_border_width(Side::SIDE_BOTTOM, Math::round(3 * EDSCALE)); + child->add_theme_style_override("pressed", sbflat); + child->end_bulk_theme_override(); } diff --git a/editor/editor_audio_buses.h b/editor/editor_audio_buses.h index 99e3214781..b1f811fbf6 100644 --- a/editor/editor_audio_buses.h +++ b/editor/editor_audio_buses.h @@ -31,7 +31,7 @@ #ifndef EDITOR_AUDIO_BUSES_H #define EDITOR_AUDIO_BUSES_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/control.h" diff --git a/editor/editor_builders.py b/editor/editor_builders.py index a25f4949c4..cfe6e56b49 100644 --- a/editor/editor_builders.py +++ b/editor/editor_builders.py @@ -7,6 +7,7 @@ import subprocess import tempfile import uuid import zlib +from methods import print_warning def make_doc_header(target, source, env): @@ -57,7 +58,7 @@ def make_translations_header(target, source, env, category): msgfmt_available = shutil.which("msgfmt") is not None if not msgfmt_available: - print("WARNING: msgfmt is not found, using .po files instead of .mo") + print_warning("msgfmt is not found, using .po files instead of .mo") xl_names = [] for i in range(len(sorted_paths)): @@ -71,8 +72,8 @@ def make_translations_header(target, source, env, category): with open(mo_path, "rb") as f: buf = f.read() except OSError as e: - print( - "WARNING: msgfmt execution failed, using .po file instead of .mo: path=%r; [%s] %s" + print_warning( + "msgfmt execution failed, using .po file instead of .mo: path=%r; [%s] %s" % (sorted_paths[i], e.__class__.__name__, e) ) with open(sorted_paths[i], "rb") as f: @@ -82,9 +83,8 @@ def make_translations_header(target, source, env, category): os.remove(mo_path) except OSError as e: # Do not fail the entire build if it cannot delete a temporary file. - print( - "WARNING: Could not delete temporary .mo file: path=%r; [%s] %s" - % (mo_path, e.__class__.__name__, e) + print_warning( + "Could not delete temporary .mo file: path=%r; [%s] %s" % (mo_path, e.__class__.__name__, e) ) else: with open(sorted_paths[i], "rb") as f: diff --git a/editor/editor_data.cpp b/editor/editor_data.cpp index bdc6504417..bd1eef8e53 100644 --- a/editor/editor_data.cpp +++ b/editor/editor_data.cpp @@ -36,9 +36,9 @@ #include "core/io/image_loader.h" #include "core/io/resource_loader.h" #include "editor/editor_node.h" -#include "editor/editor_plugin.h" #include "editor/editor_undo_redo_manager.h" #include "editor/multi_node_edit.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/script_editor_plugin.h" #include "editor/themes/editor_scale.h" #include "scene/resources/packed_scene.h" diff --git a/editor/editor_dock_manager.cpp b/editor/editor_dock_manager.cpp index b6250671ee..06dd33d8ab 100644 --- a/editor/editor_dock_manager.cpp +++ b/editor/editor_dock_manager.cpp @@ -147,7 +147,6 @@ void EditorDockManager::_update_layout() { if (!dock_context_popup->is_inside_tree() || EditorNode::get_singleton()->is_exiting()) { return; } - EditorNode::get_singleton()->edit_current(); dock_context_popup->docks_updated(); _update_docks_menu(); EditorNode::get_singleton()->save_editor_layout_delayed(); diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp index 14190a43a5..1e91a326b3 100644 --- a/editor/editor_file_system.cpp +++ b/editor/editor_file_system.cpp @@ -1578,31 +1578,7 @@ void EditorFileSystem::_update_script_classes() { update_script_mutex.lock(); for (const String &path : update_script_paths) { - ScriptServer::remove_global_class_by_path(path); // First remove, just in case it changed - - int index = -1; - EditorFileSystemDirectory *efd = find_file(path, &index); - - if (!efd || index < 0) { - // The file was removed - continue; - } - - if (!efd->files[index]->script_class_name.is_empty()) { - String lang; - for (int j = 0; j < ScriptServer::get_language_count(); j++) { - if (ScriptServer::get_language(j)->handles_global_class_type(efd->files[index]->type)) { - lang = ScriptServer::get_language(j)->get_name(); - } - } - if (lang.is_empty()) { - continue; // No lang found that can handle this global class - } - - ScriptServer::add_global_class(efd->files[index]->script_class_name, efd->files[index]->script_class_extends, lang, path); - EditorNode::get_editor_data().script_class_set_icon_path(efd->files[index]->script_class_name, efd->files[index]->script_class_icon_path); - EditorNode::get_editor_data().script_class_set_name(path, efd->files[index]->script_class_name); - } + EditorFileSystem::get_singleton()->register_global_class_script(path, path); } // Parse documentation second, as it requires the class names to be correct and registered @@ -1844,6 +1820,34 @@ HashSet<String> EditorFileSystem::get_valid_extensions() const { return valid_extensions; } +void EditorFileSystem::register_global_class_script(const String &p_search_path, const String &p_target_path) { + ScriptServer::remove_global_class_by_path(p_search_path); // First remove, just in case it changed + + int index = -1; + EditorFileSystemDirectory *efd = find_file(p_search_path, &index); + + if (!efd || index < 0) { + // The file was removed + return; + } + + if (!efd->files[index]->script_class_name.is_empty()) { + String lang; + for (int j = 0; j < ScriptServer::get_language_count(); j++) { + if (ScriptServer::get_language(j)->handles_global_class_type(efd->files[index]->type)) { + lang = ScriptServer::get_language(j)->get_name(); + } + } + if (lang.is_empty()) { + return; // No lang found that can handle this global class + } + + ScriptServer::add_global_class(efd->files[index]->script_class_name, efd->files[index]->script_class_extends, lang, p_target_path); + EditorNode::get_editor_data().script_class_set_icon_path(efd->files[index]->script_class_name, efd->files[index]->script_class_icon_path); + EditorNode::get_editor_data().script_class_set_name(p_target_path, efd->files[index]->script_class_name); + } +} + Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector<String> &p_files) { String importer_name; @@ -2324,8 +2328,9 @@ void EditorFileSystem::reimport_file_with_custom_parameters(const String &p_file } void EditorFileSystem::_reimport_thread(uint32_t p_index, ImportThreadData *p_import_data) { - p_import_data->max_index = MAX(p_import_data->reimport_from + int(p_index), p_import_data->max_index); - _reimport_file(p_import_data->reimport_files[p_import_data->reimport_from + p_index].path); + int current_max = p_import_data->reimport_from + int(p_index); + p_import_data->max_index.exchange_if_greater(current_max); + _reimport_file(p_import_data->reimport_files[current_max].path); } void EditorFileSystem::reimport_files(const Vector<String> &p_files) { @@ -2405,15 +2410,15 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) { importer->import_threaded_begin(); ImportThreadData tdata; - tdata.max_index = from; + tdata.max_index.set(from); tdata.reimport_from = from; tdata.reimport_files = reimport_files.ptr(); WorkerThreadPool::GroupID group_task = WorkerThreadPool::get_singleton()->add_template_group_task(this, &EditorFileSystem::_reimport_thread, &tdata, i - from + 1, -1, false, vformat(TTR("Import resources of type: %s"), reimport_files[from].importer)); int current_index = from - 1; do { - if (current_index < tdata.max_index) { - current_index = tdata.max_index; + if (current_index < tdata.max_index.get()) { + current_index = tdata.max_index.get(); pr.step(reimport_files[current_index].path.get_file(), current_index); } OS::get_singleton()->delay_usec(1); diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h index 782d3eee38..ad0e3f10ef 100644 --- a/editor/editor_file_system.h +++ b/editor/editor_file_system.h @@ -282,7 +282,7 @@ class EditorFileSystem : public Node { struct ImportThreadData { const ImportFile *reimport_files; int reimport_from; - int max_index = 0; + SafeNumeric<int> max_index; }; void _reimport_thread(uint32_t p_index, ImportThreadData *p_import_data); @@ -310,6 +310,7 @@ public: void scan_changes(); void update_file(const String &p_file); HashSet<String> get_valid_extensions() const; + void register_global_class_script(const String &p_search_path, const String &p_target_path); EditorFileSystemDirectory *get_filesystem_path(const String &p_path); String get_file_type(const String &p_file) const; diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index 5cc09b7104..63c2ebe3d9 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -287,6 +287,7 @@ void EditorHelp::_class_desc_select(const String &p_select) { if (table->has(link)) { // Found in the current page. if (class_desc->is_ready()) { + emit_signal(SNAME("request_save_history")); class_desc->scroll_to_paragraph((*table)[link]); } else { scroll_to = (*table)[link]; @@ -3077,6 +3078,7 @@ void EditorHelp::_bind_methods() { ClassDB::bind_method("_help_callback", &EditorHelp::_help_callback); ADD_SIGNAL(MethodInfo("go_to_help")); + ADD_SIGNAL(MethodInfo("request_save_history")); } void EditorHelp::init_gdext_pointers() { diff --git a/editor/editor_help.h b/editor/editor_help.h index 078b42b439..ca3a05275f 100644 --- a/editor/editor_help.h +++ b/editor/editor_help.h @@ -34,7 +34,7 @@ #include "core/os/thread.h" #include "editor/code_editor.h" #include "editor/doc_tools.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/menu_button.h" #include "scene/gui/panel_container.h" #include "scene/gui/popup.h" diff --git a/editor/editor_help_search.h b/editor/editor_help_search.h index 39ffc2f71b..58061dae4c 100644 --- a/editor/editor_help_search.h +++ b/editor/editor_help_search.h @@ -34,7 +34,7 @@ #include "core/templates/rb_map.h" #include "editor/code_editor.h" #include "editor/editor_help.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/option_button.h" #include "scene/gui/tree.h" diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 50cc89c618..f1e487d79b 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -3379,7 +3379,12 @@ void EditorInspector::update_tree() { if (use_doc_hints) { // `|` separators used in `EditorHelpBit`. if (theme_item_name.is_empty()) { - if (p.usage & PROPERTY_USAGE_INTERNAL) { + if (p.name.contains("shader_parameter/")) { + ShaderMaterial *shader_material = Object::cast_to<ShaderMaterial>(object); + if (shader_material) { + ep->set_tooltip_text("property|" + shader_material->get_shader()->get_path() + "|" + property_prefix + p.name); + } + } else if (p.usage & PROPERTY_USAGE_INTERNAL) { ep->set_tooltip_text("internal_property|" + classname + "|" + property_prefix + p.name); } else { ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name); @@ -4019,14 +4024,16 @@ void EditorInspector::_notification(int p_what) { } break; case NOTIFICATION_PREDELETE: { - edit(nullptr); //just in case + if (EditorNode::get_singleton() && !EditorNode::get_singleton()->is_exiting()) { + // Don't need to clean up if exiting, and object may already be freed. + edit(nullptr); + } } break; case NOTIFICATION_EXIT_TREE: { if (!sub_inspector) { get_tree()->disconnect("node_removed", callable_mp(this, &EditorInspector::_node_removed)); } - edit(nullptr); } break; case NOTIFICATION_VISIBILITY_CHANGED: { diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 5b24fb1559..5bb9aa91d2 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -90,7 +90,6 @@ #include "editor/editor_log.h" #include "editor/editor_native_shader_source_visualizer.h" #include "editor/editor_paths.h" -#include "editor/editor_plugin.h" #include "editor/editor_properties.h" #include "editor/editor_property_name_processor.h" #include "editor/editor_quick_open.h" @@ -134,12 +133,12 @@ #include "editor/inspector_dock.h" #include "editor/multi_node_edit.h" #include "editor/node_dock.h" -#include "editor/plugin_config_dialog.h" #include "editor/plugins/animation_player_editor_plugin.h" #include "editor/plugins/asset_library_editor_plugin.h" #include "editor/plugins/canvas_item_editor_plugin.h" #include "editor/plugins/debugger_editor_plugin.h" #include "editor/plugins/dedicated_server_export_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/editor_preview_plugins.h" #include "editor/plugins/editor_resource_conversion_plugin.h" #include "editor/plugins/gdextension_export_plugin.h" @@ -148,6 +147,7 @@ #include "editor/plugins/node_3d_editor_plugin.h" #include "editor/plugins/packed_scene_translation_parser_plugin.h" #include "editor/plugins/particle_process_material_editor_plugin.h" +#include "editor/plugins/plugin_config_dialog.h" #include "editor/plugins/root_motion_editor_plugin.h" #include "editor/plugins/script_text_editor.h" #include "editor/plugins/text_editor.h" @@ -456,6 +456,9 @@ void EditorNode::_update_from_settings() { void EditorNode::_gdextensions_reloaded() { // In case the developer is inspecting an object that will be changed by the reload. InspectorDock::get_inspector_singleton()->update_tree(); + + // Regenerate documentation. + EditorHelp::generate_doc(); } void EditorNode::_select_default_main_screen_plugin() { @@ -3483,6 +3486,10 @@ void EditorNode::remove_editor_plugin(EditorPlugin *p_editor, bool p_config_chan } } + if (singleton->editor_plugin_screen == p_editor) { + singleton->editor_plugin_screen = nullptr; + } + singleton->editor_table.erase(p_editor); } p_editor->make_visible(false); @@ -3509,6 +3516,7 @@ void EditorNode::add_extension_editor_plugin(const StringName &p_class_name) { EditorPlugin *plugin = Object::cast_to<EditorPlugin>(ClassDB::instantiate(p_class_name)); singleton->editor_data.add_extension_editor_plugin(p_class_name, plugin); add_editor_plugin(plugin); + plugin->enable_plugin(); } void EditorNode::remove_extension_editor_plugin(const StringName &p_class_name) { @@ -7252,7 +7260,9 @@ EditorNode::EditorNode() { add_editor_plugin(memnew(AudioBusesEditorPlugin(audio_bus_editor))); for (int i = 0; i < EditorPlugins::get_plugin_count(); i++) { - add_editor_plugin(EditorPlugins::create(i)); + EditorPlugin *plugin = EditorPlugins::create(i); + add_editor_plugin(plugin); + plugin->enable_plugin(); } for (const StringName &extension_class_name : GDExtensionEditorPlugins::get_extension_classes()) { diff --git a/editor/editor_node.h b/editor/editor_node.h index ad0a7ec5e0..54c84bc6a6 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -35,7 +35,7 @@ #include "core/templates/safe_refcount.h" #include "editor/editor_data.h" #include "editor/editor_folding.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" typedef void (*EditorNodeInitCallback)(); typedef void (*EditorPluginInitializeCallback)(); diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index ea364d8a0d..2964fb364b 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -3408,7 +3408,7 @@ void EditorPropertyResource::update_property() { } } - resource_picker->set_edited_resource(res); + resource_picker->set_edited_resource_no_check(res); } void EditorPropertyResource::collapse_all_folding() { diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index eee589489d..e082366c44 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -488,8 +488,8 @@ void EditorResourcePicker::set_create_options(Object *p_menu_node) { if (!base_type.is_empty()) { int idx = 0; - HashSet<StringName> allowed_types; - _get_allowed_types(false, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_without_convert; for (const StringName &E : allowed_types) { const String &t = E; @@ -593,23 +593,29 @@ static void _add_allowed_type(const StringName &p_type, HashSet<StringName> *p_v } } -void EditorResourcePicker::_get_allowed_types(bool p_with_convert, HashSet<StringName> *p_vector) const { +void EditorResourcePicker::_ensure_allowed_types() const { + if (!allowed_types_without_convert.is_empty()) { + return; + } + Vector<String> allowed_types = base_type.split(","); int size = allowed_types.size(); for (int i = 0; i < size; i++) { - String base = allowed_types[i].strip_edges(); + const String base = allowed_types[i].strip_edges(); + _add_allowed_type(base, &allowed_types_without_convert); + } - _add_allowed_type(base, p_vector); + allowed_types_with_convert = HashSet<StringName>(allowed_types_without_convert); - if (p_with_convert) { - if (base == "BaseMaterial3D") { - p_vector->insert("Texture2D"); - } else if (base == "ShaderMaterial") { - p_vector->insert("Shader"); - } else if (base == "Texture2D") { - p_vector->insert("Image"); - } + for (int i = 0; i < size; i++) { + const String base = allowed_types[i].strip_edges(); + if (base == "BaseMaterial3D") { + allowed_types_with_convert.insert("Texture2D"); + } else if (base == "ShaderMaterial") { + allowed_types_with_convert.insert("Shader"); + } else if (base == "Texture2D") { + allowed_types_with_convert.insert("Image"); } } } @@ -645,8 +651,8 @@ bool EditorResourcePicker::_is_drop_valid(const Dictionary &p_drag_data) const { } } - HashSet<StringName> allowed_types; - _get_allowed_types(true, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_with_convert; if (res.is_valid()) { String res_type = _get_resource_type(res); @@ -713,8 +719,8 @@ void EditorResourcePicker::drop_data_fw(const Point2 &p_point, const Variant &p_ } if (dropped_resource.is_valid()) { - HashSet<StringName> allowed_types; - _get_allowed_types(false, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_without_convert; String res_type = _get_resource_type(dropped_resource); @@ -835,8 +841,8 @@ void EditorResourcePicker::set_base_type(const String &p_base_type) { // There is a possibility that the new base type is conflicting with the existing value. // Keep the value, but warn the user that there is a potential mistake. if (!base_type.is_empty() && edited_resource.is_valid()) { - HashSet<StringName> allowed_types; - _get_allowed_types(true, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_with_convert; StringName custom_class; bool is_custom = false; @@ -857,8 +863,8 @@ String EditorResourcePicker::get_base_type() const { } Vector<String> EditorResourcePicker::get_allowed_types() const { - HashSet<StringName> allowed_types; - _get_allowed_types(false, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_without_convert; Vector<String> types; types.resize(allowed_types.size()); @@ -881,8 +887,8 @@ void EditorResourcePicker::set_edited_resource(Ref<Resource> p_resource) { } if (!base_type.is_empty()) { - HashSet<StringName> allowed_types; - _get_allowed_types(true, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_with_convert; StringName custom_class; bool is_custom = false; @@ -896,7 +902,10 @@ void EditorResourcePicker::set_edited_resource(Ref<Resource> p_resource) { ERR_FAIL_MSG(vformat("Failed to set a resource of the type '%s' because this EditorResourcePicker only accepts '%s' and its derivatives.", class_str, base_type)); } } + set_edited_resource_no_check(p_resource); +} +void EditorResourcePicker::set_edited_resource_no_check(Ref<Resource> p_resource) { edited_resource = p_resource; _update_resource(); } diff --git a/editor/editor_resource_picker.h b/editor/editor_resource_picker.h index 8146c02dff..28229e6b37 100644 --- a/editor/editor_resource_picker.h +++ b/editor/editor_resource_picker.h @@ -52,6 +52,8 @@ class EditorResourcePicker : public HBoxContainer { bool dropping = false; Vector<String> inheritors_array; + mutable HashSet<StringName> allowed_types_without_convert; + mutable HashSet<StringName> allowed_types_with_convert; Button *assign_button = nullptr; TextureRect *preview_rect = nullptr; @@ -97,7 +99,7 @@ class EditorResourcePicker : public HBoxContainer { void _button_input(const Ref<InputEvent> &p_event); String _get_resource_type(const Ref<Resource> &p_resource) const; - void _get_allowed_types(bool p_with_convert, HashSet<StringName> *p_vector) const; + void _ensure_allowed_types() const; bool _is_drop_valid(const Dictionary &p_drag_data) const; bool _is_type_valid(const String &p_type_name, const HashSet<StringName> &p_allowed_types) const; @@ -127,6 +129,7 @@ public: Vector<String> get_allowed_types() const; void set_edited_resource(Ref<Resource> p_resource); + void set_edited_resource_no_check(Ref<Resource> p_resource); Ref<Resource> get_edited_resource(); void set_toggle_mode(bool p_enable); diff --git a/editor/editor_resource_preview.cpp b/editor/editor_resource_preview.cpp index ddf230dfdb..dd698d74b6 100644 --- a/editor/editor_resource_preview.cpp +++ b/editor/editor_resource_preview.cpp @@ -118,8 +118,6 @@ Variant EditorResourcePreviewGenerator::DrawRequester::_post_semaphore() const { return Variant(); // Needed because of how the callback is used. } -EditorResourcePreview *EditorResourcePreview::singleton = nullptr; - bool EditorResourcePreview::is_threaded() const { return RSG::texture_storage->can_create_resources_async(); } @@ -291,13 +289,17 @@ void EditorResourcePreview::_iterate() { bool has_small_texture; uint64_t last_modtime; String hash; - _read_preview_cache(f, &tsize, &has_small_texture, &last_modtime, &hash, &preview_metadata); + bool outdated; + _read_preview_cache(f, &tsize, &has_small_texture, &last_modtime, &hash, &preview_metadata, &outdated); bool cache_valid = true; if (tsize != thumbnail_size) { cache_valid = false; f.unref(); + } else if (outdated) { + cache_valid = false; + f.unref(); } else if (last_modtime != modtime) { String last_md5 = f->get_line(); String md5 = FileAccess::get_md5(item.path); @@ -357,14 +359,16 @@ void EditorResourcePreview::_write_preview_cache(Ref<FileAccess> p_file, int p_t p_file->store_line(itos(p_modified_time)); p_file->store_line(p_hash); p_file->store_line(VariantUtilityFunctions::var_to_str(p_metadata).replace("\n", " ")); + p_file->store_line(itos(CURRENT_METADATA_VERSION)); } -void EditorResourcePreview::_read_preview_cache(Ref<FileAccess> p_file, int *r_thumbnail_size, bool *r_has_small_texture, uint64_t *r_modified_time, String *r_hash, Dictionary *r_metadata) { +void EditorResourcePreview::_read_preview_cache(Ref<FileAccess> p_file, int *r_thumbnail_size, bool *r_has_small_texture, uint64_t *r_modified_time, String *r_hash, Dictionary *r_metadata, bool *r_outdated) { *r_thumbnail_size = p_file->get_line().to_int(); *r_has_small_texture = p_file->get_line().to_int(); *r_modified_time = p_file->get_line().to_int(); *r_hash = p_file->get_line(); *r_metadata = VariantUtilityFunctions::str_to_var(p_file->get_line()); + *r_outdated = p_file->get_line().to_int() < CURRENT_METADATA_VERSION; } void EditorResourcePreview::_thread() { diff --git a/editor/editor_resource_preview.h b/editor/editor_resource_preview.h index 8461651732..6b67acceaa 100644 --- a/editor/editor_resource_preview.h +++ b/editor/editor_resource_preview.h @@ -76,7 +76,8 @@ public: class EditorResourcePreview : public Node { GDCLASS(EditorResourcePreview, Node); - static EditorResourcePreview *singleton; + inline static constexpr int CURRENT_METADATA_VERSION = 1; // Increment this number to invalidate all previews. + inline static EditorResourcePreview *singleton = nullptr; struct QueueItem { Ref<Resource> resource; @@ -118,7 +119,7 @@ class EditorResourcePreview : public Node { void _iterate(); void _write_preview_cache(Ref<FileAccess> p_file, int p_thumbnail_size, bool p_has_small_texture, uint64_t p_modified_time, const String &p_hash, const Dictionary &p_metadata); - void _read_preview_cache(Ref<FileAccess> p_file, int *r_thumbnail_size, bool *r_has_small_texture, uint64_t *r_modified_time, String *r_hash, Dictionary *r_metadata); + void _read_preview_cache(Ref<FileAccess> p_file, int *r_thumbnail_size, bool *r_has_small_texture, uint64_t *r_modified_time, String *r_hash, Dictionary *r_metadata, bool *r_outdated); Vector<Ref<EditorResourcePreviewGenerator>> preview_generators; diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp index 33fdd7418e..aa44189782 100644 --- a/editor/export/editor_export_platform.cpp +++ b/editor/export/editor_export_platform.cpp @@ -510,6 +510,12 @@ HashSet<String> EditorExportPlatform::get_features(const Ref<EditorExportPreset> result.insert("template_release"); } +#ifdef REAL_T_IS_DOUBLE + result.insert("double"); +#else + result.insert("single"); +#endif // REAL_T_IS_DOUBLE + if (!p_preset->get_custom_features().is_empty()) { Vector<String> tmp_custom_list = p_preset->get_custom_features().split(","); diff --git a/editor/export/project_export.cpp b/editor/export/project_export.cpp index 038e357ce2..c995e590f1 100644 --- a/editor/export/project_export.cpp +++ b/editor/export/project_export.cpp @@ -419,6 +419,12 @@ void ProjectExportDialog::_update_feature_list() { feature_set.insert(E); } +#ifdef REAL_T_IS_DOUBLE + feature_set.insert("double"); +#else + feature_set.insert("single"); +#endif // REAL_T_IS_DOUBLE + custom_feature_display->clear(); String text; bool first = true; diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index b7deb6afa6..ac4991755b 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -1568,34 +1568,7 @@ void FileSystemDock::_update_resource_paths_after_move(const HashMap<String, Str if (I) { ResourceUID::get_singleton()->set_id(I->value, new_path); } - - ScriptServer::remove_global_class_by_path(old_path); - - int index = -1; - EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->find_file(old_path, &index); - - if (!efd || index < 0) { - // The file was removed. - continue; - } - - // Update paths for global classes. - if (!efd->get_file_script_class_name(index).is_empty()) { - String lang; - for (int i = 0; i < ScriptServer::get_language_count(); i++) { - if (ScriptServer::get_language(i)->handles_global_class_type(efd->get_file_type(index))) { - lang = ScriptServer::get_language(i)->get_name(); - break; - } - } - if (lang.is_empty()) { - continue; // No language found that can handle this global class. - } - - ScriptServer::add_global_class(efd->get_file_script_class_name(index), efd->get_file_script_class_extends(index), lang, new_path); - EditorNode::get_editor_data().script_class_set_icon_path(efd->get_file_script_class_name(index), efd->get_file_script_class_icon_path(index)); - EditorNode::get_editor_data().script_class_set_name(new_path, efd->get_file_script_class_name(index)); - } + EditorFileSystem::get_singleton()->register_global_class_script(old_path, new_path); } // Rename all resources loaded, be it subresources or actual resources. @@ -2394,6 +2367,12 @@ void FileSystemDock::_file_option(int p_option, const Vector<String> &p_selected } } break; + case FILE_SHOW_IN_FILESYSTEM: { + if (!p_selected.is_empty()) { + navigate_to_path(p_selected[0]); + } + } break; + case FILE_DEPENDENCIES: { // Checkout the file dependencies. if (!p_selected.is_empty()) { @@ -3286,8 +3265,33 @@ void FileSystemDock::_file_and_folders_fill_popup(PopupMenu *p_popup, const Vect if (p_paths.size() == 1) { const String &fpath = p_paths[0]; + bool added_separator = false; + + if (favorites_list.has(fpath)) { + TreeItem *favorites_item = tree->get_root()->get_first_child(); + TreeItem *cursor_item = tree->get_selected(); + bool is_item_in_favorites = false; + while (cursor_item != nullptr) { + if (cursor_item == favorites_item) { + is_item_in_favorites = true; + break; + } + + cursor_item = cursor_item->get_parent(); + } + + if (is_item_in_favorites) { + p_popup->add_separator(); + added_separator = true; + p_popup->add_icon_item(get_editor_theme_icon(SNAME("ShowInFileSystem")), TTR("Show in FileSystem"), FILE_SHOW_IN_FILESYSTEM); + } + } + #if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED) - p_popup->add_separator(); + if (!added_separator) { + p_popup->add_separator(); + added_separator = true; + } // Opening the system file manager is not supported on the Android and web editors. const bool is_directory = fpath.ends_with("/"); @@ -4153,6 +4157,7 @@ FileSystemDock::FileSystemDock() { ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &FileSystemDock::_project_settings_changed)); add_resource_tooltip_plugin(memnew(EditorTextureTooltipPlugin)); + add_resource_tooltip_plugin(memnew(EditorAudioStreamTooltipPlugin)); } FileSystemDock::~FileSystemDock() { diff --git a/editor/filesystem_dock.h b/editor/filesystem_dock.h index 058886c91a..7449657c06 100644 --- a/editor/filesystem_dock.h +++ b/editor/filesystem_dock.h @@ -116,6 +116,7 @@ private: FILE_INSTANTIATE, FILE_ADD_FAVORITE, FILE_REMOVE_FAVORITE, + FILE_SHOW_IN_FILESYSTEM, FILE_DEPENDENCIES, FILE_OWNERS, FILE_MOVE, diff --git a/editor/gui/editor_scene_tabs.cpp b/editor/gui/editor_scene_tabs.cpp index 5d1e68f008..94fac59b9b 100644 --- a/editor/gui/editor_scene_tabs.cpp +++ b/editor/gui/editor_scene_tabs.cpp @@ -178,7 +178,7 @@ void EditorSceneTabs::_update_context_menu() { if (tab_id >= 0) { scene_tabs_context_menu->add_separator(); - scene_tabs_context_menu->add_icon_item(get_editor_theme_icon(SNAME("ShowInFileSystem")), TTR("Show in FileSystem"), EditorNode::FILE_SHOW_IN_FILESYSTEM); + scene_tabs_context_menu->add_item(TTR("Show in FileSystem"), EditorNode::FILE_SHOW_IN_FILESYSTEM); _disable_menu_option_if(EditorNode::FILE_SHOW_IN_FILESYSTEM, !ResourceLoader::exists(EditorNode::get_editor_data().get_scene_path(tab_id))); scene_tabs_context_menu->add_item(TTR("Play This Scene"), EditorNode::FILE_RUN_SCENE); _disable_menu_option_if(EditorNode::FILE_RUN_SCENE, no_root_node); diff --git a/editor/gui/editor_validation_panel.cpp b/editor/gui/editor_validation_panel.cpp index c08af1915f..80bb08517c 100644 --- a/editor/gui/editor_validation_panel.cpp +++ b/editor/gui/editor_validation_panel.cpp @@ -81,7 +81,7 @@ void EditorValidationPanel::set_update_callback(const Callable &p_callback) { } void EditorValidationPanel::update() { - ERR_FAIL_COND(update_callback.is_null()); + ERR_FAIL_COND(!update_callback.is_valid()); if (pending_update) { return; diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp index 221061f9f7..361ae2a945 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -1013,7 +1013,6 @@ void SceneTreeEditor::set_selected(Node *p_node, bool p_emit_selected) { if (!p_node) { selected = nullptr; } - _update_tree(); selected = p_node; } diff --git a/editor/import/audio_stream_import_settings.h b/editor/import/audio_stream_import_settings.h index da6344adb9..931faf45af 100644 --- a/editor/import/audio_stream_import_settings.h +++ b/editor/import/audio_stream_import_settings.h @@ -31,7 +31,7 @@ #ifndef AUDIO_STREAM_IMPORT_SETTINGS_H #define AUDIO_STREAM_IMPORT_SETTINGS_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/audio/audio_stream_player.h" #include "scene/gui/color_rect.h" #include "scene/gui/dialogs.h" diff --git a/editor/import/resource_importer_wav.cpp b/editor/import/resource_importer_wav.cpp index ab14a5f01d..6d3d474cee 100644 --- a/editor/import/resource_importer_wav.cpp +++ b/editor/import/resource_importer_wav.cpp @@ -90,7 +90,7 @@ void ResourceImporterWAV::get_import_options(const String &p_path, List<ImportOp r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_mode", PROPERTY_HINT_ENUM, "Detect From WAV,Disabled,Forward,Ping-Pong,Backward", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_begin"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_end"), -1)); - r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Disabled,RAM (Ima-ADPCM)"), 0)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Disabled,RAM (Ima-ADPCM),QOA (Quite OK Audio)"), 0)); } Error ResourceImporterWAV::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) { @@ -330,14 +330,12 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s for (int i = 0; i < new_data_frames; i++) { // Cubic interpolation should be enough. - float mu = frac; - float y0 = data[MAX(0, ipos - 1) * format_channels + c]; float y1 = data[ipos * format_channels + c]; float y2 = data[MIN(frames - 1, ipos + 1) * format_channels + c]; float y3 = data[MIN(frames - 1, ipos + 2) * format_channels + c]; - new_data.write[i * format_channels + c] = Math::cubic_interpolate(y1, y2, y0, y3, mu); + new_data.write[i * format_channels + c] = Math::cubic_interpolate(y1, y2, y0, y3, frac); // update position and always keep fractional part within ]0...1] // in order to avoid 32bit floating point precision errors @@ -456,13 +454,13 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s is16 = false; } - Vector<uint8_t> dst_data; + Vector<uint8_t> pcm_data; AudioStreamWAV::Format dst_format; if (compression == 1) { dst_format = AudioStreamWAV::FORMAT_IMA_ADPCM; if (format_channels == 1) { - _compress_ima_adpcm(data, dst_data); + _compress_ima_adpcm(data, pcm_data); } else { //byte interleave Vector<float> left; @@ -484,9 +482,9 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s _compress_ima_adpcm(right, bright); int dl = bleft.size(); - dst_data.resize(dl * 2); + pcm_data.resize(dl * 2); - uint8_t *w = dst_data.ptrw(); + uint8_t *w = pcm_data.ptrw(); const uint8_t *rl = bleft.ptr(); const uint8_t *rr = bright.ptr(); @@ -498,13 +496,14 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s } else { dst_format = is16 ? AudioStreamWAV::FORMAT_16_BITS : AudioStreamWAV::FORMAT_8_BITS; - dst_data.resize(data.size() * (is16 ? 2 : 1)); + bool enforce16 = is16 || compression == 2; + pcm_data.resize(data.size() * (enforce16 ? 2 : 1)); { - uint8_t *w = dst_data.ptrw(); + uint8_t *w = pcm_data.ptrw(); int ds = data.size(); for (int i = 0; i < ds; i++) { - if (is16) { + if (enforce16) { int16_t v = CLAMP(data[i] * 32768, -32768, 32767); encode_uint16(v, &w[i * 2]); } else { @@ -515,6 +514,23 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s } } + Vector<uint8_t> dst_data; + if (compression == 2) { + dst_format = AudioStreamWAV::FORMAT_QOA; + qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } }; + uint32_t qoa_len = 0; + + desc.samplerate = rate; + desc.samples = frames; + desc.channels = format_channels; + + void *encoded = qoa_encode((short *)pcm_data.ptrw(), &desc, &qoa_len); + dst_data.resize(qoa_len); + memcpy(dst_data.ptrw(), encoded, qoa_len); + } else { + dst_data = pcm_data; + } + Ref<AudioStreamWAV> sample; sample.instantiate(); sample->set_data(dst_data); diff --git a/editor/import_defaults_editor.cpp b/editor/import_defaults_editor.cpp index 569fdc091e..968f2a8915 100644 --- a/editor/import_defaults_editor.cpp +++ b/editor/import_defaults_editor.cpp @@ -34,9 +34,9 @@ #include "core/io/resource_importer.h" #include "editor/action_map_editor.h" #include "editor/editor_autoload_settings.h" -#include "editor/editor_plugin_settings.h" #include "editor/editor_sectioned_inspector.h" #include "editor/localization_editor.h" +#include "editor/plugins/editor_plugin_settings.h" #include "editor/shader_globals_editor.h" #include "scene/gui/center_container.h" diff --git a/editor/plugins/abstract_polygon_2d_editor.h b/editor/plugins/abstract_polygon_2d_editor.h index 70cccebf40..42170d9ffd 100644 --- a/editor/plugins/abstract_polygon_2d_editor.h +++ b/editor/plugins/abstract_polygon_2d_editor.h @@ -31,7 +31,7 @@ #ifndef ABSTRACT_POLYGON_2D_EDITOR_H #define ABSTRACT_POLYGON_2D_EDITOR_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/polygon_2d.h" #include "scene/gui/box_container.h" diff --git a/editor/plugins/animation_blend_space_1d_editor.cpp b/editor/plugins/animation_blend_space_1d_editor.cpp index 8c2b738549..1baeddbe86 100644 --- a/editor/plugins/animation_blend_space_1d_editor.cpp +++ b/editor/plugins/animation_blend_space_1d_editor.cpp @@ -583,9 +583,9 @@ void AnimationNodeBlendSpace1DEditor::_notification(int p_what) { snap->set_icon(get_editor_theme_icon(SNAME("SnapGrid"))); open_editor->set_icon(get_editor_theme_icon(SNAME("Edit"))); interpolation->clear(); - interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), "", 0); - interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), "", 1); - interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), "", 2); + interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), TTR("Continuous"), 0); + interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), TTR("Discrete"), 1); + interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), TTR("Capture"), 2); } break; case NOTIFICATION_PROCESS: { diff --git a/editor/plugins/animation_blend_space_1d_editor.h b/editor/plugins/animation_blend_space_1d_editor.h index a0ed813100..00bd3c767d 100644 --- a/editor/plugins/animation_blend_space_1d_editor.h +++ b/editor/plugins/animation_blend_space_1d_editor.h @@ -31,8 +31,8 @@ #ifndef ANIMATION_BLEND_SPACE_1D_EDITOR_H #define ANIMATION_BLEND_SPACE_1D_EDITOR_H -#include "editor/editor_plugin.h" #include "editor/plugins/animation_tree_editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/animation/animation_blend_space_1d.h" #include "scene/gui/graph_edit.h" #include "scene/gui/popup.h" diff --git a/editor/plugins/animation_blend_space_2d_editor.cpp b/editor/plugins/animation_blend_space_2d_editor.cpp index ec67fb7254..06853f0862 100644 --- a/editor/plugins/animation_blend_space_2d_editor.cpp +++ b/editor/plugins/animation_blend_space_2d_editor.cpp @@ -807,9 +807,9 @@ void AnimationNodeBlendSpace2DEditor::_notification(int p_what) { open_editor->set_icon(get_editor_theme_icon(SNAME("Edit"))); auto_triangles->set_icon(get_editor_theme_icon(SNAME("AutoTriangle"))); interpolation->clear(); - interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), "", 0); - interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), "", 1); - interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), "", 2); + interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), TTR("Continuous"), 0); + interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), TTR("Discrete"), 1); + interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), TTR("Capture"), 2); } break; case NOTIFICATION_PROCESS: { diff --git a/editor/plugins/animation_blend_space_2d_editor.h b/editor/plugins/animation_blend_space_2d_editor.h index a89a7b4511..15d81c0707 100644 --- a/editor/plugins/animation_blend_space_2d_editor.h +++ b/editor/plugins/animation_blend_space_2d_editor.h @@ -31,8 +31,8 @@ #ifndef ANIMATION_BLEND_SPACE_2D_EDITOR_H #define ANIMATION_BLEND_SPACE_2D_EDITOR_H -#include "editor/editor_plugin.h" #include "editor/plugins/animation_tree_editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/animation/animation_blend_space_2d.h" #include "scene/gui/graph_edit.h" #include "scene/gui/popup.h" diff --git a/editor/plugins/animation_library_editor.h b/editor/plugins/animation_library_editor.h index a268e68932..c8d9274f4f 100644 --- a/editor/plugins/animation_library_editor.h +++ b/editor/plugins/animation_library_editor.h @@ -32,7 +32,7 @@ #define ANIMATION_LIBRARY_EDITOR_H #include "editor/animation_track_editor.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/animation/animation_mixer.h" #include "scene/gui/dialogs.h" #include "scene/gui/tree.h" diff --git a/editor/plugins/animation_player_editor_plugin.h b/editor/plugins/animation_player_editor_plugin.h index a1175e2a0f..70b31759fc 100644 --- a/editor/plugins/animation_player_editor_plugin.h +++ b/editor/plugins/animation_player_editor_plugin.h @@ -32,8 +32,8 @@ #define ANIMATION_PLAYER_EDITOR_PLUGIN_H #include "editor/animation_track_editor.h" -#include "editor/editor_plugin.h" #include "editor/plugins/animation_library_editor.h" +#include "editor/plugins/editor_plugin.h" #include "scene/animation/animation_player.h" #include "scene/gui/dialogs.h" #include "scene/gui/slider.h" diff --git a/editor/plugins/animation_tree_editor_plugin.h b/editor/plugins/animation_tree_editor_plugin.h index da979a5315..8dc820695a 100644 --- a/editor/plugins/animation_tree_editor_plugin.h +++ b/editor/plugins/animation_tree_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef ANIMATION_TREE_EDITOR_PLUGIN_H #define ANIMATION_TREE_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/animation/animation_tree.h" #include "scene/gui/graph_edit.h" diff --git a/editor/plugins/asset_library_editor_plugin.h b/editor/plugins/asset_library_editor_plugin.h index cb38933bf4..a80e7c8f96 100644 --- a/editor/plugins/asset_library_editor_plugin.h +++ b/editor/plugins/asset_library_editor_plugin.h @@ -32,8 +32,8 @@ #define ASSET_LIBRARY_EDITOR_PLUGIN_H #include "editor/editor_asset_installer.h" -#include "editor/editor_plugin.h" -#include "editor/editor_plugin_settings.h" +#include "editor/plugins/editor_plugin.h" +#include "editor/plugins/editor_plugin_settings.h" #include "scene/gui/box_container.h" #include "scene/gui/check_box.h" #include "scene/gui/grid_container.h" diff --git a/editor/plugins/audio_stream_editor_plugin.h b/editor/plugins/audio_stream_editor_plugin.h index 52aa5f6150..0501409c17 100644 --- a/editor/plugins/audio_stream_editor_plugin.h +++ b/editor/plugins/audio_stream_editor_plugin.h @@ -32,7 +32,7 @@ #define AUDIO_STREAM_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/audio/audio_stream_player.h" #include "scene/gui/button.h" #include "scene/gui/color_rect.h" diff --git a/editor/plugins/audio_stream_randomizer_editor_plugin.h b/editor/plugins/audio_stream_randomizer_editor_plugin.h index 535ab4114b..9d2fc76e9e 100644 --- a/editor/plugins/audio_stream_randomizer_editor_plugin.h +++ b/editor/plugins/audio_stream_randomizer_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef AUDIO_STREAM_RANDOMIZER_EDITOR_PLUGIN_H #define AUDIO_STREAM_RANDOMIZER_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "servers/audio/audio_stream.h" class AudioStreamRandomizerEditorPlugin : public EditorPlugin { diff --git a/editor/plugins/bit_map_editor_plugin.h b/editor/plugins/bit_map_editor_plugin.h index afab1da2f7..030536ab6b 100644 --- a/editor/plugins/bit_map_editor_plugin.h +++ b/editor/plugins/bit_map_editor_plugin.h @@ -32,7 +32,7 @@ #define BIT_MAP_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/resources/bit_map.h" class TextureRect; diff --git a/editor/plugins/bone_map_editor_plugin.cpp b/editor/plugins/bone_map_editor_plugin.cpp index e67f3bd596..2bf9ad9e09 100644 --- a/editor/plugins/bone_map_editor_plugin.cpp +++ b/editor/plugins/bone_map_editor_plugin.cpp @@ -861,6 +861,8 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { picklist.clear(); // 4-1. Guess Finger + int tips_index = -1; + bool thumb_tips_size = 0; bool named_finger_is_found = false; LocalVector<String> fingers; fingers.push_back("thumb|pollex"); @@ -894,6 +896,33 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { search_path.push_back(finger); finger = skeleton->get_bone_parent(finger); } + // Tips detection by name matching with "distal" from root. + for (int j = search_path.size() - 1; j >= 0; j--) { + if (RegEx("distal").search(skeleton->get_bone_name(search_path[j]).to_lower()).is_valid()) { + tips_index = j - 1; + break; + } + } + // Tips detection by name matching with "tip|leaf" from end. + if (tips_index < 0) { + for (int j = 0; j < search_path.size(); j++) { + if (RegEx("tip|leaf").search(skeleton->get_bone_name(search_path[j]).to_lower()).is_valid()) { + tips_index = j; + break; + } + } + } + // Tips detection by thumb children size. + if (tips_index < 0) { + if (i == 0) { + thumb_tips_size = MAX(0, search_path.size() - 3); + } + tips_index = thumb_tips_size - 1; + } + // Remove tips. + for (int j = 0; j <= tips_index; j++) { + search_path.remove_at(0); + } search_path.reverse(); if (search_path.size() == 1) { p_bone_map->_set_skeleton_bone_name(left_fingers_map[i][0], skeleton->get_bone_name(search_path[0])); @@ -941,6 +970,14 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { } } search_path.push_back(finger_root); + // Tips detection by thumb children size. + if (i == 0) { + thumb_tips_size = MAX(0, search_path.size() - 3); + } + tips_index = thumb_tips_size - 1; + for (int j = 0; j <= tips_index; j++) { + search_path.remove_at(0); + } search_path.reverse(); if (search_path.size() == 1) { p_bone_map->_set_skeleton_bone_name(left_fingers_map[i][0], skeleton->get_bone_name(search_path[0])); @@ -958,6 +995,9 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { picklist.clear(); } } + + tips_index = -1; + thumb_tips_size = 0; named_finger_is_found = false; if (right_hand_or_palm != -1) { LocalVector<LocalVector<String>> right_fingers_map; @@ -985,6 +1025,33 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { search_path.push_back(finger); finger = skeleton->get_bone_parent(finger); } + // Tips detection by name matching with "distal" from root. + for (int j = search_path.size() - 1; j >= 0; j--) { + if (RegEx("distal").search(skeleton->get_bone_name(search_path[j]).to_lower()).is_valid()) { + tips_index = j - 1; + break; + } + } + // Tips detection by name matching with "tip|leaf" from end. + if (tips_index < 0) { + for (int j = 0; j < search_path.size(); j++) { + if (RegEx("tip|leaf").search(skeleton->get_bone_name(search_path[j]).to_lower()).is_valid()) { + tips_index = j; + break; + } + } + } + // Tips detection by thumb children size. + if (tips_index < 0) { + if (i == 0) { + thumb_tips_size = MAX(0, search_path.size() - 3); + } + tips_index = thumb_tips_size - 1; + } + // Remove tips. + for (int j = 0; j <= tips_index; j++) { + search_path.remove_at(0); + } search_path.reverse(); if (search_path.size() == 1) { p_bone_map->_set_skeleton_bone_name(right_fingers_map[i][0], skeleton->get_bone_name(search_path[0])); @@ -1032,6 +1099,14 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { } } search_path.push_back(finger_root); + // Tips detection by thumb children size. + if (i == 0) { + thumb_tips_size = MAX(0, search_path.size() - 3); + } + tips_index = thumb_tips_size - 1; + for (int j = 0; j <= tips_index; j++) { + search_path.remove_at(0); + } search_path.reverse(); if (search_path.size() == 1) { p_bone_map->_set_skeleton_bone_name(right_fingers_map[i][0], skeleton->get_bone_name(search_path[0])); diff --git a/editor/plugins/bone_map_editor_plugin.h b/editor/plugins/bone_map_editor_plugin.h index 2e7d1ff124..f3aa2fc84d 100644 --- a/editor/plugins/bone_map_editor_plugin.h +++ b/editor/plugins/bone_map_editor_plugin.h @@ -32,8 +32,8 @@ #define BONE_MAP_EDITOR_PLUGIN_H #include "editor/editor_node.h" -#include "editor/editor_plugin.h" #include "editor/editor_properties.h" +#include "editor/plugins/editor_plugin.h" #include "modules/modules_enabled.gen.h" // For regex. #ifdef MODULE_REGEX_ENABLED diff --git a/editor/plugins/camera_3d_editor_plugin.h b/editor/plugins/camera_3d_editor_plugin.h index 7d5fae6f2b..2e4d8a1ee3 100644 --- a/editor/plugins/camera_3d_editor_plugin.h +++ b/editor/plugins/camera_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef CAMERA_3D_EDITOR_PLUGIN_H #define CAMERA_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/camera_3d.h" class Camera3DEditor : public Control { diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 8a9118a03e..79a4269f01 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -467,7 +467,7 @@ Point2 CanvasItemEditor::snap_point(Point2 p_target, unsigned int p_modes, unsig if (((snap_pixel && (p_modes & SNAP_PIXEL)) || (p_forced_modes & SNAP_PIXEL)) && rotation == 0.0) { // Pixel - output = output.snapped(Size2(1, 1)); + output = output.snappedf(1); } snap_transform = Transform2D(rotation, output); @@ -1344,22 +1344,33 @@ bool CanvasItemEditor::_gui_input_pivot(const Ref<InputEvent> &p_event) { // Drag the pivot (in pivot mode / with V key) if (drag_type == DRAG_NONE) { + bool move_temp_pivot = ((b.is_valid() && b->is_shift_pressed()) || (k.is_valid() && k->is_shift_pressed())); + if ((b.is_valid() && b->is_pressed() && b->get_button_index() == MouseButton::LEFT && tool == TOOL_EDIT_PIVOT) || - (k.is_valid() && k->is_pressed() && !k->is_echo() && k->get_keycode() == Key::V && tool == TOOL_SELECT && k->get_modifiers_mask().is_empty())) { + (k.is_valid() && k->is_pressed() && !k->is_echo() && k->get_keycode() == Key::V && tool == TOOL_SELECT && (k->get_modifiers_mask().is_empty() || move_temp_pivot))) { List<CanvasItem *> selection = _get_edited_canvas_items(); // Filters the selection with nodes that allow setting the pivot drag_selection = List<CanvasItem *>(); for (CanvasItem *ci : selection) { - if (ci->_edit_use_pivot()) { + if (ci->_edit_use_pivot() || move_temp_pivot) { drag_selection.push_back(ci); } } // Start dragging if we still have nodes if (drag_selection.size() > 0) { + Vector2 event_pos = (b.is_valid()) ? b->get_position() : viewport->get_local_mouse_position(); + + if (move_temp_pivot) { + drag_type = DRAG_TEMP_PIVOT; + temp_pivot = transform.affine_inverse().xform(event_pos); + viewport->queue_redraw(); + return true; + } + _save_canvas_item_state(drag_selection); - drag_from = transform.affine_inverse().xform((b.is_valid()) ? b->get_position() : viewport->get_local_mouse_position()); + drag_from = transform.affine_inverse().xform(event_pos); Vector2 new_pos; if (drag_selection.size() == 1) { new_pos = snap_point(drag_from, SNAP_NODE_SIDES | SNAP_NODE_CENTER | SNAP_NODE_ANCHORS | SNAP_OTHER_NODES | SNAP_GRID | SNAP_PIXEL, 0, drag_selection[0]); @@ -1416,6 +1427,20 @@ bool CanvasItemEditor::_gui_input_pivot(const Ref<InputEvent> &p_event) { return true; } } + + if (drag_type == DRAG_TEMP_PIVOT) { + if (m.is_valid()) { + temp_pivot = transform.affine_inverse().xform(m->get_position()); + viewport->queue_redraw(); + return true; + } + + if ((b.is_valid() && !b->is_pressed() && b->get_button_index() == MouseButton::LEFT && tool == TOOL_EDIT_PIVOT) || + (k.is_valid() && !k->is_pressed() && k->get_keycode() == Key::V)) { + drag_type = DRAG_NONE; + return true; + } + } return false; } @@ -1441,7 +1466,9 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref<InputEvent> &p_event) { drag_type = DRAG_ROTATE; drag_from = transform.affine_inverse().xform(b->get_position()); CanvasItem *ci = drag_selection[0]; - if (ci->_edit_use_pivot()) { + if (!Math::is_inf(temp_pivot.x) || !Math::is_inf(temp_pivot.y)) { + drag_rotation_center = temp_pivot; + } else if (ci->_edit_use_pivot()) { drag_rotation_center = ci->get_global_transform_with_canvas().xform(ci->_edit_get_pivot()); } else { drag_rotation_center = ci->get_global_transform_with_canvas().get_origin(); @@ -1461,7 +1488,16 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref<InputEvent> &p_event) { drag_to = transform.affine_inverse().xform(m->get_position()); //Rotate the opposite way if the canvas item's compounded scale has an uneven number of negative elements bool opposite = (ci->get_global_transform().get_scale().sign().dot(ci->get_transform().get_scale().sign()) == 0); - ci->_edit_set_rotation(snap_angle(ci->_edit_get_rotation() + (opposite ? -1 : 1) * (drag_from - drag_rotation_center).angle_to(drag_to - drag_rotation_center), ci->_edit_get_rotation())); + real_t prev_rotation = ci->_edit_get_rotation(); + real_t new_rotation = snap_angle(ci->_edit_get_rotation() + (opposite ? -1 : 1) * (drag_from - drag_rotation_center).angle_to(drag_to - drag_rotation_center), prev_rotation); + + ci->_edit_set_rotation(new_rotation); + if (!Math::is_inf(temp_pivot.x) || !Math::is_inf(temp_pivot.y)) { + Transform2D xform = ci->get_global_transform_with_canvas() * ci->get_transform().affine_inverse(); + Vector2 radius = xform.xform(ci->_edit_get_position()) - temp_pivot; + radius = radius.rotated(new_rotation - prev_rotation); + ci->_edit_set_position(xform.affine_inverse().xform(temp_pivot + radius)); + } viewport->queue_redraw(); } return true; @@ -1589,7 +1625,7 @@ bool CanvasItemEditor::_gui_input_anchors(const Ref<InputEvent> &p_event) { previous_anchor = xform.affine_inverse().xform(_anchor_to_position(control, previous_anchor)); Vector2 new_anchor = xform.xform(snap_point(previous_anchor + (drag_to - drag_from), SNAP_GRID | SNAP_OTHER_NODES, SNAP_NODE_PARENT | SNAP_NODE_SIDES | SNAP_NODE_CENTER, control)); - new_anchor = _position_to_anchor(control, new_anchor).snapped(Vector2(0.001, 0.001)); + new_anchor = _position_to_anchor(control, new_anchor).snappedf(0.001); bool use_single_axis = m->is_shift_pressed(); Vector2 drag_vector = xform.xform(drag_to) - xform.xform(drag_from); @@ -3161,7 +3197,7 @@ void CanvasItemEditor::_draw_ruler_tool() { } else { if (grid_snap_active) { Ref<Texture2D> position_icon = get_editor_theme_icon(SNAME("EditorPosition")); - viewport->draw_texture(get_editor_theme_icon(SNAME("EditorPosition")), (ruler_tool_origin - view_offset) * zoom - position_icon->get_size() / 2); + viewport->draw_texture(position_icon, (ruler_tool_origin - view_offset) * zoom - position_icon->get_size() / 2); } } } @@ -3583,6 +3619,10 @@ void CanvasItemEditor::_draw_selection() { get_theme_color(SNAME("accent_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.6), Math::round(2 * EDSCALE)); } + + if (!Math::is_inf(temp_pivot.x) || !Math::is_inf(temp_pivot.y)) { + viewport->draw_texture(pivot_icon, (temp_pivot - view_offset) * zoom - (pivot_icon->get_size() / 2).floor(), get_theme_color(SNAME("accent_color"), SNAME("Editor"))); + } } void CanvasItemEditor::_draw_straight_line(Point2 p_from, Point2 p_to, Color p_color) { @@ -3931,8 +3971,6 @@ void CanvasItemEditor::_notification(int p_what) { } break; case NOTIFICATION_PROCESS: { - int nb_having_pivot = 0; - // Update the viewport if the canvas_item changes List<CanvasItem *> selection = _get_edited_canvas_items(true); for (CanvasItem *ci : selection) { @@ -3972,14 +4010,10 @@ void CanvasItemEditor::_notification(int p_what) { viewport->queue_redraw(); } } - - if (ci->_edit_use_pivot()) { - nb_having_pivot++; - } } - // Activate / Deactivate the pivot tool - pivot_button->set_disabled(nb_having_pivot == 0); + // Activate / Deactivate the pivot tool. + pivot_button->set_disabled(selection.is_empty()); // Update the viewport if bones changes for (KeyValue<BoneKey, BoneList> &E : bone_list) { @@ -4048,6 +4082,11 @@ void CanvasItemEditor::_selection_changed() { _reset_drag(); } selected_from_canvas = false; + + if (temp_pivot != Vector2(INFINITY, INFINITY)) { + temp_pivot = Vector2(INFINITY, INFINITY); + viewport->queue_redraw(); + } } void CanvasItemEditor::edit(CanvasItem *p_canvas_item) { @@ -4202,6 +4241,18 @@ void CanvasItemEditor::_button_tool_select(int p_index) { tool = (Tool)p_index; + if (p_index == TOOL_EDIT_PIVOT && Input::get_singleton()->is_key_pressed(Key::SHIFT)) { + // Special action that places temporary rotation pivot in the middle of the selection. + List<CanvasItem *> selection = _get_edited_canvas_items(); + if (!selection.is_empty()) { + Vector2 center; + for (const CanvasItem *ci : selection) { + center += ci->_edit_get_position(); + } + temp_pivot = center / selection.size(); + } + } + viewport->queue_redraw(); _update_cursor(); } @@ -5279,7 +5330,7 @@ CanvasItemEditor::CanvasItemEditor() { main_menu_hbox->add_child(pivot_button); pivot_button->set_toggle_mode(true); pivot_button->connect("pressed", callable_mp(this, &CanvasItemEditor::_button_tool_select).bind(TOOL_EDIT_PIVOT)); - pivot_button->set_tooltip_text(TTR("Click to change object's rotation pivot.")); + pivot_button->set_tooltip_text(TTR("Click to change object's rotation pivot.") + "\n" + TTR("Shift: Set temporary rotation pivot.") + "\n" + TTR("Click this button while holding Shift to put the rotation pivot in the center of the selected nodes.")); pan_button = memnew(Button); pan_button->set_theme_type_variation("FlatButton"); diff --git a/editor/plugins/canvas_item_editor_plugin.h b/editor/plugins/canvas_item_editor_plugin.h index c4b995b048..6d951a77ec 100644 --- a/editor/plugins/canvas_item_editor_plugin.h +++ b/editor/plugins/canvas_item_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef CANVAS_ITEM_EDITOR_PLUGIN_H #define CANVAS_ITEM_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/base_button.h" #include "scene/gui/box_container.h" @@ -174,6 +174,7 @@ private: DRAG_SCALE_BOTH, DRAG_ROTATE, DRAG_PIVOT, + DRAG_TEMP_PIVOT, DRAG_V_GUIDE, DRAG_H_GUIDE, DRAG_DOUBLE_GUIDE, @@ -251,6 +252,7 @@ private: bool key_scale = false; bool pan_pressed = false; + Vector2 temp_pivot = Vector2(INFINITY, INFINITY); bool ruler_tool_active = false; Point2 ruler_tool_origin; diff --git a/editor/plugins/cast_2d_editor_plugin.h b/editor/plugins/cast_2d_editor_plugin.h index 36b302f311..cbe7e03008 100644 --- a/editor/plugins/cast_2d_editor_plugin.h +++ b/editor/plugins/cast_2d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef CAST_2D_EDITOR_PLUGIN_H #define CAST_2D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/node_2d.h" class CanvasItemEditor; diff --git a/editor/plugins/collision_shape_2d_editor_plugin.h b/editor/plugins/collision_shape_2d_editor_plugin.h index 19b2de3821..672e1d9ce0 100644 --- a/editor/plugins/collision_shape_2d_editor_plugin.h +++ b/editor/plugins/collision_shape_2d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef COLLISION_SHAPE_2D_EDITOR_PLUGIN_H #define COLLISION_SHAPE_2D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/physics/collision_shape_2d.h" class CanvasItemEditor; diff --git a/editor/plugins/control_editor_plugin.cpp b/editor/plugins/control_editor_plugin.cpp index 5b0831eeb8..939527b56f 100644 --- a/editor/plugins/control_editor_plugin.cpp +++ b/editor/plugins/control_editor_plugin.cpp @@ -413,7 +413,15 @@ bool EditorInspectorPluginControl::can_handle(Object *p_object) { return Object::cast_to<Control>(p_object) != nullptr; } +void EditorInspectorPluginControl::parse_category(Object *p_object, const String &p_category) { + inside_control_category = p_category == "Control"; +} + void EditorInspectorPluginControl::parse_group(Object *p_object, const String &p_group) { + if (!inside_control_category) { + return; + } + Control *control = Object::cast_to<Control>(p_object); if (!control || p_group != "Layout") { return; diff --git a/editor/plugins/control_editor_plugin.h b/editor/plugins/control_editor_plugin.h index 4a411c0241..be52187cd3 100644 --- a/editor/plugins/control_editor_plugin.h +++ b/editor/plugins/control_editor_plugin.h @@ -32,7 +32,7 @@ #define CONTROL_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/check_box.h" @@ -127,8 +127,11 @@ public: class EditorInspectorPluginControl : public EditorInspectorPlugin { GDCLASS(EditorInspectorPluginControl, EditorInspectorPlugin); + bool inside_control_category = false; + public: virtual bool can_handle(Object *p_object) override; + virtual void parse_category(Object *p_object, const String &p_category) override; virtual void parse_group(Object *p_object, const String &p_group) override; virtual bool parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide = false) override; }; diff --git a/editor/plugins/cpu_particles_2d_editor_plugin.h b/editor/plugins/cpu_particles_2d_editor_plugin.h index a408f771eb..4d59c9981e 100644 --- a/editor/plugins/cpu_particles_2d_editor_plugin.h +++ b/editor/plugins/cpu_particles_2d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef CPU_PARTICLES_2D_EDITOR_PLUGIN_H #define CPU_PARTICLES_2D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/cpu_particles_2d.h" #include "scene/2d/physics/collision_polygon_2d.h" #include "scene/gui/box_container.h" diff --git a/editor/plugins/curve_editor_plugin.h b/editor/plugins/curve_editor_plugin.h index b6a74d9b93..c844f42029 100644 --- a/editor/plugins/curve_editor_plugin.h +++ b/editor/plugins/curve_editor_plugin.h @@ -32,8 +32,8 @@ #define CURVE_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" #include "editor/editor_resource_preview.h" +#include "editor/plugins/editor_plugin.h" #include "scene/resources/curve.h" class EditorSpinSlider; diff --git a/editor/plugins/debugger_editor_plugin.h b/editor/plugins/debugger_editor_plugin.h index b7453e3e81..a6df83496e 100644 --- a/editor/plugins/debugger_editor_plugin.h +++ b/editor/plugins/debugger_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef DEBUGGER_EDITOR_PLUGIN_H #define DEBUGGER_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class EditorFileServer; class MenuButton; diff --git a/editor/editor_plugin.compat.inc b/editor/plugins/editor_plugin.compat.inc index 7edf938604..7edf938604 100644 --- a/editor/editor_plugin.compat.inc +++ b/editor/plugins/editor_plugin.compat.inc diff --git a/editor/editor_plugin.cpp b/editor/plugins/editor_plugin.cpp index f42a1555a2..f42a1555a2 100644 --- a/editor/editor_plugin.cpp +++ b/editor/plugins/editor_plugin.cpp diff --git a/editor/editor_plugin.h b/editor/plugins/editor_plugin.h index f45a512b89..f45a512b89 100644 --- a/editor/editor_plugin.h +++ b/editor/plugins/editor_plugin.h diff --git a/editor/editor_plugin_settings.cpp b/editor/plugins/editor_plugin_settings.cpp index 2920cf19c0..2920cf19c0 100644 --- a/editor/editor_plugin_settings.cpp +++ b/editor/plugins/editor_plugin_settings.cpp diff --git a/editor/editor_plugin_settings.h b/editor/plugins/editor_plugin_settings.h index 8db8ffd78c..5b470b3e58 100644 --- a/editor/editor_plugin_settings.h +++ b/editor/plugins/editor_plugin_settings.h @@ -32,7 +32,7 @@ #define EDITOR_PLUGIN_SETTINGS_H #include "editor/editor_data.h" -#include "editor/plugin_config_dialog.h" +#include "editor/plugins/plugin_config_dialog.h" class Tree; diff --git a/editor/plugins/editor_preview_plugins.cpp b/editor/plugins/editor_preview_plugins.cpp index 70213b280c..f2b38536b5 100644 --- a/editor/plugins/editor_preview_plugins.cpp +++ b/editor/plugins/editor_preview_plugins.cpp @@ -130,7 +130,7 @@ Ref<Texture2D> EditorTexturePreviewPlugin::generate(const Ref<Resource> &p_from, if (new_size.y > p_size.y) { new_size = Vector2(new_size.x * p_size.y / new_size.y, p_size.y); } - Vector2i new_size_i = Vector2i(new_size).max(Vector2i(1, 1)); + Vector2i new_size_i = Vector2i(new_size).maxi(1); img->resize(new_size_i.x, new_size_i.y, Image::INTERPOLATE_CUBIC); post_process_preview(img); @@ -678,6 +678,8 @@ Ref<Texture2D> EditorAudioStreamPreviewPlugin::generate(const Ref<Resource> &p_f } } + p_metadata["length"] = stream->get_length(); + //post_process_preview(img); Ref<Image> image = Image::create_from_data(w, h, false, Image::FORMAT_RGB8, img); diff --git a/editor/plugins/editor_resource_tooltip_plugins.cpp b/editor/plugins/editor_resource_tooltip_plugins.cpp index fab8ee9f59..dfeb59214c 100644 --- a/editor/plugins/editor_resource_tooltip_plugins.cpp +++ b/editor/plugins/editor_resource_tooltip_plugins.cpp @@ -103,6 +103,7 @@ bool EditorTextureTooltipPlugin::handles(const String &p_resource_type) const { Control *EditorTextureTooltipPlugin::make_tooltip_for_path(const String &p_resource_path, const Dictionary &p_metadata, Control *p_base) const { HBoxContainer *hb = memnew(HBoxContainer); VBoxContainer *vb = Object::cast_to<VBoxContainer>(p_base); + DEV_ASSERT(vb); vb->set_alignment(BoxContainer::ALIGNMENT_CENTER); Vector2 dimensions = p_metadata.get("dimensions", Vector2()); @@ -117,3 +118,29 @@ Control *EditorTextureTooltipPlugin::make_tooltip_for_path(const String &p_resou hb->add_child(vb); return hb; } + +// EditorAudioStreamTooltipPlugin + +bool EditorAudioStreamTooltipPlugin::handles(const String &p_resource_type) const { + return ClassDB::is_parent_class(p_resource_type, "AudioStream"); +} + +Control *EditorAudioStreamTooltipPlugin::make_tooltip_for_path(const String &p_resource_path, const Dictionary &p_metadata, Control *p_base) const { + VBoxContainer *vb = Object::cast_to<VBoxContainer>(p_base); + DEV_ASSERT(vb); + + double length = p_metadata.get("length", 0.0); + if (length >= 60.0) { + vb->add_child(memnew(Label(vformat(TTR("Length: %0dm %0ds"), int(length / 60.0), int(fmod(length, 60)))))); + } else if (length >= 1.0) { + vb->add_child(memnew(Label(vformat(TTR("Length: %0.1fs"), length)))); + } else { + vb->add_child(memnew(Label(vformat(TTR("Length: %0.3fs"), length)))); + } + + TextureRect *tr = memnew(TextureRect); + vb->add_child(tr); + request_thumbnail(p_resource_path, tr); + + return vb; +} diff --git a/editor/plugins/editor_resource_tooltip_plugins.h b/editor/plugins/editor_resource_tooltip_plugins.h index e3a27de0bb..43be8fd8e8 100644 --- a/editor/plugins/editor_resource_tooltip_plugins.h +++ b/editor/plugins/editor_resource_tooltip_plugins.h @@ -33,9 +33,8 @@ #include "core/object/gdvirtual.gen.inc" #include "core/object/ref_counted.h" -#include <scene/gui/control.h> +#include "scene/gui/control.h" -class Control; class Texture2D; class TextureRect; class VBoxContainer; @@ -67,4 +66,12 @@ public: virtual Control *make_tooltip_for_path(const String &p_resource_path, const Dictionary &p_metadata, Control *p_base) const override; }; +class EditorAudioStreamTooltipPlugin : public EditorResourceTooltipPlugin { + GDCLASS(EditorAudioStreamTooltipPlugin, EditorResourceTooltipPlugin); + +public: + virtual bool handles(const String &p_resource_type) const override; + virtual Control *make_tooltip_for_path(const String &p_resource_path, const Dictionary &p_metadata, Control *p_base) const override; +}; + #endif // EDITOR_RESOURCE_TOOLTIP_PLUGINS_H diff --git a/editor/plugins/font_config_plugin.h b/editor/plugins/font_config_plugin.h index 1adb578950..7b2d26da4a 100644 --- a/editor/plugins/font_config_plugin.h +++ b/editor/plugins/font_config_plugin.h @@ -32,9 +32,9 @@ #define FONT_CONFIG_PLUGIN_H #include "core/io/marshalls.h" -#include "editor/editor_plugin.h" #include "editor/editor_properties.h" #include "editor/editor_properties_array_dict.h" +#include "editor/plugins/editor_plugin.h" /*************************************************************************/ diff --git a/editor/plugins/gizmos/navigation_link_3d_gizmo_plugin.cpp b/editor/plugins/gizmos/navigation_link_3d_gizmo_plugin.cpp index 3717b4b1a3..51d15a0a70 100644 --- a/editor/plugins/gizmos/navigation_link_3d_gizmo_plugin.cpp +++ b/editor/plugins/gizmos/navigation_link_3d_gizmo_plugin.cpp @@ -165,7 +165,7 @@ void NavigationLink3DGizmoPlugin::set_handle(const EditorNode3DGizmo *p_gizmo, i if (Node3DEditor::get_singleton()->is_snap_enabled()) { double snap = Node3DEditor::get_singleton()->get_translate_snap(); - intersection.snap(Vector3(snap, snap, snap)); + intersection.snapf(snap); } position = gi.xform(intersection); diff --git a/editor/plugins/gizmos/navigation_region_3d_gizmo_plugin.cpp b/editor/plugins/gizmos/navigation_region_3d_gizmo_plugin.cpp index 088b09ea46..14105f0b3b 100644 --- a/editor/plugins/gizmos/navigation_region_3d_gizmo_plugin.cpp +++ b/editor/plugins/gizmos/navigation_region_3d_gizmo_plugin.cpp @@ -108,8 +108,8 @@ void NavigationRegion3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { for (int j = 0; j < 3; j++) { tw[tidx++] = f.vertex[j]; _EdgeKey ek; - ek.from = f.vertex[j].snapped(Vector3(CMP_EPSILON, CMP_EPSILON, CMP_EPSILON)); - ek.to = f.vertex[(j + 1) % 3].snapped(Vector3(CMP_EPSILON, CMP_EPSILON, CMP_EPSILON)); + ek.from = f.vertex[j].snappedf(CMP_EPSILON); + ek.to = f.vertex[(j + 1) % 3].snappedf(CMP_EPSILON); if (ek.from < ek.to) { SWAP(ek.from, ek.to); } diff --git a/editor/plugins/gizmos/occluder_instance_3d_gizmo_plugin.cpp b/editor/plugins/gizmos/occluder_instance_3d_gizmo_plugin.cpp index 37c3a05b50..d6f649ab9c 100644 --- a/editor/plugins/gizmos/occluder_instance_3d_gizmo_plugin.cpp +++ b/editor/plugins/gizmos/occluder_instance_3d_gizmo_plugin.cpp @@ -163,9 +163,9 @@ void OccluderInstance3DGizmoPlugin::set_handle(const EditorNode3DGizmo *p_gizmo, if (p_id == 2) { Vector2 s = Vector2(intersection.x, intersection.y) * 2.0f; if (snap_enabled) { - s = s.snapped(Vector2(snap, snap)); + s = s.snappedf(snap); } - s = s.max(Vector2(0.001, 0.001)); + s = s.maxf(0.001); qo->set_size(s); } else { float d = intersection[p_id]; diff --git a/editor/plugins/gpu_particles_2d_editor_plugin.h b/editor/plugins/gpu_particles_2d_editor_plugin.h index aad623ee60..bb0ca5de3a 100644 --- a/editor/plugins/gpu_particles_2d_editor_plugin.h +++ b/editor/plugins/gpu_particles_2d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef GPU_PARTICLES_2D_EDITOR_PLUGIN_H #define GPU_PARTICLES_2D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/gpu_particles_2d.h" #include "scene/2d/physics/collision_polygon_2d.h" #include "scene/gui/box_container.h" diff --git a/editor/plugins/gpu_particles_3d_editor_plugin.h b/editor/plugins/gpu_particles_3d_editor_plugin.h index 176a45df56..3b2ab2f8ca 100644 --- a/editor/plugins/gpu_particles_3d_editor_plugin.h +++ b/editor/plugins/gpu_particles_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef GPU_PARTICLES_3D_EDITOR_PLUGIN_H #define GPU_PARTICLES_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/gpu_particles_3d.h" #include "scene/gui/spin_box.h" diff --git a/editor/plugins/gpu_particles_collision_sdf_editor_plugin.h b/editor/plugins/gpu_particles_collision_sdf_editor_plugin.h index 9f59f6e2cd..bba8bd2584 100644 --- a/editor/plugins/gpu_particles_collision_sdf_editor_plugin.h +++ b/editor/plugins/gpu_particles_collision_sdf_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef GPU_PARTICLES_COLLISION_SDF_EDITOR_PLUGIN_H #define GPU_PARTICLES_COLLISION_SDF_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/gpu_particles_collision_3d.h" #include "scene/resources/material.h" diff --git a/editor/plugins/gradient_editor_plugin.h b/editor/plugins/gradient_editor_plugin.h index 06d79d55ab..f211e4ef30 100644 --- a/editor/plugins/gradient_editor_plugin.h +++ b/editor/plugins/gradient_editor_plugin.h @@ -32,7 +32,7 @@ #define GRADIENT_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class EditorSpinSlider; class ColorPicker; diff --git a/editor/plugins/gradient_texture_2d_editor_plugin.cpp b/editor/plugins/gradient_texture_2d_editor_plugin.cpp index bb6096ea34..ebc00c49bb 100644 --- a/editor/plugins/gradient_texture_2d_editor_plugin.cpp +++ b/editor/plugins/gradient_texture_2d_editor_plugin.cpp @@ -42,7 +42,7 @@ Point2 GradientTexture2DEdit::_get_handle_pos(const Handle p_handle) { // Get the handle's mouse position in pixels relative to offset. - return (p_handle == HANDLE_FROM ? texture->get_fill_from() : texture->get_fill_to()).clamp(Vector2(), Vector2(1, 1)) * size; + return (p_handle == HANDLE_FROM ? texture->get_fill_from() : texture->get_fill_to()).clampf(0, 1) * size; } GradientTexture2DEdit::Handle GradientTexture2DEdit::get_handle_at(const Vector2 &p_pos) { @@ -112,9 +112,9 @@ void GradientTexture2DEdit::gui_input(const Ref<InputEvent> &p_event) { return; } - Vector2 new_pos = (mpos / size).clamp(Vector2(0, 0), Vector2(1, 1)); + Vector2 new_pos = (mpos / size).clampf(0, 1); if (snap_enabled || mm->is_command_or_control_pressed()) { - new_pos = new_pos.snapped(Vector2(1.0 / snap_count, 1.0 / snap_count)); + new_pos = new_pos.snappedf(1.0 / snap_count); } // Allow to snap to an axis with Shift. diff --git a/editor/plugins/gradient_texture_2d_editor_plugin.h b/editor/plugins/gradient_texture_2d_editor_plugin.h index 33570593cc..a9f247c3cc 100644 --- a/editor/plugins/gradient_texture_2d_editor_plugin.h +++ b/editor/plugins/gradient_texture_2d_editor_plugin.h @@ -32,7 +32,7 @@ #define GRADIENT_TEXTURE_2D_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class Button; class EditorSpinSlider; diff --git a/editor/plugins/input_event_editor_plugin.h b/editor/plugins/input_event_editor_plugin.h index 779e59edd4..48c16a4807 100644 --- a/editor/plugins/input_event_editor_plugin.h +++ b/editor/plugins/input_event_editor_plugin.h @@ -33,7 +33,7 @@ #include "editor/action_map_editor.h" #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class InputEventConfigContainer : public VBoxContainer { GDCLASS(InputEventConfigContainer, VBoxContainer); diff --git a/editor/plugins/lightmap_gi_editor_plugin.h b/editor/plugins/lightmap_gi_editor_plugin.h index 1234438c8d..3e739adf9e 100644 --- a/editor/plugins/lightmap_gi_editor_plugin.h +++ b/editor/plugins/lightmap_gi_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef LIGHTMAP_GI_EDITOR_PLUGIN_H #define LIGHTMAP_GI_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/lightmap_gi.h" #include "scene/resources/material.h" diff --git a/editor/plugins/material_editor_plugin.h b/editor/plugins/material_editor_plugin.h index c60de1ade9..fb6bafc0ef 100644 --- a/editor/plugins/material_editor_plugin.h +++ b/editor/plugins/material_editor_plugin.h @@ -32,7 +32,7 @@ #define MATERIAL_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/editor_resource_conversion_plugin.h" #include "scene/resources/3d/primitive_meshes.h" #include "scene/resources/material.h" diff --git a/editor/plugins/mesh_editor_plugin.h b/editor/plugins/mesh_editor_plugin.h index a8ef476f84..85d92e7800 100644 --- a/editor/plugins/mesh_editor_plugin.h +++ b/editor/plugins/mesh_editor_plugin.h @@ -32,7 +32,7 @@ #define MESH_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/camera_3d.h" #include "scene/3d/light_3d.h" #include "scene/3d/mesh_instance_3d.h" diff --git a/editor/plugins/mesh_instance_3d_editor_plugin.h b/editor/plugins/mesh_instance_3d_editor_plugin.h index ce7d23239c..20c151fb92 100644 --- a/editor/plugins/mesh_instance_3d_editor_plugin.h +++ b/editor/plugins/mesh_instance_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef MESH_INSTANCE_3D_EDITOR_PLUGIN_H #define MESH_INSTANCE_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/mesh_instance_3d.h" #include "scene/gui/option_button.h" diff --git a/editor/plugins/mesh_library_editor_plugin.h b/editor/plugins/mesh_library_editor_plugin.h index 3895d10c37..94f46beea1 100644 --- a/editor/plugins/mesh_library_editor_plugin.h +++ b/editor/plugins/mesh_library_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef MESH_LIBRARY_EDITOR_PLUGIN_H #define MESH_LIBRARY_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/resources/3d/mesh_library.h" class EditorFileDialog; diff --git a/editor/plugins/multimesh_editor_plugin.h b/editor/plugins/multimesh_editor_plugin.h index b21a932809..5051926c64 100644 --- a/editor/plugins/multimesh_editor_plugin.h +++ b/editor/plugins/multimesh_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef MULTIMESH_EDITOR_PLUGIN_H #define MULTIMESH_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/multimesh_instance_3d.h" #include "scene/gui/slider.h" #include "scene/gui/spin_box.h" diff --git a/editor/plugins/navigation_link_2d_editor_plugin.h b/editor/plugins/navigation_link_2d_editor_plugin.h index ea731ca2cd..7a4be18c31 100644 --- a/editor/plugins/navigation_link_2d_editor_plugin.h +++ b/editor/plugins/navigation_link_2d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef NAVIGATION_LINK_2D_EDITOR_PLUGIN_H #define NAVIGATION_LINK_2D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/navigation_link_2d.h" class CanvasItemEditor; diff --git a/editor/plugins/navigation_obstacle_3d_editor_plugin.cpp b/editor/plugins/navigation_obstacle_3d_editor_plugin.cpp index 869f5b3b10..61b43eaaf1 100644 --- a/editor/plugins/navigation_obstacle_3d_editor_plugin.cpp +++ b/editor/plugins/navigation_obstacle_3d_editor_plugin.cpp @@ -330,9 +330,7 @@ EditorPlugin::AfterGUIInput NavigationObstacle3DEditor::forward_3d_gui_input(Cam } if (!snap_ignore && Node3DEditor::get_singleton()->is_snap_enabled()) { - cpoint = cpoint.snapped(Vector2( - Node3DEditor::get_singleton()->get_translate_snap(), - Node3DEditor::get_singleton()->get_translate_snap())); + cpoint = cpoint.snappedf(Node3DEditor::get_singleton()->get_translate_snap()); } edited_point_pos = cpoint; diff --git a/editor/plugins/navigation_obstacle_3d_editor_plugin.h b/editor/plugins/navigation_obstacle_3d_editor_plugin.h index 74094dc86f..c62a5a281b 100644 --- a/editor/plugins/navigation_obstacle_3d_editor_plugin.h +++ b/editor/plugins/navigation_obstacle_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef NAVIGATION_OBSTACLE_3D_EDITOR_PLUGIN_H #define NAVIGATION_OBSTACLE_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/mesh_instance_3d.h" #include "scene/3d/physics/collision_polygon_3d.h" #include "scene/gui/box_container.h" diff --git a/editor/plugins/navigation_polygon_editor_plugin.h b/editor/plugins/navigation_polygon_editor_plugin.h index bf2474bc55..4d6d245cc5 100644 --- a/editor/plugins/navigation_polygon_editor_plugin.h +++ b/editor/plugins/navigation_polygon_editor_plugin.h @@ -33,7 +33,7 @@ #include "editor/plugins/abstract_polygon_2d_editor.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class AcceptDialog; class HBoxContainer; diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index be14132185..4a418e62ca 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -1448,7 +1448,7 @@ Transform3D Node3DEditorViewport::_compute_transform(TransformMode p_mode, const switch (p_mode) { case TRANSFORM_SCALE: { if (_edit.snap || spatial_editor->is_snap_enabled()) { - p_motion.snap(Vector3(p_extra, p_extra, p_extra)); + p_motion.snapf(p_extra); } Transform3D s; if (p_local) { @@ -1469,7 +1469,7 @@ Transform3D Node3DEditorViewport::_compute_transform(TransformMode p_mode, const } case TRANSFORM_TRANSLATE: { if (_edit.snap || spatial_editor->is_snap_enabled()) { - p_motion.snap(Vector3(p_extra, p_extra, p_extra)); + p_motion.snapf(p_extra); } if (p_local) { @@ -4786,7 +4786,7 @@ void Node3DEditorViewport::update_transform(bool p_shift) { snap = spatial_editor->get_scale_snap() / 100; } Vector3 motion_snapped = motion; - motion_snapped.snap(Vector3(snap, snap, snap)); + motion_snapped.snapf(snap); // This might not be necessary anymore after issue #288 is solved (in 4.0?). // TRANSLATORS: Refers to changing the scale of a node in the 3D editor. set_message(TTR("Scaling:") + " (" + String::num(motion_snapped.x, snap_step_decimals) + ", " + @@ -4858,7 +4858,7 @@ void Node3DEditorViewport::update_transform(bool p_shift) { snap = spatial_editor->get_translate_snap(); } Vector3 motion_snapped = motion; - motion_snapped.snap(Vector3(snap, snap, snap)); + motion_snapped.snapf(snap); // TRANSLATORS: Refers to changing the position of a node in the 3D editor. set_message(TTR("Translating:") + " (" + String::num(motion_snapped.x, snap_step_decimals) + ", " + String::num(motion_snapped.y, snap_step_decimals) + ", " + String::num(motion_snapped.z, snap_step_decimals) + ")"); @@ -8997,7 +8997,7 @@ void Node3DEditorPlugin::set_state(const Dictionary &p_state) { Vector3 Node3DEditor::snap_point(Vector3 p_target, Vector3 p_start) const { if (is_snap_enabled()) { real_t snap = get_translate_snap(); - p_target.snap(Vector3(snap, snap, snap)); + p_target.snapf(snap); } return p_target; } diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h index 66fa932f7c..96210de403 100644 --- a/editor/plugins/node_3d_editor_plugin.h +++ b/editor/plugins/node_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef NODE_3D_EDITOR_PLUGIN_H #define NODE_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/node_3d_editor_gizmos.h" #include "editor/themes/editor_scale.h" #include "scene/gui/box_container.h" diff --git a/editor/plugins/occluder_instance_3d_editor_plugin.h b/editor/plugins/occluder_instance_3d_editor_plugin.h index 54ac95c9e1..7920ff59c9 100644 --- a/editor/plugins/occluder_instance_3d_editor_plugin.h +++ b/editor/plugins/occluder_instance_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef OCCLUDER_INSTANCE_3D_EDITOR_PLUGIN_H #define OCCLUDER_INSTANCE_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/occluder_instance_3d.h" #include "scene/resources/material.h" diff --git a/editor/plugins/packed_scene_editor_plugin.h b/editor/plugins/packed_scene_editor_plugin.h index 308bcf1c05..0e18d98978 100644 --- a/editor/plugins/packed_scene_editor_plugin.h +++ b/editor/plugins/packed_scene_editor_plugin.h @@ -32,7 +32,7 @@ #define PACKED_SCENE_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/box_container.h" class PackedSceneEditor : public VBoxContainer { diff --git a/editor/plugins/parallax_background_editor_plugin.h b/editor/plugins/parallax_background_editor_plugin.h index ba394d04dc..07a562edac 100644 --- a/editor/plugins/parallax_background_editor_plugin.h +++ b/editor/plugins/parallax_background_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef PARALLAX_BACKGROUND_EDITOR_PLUGIN_H #define PARALLAX_BACKGROUND_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class HBoxContainer; class MenuButton; diff --git a/editor/plugins/particle_process_material_editor_plugin.h b/editor/plugins/particle_process_material_editor_plugin.h index 0d9725397e..623d3d4ba3 100644 --- a/editor/plugins/particle_process_material_editor_plugin.h +++ b/editor/plugins/particle_process_material_editor_plugin.h @@ -31,8 +31,8 @@ #ifndef PARTICLE_PROCESS_MATERIAL_EDITOR_PLUGIN_H #define PARTICLE_PROCESS_MATERIAL_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" #include "editor/editor_properties.h" +#include "editor/plugins/editor_plugin.h" class Button; class EditorSpinSlider; diff --git a/editor/plugins/path_2d_editor_plugin.h b/editor/plugins/path_2d_editor_plugin.h index 8efd651494..7d1a64160b 100644 --- a/editor/plugins/path_2d_editor_plugin.h +++ b/editor/plugins/path_2d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef PATH_2D_EDITOR_PLUGIN_H #define PATH_2D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/path_2d.h" #include "scene/gui/box_container.h" diff --git a/editor/plugins/path_3d_editor_plugin.cpp b/editor/plugins/path_3d_editor_plugin.cpp index 4e317114a3..f09318277a 100644 --- a/editor/plugins/path_3d_editor_plugin.cpp +++ b/editor/plugins/path_3d_editor_plugin.cpp @@ -117,7 +117,7 @@ void Path3DGizmo::set_handle(int p_id, bool p_secondary, Camera3D *p_camera, con if (p.intersects_ray(ray_from, ray_dir, &inters)) { if (Node3DEditor::get_singleton()->is_snap_enabled()) { float snap = Node3DEditor::get_singleton()->get_translate_snap(); - inters.snap(Vector3(snap, snap, snap)); + inters.snapf(snap); } Vector3 local = gi.xform(inters); @@ -146,7 +146,7 @@ void Path3DGizmo::set_handle(int p_id, bool p_secondary, Camera3D *p_camera, con Vector3 local = gi.xform(inters) - base; if (Node3DEditor::get_singleton()->is_snap_enabled()) { float snap = Node3DEditor::get_singleton()->get_translate_snap(); - local.snap(Vector3(snap, snap, snap)); + local.snapf(snap); } if (info.type == HandleType::HANDLE_TYPE_IN) { diff --git a/editor/plugins/path_3d_editor_plugin.h b/editor/plugins/path_3d_editor_plugin.h index 6a933a419f..ee73df1617 100644 --- a/editor/plugins/path_3d_editor_plugin.h +++ b/editor/plugins/path_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef PATH_3D_EDITOR_PLUGIN_H #define PATH_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/node_3d_editor_gizmos.h" #include "scene/3d/camera_3d.h" #include "scene/3d/path_3d.h" diff --git a/editor/plugins/physical_bone_3d_editor_plugin.h b/editor/plugins/physical_bone_3d_editor_plugin.h index 5c49e641a5..fb6f30cc57 100644 --- a/editor/plugins/physical_bone_3d_editor_plugin.h +++ b/editor/plugins/physical_bone_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef PHYSICAL_BONE_3D_EDITOR_PLUGIN_H #define PHYSICAL_BONE_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" diff --git a/editor/plugin_config_dialog.cpp b/editor/plugins/plugin_config_dialog.cpp index 6f54a1c57a..a535b18b9d 100644 --- a/editor/plugin_config_dialog.cpp +++ b/editor/plugins/plugin_config_dialog.cpp @@ -33,9 +33,10 @@ #include "core/io/config_file.h" #include "core/io/dir_access.h" #include "core/object/script_language.h" +#include "editor/editor_file_system.h" #include "editor/editor_node.h" -#include "editor/editor_plugin.h" #include "editor/gui/editor_validation_panel.h" +#include "editor/plugins/editor_plugin.h" #include "editor/project_settings_editor.h" #include "editor/themes/editor_scale.h" #include "scene/gui/grid_container.h" @@ -58,29 +59,35 @@ void PluginConfigDialog::_on_confirmed() { return; } } - - int lang_idx = script_option_edit->get_selected(); - ScriptLanguage *language = ScriptServer::get_language(lang_idx); - if (language == nullptr) { - return; - } - String ext = language->get_extension(); - String script_name = script_edit->get_text().is_empty() ? _get_subfolder() : script_edit->get_text(); - if (script_name.get_extension() != ext) { - script_name += "." + ext; - } - String script_path = path.path_join(script_name); - + // Create the plugin.cfg file. Ref<ConfigFile> cf = memnew(ConfigFile); + cf->load(path.path_join("plugin.cfg")); cf->set_value("plugin", "name", name_edit->get_text()); cf->set_value("plugin", "description", desc_edit->get_text()); cf->set_value("plugin", "author", author_edit->get_text()); cf->set_value("plugin", "version", version_edit->get_text()); - cf->set_value("plugin", "script", script_name); - + // Language-specific settings. + int lang_index = script_option_edit->get_selected(); + _create_script_for_plugin(path, cf, lang_index); + // Save and inform the editor. cf->save(path.path_join("plugin.cfg")); + EditorNode::get_singleton()->get_project_settings()->update_plugins(); + EditorFileSystem::get_singleton()->scan(); + _clear_fields(); +} - if (!_edit_mode) { +void PluginConfigDialog::_create_script_for_plugin(const String &p_plugin_path, Ref<ConfigFile> p_config_file, int p_script_lang_index) { + ScriptLanguage *language = ScriptServer::get_language(p_script_lang_index); + ERR_FAIL_COND(language == nullptr); + String ext = language->get_extension(); + String script_name = script_edit->get_text().is_empty() ? _get_subfolder() : script_edit->get_text(); + if (script_name.get_extension() != ext) { + script_name += "." + ext; + } + String script_path = p_plugin_path.path_join(script_name); + p_config_file->set_value("plugin", "script", script_name); + // If the requested script does not exist, create it. + if (!_edit_mode && !FileAccess::exists(script_path)) { String class_name = script_name.get_basename(); String template_content = ""; Vector<ScriptLanguage::ScriptTemplate> templates = language->get_built_in_templates("EditorPlugin"); @@ -90,12 +97,9 @@ void PluginConfigDialog::_on_confirmed() { Ref<Script> scr = language->make_template(template_content, class_name, "EditorPlugin"); scr->set_path(script_path, true); ResourceSaver::save(scr); - + p_config_file->save(p_plugin_path.path_join("plugin.cfg")); emit_signal(SNAME("plugin_ready"), scr.ptr(), active_edit->is_pressed() ? _to_absolute_plugin_path(_get_subfolder()) : ""); - } else { - EditorNode::get_singleton()->get_project_settings()->update_plugins(); } - _clear_fields(); } void PluginConfigDialog::_on_canceled() { @@ -103,19 +107,9 @@ void PluginConfigDialog::_on_canceled() { } void PluginConfigDialog::_on_required_text_changed() { - int lang_idx = script_option_edit->get_selected(); - ScriptLanguage *language = ScriptServer::get_language(lang_idx); - if (language == nullptr) { - return; - } - String ext = language->get_extension(); - if (name_edit->get_text().is_empty()) { validation_panel->set_message(MSG_ID_PLUGIN, TTR("Plugin name cannot be blank."), EditorValidationPanel::MSG_ERROR); } - if ((!script_edit->get_text().get_extension().is_empty() && script_edit->get_text().get_extension() != ext) || script_edit->get_text().ends_with(".")) { - validation_panel->set_message(MSG_ID_SCRIPT, vformat(TTR("Script extension must match chosen language extension (.%s)."), ext), EditorValidationPanel::MSG_ERROR); - } if (subfolder_edit->is_visible()) { if (!subfolder_edit->get_text().is_empty() && !subfolder_edit->get_text().is_valid_filename()) { validation_panel->set_message(MSG_ID_SUBFOLDER, TTR("Subfolder name is not a valid folder name."), EditorValidationPanel::MSG_ERROR); @@ -128,6 +122,16 @@ void PluginConfigDialog::_on_required_text_changed() { } else { validation_panel->set_message(MSG_ID_SUBFOLDER, "", EditorValidationPanel::MSG_OK); } + // Language and script validation. + int lang_idx = script_option_edit->get_selected(); + ScriptLanguage *language = ScriptServer::get_language(lang_idx); + if (language == nullptr) { + return; + } + String ext = language->get_extension(); + if ((!script_edit->get_text().get_extension().is_empty() && script_edit->get_text().get_extension() != ext) || script_edit->get_text().ends_with(".")) { + validation_panel->set_message(MSG_ID_SCRIPT, vformat(TTR("Script extension must match chosen language extension (.%s)."), ext), EditorValidationPanel::MSG_ERROR); + } if (active_edit->is_visible()) { if (language->get_name() == "C#") { active_edit->set_pressed(false); @@ -295,10 +299,10 @@ PluginConfigDialog::PluginConfigDialog() { grid->add_child(script_option_edit); // Plugin Script Name - Label *script_lb = memnew(Label); - script_lb->set_text(TTR("Script Name:")); - script_lb->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); - grid->add_child(script_lb); + Label *script_name_label = memnew(Label); + script_name_label->set_text(TTR("Script Name:")); + script_name_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(script_name_label); script_edit = memnew(LineEdit); script_edit->set_tooltip_text(TTR("Optional. The path to the script (relative to the add-on folder). If left empty, will default to \"plugin.gd\".")); @@ -307,11 +311,11 @@ PluginConfigDialog::PluginConfigDialog() { grid->add_child(script_edit); // Activate now checkbox - Label *active_lb = memnew(Label); - active_lb->set_text(TTR("Activate now?")); - active_lb->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); - grid->add_child(active_lb); - plugin_edit_hidden_controls.push_back(active_lb); + Label *active_label = memnew(Label); + active_label->set_text(TTR("Activate now?")); + active_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(active_label); + plugin_edit_hidden_controls.push_back(active_label); active_edit = memnew(CheckBox); active_edit->set_pressed(true); diff --git a/editor/plugin_config_dialog.h b/editor/plugins/plugin_config_dialog.h index 2fdf6368a0..7d6eab5e18 100644 --- a/editor/plugin_config_dialog.h +++ b/editor/plugins/plugin_config_dialog.h @@ -39,6 +39,7 @@ #include "scene/gui/text_edit.h" #include "scene/gui/texture_rect.h" +class ConfigFile; class EditorValidationPanel; class PluginConfigDialog : public ConfirmationDialog { @@ -70,6 +71,7 @@ class PluginConfigDialog : public ConfirmationDialog { void _on_confirmed(); void _on_canceled(); void _on_required_text_changed(); + void _create_script_for_plugin(const String &p_plugin_path, Ref<ConfigFile> p_config_file, int p_script_lang_index); String _get_subfolder(); static String _to_absolute_plugin_path(const String &p_plugin_name); diff --git a/editor/plugins/polygon_3d_editor_plugin.cpp b/editor/plugins/polygon_3d_editor_plugin.cpp index da84afc4d7..7c41093774 100644 --- a/editor/plugins/polygon_3d_editor_plugin.cpp +++ b/editor/plugins/polygon_3d_editor_plugin.cpp @@ -334,9 +334,7 @@ EditorPlugin::AfterGUIInput Polygon3DEditor::forward_3d_gui_input(Camera3D *p_ca } if (!snap_ignore && Node3DEditor::get_singleton()->is_snap_enabled()) { - cpoint = cpoint.snapped(Vector2( - Node3DEditor::get_singleton()->get_translate_snap(), - Node3DEditor::get_singleton()->get_translate_snap())); + cpoint = cpoint.snappedf(Node3DEditor::get_singleton()->get_translate_snap()); } edited_point_pos = cpoint; diff --git a/editor/plugins/polygon_3d_editor_plugin.h b/editor/plugins/polygon_3d_editor_plugin.h index 85cfd807e4..f496638515 100644 --- a/editor/plugins/polygon_3d_editor_plugin.h +++ b/editor/plugins/polygon_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef POLYGON_3D_EDITOR_PLUGIN_H #define POLYGON_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/mesh_instance_3d.h" #include "scene/3d/physics/collision_polygon_3d.h" #include "scene/gui/box_container.h" diff --git a/editor/plugins/resource_preloader_editor_plugin.h b/editor/plugins/resource_preloader_editor_plugin.h index 7a4cabbb69..76ef2fe9a4 100644 --- a/editor/plugins/resource_preloader_editor_plugin.h +++ b/editor/plugins/resource_preloader_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef RESOURCE_PRELOADER_EDITOR_PLUGIN_H #define RESOURCE_PRELOADER_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/dialogs.h" #include "scene/gui/panel_container.h" #include "scene/gui/tree.h" diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index cc9e887448..c48af90622 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -273,6 +273,7 @@ void ScriptEditorBase::_bind_methods() { ADD_SIGNAL(MethodInfo("request_help", PropertyInfo(Variant::STRING, "topic"))); ADD_SIGNAL(MethodInfo("request_open_script_at_line", PropertyInfo(Variant::OBJECT, "script"), PropertyInfo(Variant::INT, "line"))); ADD_SIGNAL(MethodInfo("request_save_history")); + ADD_SIGNAL(MethodInfo("request_save_previous_state", PropertyInfo(Variant::INT, "line"))); ADD_SIGNAL(MethodInfo("go_to_help", PropertyInfo(Variant::STRING, "what"))); ADD_SIGNAL(MethodInfo("search_in_files_requested", PropertyInfo(Variant::STRING, "text"))); ADD_SIGNAL(MethodInfo("replace_in_files_requested", PropertyInfo(Variant::STRING, "text"))); @@ -639,6 +640,32 @@ void ScriptEditor::_save_history() { _update_history_arrows(); } +void ScriptEditor::_save_previous_state(Dictionary p_state) { + if (lock_history) { + // Done as a result of a deferred call triggered by set_edit_state(). + lock_history = false; + return; + } + + if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) { + Node *n = tab_container->get_current_tab_control(); + + if (Object::cast_to<ScriptTextEditor>(n)) { + history.write[history_pos].state = p_state; + } + } + + history.resize(history_pos + 1); + ScriptHistory sh; + sh.control = tab_container->get_current_tab_control(); + sh.state = Variant(); + + history.push_back(sh); + history_pos++; + + _update_history_arrows(); +} + void ScriptEditor::_go_to_tab(int p_idx) { ScriptEditorBase *current = _get_current_editor(); if (current) { @@ -668,8 +695,10 @@ void ScriptEditor::_go_to_tab(int p_idx) { sh.control = c; sh.state = Variant(); - history.push_back(sh); - history_pos++; + if (!lock_history && (history.is_empty() || history[history.size() - 1].control != sh.control)) { + history.push_back(sh); + history_pos++; + } tab_container->set_current_tab(p_idx); @@ -2185,8 +2214,11 @@ void ScriptEditor::_update_script_names() { sd.index = i; sedata.set(i, sd); } + + lock_history = true; _go_to_tab(new_prev_tab); _go_to_tab(new_cur_tab); + lock_history = false; _sort_list_on_update = false; } @@ -2474,6 +2506,10 @@ bool ScriptEditor::edit(const Ref<Resource> &p_resource, int p_line, int p_col, if (script_editor_cache->has_section(p_resource->get_path())) { se->set_edit_state(script_editor_cache->get_value(p_resource->get_path(), "state")); + ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(se); + if (ste) { + ste->store_previous_state(); + } } _sort_list_on_update = true; @@ -2485,6 +2521,7 @@ bool ScriptEditor::edit(const Ref<Resource> &p_resource, int p_line, int p_col, se->connect("request_open_script_at_line", callable_mp(this, &ScriptEditor::_goto_script_line)); se->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto)); se->connect("request_save_history", callable_mp(this, &ScriptEditor::_save_history)); + se->connect("request_save_previous_state", callable_mp(this, &ScriptEditor::_save_previous_state)); se->connect("search_in_files_requested", callable_mp(this, &ScriptEditor::_on_find_in_files_requested)); se->connect("replace_in_files_requested", callable_mp(this, &ScriptEditor::_on_replace_in_files_requested)); se->connect("go_to_method", callable_mp(this, &ScriptEditor::script_goto_method)); @@ -3421,6 +3458,7 @@ void ScriptEditor::_help_class_open(const String &p_class) { _go_to_tab(tab_container->get_tab_count() - 1); eh->go_to_class(p_class); eh->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto)); + eh->connect("request_save_history", callable_mp(this, &ScriptEditor::_save_history)); _add_recent_script(p_class); _sort_list_on_update = true; _update_script_names(); @@ -3549,6 +3587,7 @@ void ScriptEditor::_update_history_pos(int p_new_pos) { ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(n); if (seb) { + lock_history = true; seb->set_edit_state(history[history_pos].state); seb->ensure_focus(); @@ -3558,9 +3597,10 @@ void ScriptEditor::_update_history_pos(int p_new_pos) { } } - if (Object::cast_to<EditorHelp>(n)) { - Object::cast_to<EditorHelp>(n)->set_scroll(history[history_pos].state); - Object::cast_to<EditorHelp>(n)->set_focused(); + EditorHelp *eh = Object::cast_to<EditorHelp>(n); + if (eh) { + eh->set_scroll(history[history_pos].state); + eh->set_focused(); } n->set_meta("__editor_pass", ++edit_pass); diff --git a/editor/plugins/script_editor_plugin.h b/editor/plugins/script_editor_plugin.h index f87cb0958c..e6bb8f14a9 100644 --- a/editor/plugins/script_editor_plugin.h +++ b/editor/plugins/script_editor_plugin.h @@ -32,7 +32,7 @@ #define SCRIPT_EDITOR_PLUGIN_H #include "core/object/script_language.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/dialogs.h" #include "scene/gui/panel_container.h" #include "scene/resources/syntax_highlighter.h" @@ -472,12 +472,14 @@ class ScriptEditor : public PanelContainer { void _history_back(); bool waiting_update_names; + bool lock_history = false; void _help_class_open(const String &p_class); void _help_class_goto(const String &p_desc); bool _help_tab_goto(const String &p_name, const String &p_desc); void _update_history_arrows(); void _save_history(); + void _save_previous_state(Dictionary p_state); void _go_to_tab(int p_idx); void _update_history_pos(int p_new_pos); void _update_script_colors(); diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 0a6eacf11d..561edcf8bf 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -284,8 +284,7 @@ void ScriptTextEditor::_warning_clicked(const Variant &p_line) { if (prev_line.contains("@warning_ignore")) { const int closing_bracket_idx = prev_line.find(")"); const String text_to_insert = ", " + code.quote(quote_style); - prev_line = prev_line.insert(closing_bracket_idx, text_to_insert); - text_editor->set_line(line - 1, prev_line); + text_editor->insert_text(text_to_insert, line - 1, closing_bracket_idx); } else { const int indent = text_editor->get_indent_level(line) / text_editor->get_indent_size(); String annotation_indent; @@ -352,22 +351,26 @@ void ScriptTextEditor::add_callback(const String &p_function, const PackedString if (!language->can_make_function()) { return; } - + code_editor->get_text_editor()->begin_complex_operation(); + code_editor->get_text_editor()->remove_secondary_carets(); + code_editor->get_text_editor()->deselect(); String code = code_editor->get_text_editor()->get_text(); int pos = language->find_function(p_function, code); - code_editor->get_text_editor()->remove_secondary_carets(); if (pos == -1) { - //does not exist - code_editor->get_text_editor()->deselect(); - pos = code_editor->get_text_editor()->get_line_count() + 2; + // Function does not exist, create it at the end of the file. + int last_line = code_editor->get_text_editor()->get_line_count() - 1; String func = language->make_function("", p_function, p_args); - //code=code+func; - code_editor->get_text_editor()->set_caret_line(pos + 1); - code_editor->get_text_editor()->set_caret_column(1000000); //none shall be that big - code_editor->get_text_editor()->insert_text_at_caret("\n\n" + func); + code_editor->get_text_editor()->insert_text("\n\n" + func, last_line, code_editor->get_text_editor()->get_line(last_line).length()); + pos = last_line + 3; + } + // Put caret on the line after the function, after the indent. + int indent_column = 1; + if (EDITOR_GET("text_editor/behavior/indent/type")) { + indent_column = EDITOR_GET("text_editor/behavior/indent/size"); } - code_editor->get_text_editor()->set_caret_line(pos); - code_editor->get_text_editor()->set_caret_column(1); + code_editor->get_text_editor()->set_caret_line(pos, true, true, -1); + code_editor->get_text_editor()->set_caret_column(indent_column); + code_editor->get_text_editor()->end_complex_operation(); } bool ScriptTextEditor::show_members_overview() { @@ -412,6 +415,14 @@ Variant ScriptTextEditor::get_navigation_state() { return code_editor->get_navigation_state(); } +Variant ScriptTextEditor::get_previous_state() { + return code_editor->get_previous_state(); +} + +void ScriptTextEditor::store_previous_state() { + return code_editor->store_previous_state(); +} + void ScriptTextEditor::_convert_case(CodeTextEditor::CaseStyle p_case) { code_editor->convert_case(p_case); } @@ -904,6 +915,18 @@ void ScriptTextEditor::_breakpoint_toggled(int p_row) { EditorDebuggerNode::get_singleton()->set_breakpoint(script->get_path(), p_row + 1, code_editor->get_text_editor()->is_line_breakpointed(p_row)); } +void ScriptTextEditor::_on_caret_moved() { + int current_line = code_editor->get_text_editor()->get_caret_line(); + if (ABS(current_line - previous_line) >= 10) { + Dictionary nav_state = get_navigation_state(); + nav_state["row"] = previous_line; + nav_state["scroll_position"] = -1; + emit_signal(SNAME("request_save_previous_state"), nav_state); + store_previous_state(); + } + previous_line = current_line; +} + void ScriptTextEditor::_lookup_symbol(const String &p_symbol, int p_row, int p_column) { Node *base = get_tree()->get_edited_scene_root(); if (base) { @@ -1315,10 +1338,10 @@ void ScriptTextEditor::_edit_option(int p_op) { callable_mp((Control *)tx, &Control::grab_focus).call_deferred(); } break; case EDIT_MOVE_LINE_UP: { - code_editor->move_lines_up(); + code_editor->get_text_editor()->move_lines_up(); } break; case EDIT_MOVE_LINE_DOWN: { - code_editor->move_lines_down(); + code_editor->get_text_editor()->move_lines_down(); } break; case EDIT_INDENT: { Ref<Script> scr = script; @@ -1335,24 +1358,16 @@ void ScriptTextEditor::_edit_option(int p_op) { tx->unindent_lines(); } break; case EDIT_DELETE_LINE: { - code_editor->delete_lines(); + code_editor->get_text_editor()->delete_lines(); } break; case EDIT_DUPLICATE_SELECTION: { - code_editor->duplicate_selection(); + code_editor->get_text_editor()->duplicate_selection(); } break; case EDIT_DUPLICATE_LINES: { code_editor->get_text_editor()->duplicate_lines(); } break; case EDIT_TOGGLE_FOLD_LINE: { - int previous_line = -1; - for (int caret_idx : tx->get_caret_index_edit_order()) { - int line_idx = tx->get_caret_line(caret_idx); - if (line_idx != previous_line) { - tx->toggle_foldable_line(line_idx); - previous_line = line_idx; - } - } - tx->queue_redraw(); + tx->toggle_foldable_lines_at_carets(); } break; case EDIT_FOLD_ALL_LINES: { tx->fold_all_lines(); @@ -1379,24 +1394,34 @@ void ScriptTextEditor::_edit_option(int p_op) { } tx->begin_complex_operation(); - int begin, end; + tx->begin_multicaret_edit(); + int begin = tx->get_line_count() - 1, end = 0; if (tx->has_selection()) { - begin = tx->get_selection_from_line(); - end = tx->get_selection_to_line(); - // ignore if the cursor is not past the first column - if (tx->get_selection_to_column() == 0) { - end--; + // Auto indent all lines that have a caret or selection on it. + Vector<Point2i> line_ranges = tx->get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + scr->get_language()->auto_indent_code(text, line_range.x, line_range.y); + if (line_range.x < begin) { + begin = line_range.x; + } + if (line_range.y > end) { + end = line_range.y; + } } } else { + // Auto indent entire text. begin = 0; end = tx->get_line_count() - 1; + scr->get_language()->auto_indent_code(text, begin, end); } - scr->get_language()->auto_indent_code(text, begin, end); + + // Apply auto indented code. Vector<String> lines = text.split("\n"); for (int i = begin; i <= end; ++i) { tx->set_line(i, lines[i]); } + tx->end_multicaret_edit(); tx->end_complex_operation(); } break; case EDIT_TRIM_TRAILING_WHITESAPCE: { @@ -1495,13 +1520,12 @@ void ScriptTextEditor::_edit_option(int p_op) { code_editor->remove_all_bookmarks(); } break; case DEBUG_TOGGLE_BREAKPOINT: { - Vector<int> caret_edit_order = tx->get_caret_index_edit_order(); - caret_edit_order.reverse(); + Vector<int> sorted_carets = tx->get_sorted_carets(); int last_line = -1; - for (const int &c : caret_edit_order) { - int from = tx->has_selection(c) ? tx->get_selection_from_line(c) : tx->get_caret_line(c); + for (const int &c : sorted_carets) { + int from = tx->get_selection_from_line(c); from += from == last_line ? 1 : 0; - int to = tx->has_selection(c) ? tx->get_selection_to_line(c) : tx->get_caret_line(c); + int to = tx->get_selection_to_line(c); if (to < from) { continue; } @@ -1988,45 +2012,32 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { tx->apply_ime(); Point2i pos = tx->get_line_column_at_pos(local_pos); - int row = pos.y; - int col = pos.x; + int mouse_line = pos.y; + int mouse_column = pos.x; tx->set_move_caret_on_right_click_enabled(EDITOR_GET("text_editor/behavior/navigation/move_caret_on_right_click")); - int caret_clicked = -1; + int selection_clicked = -1; if (tx->is_move_caret_on_right_click_enabled()) { - if (tx->has_selection()) { - for (int i = 0; i < tx->get_caret_count(); i++) { - int from_line = tx->get_selection_from_line(i); - int to_line = tx->get_selection_to_line(i); - int from_column = tx->get_selection_from_column(i); - int to_column = tx->get_selection_to_column(i); - - if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) { - // Right click in one of the selected text - caret_clicked = i; - break; - } - } - } - if (caret_clicked < 0) { + selection_clicked = tx->get_selection_at_line_column(mouse_line, mouse_column, true); + if (selection_clicked < 0) { tx->deselect(); tx->remove_secondary_carets(); - caret_clicked = 0; - tx->set_caret_line(row, false, false); - tx->set_caret_column(col); + selection_clicked = 0; + tx->set_caret_line(mouse_line, false, false, -1); + tx->set_caret_column(mouse_column); } } String word_at_pos = tx->get_word_at_pos(local_pos); if (word_at_pos.is_empty()) { - word_at_pos = tx->get_word_under_caret(caret_clicked); + word_at_pos = tx->get_word_under_caret(selection_clicked); } if (word_at_pos.is_empty()) { - word_at_pos = tx->get_selected_text(caret_clicked); + word_at_pos = tx->get_selected_text(selection_clicked); } bool has_color = (word_at_pos == "Color"); - bool foldable = tx->can_fold_line(row) || tx->is_line_folded(row); + bool foldable = tx->can_fold_line(mouse_line) || tx->is_line_folded(mouse_line); bool open_docs = false; bool goto_definition = false; @@ -2044,9 +2055,9 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { } if (has_color) { - String line = tx->get_line(row); - color_position.x = row; - color_position.y = col; + String line = tx->get_line(mouse_line); + color_position.x = mouse_line; + color_position.y = mouse_column; int begin = -1; int end = -1; @@ -2056,7 +2067,7 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { COLOR_NAME, // Color.COLOR_NAME } expression_pattern = NOT_PARSED; - for (int i = col; i < line.length(); i++) { + for (int i = mouse_column; i < line.length(); i++) { if (line[i] == '(') { if (expression_pattern == NOT_PARSED) { begin = i; @@ -2135,7 +2146,6 @@ void ScriptTextEditor::_color_changed(const Color &p_color) { code_editor->get_text_editor()->begin_complex_operation(); code_editor->get_text_editor()->set_line(color_position.x, line_with_replaced_args); code_editor->get_text_editor()->end_complex_operation(); - code_editor->get_text_editor()->queue_redraw(); } void ScriptTextEditor::_prepare_edit_menu() { @@ -2352,6 +2362,7 @@ ScriptTextEditor::ScriptTextEditor() { code_editor->get_text_editor()->set_draw_breakpoints_gutter(true); code_editor->get_text_editor()->set_draw_executing_lines_gutter(true); code_editor->get_text_editor()->connect("breakpoint_toggled", callable_mp(this, &ScriptTextEditor::_breakpoint_toggled)); + code_editor->get_text_editor()->connect("caret_changed", callable_mp(this, &ScriptTextEditor::_on_caret_moved)); connection_gutter = 1; code_editor->get_text_editor()->add_gutter(connection_gutter); diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index 2ea73d4c73..de89fe458c 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -99,6 +99,7 @@ class ScriptTextEditor : public ScriptEditorBase { Color marked_line_color = Color(1, 1, 1); Color folded_code_region_color = Color(1, 1, 1); + int previous_line = 0; PopupPanel *color_panel = nullptr; ColorPicker *color_picker = nullptr; @@ -164,6 +165,8 @@ protected: void _breakpoint_item_pressed(int p_idx); void _breakpoint_toggled(int p_row); + void _on_caret_moved(); + void _validate_script(); // No longer virtual. void _update_warnings(); void _update_errors(); @@ -260,6 +263,9 @@ public: virtual void validate() override; + Variant get_previous_state(); + void store_previous_state(); + ScriptTextEditor(); ~ScriptTextEditor(); }; diff --git a/editor/plugins/shader_editor_plugin.h b/editor/plugins/shader_editor_plugin.h index 2558184982..386261d844 100644 --- a/editor/plugins/shader_editor_plugin.h +++ b/editor/plugins/shader_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef SHADER_EDITOR_PLUGIN_H #define SHADER_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class HSplitContainer; class ItemList; diff --git a/editor/plugins/shader_file_editor_plugin.h b/editor/plugins/shader_file_editor_plugin.h index 989668e37d..9a915513ef 100644 --- a/editor/plugins/shader_file_editor_plugin.h +++ b/editor/plugins/shader_file_editor_plugin.h @@ -32,7 +32,7 @@ #define SHADER_FILE_EDITOR_PLUGIN_H #include "editor/code_editor.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/menu_button.h" #include "scene/gui/panel_container.h" #include "scene/gui/rich_text_label.h" diff --git a/editor/plugins/skeleton_2d_editor_plugin.h b/editor/plugins/skeleton_2d_editor_plugin.h index 9f3f1c3b34..74fd59f1c4 100644 --- a/editor/plugins/skeleton_2d_editor_plugin.h +++ b/editor/plugins/skeleton_2d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef SKELETON_2D_EDITOR_PLUGIN_H #define SKELETON_2D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/skeleton_2d.h" class AcceptDialog; diff --git a/editor/plugins/skeleton_3d_editor_plugin.h b/editor/plugins/skeleton_3d_editor_plugin.h index f62d017c40..79dc16ae2f 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.h +++ b/editor/plugins/skeleton_3d_editor_plugin.h @@ -31,9 +31,9 @@ #ifndef SKELETON_3D_EDITOR_PLUGIN_H #define SKELETON_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" #include "editor/editor_properties.h" #include "editor/gui/editor_file_dialog.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/node_3d_editor_plugin.h" #include "scene/3d/camera_3d.h" #include "scene/3d/mesh_instance_3d.h" diff --git a/editor/plugins/skeleton_ik_3d_editor_plugin.h b/editor/plugins/skeleton_ik_3d_editor_plugin.h index 3d311e581e..2ef5467263 100644 --- a/editor/plugins/skeleton_ik_3d_editor_plugin.h +++ b/editor/plugins/skeleton_ik_3d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef SKELETON_IK_3D_EDITOR_PLUGIN_H #define SKELETON_IK_3D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class Button; class SkeletonIK3D; diff --git a/editor/plugins/sprite_2d_editor_plugin.h b/editor/plugins/sprite_2d_editor_plugin.h index 1121481341..7cbde77081 100644 --- a/editor/plugins/sprite_2d_editor_plugin.h +++ b/editor/plugins/sprite_2d_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef SPRITE_2D_EDITOR_PLUGIN_H #define SPRITE_2D_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/sprite_2d.h" #include "scene/gui/spin_box.h" diff --git a/editor/plugins/sprite_frames_editor_plugin.h b/editor/plugins/sprite_frames_editor_plugin.h index e9fbaf7dde..0e26a793a7 100644 --- a/editor/plugins/sprite_frames_editor_plugin.h +++ b/editor/plugins/sprite_frames_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef SPRITE_FRAMES_EDITOR_PLUGIN_H #define SPRITE_FRAMES_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/2d/animated_sprite_2d.h" #include "scene/3d/sprite_3d.h" #include "scene/gui/button.h" diff --git a/editor/plugins/style_box_editor_plugin.h b/editor/plugins/style_box_editor_plugin.h index 824f8db8e4..e793c2c2f3 100644 --- a/editor/plugins/style_box_editor_plugin.h +++ b/editor/plugins/style_box_editor_plugin.h @@ -32,7 +32,7 @@ #define STYLE_BOX_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/texture_rect.h" class Button; diff --git a/editor/plugins/sub_viewport_preview_editor_plugin.h b/editor/plugins/sub_viewport_preview_editor_plugin.h index 999d7d8b43..d05e90b61e 100644 --- a/editor/plugins/sub_viewport_preview_editor_plugin.h +++ b/editor/plugins/sub_viewport_preview_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef SUB_VIEWPORT_PREVIEW_EDITOR_PLUGIN_H #define SUB_VIEWPORT_PREVIEW_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/texture_editor_plugin.h" #include "scene/main/viewport.h" diff --git a/editor/plugins/text_editor.cpp b/editor/plugins/text_editor.cpp index 6070e08739..e19d9d933a 100644 --- a/editor/plugins/text_editor.cpp +++ b/editor/plugins/text_editor.cpp @@ -380,10 +380,10 @@ void TextEditor::_edit_option(int p_op) { callable_mp((Control *)tx, &Control::grab_focus).call_deferred(); } break; case EDIT_MOVE_LINE_UP: { - code_editor->move_lines_up(); + code_editor->get_text_editor()->move_lines_up(); } break; case EDIT_MOVE_LINE_DOWN: { - code_editor->move_lines_down(); + code_editor->get_text_editor()->move_lines_down(); } break; case EDIT_INDENT: { tx->indent_lines(); @@ -392,24 +392,16 @@ void TextEditor::_edit_option(int p_op) { tx->unindent_lines(); } break; case EDIT_DELETE_LINE: { - code_editor->delete_lines(); + code_editor->get_text_editor()->delete_lines(); } break; case EDIT_DUPLICATE_SELECTION: { - code_editor->duplicate_selection(); + code_editor->get_text_editor()->duplicate_selection(); } break; case EDIT_DUPLICATE_LINES: { code_editor->get_text_editor()->duplicate_lines(); } break; case EDIT_TOGGLE_FOLD_LINE: { - int previous_line = -1; - for (int caret_idx : tx->get_caret_index_edit_order()) { - int line_idx = tx->get_caret_line(caret_idx); - if (line_idx != previous_line) { - tx->toggle_foldable_line(line_idx); - previous_line = line_idx; - } - } - tx->queue_redraw(); + tx->toggle_foldable_lines_at_carets(); } break; case EDIT_FOLD_ALL_LINES: { tx->fold_all_lines(); @@ -531,7 +523,7 @@ void TextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { } } if (!tx->has_selection()) { - tx->set_caret_line(row, true, false); + tx->set_caret_line(row, true, false, -1); tx->set_caret_column(col); } } diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp index 6e786c1d94..83a1700306 100644 --- a/editor/plugins/text_shader_editor.cpp +++ b/editor/plugins/text_shader_editor.cpp @@ -311,6 +311,9 @@ void ShaderTextEditor::_load_theme_settings() { syntax_highlighter->add_color_region("/*", "*/", comment_color, false); syntax_highlighter->add_color_region("//", "", comment_color, true); + const Color doc_comment_color = EDITOR_GET("text_editor/theme/highlighting/doc_comment_color"); + syntax_highlighter->add_color_region("/**", "*/", doc_comment_color, false); + // Disabled preprocessor branches use translucent text color to be easier to distinguish from comments. syntax_highlighter->set_disabled_branch_color(Color(EDITOR_GET("text_editor/theme/highlighting/text_color")) * Color(1, 1, 1, 0.5)); @@ -650,10 +653,10 @@ void TextShaderEditor::_menu_option(int p_option) { code_editor->get_text_editor()->select_all(); } break; case EDIT_MOVE_LINE_UP: { - code_editor->move_lines_up(); + code_editor->get_text_editor()->move_lines_up(); } break; case EDIT_MOVE_LINE_DOWN: { - code_editor->move_lines_down(); + code_editor->get_text_editor()->move_lines_down(); } break; case EDIT_INDENT: { if (shader.is_null() && shader_inc.is_null()) { @@ -668,10 +671,10 @@ void TextShaderEditor::_menu_option(int p_option) { code_editor->get_text_editor()->unindent_lines(); } break; case EDIT_DELETE_LINE: { - code_editor->delete_lines(); + code_editor->get_text_editor()->delete_lines(); } break; case EDIT_DUPLICATE_SELECTION: { - code_editor->duplicate_selection(); + code_editor->get_text_editor()->duplicate_selection(); } break; case EDIT_DUPLICATE_LINES: { code_editor->get_text_editor()->duplicate_lines(); @@ -1007,7 +1010,7 @@ void TextShaderEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { } } if (!tx->has_selection()) { - tx->set_caret_line(row, true, false); + tx->set_caret_line(row, true, false, -1); tx->set_caret_column(col); } } diff --git a/editor/plugins/texture_3d_editor_plugin.h b/editor/plugins/texture_3d_editor_plugin.h index 2509cf86ba..7a33a97a8f 100644 --- a/editor/plugins/texture_3d_editor_plugin.h +++ b/editor/plugins/texture_3d_editor_plugin.h @@ -32,7 +32,7 @@ #define TEXTURE_3D_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/spin_box.h" #include "scene/resources/shader.h" #include "scene/resources/texture.h" diff --git a/editor/plugins/texture_editor_plugin.h b/editor/plugins/texture_editor_plugin.h index f045e7b1b0..ea31429238 100644 --- a/editor/plugins/texture_editor_plugin.h +++ b/editor/plugins/texture_editor_plugin.h @@ -32,7 +32,7 @@ #define TEXTURE_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/margin_container.h" #include "scene/resources/texture.h" diff --git a/editor/plugins/texture_layered_editor_plugin.h b/editor/plugins/texture_layered_editor_plugin.h index ea807c0080..83729f922e 100644 --- a/editor/plugins/texture_layered_editor_plugin.h +++ b/editor/plugins/texture_layered_editor_plugin.h @@ -32,7 +32,7 @@ #define TEXTURE_LAYERED_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/spin_box.h" #include "scene/resources/shader.h" #include "scene/resources/texture.h" diff --git a/editor/plugins/texture_region_editor_plugin.cpp b/editor/plugins/texture_region_editor_plugin.cpp index 9498e980ec..5557eec694 100644 --- a/editor/plugins/texture_region_editor_plugin.cpp +++ b/editor/plugins/texture_region_editor_plugin.cpp @@ -328,7 +328,7 @@ void TextureRegionEditor::_texture_overlay_input(const Ref<InputEvent> &p_input) drag_from = mtx.affine_inverse().xform(mb->get_position()); if (snap_mode == SNAP_PIXEL) { - drag_from = drag_from.snapped(Vector2(1, 1)); + drag_from = drag_from.snappedf(1); } else if (snap_mode == SNAP_GRID) { drag_from = snap_point(drag_from); } @@ -566,7 +566,7 @@ void TextureRegionEditor::_texture_overlay_input(const Ref<InputEvent> &p_input) } else { Vector2 new_pos = mtx.affine_inverse().xform(mm->get_position()); if (snap_mode == SNAP_PIXEL) { - new_pos = new_pos.snapped(Vector2(1, 1)); + new_pos = new_pos.snappedf(1); } else if (snap_mode == SNAP_GRID) { new_pos = snap_point(new_pos); } diff --git a/editor/plugins/texture_region_editor_plugin.h b/editor/plugins/texture_region_editor_plugin.h index 59a1b56c19..0e71ec16e0 100644 --- a/editor/plugins/texture_region_editor_plugin.h +++ b/editor/plugins/texture_region_editor_plugin.h @@ -32,7 +32,7 @@ #define TEXTURE_REGION_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/dialogs.h" class AtlasTexture; diff --git a/editor/plugins/theme_editor_plugin.h b/editor/plugins/theme_editor_plugin.h index 0bc02789aa..ba8e3a30b7 100644 --- a/editor/plugins/theme_editor_plugin.h +++ b/editor/plugins/theme_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef THEME_EDITOR_PLUGIN_H #define THEME_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/theme_editor_preview.h" #include "scene/gui/dialogs.h" #include "scene/gui/margin_container.h" diff --git a/editor/plugins/tiles/tile_data_editors.cpp b/editor/plugins/tiles/tile_data_editors.cpp index f047e4ff16..86a291db0e 100644 --- a/editor/plugins/tiles/tile_data_editors.cpp +++ b/editor/plugins/tiles/tile_data_editors.cpp @@ -458,7 +458,7 @@ void GenericTilePolygonEditor::_snap_point(Point2 &r_point) { break; case SNAP_HALF_PIXEL: - r_point = r_point.snapped(Vector2(0.5, 0.5)); + r_point = r_point.snappedf(0.5); break; case SNAP_GRID: { diff --git a/editor/plugins/tiles/tile_proxies_manager_dialog.cpp b/editor/plugins/tiles/tile_proxies_manager_dialog.cpp index ca4ffeecc2..a75ab45106 100644 --- a/editor/plugins/tiles/tile_proxies_manager_dialog.cpp +++ b/editor/plugins/tiles/tile_proxies_manager_dialog.cpp @@ -257,13 +257,13 @@ bool TileProxiesManagerDialog::_set(const StringName &p_name, const Variant &p_v if (p_name == "from_source") { from.source_id = MAX(int(p_value), -1); } else if (p_name == "from_coords") { - from.set_atlas_coords(Vector2i(p_value).max(Vector2i(-1, -1))); + from.set_atlas_coords(Vector2i(p_value).maxi(-1)); } else if (p_name == "from_alternative") { from.alternative_tile = MAX(int(p_value), -1); } else if (p_name == "to_source") { to.source_id = MAX(int(p_value), 0); } else if (p_name == "to_coords") { - to.set_atlas_coords(Vector2i(p_value).max(Vector2i(0, 0))); + to.set_atlas_coords(Vector2i(p_value).maxi(0)); } else if (p_name == "to_alternative") { to.alternative_tile = MAX(int(p_value), 0); } else { diff --git a/editor/plugins/tiles/tile_set_atlas_source_editor.cpp b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp index 1da2f89c1c..1a1b14bb84 100644 --- a/editor/plugins/tiles/tile_set_atlas_source_editor.cpp +++ b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp @@ -1088,7 +1088,7 @@ void TileSetAtlasSourceEditor::_tile_atlas_control_gui_input(const Ref<InputEven if (drag_type == DRAG_TYPE_CREATE_BIG_TILE) { // Create big tile. - new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); + new_base_tiles_coords = new_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1)); Rect2i new_rect = Rect2i(start_base_tiles_coords, new_base_tiles_coords - start_base_tiles_coords).abs(); new_rect.size += Vector2i(1, 1); @@ -1100,8 +1100,8 @@ void TileSetAtlasSourceEditor::_tile_atlas_control_gui_input(const Ref<InputEven } } else if (drag_type == DRAG_TYPE_CREATE_TILES) { // Create tiles. - last_base_tiles_coords = last_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); - new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); + last_base_tiles_coords = last_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1)); + new_base_tiles_coords = new_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1)); Vector<Point2i> line = Geometry2D::bresenham_line(last_base_tiles_coords, new_base_tiles_coords); for (int i = 0; i < line.size(); i++) { @@ -1115,8 +1115,8 @@ void TileSetAtlasSourceEditor::_tile_atlas_control_gui_input(const Ref<InputEven } else if (drag_type == DRAG_TYPE_REMOVE_TILES) { // Remove tiles. - last_base_tiles_coords = last_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); - new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); + last_base_tiles_coords = last_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1)); + new_base_tiles_coords = new_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1)); Vector<Point2i> line = Geometry2D::bresenham_line(last_base_tiles_coords, new_base_tiles_coords); for (int i = 0; i < line.size(); i++) { @@ -1838,7 +1838,7 @@ void TileSetAtlasSourceEditor::_tile_atlas_control_draw() { Vector2i separation = tile_set_atlas_source->get_separation(); Vector2i tile_size = tile_set_atlas_source->get_texture_region_size(); Vector2i origin = margins + (area.position * (tile_size + separation)); - Vector2i size = area.size * tile_size + (area.size - Vector2i(1, 1)).max(Vector2i(0, 0)) * separation; + Vector2i size = area.size * tile_size + (area.size - Vector2i(1, 1)).maxi(0) * separation; TilesEditorUtils::draw_selection_rect(tile_atlas_control, Rect2i(origin, size)); } else { Vector2i grid_size = tile_set_atlas_source->get_atlas_grid_size(); @@ -2157,7 +2157,7 @@ void TileSetAtlasSourceEditor::_undo_redo_inspector_callback(Object *p_undo_redo Vector2i TileSetAtlasSourceEditor::_get_drag_offset_tile_coords(const Vector2i &p_offset) const { Vector2i half_tile_size = tile_set->get_tile_size() / 2; Vector2i new_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position() + half_tile_size * p_offset); - return new_base_tiles_coords.max(Vector2i(-1, -1)).min(tile_set_atlas_source->get_atlas_grid_size()); + return new_base_tiles_coords.maxi(-1).min(tile_set_atlas_source->get_atlas_grid_size()); } void TileSetAtlasSourceEditor::edit(Ref<TileSet> p_tile_set, TileSetAtlasSource *p_tile_set_atlas_source, int p_source_id) { diff --git a/editor/plugins/tiles/tile_set_editor.cpp b/editor/plugins/tiles/tile_set_editor.cpp index fe02e3096c..8c41858d62 100644 --- a/editor/plugins/tiles/tile_set_editor.cpp +++ b/editor/plugins/tiles/tile_set_editor.cpp @@ -881,7 +881,9 @@ TileSetEditor::TileSetEditor() { sources_add_button->set_flat(false); sources_add_button->set_theme_type_variation("FlatButton"); sources_add_button->get_popup()->add_item(TTR("Atlas")); + sources_add_button->get_popup()->set_item_tooltip(-1, TTR("A palette of tiles made from a texture.")); sources_add_button->get_popup()->add_item(TTR("Scenes Collection")); + sources_add_button->get_popup()->set_item_tooltip(-1, TTR("A collection of scenes that can be instantiated and placed as tiles.")); sources_add_button->get_popup()->connect("id_pressed", callable_mp(this, &TileSetEditor::_source_add_id_pressed)); sources_bottom_actions->add_child(sources_add_button); diff --git a/editor/plugins/tiles/tiles_editor_plugin.h b/editor/plugins/tiles/tiles_editor_plugin.h index 23a6a52a5c..f1da9e31fa 100644 --- a/editor/plugins/tiles/tiles_editor_plugin.h +++ b/editor/plugins/tiles/tiles_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef TILES_EDITOR_PLUGIN_H #define TILES_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/box_container.h" #include "tile_atlas_view.h" diff --git a/editor/plugins/version_control_editor_plugin.h b/editor/plugins/version_control_editor_plugin.h index 8ecb7c5029..4e60cb0a84 100644 --- a/editor/plugins/version_control_editor_plugin.h +++ b/editor/plugins/version_control_editor_plugin.h @@ -31,8 +31,8 @@ #ifndef VERSION_CONTROL_EDITOR_PLUGIN_H #define VERSION_CONTROL_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" #include "editor/editor_vcs_interface.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/check_button.h" #include "scene/gui/container.h" #include "scene/gui/file_dialog.h" diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index 0dd845270a..9e706ce623 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -1002,7 +1002,7 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool button->hide(); } - if (i == 0 && custom_editor) { + if (j == 0 && custom_editor) { hb->add_child(custom_editor); custom_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); } else { @@ -1019,22 +1019,22 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool type_box->add_item(TTR("Boolean")); type_box->add_item(TTR("Transform")); type_box->add_item(TTR("Sampler")); - type_box->select(group_node->get_input_port_type(i)); + type_box->select(group_node->get_input_port_type(j)); type_box->set_custom_minimum_size(Size2(100 * EDSCALE, 0)); - type_box->connect("item_selected", callable_mp(editor, &VisualShaderEditor::_change_input_port_type).bind(p_id, i), CONNECT_DEFERRED); + type_box->connect("item_selected", callable_mp(editor, &VisualShaderEditor::_change_input_port_type).bind(p_id, j), CONNECT_DEFERRED); LineEdit *name_box = memnew(LineEdit); hb->add_child(name_box); name_box->set_custom_minimum_size(Size2(65 * EDSCALE, 0)); name_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); name_box->set_text(name_left); - name_box->connect("text_submitted", callable_mp(editor, &VisualShaderEditor::_change_input_port_name).bind(name_box, p_id, i), CONNECT_DEFERRED); - name_box->connect("focus_exited", callable_mp(editor, &VisualShaderEditor::_port_name_focus_out).bind(name_box, p_id, i, false), CONNECT_DEFERRED); + name_box->connect("text_submitted", callable_mp(editor, &VisualShaderEditor::_change_input_port_name).bind(name_box, p_id, j), CONNECT_DEFERRED); + name_box->connect("focus_exited", callable_mp(editor, &VisualShaderEditor::_port_name_focus_out).bind(name_box, p_id, j, false), CONNECT_DEFERRED); Button *remove_btn = memnew(Button); remove_btn->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("Remove"), EditorStringName(EditorIcons))); remove_btn->set_tooltip_text(TTR("Remove") + " " + name_left); - remove_btn->connect("pressed", callable_mp(editor, &VisualShaderEditor::_remove_input_port).bind(p_id, i), CONNECT_DEFERRED); + remove_btn->connect("pressed", callable_mp(editor, &VisualShaderEditor::_remove_input_port).bind(p_id, j), CONNECT_DEFERRED); hb->add_child(remove_btn); } else { Label *label = memnew(Label); @@ -1043,7 +1043,7 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool label->add_theme_style_override("normal", editor->get_theme_stylebox(SNAME("label_style"), SNAME("VShaderEditor"))); //more compact hb->add_child(label); - if (vsnode->is_input_port_default(i, mode) && !port_left_used) { + if (vsnode->is_input_port_default(j, mode) && !port_left_used) { Label *hint_label = memnew(Label); hint_label->set_text(TTR("[default]")); hint_label->add_theme_color_override("font_color", editor->get_theme_color(SNAME("font_readonly_color"), SNAME("TextEdit"))); @@ -4673,7 +4673,7 @@ void VisualShaderEditor::_show_members_dialog(bool at_mouse_pos, VisualShaderNod // Keep dialog within window bounds. Rect2 window_rect = Rect2(get_window()->get_position(), get_window()->get_size()); Rect2 dialog_rect = Rect2(members_dialog->get_position(), members_dialog->get_size()); - Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).max(Vector2()); + Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).maxf(0); members_dialog->set_position(members_dialog->get_position() - difference); callable_mp((Control *)node_filter, &Control::grab_focus).call_deferred(); // Still not visible. @@ -4702,7 +4702,7 @@ void VisualShaderEditor::_show_add_varying_dialog() { // Keep dialog within window bounds. Rect2 window_rect = Rect2(DisplayServer::get_singleton()->window_get_position(), DisplayServer::get_singleton()->window_get_size()); Rect2 dialog_rect = Rect2(add_varying_dialog->get_position(), add_varying_dialog->get_size()); - Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).max(Vector2()); + Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).maxf(0); add_varying_dialog->set_position(add_varying_dialog->get_position() - difference); } @@ -4713,7 +4713,7 @@ void VisualShaderEditor::_show_remove_varying_dialog() { // Keep dialog within window bounds. Rect2 window_rect = Rect2(DisplayServer::get_singleton()->window_get_position(), DisplayServer::get_singleton()->window_get_size()); Rect2 dialog_rect = Rect2(remove_varying_dialog->get_position(), remove_varying_dialog->get_size()); - Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).max(Vector2()); + Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).maxf(0); remove_varying_dialog->set_position(remove_varying_dialog->get_position() - difference); } diff --git a/editor/plugins/visual_shader_editor_plugin.h b/editor/plugins/visual_shader_editor_plugin.h index e499bbde1e..246e44a40d 100644 --- a/editor/plugins/visual_shader_editor_plugin.h +++ b/editor/plugins/visual_shader_editor_plugin.h @@ -31,8 +31,8 @@ #ifndef VISUAL_SHADER_EDITOR_PLUGIN_H #define VISUAL_SHADER_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" #include "editor/editor_properties.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/editor_resource_conversion_plugin.h" #include "scene/gui/graph_edit.h" #include "scene/resources/syntax_highlighter.h" diff --git a/editor/plugins/voxel_gi_editor_plugin.h b/editor/plugins/voxel_gi_editor_plugin.h index ad68ff5d91..58ef22ddc6 100644 --- a/editor/plugins/voxel_gi_editor_plugin.h +++ b/editor/plugins/voxel_gi_editor_plugin.h @@ -31,7 +31,7 @@ #ifndef VOXEL_GI_EDITOR_PLUGIN_H #define VOXEL_GI_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/3d/voxel_gi.h" #include "scene/resources/material.h" diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 70cef0e345..4fe91d1cc5 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -802,18 +802,20 @@ void ProjectManager::_apply_project_tags() { } } - ConfigFile cfg; const String project_godot = project_list->get_selected_projects()[0].path.path_join("project.godot"); - Error err = cfg.load(project_godot); - if (err != OK) { - tag_edit_error->set_text(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err)); + ProjectSettings *cfg = memnew(ProjectSettings(project_godot)); + if (!cfg->is_project_loaded()) { + memdelete(cfg); + tag_edit_error->set_text(vformat(TTR("Couldn't load project at '%s'. It may be missing or corrupted."), project_godot)); tag_edit_error->show(); callable_mp((Window *)tag_manage_dialog, &Window::show).call_deferred(); // Make sure the dialog does not disappear. return; } else { tags.sort(); - cfg.set_value("application", "config/tags", tags); - err = cfg.save(project_godot); + cfg->set("application/config/tags", tags); + Error err = cfg->save_custom(project_godot); + memdelete(cfg); + if (err != OK) { tag_edit_error->set_text(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err)); tag_edit_error->show(); diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index 70e8484a78..c4aeac434b 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -287,6 +287,8 @@ void ProjectSettingsEditor::_add_feature_overrides() { presets.insert("s3tc"); presets.insert("etc2"); presets.insert("editor"); + presets.insert("editor_hint"); + presets.insert("editor_runtime"); presets.insert("template_debug"); presets.insert("template_release"); presets.insert("debug"); diff --git a/editor/project_settings_editor.h b/editor/project_settings_editor.h index 55f4677fb7..fb294c6bbd 100644 --- a/editor/project_settings_editor.h +++ b/editor/project_settings_editor.h @@ -35,11 +35,11 @@ #include "editor/action_map_editor.h" #include "editor/editor_autoload_settings.h" #include "editor/editor_data.h" -#include "editor/editor_plugin_settings.h" #include "editor/editor_sectioned_inspector.h" #include "editor/group_settings_editor.h" #include "editor/import_defaults_editor.h" #include "editor/localization_editor.h" +#include "editor/plugins/editor_plugin_settings.h" #include "editor/shader_globals_editor.h" #include "scene/gui/tab_container.h" diff --git a/editor/shader_create_dialog.cpp b/editor/shader_create_dialog.cpp index 8d77b14ab0..43fd559393 100644 --- a/editor/shader_create_dialog.cpp +++ b/editor/shader_create_dialog.cpp @@ -243,7 +243,12 @@ void fog() { emit_signal(SNAME("shader_include_created"), shader_inc); } else { - if (!is_built_in) { + if (is_built_in) { + Node *edited_scene = get_tree()->get_edited_scene_root(); + if (likely(edited_scene)) { + shader->set_path(edited_scene->get_scene_file_path() + "::"); + } + } else { String lpath = ProjectSettings::get_singleton()->localize_path(file_path->get_text()); shader->set_path(lpath); diff --git a/editor/shader_globals_editor.cpp b/editor/shader_globals_editor.cpp index 86a78d813e..216ccd71ab 100644 --- a/editor/shader_globals_editor.cpp +++ b/editor/shader_globals_editor.cpp @@ -219,7 +219,7 @@ protected: case RS::GLOBAL_VAR_TYPE_SAMPLER2DARRAY: { pinfo.type = Variant::OBJECT; pinfo.hint = PROPERTY_HINT_RESOURCE_TYPE; - pinfo.hint_string = "Texture2DArray"; + pinfo.hint_string = "Texture2DArray,CompressedTexture2DArray"; } break; case RS::GLOBAL_VAR_TYPE_SAMPLER3D: { pinfo.type = Variant::OBJECT; @@ -229,7 +229,7 @@ protected: case RS::GLOBAL_VAR_TYPE_SAMPLERCUBE: { pinfo.type = Variant::OBJECT; pinfo.hint = PROPERTY_HINT_RESOURCE_TYPE; - pinfo.hint_string = "Cubemap"; + pinfo.hint_string = "Cubemap,CompressedCubemap"; } break; default: { } break; diff --git a/editor/shader_globals_editor.h b/editor/shader_globals_editor.h index de871f76bf..fd94e483ff 100644 --- a/editor/shader_globals_editor.h +++ b/editor/shader_globals_editor.h @@ -33,8 +33,8 @@ #include "editor/editor_autoload_settings.h" #include "editor/editor_data.h" -#include "editor/editor_plugin_settings.h" #include "editor/editor_sectioned_inspector.h" +#include "editor/plugins/editor_plugin_settings.h" #include "scene/gui/tab_container.h" class ShaderGlobalsEditorInterface; diff --git a/gles3_builders.py b/gles3_builders.py index cf7c74f32d..78882f97fd 100644 --- a/gles3_builders.py +++ b/gles3_builders.py @@ -1,7 +1,7 @@ """Functions used to generate source files during build time""" import os.path - +from methods import print_error from typing import Optional @@ -94,11 +94,11 @@ def include_file_in_gles3_header(filename: str, header_data: GLES3HeaderStruct, if not included_file in header_data.vertex_included_files and header_data.reading == "vertex": header_data.vertex_included_files += [included_file] if include_file_in_gles3_header(included_file, header_data, depth + 1) is None: - print("Error in file '" + filename + "': #include " + includeline + "could not be found!") + print_error(f'In file "{filename}": #include "{includeline}" could not be found!"') elif not included_file in header_data.fragment_included_files and header_data.reading == "fragment": header_data.fragment_included_files += [included_file] if include_file_in_gles3_header(included_file, header_data, depth + 1) is None: - print("Error in file '" + filename + "': #include " + includeline + "could not be found!") + print_error(f'In file "{filename}": #include "{includeline}" could not be found!"') line = fs.readline() diff --git a/glsl_builders.py b/glsl_builders.py index 5a17e3ca7f..22f4de74b1 100644 --- a/glsl_builders.py +++ b/glsl_builders.py @@ -1,6 +1,7 @@ """Functions used to generate source files during build time""" import os.path +from methods import print_error from typing import Optional, Iterable @@ -79,15 +80,15 @@ def include_file_in_rd_header(filename: str, header_data: RDHeaderStruct, depth: if not included_file in header_data.vertex_included_files and header_data.reading == "vertex": header_data.vertex_included_files += [included_file] if include_file_in_rd_header(included_file, header_data, depth + 1) is None: - print("Error in file '" + filename + "': #include " + includeline + "could not be found!") + print_error(f'In file "{filename}": #include "{includeline}" could not be found!"') elif not included_file in header_data.fragment_included_files and header_data.reading == "fragment": header_data.fragment_included_files += [included_file] if include_file_in_rd_header(included_file, header_data, depth + 1) is None: - print("Error in file '" + filename + "': #include " + includeline + "could not be found!") + print_error(f'In file "{filename}": #include "{includeline}" could not be found!"') elif not included_file in header_data.compute_included_files and header_data.reading == "compute": header_data.compute_included_files += [included_file] if include_file_in_rd_header(included_file, header_data, depth + 1) is None: - print("Error in file '" + filename + "': #include " + includeline + "could not be found!") + print_error(f'In file "{filename}": #include "{includeline}" could not be found!"') line = fs.readline() diff --git a/main/main.cpp b/main/main.cpp index 905740fd90..90c84114f2 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1746,6 +1746,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph } } + OS::get_singleton()->_in_editor = editor; if (globals->setup(project_path, main_pack, upwards, editor) == OK) { #ifdef TOOLS_ENABLED found_project = true; @@ -2404,6 +2405,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph // OpenXR project extensions settings. GLOBAL_DEF_BASIC("xr/openxr/extensions/hand_tracking", true); + GLOBAL_DEF_RST_BASIC("xr/openxr/extensions/hand_interaction_profile", false); GLOBAL_DEF_BASIC("xr/openxr/extensions/eye_gaze_interaction", false); #ifdef TOOLS_ENABLED @@ -3181,6 +3183,7 @@ int Main::start() { #ifdef TOOLS_ENABLED String doc_tool_path; + bool doc_tool_implicit_cwd = false; BitField<DocTools::GenerateFlags> gen_flags; String _export_preset; bool export_debug = false; @@ -3251,6 +3254,7 @@ int Main::start() { if (doc_tool_path.begins_with("-")) { // Assuming other command line arg, so default to cwd. doc_tool_path = "."; + doc_tool_implicit_cwd = true; parsed_pair = false; } #ifdef MODULE_GDSCRIPT_ENABLED @@ -3281,6 +3285,7 @@ int Main::start() { // Handle case where no path is given to --doctool. else if (args[i] == "--doctool") { doc_tool_path = "."; + doc_tool_implicit_cwd = true; } #endif } @@ -3307,6 +3312,11 @@ int Main::start() { { Ref<DirAccess> da = DirAccess::open(doc_tool_path); ERR_FAIL_COND_V_MSG(da.is_null(), EXIT_FAILURE, "Argument supplied to --doctool must be a valid directory path."); + // Ensure that doctool is running in the root dir, but only if + // user did not manually specify a path as argument. + if (doc_tool_implicit_cwd) { + ERR_FAIL_COND_V_MSG(!da->dir_exists("doc"), EXIT_FAILURE, "--doctool must be run from the Godot repository's root folder, or specify a path that points there."); + } } #ifndef MODULE_MONO_ENABLED @@ -3635,7 +3645,7 @@ int Main::start() { } } - if (doc_tool_path == ".") { + if (doc_tool_implicit_cwd) { doc_tool_path = "./docs"; } diff --git a/methods.py b/methods.py index 01b127ea30..0c29632f10 100644 --- a/methods.py +++ b/methods.py @@ -5,6 +5,7 @@ import glob import subprocess from collections import OrderedDict from collections.abc import Mapping +from enum import Enum from typing import Iterator from pathlib import Path from os.path import normpath, basename @@ -15,6 +16,10 @@ base_folder_only = os.path.basename(os.path.normpath(base_folder_path)) # Listing all the folders we have converted # for SCU in scu_builders.py _scu_folders = set() +# Colors are disabled in non-TTY environments such as pipes. This means +# that if output is redirected to a file, it won't contain color codes. +# Colors are always enabled on continuous integration. +_colorize = bool(sys.stdout.isatty() or os.environ.get("CI")) def set_scu_folders(scu_folders): @@ -22,13 +27,57 @@ def set_scu_folders(scu_folders): _scu_folders = scu_folders +class ANSI(Enum): + """ + Enum class for adding ansi colorcodes directly into strings. + Automatically converts values to strings representing their + internal value, or an empty string in a non-colorized scope. + """ + + RESET = "\x1b[0m" + + BOLD = "\x1b[1m" + ITALIC = "\x1b[3m" + UNDERLINE = "\x1b[4m" + STRIKETHROUGH = "\x1b[9m" + REGULAR = "\x1b[22;23;24;29m" + + BLACK = "\x1b[30m" + RED = "\x1b[31m" + GREEN = "\x1b[32m" + YELLOW = "\x1b[33m" + BLUE = "\x1b[34m" + MAGENTA = "\x1b[35m" + CYAN = "\x1b[36m" + WHITE = "\x1b[37m" + + PURPLE = "\x1b[38;5;93m" + PINK = "\x1b[38;5;206m" + ORANGE = "\x1b[38;5;214m" + GRAY = "\x1b[38;5;244m" + + def __str__(self) -> str: + global _colorize + return str(self.value) if _colorize else "" + + +def print_warning(*values: object) -> None: + """Prints a warning message with formatting.""" + print(f"{ANSI.YELLOW}{ANSI.BOLD}WARNING:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr) + + +def print_error(*values: object) -> None: + """Prints an error message with formatting.""" + print(f"{ANSI.RED}{ANSI.BOLD}ERROR:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr) + + def add_source_files_orig(self, sources, files, allow_gen=False): # Convert string to list of absolute paths (including expanding wildcard) if isinstance(files, (str, bytes)): # Keep SCons project-absolute path as they are (no wildcard support) if files.startswith("#"): if "*" in files: - print("ERROR: Wildcards can't be expanded in SCons project-absolute path: '{}'".format(files)) + print_error("Wildcards can't be expanded in SCons project-absolute path: '{}'".format(files)) return files = [files] else: @@ -44,7 +93,7 @@ def add_source_files_orig(self, sources, files, allow_gen=False): for path in files: obj = self.Object(path) if obj in sources: - print('WARNING: Object "{}" already included in environment sources.'.format(obj)) + print_warning('Object "{}" already included in environment sources.'.format(obj)) continue sources.append(obj) @@ -517,7 +566,7 @@ def module_check_dependencies(self, module): missing_deps.append(dep) if missing_deps != []: - print( + print_warning( "Disabling '{}' module as the following dependencies are not satisfied: {}".format( module, ", ".join(missing_deps) ) @@ -576,9 +625,7 @@ def use_windows_spawn_fix(self, platform=None): _, err = proc.communicate() rv = proc.wait() if rv: - print("=====") - print(err) - print("=====") + print_error(err) return rv def mySpawn(sh, escape, cmd, args, env): @@ -601,65 +648,34 @@ def use_windows_spawn_fix(self, platform=None): self["SPAWN"] = mySpawn -def no_verbose(sys, env): - colors = {} - - # Colors are disabled in non-TTY environments such as pipes. This means - # that if output is redirected to a file, it will not contain color codes - if sys.stdout.isatty(): - colors["blue"] = "\033[0;94m" - colors["bold_blue"] = "\033[1;94m" - colors["reset"] = "\033[0m" - else: - colors["blue"] = "" - colors["bold_blue"] = "" - colors["reset"] = "" +def no_verbose(env): + colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET] # There is a space before "..." to ensure that source file names can be # Ctrl + clicked in the VS Code terminal. - compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - link_program_message = "{}Linking Program {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - generated_file_message = "{}Generating {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - - env.Append(CXXCOMSTR=[compile_source_message]) - env.Append(CCCOMSTR=[compile_source_message]) - env.Append(SHCCCOMSTR=[compile_shared_source_message]) - env.Append(SHCXXCOMSTR=[compile_shared_source_message]) - env.Append(ARCOMSTR=[link_library_message]) - env.Append(RANLIBCOMSTR=[ranlib_library_message]) - env.Append(SHLINKCOMSTR=[link_shared_library_message]) - env.Append(LINKCOMSTR=[link_program_message]) - env.Append(JARCOMSTR=[java_library_message]) - env.Append(JAVACCOMSTR=[java_compile_source_message]) - env.Append(RCCOMSTR=[compiled_resource_message]) - env.Append(GENCOMSTR=[generated_file_message]) + compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors) + link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors) + link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors) + ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors) + link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors) + java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors) + compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors) + generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors) + + env["CXXCOMSTR"] = compile_source_message + env["CCCOMSTR"] = compile_source_message + env["SHCCCOMSTR"] = compile_shared_source_message + env["SHCXXCOMSTR"] = compile_shared_source_message + env["ARCOMSTR"] = link_library_message + env["RANLIBCOMSTR"] = ranlib_library_message + env["SHLINKCOMSTR"] = link_shared_library_message + env["LINKCOMSTR"] = link_program_message + env["JARCOMSTR"] = java_library_message + env["JAVACCOMSTR"] = java_compile_source_message + env["RCCOMSTR"] = compiled_resource_message + env["GENCOMSTR"] = generated_file_message def detect_visual_c_compiler_version(tools_env): @@ -790,7 +806,7 @@ def generate_cpp_hint_file(filename): with open(filename, "w", encoding="utf-8", newline="\n") as fd: fd.write("#define GDCLASS(m_class, m_inherits)\n") except OSError: - print("Could not write cpp.hint file.") + print_warning("Could not write cpp.hint file.") def glob_recursive(pattern, node="."): @@ -881,7 +897,7 @@ def detect_darwin_sdk_path(platform, env): if sdk_path: env[var_name] = sdk_path except (subprocess.CalledProcessError, OSError): - print("Failed to find SDK path while running xcrun --sdk {} --show-sdk-path.".format(sdk_name)) + print_error("Failed to find SDK path while running xcrun --sdk {} --show-sdk-path.".format(sdk_name)) raise @@ -891,7 +907,7 @@ def is_vanilla_clang(env): try: version = subprocess.check_output([env.subst(env["CXX"]), "--version"]).strip().decode("utf-8") except (subprocess.CalledProcessError, OSError): - print("Couldn't parse CXX environment variable to infer compiler version.") + print_warning("Couldn't parse CXX environment variable to infer compiler version.") return False return not version.startswith("Apple") @@ -928,7 +944,7 @@ def get_compiler_version(env): .decode("utf-8") ) except (subprocess.CalledProcessError, OSError): - print("Couldn't parse CXX environment variable to infer compiler version.") + print_warning("Couldn't parse CXX environment variable to infer compiler version.") return ret else: # TODO: Implement for MSVC diff --git a/misc/dist/html/full-size.html b/misc/dist/html/full-size.html index 8ae25362f8..874fe2695e 100644 --- a/misc/dist/html/full-size.html +++ b/misc/dist/html/full-size.html @@ -2,135 +2,95 @@ <html lang="en"> <head> <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, user-scalable=no"> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0"> <title>$GODOT_PROJECT_NAME</title> <style> -body { - touch-action: none; +html, body, #canvas { margin: 0; - border: 0 none; padding: 0; - text-align: center; + border: 0; +} + +body { + color: white; background-color: black; + overflow: hidden; + touch-action: none; } #canvas { display: block; - margin: 0; - color: white; } #canvas:focus { outline: none; } -.godot { - font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif; - color: #e0e0e0; - background-color: #3b3943; - background-image: linear-gradient(to bottom, #403e48, #35333c); - border: 1px solid #45434e; - box-shadow: 0 0 1px 1px #2f2d35; -} - -/* Status display */ - -#status { +#status, #status-splash, #status-progress { position: absolute; left: 0; - top: 0; right: 0; +} + +#status, #status-splash { + top: 0; bottom: 0; +} + +#status { + background-color: #38363A; display: flex; + flex-direction: column; justify-content: center; align-items: center; - /* don't consume click events - make children visible explicitly */ visibility: hidden; } -#status-progress { - width: 366px; - height: 7px; - background-color: #38363A; - border: 1px solid #444246; - padding: 1px; - box-shadow: 0 0 2px 1px #1B1C22; - border-radius: 2px; - visibility: visible; -} - -@media only screen and (orientation:portrait) { - #status-progress { - width: 61.8%; - } +#status-splash { + max-height: 100%; + max-width: 100%; + margin: auto; } -#status-progress-inner { - height: 100%; - width: 0; - box-sizing: border-box; - transition: width 0.5s linear; - background-color: #202020; - border: 1px solid #222223; - box-shadow: 0 0 1px 1px #27282E; - border-radius: 3px; +#status-progress, #status-notice { + display: none; } -#status-indeterminate { - height: 42px; - visibility: visible; - position: relative; -} - -#status-indeterminate > div { - width: 4.5px; - height: 0; - border-style: solid; - border-width: 9px 3px 0 3px; - border-color: #2b2b2b transparent transparent transparent; - transform-origin: center 21px; - position: absolute; +#status-progress { + bottom: 10%; + width: 50%; + margin: 0 auto; } -#status-indeterminate > div:nth-child(1) { transform: rotate( 22.5deg); } -#status-indeterminate > div:nth-child(2) { transform: rotate( 67.5deg); } -#status-indeterminate > div:nth-child(3) { transform: rotate(112.5deg); } -#status-indeterminate > div:nth-child(4) { transform: rotate(157.5deg); } -#status-indeterminate > div:nth-child(5) { transform: rotate(202.5deg); } -#status-indeterminate > div:nth-child(6) { transform: rotate(247.5deg); } -#status-indeterminate > div:nth-child(7) { transform: rotate(292.5deg); } -#status-indeterminate > div:nth-child(8) { transform: rotate(337.5deg); } - #status-notice { - margin: 0 100px; + background-color: #5b3943; + border-radius: 0.5rem; + border: 1px solid #9b3943; + color: #e0e0e0; + font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif; line-height: 1.3; - visibility: visible; - padding: 4px 6px; - visibility: visible; + margin: 0 2rem; + overflow: hidden; + padding: 1rem; + text-align: center; + z-index: 1; } </style> $GODOT_HEAD_INCLUDE </head> <body> <canvas id="canvas"> - HTML5 canvas appears to be unsupported in the current browser.<br > - Please try updating or use a different browser. + Your browser does not support the canvas tag. </canvas> + + <noscript> + Your browser does not support JavaScript. + </noscript> + <div id="status"> - <div id="status-progress" style="display: none;" oncontextmenu="event.preventDefault();"> - <div id ="status-progress-inner"></div> - </div> - <div id="status-indeterminate" style="display: none;" oncontextmenu="event.preventDefault();"> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - </div> - <div id="status-notice" class="godot" style="display: none;"></div> + <img id="status-splash" src="$GODOT_SPLASH" alt=""> + <progress id="status-progress"></progress> + <div id="status-notice"></div> </div> <script src="$GODOT_URL"></script> @@ -140,58 +100,25 @@ const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED; const engine = new Engine(GODOT_CONFIG); (function () { - const INDETERMINATE_STATUS_STEP_MS = 100; + const statusOverlay = document.getElementById('status'); const statusProgress = document.getElementById('status-progress'); - const statusProgressInner = document.getElementById('status-progress-inner'); - const statusIndeterminate = document.getElementById('status-indeterminate'); const statusNotice = document.getElementById('status-notice'); let initializing = true; - let statusMode = 'hidden'; - - let animationCallbacks = []; - function animate(time) { - animationCallbacks.forEach((callback) => callback(time)); - requestAnimationFrame(animate); - } - requestAnimationFrame(animate); - - function animateStatusIndeterminate(ms) { - const i = Math.floor((ms / INDETERMINATE_STATUS_STEP_MS) % 8); - if (statusIndeterminate.children[i].style.borderTopColor === '') { - Array.prototype.slice.call(statusIndeterminate.children).forEach((child) => { - child.style.borderTopColor = ''; - }); - statusIndeterminate.children[i].style.borderTopColor = '#dfdfdf'; - } - } + let statusMode = ''; function setStatusMode(mode) { if (statusMode === mode || !initializing) { return; } - [statusProgress, statusIndeterminate, statusNotice].forEach((elem) => { - elem.style.display = 'none'; - }); - animationCallbacks = animationCallbacks.filter(function (value) { - return (value !== animateStatusIndeterminate); - }); - switch (mode) { - case 'progress': - statusProgress.style.display = 'block'; - break; - case 'indeterminate': - statusIndeterminate.style.display = 'block'; - animationCallbacks.push(animateStatusIndeterminate); - break; - case 'notice': - statusNotice.style.display = 'block'; - break; - case 'hidden': - break; - default: - throw new Error('Invalid status mode'); + if (mode === 'hidden') { + statusOverlay.remove(); + initializing = false; + return; } + statusOverlay.style.visibility = 'visible'; + statusProgress.style.display = mode === 'progress' ? 'block' : 'none'; + statusNotice.style.display = mode === 'notice' ? 'block' : 'none'; statusMode = mode; } @@ -217,6 +144,7 @@ const engine = new Engine(GODOT_CONFIG); const missing = Engine.getMissingFeatures({ threads: GODOT_THREADS_ENABLED, }); + if (missing.length !== 0) { if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) { // There's a chance that installing the service worker would fix the issue @@ -242,25 +170,19 @@ const engine = new Engine(GODOT_CONFIG); displayFailureNotice(missingMsg + missing.join('\n')); } } else { - setStatusMode('indeterminate'); + setStatusMode('progress'); engine.startGame({ 'onProgress': function (current, total) { - if (total > 0) { - statusProgressInner.style.width = `${(current / total) * 100}%`; - setStatusMode('progress'); - if (current === total) { - // wait for progress bar animation - setTimeout(() => { - setStatusMode('indeterminate'); - }, 500); - } + if (current > 0 && total > 0) { + statusProgress.value = current; + statusProgress.max = total; } else { - setStatusMode('indeterminate'); + statusProgress.removeAttribute('value'); + statusProgress.removeAttribute('max'); } }, }).then(() => { setStatusMode('hidden'); - initializing = false; }, displayFailureNotice); } }()); diff --git a/misc/dist/ios_xcode/PrivacyInfo.xcprivacy b/misc/dist/ios_xcode/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..bc4a893d58 --- /dev/null +++ b/misc/dist/ios_xcode/PrivacyInfo.xcprivacy @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>NSPrivacyAccessedAPITypes</key> + $priv_api_types + $priv_tracking + $priv_collection +</dict> +</plist> diff --git a/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj b/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj index e9efea8809..a5de7e8872 100644 --- a/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj +++ b/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 9039D3BE24C093AC0020482C /* MoltenVK.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9039D3BD24C093AC0020482C /* MoltenVK.xcframework */; }; D0BCFE4618AEBDA2004A7AAE /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0BCFE4418AEBDA2004A7AAE /* InfoPlist.strings */; }; D0BCFE7818AEBFEB004A7AAE /* $binary.pck in Resources */ = {isa = PBXBuildFile; fileRef = D0BCFE7718AEBFEB004A7AAE /* $binary.pck */; }; + F965960D2BC2C3A800579C7E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F965960C2BC2C3A800579C7E /* PrivacyInfo.xcprivacy */; }; $pbx_launch_screen_build_reference /* End PBXBuildFile section */ @@ -47,6 +48,7 @@ D0BCFE4518AEBDA2004A7AAE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; $pbx_locale_file_reference D0BCFE7718AEBFEB004A7AAE /* $binary.pck */ = {isa = PBXFileReference; lastKnownFileType = file; path = "$binary.pck"; sourceTree = "<group>"; }; + F965960C2BC2C3A800579C7E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; $pbx_launch_screen_file_reference /* End PBXFileReference section */ @@ -75,6 +77,7 @@ D0BCFE4118AEBDA2004A7AAE /* $binary */, D0BCFE3618AEBDA2004A7AAE /* Frameworks */, D0BCFE3518AEBDA2004A7AAE /* Products */, + F965960C2BC2C3A800579C7E /* PrivacyInfo.xcprivacy */, $additional_pbx_resources_refs ); sourceTree = "<group>"; @@ -186,6 +189,7 @@ D0BCFE7818AEBFEB004A7AAE /* $binary.pck in Resources */, $pbx_launch_screen_build_phase D0BCFE4618AEBDA2004A7AAE /* InfoPlist.strings in Resources */, + F965960D2BC2C3A800579C7E /* PrivacyInfo.xcprivacy in Resources */, $additional_pbx_resources_build ); runOnlyForDeploymentPostprocessing = 0; diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected index 4706ed37f0..f935a512b9 100644 --- a/misc/extension_api_validation/4.2-stable.expected +++ b/misc/extension_api_validation/4.2-stable.expected @@ -315,3 +315,18 @@ Validate extension JSON: Error: Field 'classes/TextServer/methods/shaped_text_ge Validate extension JSON: Error: Field 'classes/TextServerExtension/methods/_shaped_text_get_word_breaks/arguments': size changed value in new API, from 2 to 3. Added optional argument. Compatibility method registered. + + +GH-86978 +-------- +Validate extension JSON: Error: Field 'classes/TextEdit/methods/set_selection_mode/arguments': size changed value in new API, from 4 to 1. + +Removed optional arguments set_selection_mode, use set_selection_origin_line/column instead. +Compatibility methods registered. + + +GH-84472 +-------- +Validate extension JSON: Error: Field 'classes/CanvasItem/methods/draw_circle/arguments': size changed value in new API, from 3 to 6. + +Optional arguments added. Compatibility methods registered. diff --git a/modules/basis_universal/image_compress_basisu.cpp b/modules/basis_universal/image_compress_basisu.cpp index 72e7977eef..531b738041 100644 --- a/modules/basis_universal/image_compress_basisu.cpp +++ b/modules/basis_universal/image_compress_basisu.cpp @@ -96,17 +96,74 @@ Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedCha } break; } + // Copy the source image data with mipmaps into BasisU. { - // Encode the image with mipmaps. + const int orig_width = image->get_width(); + const int orig_height = image->get_height(); + + bool is_res_div_4 = (orig_width % 4 == 0) && (orig_height % 4 == 0); + + // Image's resolution rounded up to the nearest values divisible by 4. + int next_width = orig_width <= 2 ? orig_width : (orig_width + 3) & ~3; + int next_height = orig_height <= 2 ? orig_height : (orig_height + 3) & ~3; + Vector<uint8_t> image_data = image->get_data(); basisu::vector<basisu::image> basisu_mipmaps; + // Buffer for storing padded mipmap data. + Vector<uint32_t> mip_data_padded; + for (int32_t i = 0; i <= image->get_mipmap_count(); i++) { int ofs, size, width, height; image->get_mipmap_offset_size_and_dimensions(i, ofs, size, width, height); + const uint8_t *image_mip_data = image_data.ptr() + ofs; + + // Pad the mipmap's data if its resolution isn't divisible by 4. + if (image->has_mipmaps() && !is_res_div_4 && (width > 2 && height > 2) && (width != next_width || height != next_height)) { + // Source mip's data interpreted as 32-bit RGBA blocks to help with copying pixel data. + const uint32_t *mip_src_data = reinterpret_cast<const uint32_t *>(image_mip_data); + + // Reserve space in the padded buffer. + mip_data_padded.resize(next_width * next_height); + uint32_t *data_padded_ptr = mip_data_padded.ptrw(); + + // Pad mipmap to the nearest block by smearing. + int x = 0, y = 0; + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + data_padded_ptr[next_width * y + x] = mip_src_data[width * y + x]; + } + + // First, smear in x. + for (; x < next_width; x++) { + data_padded_ptr[next_width * y + x] = data_padded_ptr[next_width * y + x - 1]; + } + } + + // Then, smear in y. + for (; y < next_height; y++) { + for (x = 0; x < next_width; x++) { + data_padded_ptr[next_width * y + x] = data_padded_ptr[next_width * y + x - next_width]; + } + } + + // Override the image_mip_data pointer with our temporary Vector. + image_mip_data = reinterpret_cast<const uint8_t *>(mip_data_padded.ptr()); + + // Override the mipmap's properties. + width = next_width; + height = next_height; + size = mip_data_padded.size() * 4; + } + + // Get the next mipmap's resolution. + next_width /= 2; + next_height /= 2; + + // Copy the source mipmap's data to a BasisU image. basisu::image basisu_image(width, height); - memcpy(basisu_image.get_ptr(), image_data.ptr() + ofs, size); + memcpy(basisu_image.get_ptr(), image_mip_data, size); if (i == 0) { params.m_source_images.push_back(basisu_image); @@ -132,10 +189,10 @@ Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedCha // Copy the encoded data to the buffer. { - uint8_t *w = basisu_data.ptrw(); - *(uint32_t *)w = decompress_format; + uint8_t *wb = basisu_data.ptrw(); + *(uint32_t *)wb = decompress_format; - memcpy(w + 4, basisu_out.get_ptr(), basisu_out.size()); + memcpy(wb + 4, basisu_out.get_ptr(), basisu_out.size()); } return basisu_data; @@ -238,8 +295,7 @@ Ref<Image> basis_universal_unpacker_ptr(const uint8_t *p_data, int p_size) { uint8_t *dst = out_data.ptrw(); memset(dst, 0, out_data.size()); - uint32_t mip_count = Image::get_image_required_mipmaps(basisu_info.m_orig_width, basisu_info.m_orig_height, image_format); - for (uint32_t i = 0; i <= mip_count; i++) { + for (uint32_t i = 0; i < basisu_info.m_total_levels; i++) { basist::basisu_image_level_info basisu_level; transcoder.get_image_level_info(src_ptr, src_size, basisu_level, 0, i); diff --git a/modules/csg/editor/csg_gizmos.h b/modules/csg/editor/csg_gizmos.h index 6281db0a21..de19b33e7d 100644 --- a/modules/csg/editor/csg_gizmos.h +++ b/modules/csg/editor/csg_gizmos.h @@ -35,7 +35,7 @@ #include "../csg_shape.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "editor/plugins/node_3d_editor_gizmos.h" class Gizmo3DHelper; diff --git a/modules/fbx/fbx_document.cpp b/modules/fbx/fbx_document.cpp index e92609f42f..5f94a80566 100644 --- a/modules/fbx/fbx_document.cpp +++ b/modules/fbx/fbx_document.cpp @@ -86,6 +86,17 @@ static Quaternion _as_quaternion(const ufbx_quat &p_quat) { return Quaternion(real_t(p_quat.x), real_t(p_quat.y), real_t(p_quat.z), real_t(p_quat.w)); } +static Transform3D _as_transform(const ufbx_transform &p_xform) { + Transform3D result; + result.origin = FBXDocument::_as_vec3(p_xform.translation); + result.basis.set_quaternion_scale(_as_quaternion(p_xform.rotation), FBXDocument::_as_vec3(p_xform.scale)); + return result; +} + +static real_t _relative_error(const Vector3 &p_a, const Vector3 &p_b) { + return p_a.distance_to(p_b) / MAX(p_a.length(), p_b.length()); +} + static Color _material_color(const ufbx_material_map &p_map) { if (p_map.value_components == 1) { float r = float(p_map.value_real); @@ -196,6 +207,16 @@ static uint32_t _decode_vertex_index(const Vector3 &p_vertex) { return uint32_t(p_vertex.x) | uint32_t(p_vertex.y) << 16; } +static ufbx_skin_deformer *_find_skin_deformer(ufbx_skin_cluster *p_cluster) { + for (const ufbx_connection &conn : p_cluster->element.connections_src) { + ufbx_skin_deformer *deformer = ufbx_as_skin_deformer(conn.dst); + if (deformer) { + return deformer; + } + } + return nullptr; +} + struct ThreadPoolFBX { struct Group { ufbx_thread_pool_context ctx = {}; @@ -333,23 +354,67 @@ Error FBXDocument::_parse_nodes(Ref<FBXState> p_state) { } { - node->transform.origin = _as_vec3(fbx_node->local_transform.translation); - node->transform.basis.set_quaternion_scale(_as_quaternion(fbx_node->local_transform.rotation), _as_vec3(fbx_node->local_transform.scale)); - - if (fbx_node->bind_pose) { - ufbx_bone_pose *pose = ufbx_get_bone_pose(fbx_node->bind_pose, fbx_node); - ufbx_transform rest_transform = ufbx_matrix_to_transform(&pose->bone_to_parent); - - Vector3 rest_position = _as_vec3(rest_transform.translation); - Quaternion rest_rotation = _as_quaternion(rest_transform.rotation); - Vector3 rest_scale = _as_vec3(rest_transform.scale); - Transform3D godot_rest_xform; - godot_rest_xform.basis.set_quaternion_scale(rest_rotation, rest_scale); - godot_rest_xform.origin = rest_position; - node->set_additional_data("GODOT_rest_transform", godot_rest_xform); - } else { - node->set_additional_data("GODOT_rest_transform", node->transform); + node->transform = _as_transform(fbx_node->local_transform); + + bool found_rest_xform = false; + bool bad_rest_xform = false; + Transform3D candidate_rest_xform; + + if (fbx_node->parent) { + // Attempt to resolve a rest pose for bones: This uses internal FBX connections to find + // all skin clusters connected to the bone. + for (const ufbx_connection &child_conn : fbx_node->element.connections_src) { + ufbx_skin_cluster *child_cluster = ufbx_as_skin_cluster(child_conn.dst); + if (!child_cluster) + continue; + ufbx_skin_deformer *child_deformer = _find_skin_deformer(child_cluster); + if (!child_deformer) + continue; + + // Found a skin cluster: Now iterate through all the skin clusters of the parent and + // try to find one that used by the same deformer. + for (const ufbx_connection &parent_conn : fbx_node->parent->element.connections_src) { + ufbx_skin_cluster *parent_cluster = ufbx_as_skin_cluster(parent_conn.dst); + if (!parent_cluster) + continue; + ufbx_skin_deformer *parent_deformer = _find_skin_deformer(parent_cluster); + if (parent_deformer != child_deformer) + continue; + + // Success: Found two skin clusters from the same deformer, now we can resolve the + // local bind pose from the difference between the two world-space bind poses. + ufbx_matrix child_to_world = child_cluster->bind_to_world; + ufbx_matrix world_to_parent = ufbx_matrix_invert(&parent_cluster->bind_to_world); + ufbx_matrix child_to_parent = ufbx_matrix_mul(&world_to_parent, &child_to_world); + Transform3D xform = _as_transform(ufbx_matrix_to_transform(&child_to_parent)); + + if (!found_rest_xform) { + // Found the first bind pose for the node, assume that this one is good + found_rest_xform = true; + candidate_rest_xform = xform; + } else if (!bad_rest_xform) { + // Found another: Let's hope it's similar to the previous one, if not warn and + // use the initial pose, which is used by default if rest pose is not found. + real_t error = 0.0f; + error += _relative_error(candidate_rest_xform.origin, xform.origin); + for (int i = 0; i < 3; i++) { + error += _relative_error(candidate_rest_xform.basis.rows[i], xform.basis.rows[i]); + } + const real_t max_error = 0.01f; + if (error >= max_error) { + WARN_PRINT(vformat("FBX: Node '%s' has multiple bind poses, using initial pose as rest pose.", node->get_name())); + bad_rest_xform = true; + } + } + } + } + } + + Transform3D godot_rest_xform = node->transform; + if (found_rest_xform && !bad_rest_xform) { + godot_rest_xform = candidate_rest_xform; } + node->set_additional_data("GODOT_rest_transform", godot_rest_xform); } for (const ufbx_node *child : fbx_node->children) { diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 921ed0b416..f238958f25 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -2892,7 +2892,7 @@ String ResourceFormatLoaderGDScript::get_resource_type(const String &p_path) con return ""; } -void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types) { +void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List<String> *r_dependencies, bool p_add_types) { Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::READ); ERR_FAIL_COND_MSG(file.is_null(), "Cannot open file '" + p_path + "'."); @@ -2906,8 +2906,13 @@ void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List<S return; } + GDScriptAnalyzer analyzer(&parser); + if (OK != analyzer.analyze()) { + return; + } + for (const String &E : parser.get_dependencies()) { - p_dependencies->push_back(E); + r_dependencies->push_back(E); } } diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 781e284bfc..7bd68ac0b1 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -633,7 +633,7 @@ public: virtual void get_recognized_extensions(List<String> *p_extensions) const override; virtual bool handles_type(const String &p_type) const override; virtual String get_resource_type(const String &p_path) const override; - virtual void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false) override; + virtual void get_dependencies(const String &p_path, List<String> *r_dependencies, bool p_add_types = false) override; }; class ResourceFormatSaverGDScript : public ResourceFormatSaver { diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 0636ac5083..28a44357eb 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -562,6 +562,11 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c class_type.native_type = result.native_type; p_class->set_datatype(class_type); + // Add base class to the list of dependencies. + if (result.kind == GDScriptParser::DataType::CLASS) { + parser->add_dependency(result.script_path); + } + // Apply annotations. for (GDScriptParser::AnnotationNode *&E : p_class->annotations) { resolve_annotation(E); @@ -722,13 +727,32 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type } } else if (ProjectSettings::get_singleton()->has_autoload(first) && ProjectSettings::get_singleton()->get_autoload(first).is_singleton) { const ProjectSettings::AutoloadInfo &autoload = ProjectSettings::get_singleton()->get_autoload(first); - Ref<GDScriptParserRef> ref = parser->get_depended_parser_for(autoload.path); + String script_path; + if (ResourceLoader::get_resource_type(autoload.path) == "PackedScene") { + // Try to get script from scene if possible. + if (GDScriptLanguage::get_singleton()->has_any_global_constant(autoload.name)) { + Variant constant = GDScriptLanguage::get_singleton()->get_any_global_constant(autoload.name); + Node *node = Object::cast_to<Node>(constant); + if (node != nullptr) { + Ref<GDScript> scr = node->get_script(); + if (scr.is_valid()) { + script_path = scr->get_script_path(); + } + } + } + } else if (ResourceLoader::get_resource_type(autoload.path) == "GDScript") { + script_path = autoload.path; + } + if (script_path.is_empty()) { + return bad_type; + } + Ref<GDScriptParserRef> ref = parser->get_depended_parser_for(script_path); if (ref.is_null()) { - push_error(vformat(R"(The referenced autoload "%s" (from "%s") could not be loaded.)", first, autoload.path), p_type); + push_error(vformat(R"(The referenced autoload "%s" (from "%s") could not be loaded.)", first, script_path), p_type); return bad_type; } if (ref->raise_status(GDScriptParserRef::INHERITANCE_SOLVED) != OK) { - push_error(vformat(R"(Could not parse singleton "%s" from "%s".)", first, autoload.path), p_type); + push_error(vformat(R"(Could not parse singleton "%s" from "%s".)", first, script_path), p_type); return bad_type; } result = ref->get_parser()->head->get_datatype(); @@ -849,6 +873,11 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type } p_type->set_datatype(result); + + if (result.kind == GDScriptParser::DataType::CLASS || result.kind == GDScriptParser::DataType::SCRIPT) { + parser->add_dependency(result.script_path); + } + return result; } @@ -3769,7 +3798,7 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod } break; case GDScriptParser::ClassNode::Member::FUNCTION: { - if (is_base && (!base.is_meta_type || member.function->is_static)) { + if (is_base && (!base.is_meta_type || member.function->is_static || is_constructor)) { p_identifier->set_datatype(make_callable_type(member.function->info)); p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_FUNCTION; return; @@ -4063,6 +4092,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident if (ScriptServer::is_global_class(name)) { p_identifier->set_datatype(make_global_class_meta_type(name, p_identifier)); + parser->add_dependency(p_identifier->get_datatype().script_path); return; } @@ -4105,6 +4135,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident } result.is_constant = true; p_identifier->set_datatype(result); + parser->add_dependency(autoload.path); return; } } @@ -4224,7 +4255,6 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { push_error("Preloaded path must be a constant string.", p_preload->path); } else { p_preload->resolved_path = p_preload->path->reduced_value; - // TODO: Save this as script dependency. if (p_preload->resolved_path.is_relative_path()) { p_preload->resolved_path = parser->script_path.get_base_dir().path_join(p_preload->resolved_path); } @@ -4255,6 +4285,8 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { push_error(vformat(R"(Could not preload resource file "%s".)", p_preload->resolved_path), p_preload->path); } } + + parser->add_dependency(p_preload->resolved_path); } } diff --git a/modules/gdscript/gdscript_byte_codegen.cpp b/modules/gdscript/gdscript_byte_codegen.cpp index bfe090edb0..5a50bd8648 100644 --- a/modules/gdscript/gdscript_byte_codegen.cpp +++ b/modules/gdscript/gdscript_byte_codegen.cpp @@ -1196,23 +1196,49 @@ void GDScriptByteCodeGenerator::write_call_builtin_type_static(const Address &p_ } void GDScriptByteCodeGenerator::write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector<Address> &p_arguments) { - bool is_validated = false; - MethodBind *method = ClassDB::get_method(p_class, p_method); - if (!is_validated) { - // Perform regular call. - append_opcode_and_argcount(GDScriptFunction::OPCODE_CALL_NATIVE_STATIC, p_arguments.size() + 1); - for (int i = 0; i < p_arguments.size(); i++) { - append(p_arguments[i]); + // Perform regular call. + append_opcode_and_argcount(GDScriptFunction::OPCODE_CALL_NATIVE_STATIC, p_arguments.size() + 1); + for (int i = 0; i < p_arguments.size(); i++) { + append(p_arguments[i]); + } + CallTarget ct = get_call_target(p_target); + append(ct.target); + append(method); + append(p_arguments.size()); + ct.cleanup(); + return; +} + +void GDScriptByteCodeGenerator::write_call_native_static_validated(const GDScriptCodeGenerator::Address &p_target, MethodBind *p_method, const Vector<GDScriptCodeGenerator::Address> &p_arguments) { + Variant::Type return_type = Variant::NIL; + bool has_return = p_method->has_return(); + + if (has_return) { + PropertyInfo return_info = p_method->get_return_info(); + return_type = return_info.type; + } + + CallTarget ct = get_call_target(p_target, return_type); + + if (has_return) { + Variant::Type temp_type = temporaries[ct.target.address].type; + if (temp_type != return_type) { + write_type_adjust(ct.target, return_type); } - CallTarget ct = get_call_target(p_target); - append(ct.target); - append(method); - append(p_arguments.size()); - ct.cleanup(); - return; } + + GDScriptFunction::Opcode code = p_method->has_return() ? GDScriptFunction::OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN : GDScriptFunction::OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN; + append_opcode_and_argcount(code, 1 + p_arguments.size()); + + for (int i = 0; i < p_arguments.size(); i++) { + append(p_arguments[i]); + } + append(ct.target); + append(p_arguments.size()); + append(p_method); + ct.cleanup(); } void GDScriptByteCodeGenerator::write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) { diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h index 5a736b2554..34f56a2f5c 100644 --- a/modules/gdscript/gdscript_byte_codegen.h +++ b/modules/gdscript/gdscript_byte_codegen.h @@ -518,6 +518,7 @@ public: virtual void write_call_builtin_type(const Address &p_target, const Address &p_base, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) override; virtual void write_call_builtin_type_static(const Address &p_target, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) override; virtual void write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector<Address> &p_arguments) override; + virtual void write_call_native_static_validated(const Address &p_target, MethodBind *p_method, const Vector<Address> &p_arguments) override; virtual void write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) override; virtual void write_call_method_bind_validated(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) override; virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) override; diff --git a/modules/gdscript/gdscript_codegen.h b/modules/gdscript/gdscript_codegen.h index 4c33ed499a..c1c0b61395 100644 --- a/modules/gdscript/gdscript_codegen.h +++ b/modules/gdscript/gdscript_codegen.h @@ -131,6 +131,7 @@ public: virtual void write_call_builtin_type(const Address &p_target, const Address &p_base, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_builtin_type_static(const Address &p_target, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector<Address> &p_arguments) = 0; + virtual void write_call_native_static_validated(const Address &p_target, MethodBind *p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_method_bind_validated(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) = 0; diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index 734e37bc09..a8a7f3d9f7 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -673,7 +673,15 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code } else if (!call->is_super && subscript->base->type == GDScriptParser::Node::IDENTIFIER && call->function_name != SNAME("new") && ClassDB::class_exists(static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name) && !Engine::get_singleton()->has_singleton(static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name)) { // It's a static native method call. - gen->write_call_native_static(result, static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name, subscript->attribute->name, arguments); + StringName class_name = static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name; + MethodBind *method = ClassDB::get_method(class_name, subscript->attribute->name); + if (_can_use_validate_call(method, arguments)) { + // Exact arguments, use validated call. + gen->write_call_native_static_validated(result, method, arguments); + } else { + // Not exact arguments, use regular static call + gen->write_call_native_static(result, class_name, subscript->attribute->name, arguments); + } } else { GDScriptCodeGenerator::Address base = _parse_expression(codegen, r_error, subscript->base); if (r_error) { diff --git a/modules/gdscript/gdscript_disassembler.cpp b/modules/gdscript/gdscript_disassembler.cpp index c7873dcd52..8dd04c76dd 100644 --- a/modules/gdscript/gdscript_disassembler.cpp +++ b/modules/gdscript/gdscript_disassembler.cpp @@ -678,6 +678,50 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const { incr += 4 + argc; } break; + case OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN: { + int instr_var_args = _code_ptr[++ip]; + text += "call native static method validated (return) "; + MethodBind *method = _methods_ptr[_code_ptr[ip + 2 + instr_var_args]]; + int argc = _code_ptr[ip + 1 + instr_var_args]; + text += DADDR(1 + argc) + " = "; + text += method->get_instance_class(); + text += "."; + text += method->get_name(); + text += "("; + for (int i = 0; i < argc; i++) { + if (i > 0) + text += ", "; + text += DADDR(1 + i); + } + text += ")"; + incr = 4 + argc; + } break; + + case OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN: { + int instr_var_args = _code_ptr[++ip]; + + text += "call native static method validated (no return) "; + + MethodBind *method = _methods_ptr[_code_ptr[ip + 2 + instr_var_args]]; + + int argc = _code_ptr[ip + 1 + instr_var_args]; + + text += method->get_instance_class(); + text += "."; + text += method->get_name(); + text += "("; + + for (int i = 0; i < argc; i++) { + if (i > 0) { + text += ", "; + } + text += DADDR(1 + i); + } + text += ")"; + + incr = 4 + argc; + } break; + case OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN: { int instr_var_args = _code_ptr[++ip]; text += "call method-bind validated (return) "; diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index 184d256bcd..430b96115b 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -264,6 +264,8 @@ public: OPCODE_CALL_METHOD_BIND_RET, OPCODE_CALL_BUILTIN_STATIC, OPCODE_CALL_NATIVE_STATIC, + OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN, + OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN, OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN, OPCODE_CALL_METHOD_BIND_VALIDATED_NO_RETURN, OPCODE_AWAIT, diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 77cb8136f8..7fb9ffe9a5 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1429,6 +1429,8 @@ private: void reset_extents(Node *p_node, GDScriptTokenizer::Token p_token); void reset_extents(Node *p_node, Node *p_from); + HashSet<String> dependencies; + template <typename T> T *alloc_node() { T *node = memnew(T); @@ -1572,9 +1574,11 @@ public: bool annotation_exists(const String &p_annotation_name) const; const List<ParserError> &get_errors() const { return errors; } - const List<String> get_dependencies() const { - // TODO: Keep track of deps. - return List<String>(); + const HashSet<String> &get_dependencies() const { + return dependencies; + } + void add_dependency(const String &p_dependency) { + dependencies.insert(p_dependency); } #ifdef DEBUG_ENABLED const List<GDScriptWarning> &get_warnings() const { return warnings; } diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp index 3cb011b251..4e76965889 100644 --- a/modules/gdscript/gdscript_vm.cpp +++ b/modules/gdscript/gdscript_vm.cpp @@ -211,156 +211,158 @@ void (*type_init_function_table[])(Variant *) = { }; #if defined(__GNUC__) -#define OPCODES_TABLE \ - static const void *switch_table_ops[] = { \ - &&OPCODE_OPERATOR, \ - &&OPCODE_OPERATOR_VALIDATED, \ - &&OPCODE_TYPE_TEST_BUILTIN, \ - &&OPCODE_TYPE_TEST_ARRAY, \ - &&OPCODE_TYPE_TEST_NATIVE, \ - &&OPCODE_TYPE_TEST_SCRIPT, \ - &&OPCODE_SET_KEYED, \ - &&OPCODE_SET_KEYED_VALIDATED, \ - &&OPCODE_SET_INDEXED_VALIDATED, \ - &&OPCODE_GET_KEYED, \ - &&OPCODE_GET_KEYED_VALIDATED, \ - &&OPCODE_GET_INDEXED_VALIDATED, \ - &&OPCODE_SET_NAMED, \ - &&OPCODE_SET_NAMED_VALIDATED, \ - &&OPCODE_GET_NAMED, \ - &&OPCODE_GET_NAMED_VALIDATED, \ - &&OPCODE_SET_MEMBER, \ - &&OPCODE_GET_MEMBER, \ - &&OPCODE_SET_STATIC_VARIABLE, \ - &&OPCODE_GET_STATIC_VARIABLE, \ - &&OPCODE_ASSIGN, \ - &&OPCODE_ASSIGN_NULL, \ - &&OPCODE_ASSIGN_TRUE, \ - &&OPCODE_ASSIGN_FALSE, \ - &&OPCODE_ASSIGN_TYPED_BUILTIN, \ - &&OPCODE_ASSIGN_TYPED_ARRAY, \ - &&OPCODE_ASSIGN_TYPED_NATIVE, \ - &&OPCODE_ASSIGN_TYPED_SCRIPT, \ - &&OPCODE_CAST_TO_BUILTIN, \ - &&OPCODE_CAST_TO_NATIVE, \ - &&OPCODE_CAST_TO_SCRIPT, \ - &&OPCODE_CONSTRUCT, \ - &&OPCODE_CONSTRUCT_VALIDATED, \ - &&OPCODE_CONSTRUCT_ARRAY, \ - &&OPCODE_CONSTRUCT_TYPED_ARRAY, \ - &&OPCODE_CONSTRUCT_DICTIONARY, \ - &&OPCODE_CALL, \ - &&OPCODE_CALL_RETURN, \ - &&OPCODE_CALL_ASYNC, \ - &&OPCODE_CALL_UTILITY, \ - &&OPCODE_CALL_UTILITY_VALIDATED, \ - &&OPCODE_CALL_GDSCRIPT_UTILITY, \ - &&OPCODE_CALL_BUILTIN_TYPE_VALIDATED, \ - &&OPCODE_CALL_SELF_BASE, \ - &&OPCODE_CALL_METHOD_BIND, \ - &&OPCODE_CALL_METHOD_BIND_RET, \ - &&OPCODE_CALL_BUILTIN_STATIC, \ - &&OPCODE_CALL_NATIVE_STATIC, \ - &&OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN, \ - &&OPCODE_CALL_METHOD_BIND_VALIDATED_NO_RETURN, \ - &&OPCODE_AWAIT, \ - &&OPCODE_AWAIT_RESUME, \ - &&OPCODE_CREATE_LAMBDA, \ - &&OPCODE_CREATE_SELF_LAMBDA, \ - &&OPCODE_JUMP, \ - &&OPCODE_JUMP_IF, \ - &&OPCODE_JUMP_IF_NOT, \ - &&OPCODE_JUMP_TO_DEF_ARGUMENT, \ - &&OPCODE_JUMP_IF_SHARED, \ - &&OPCODE_RETURN, \ - &&OPCODE_RETURN_TYPED_BUILTIN, \ - &&OPCODE_RETURN_TYPED_ARRAY, \ - &&OPCODE_RETURN_TYPED_NATIVE, \ - &&OPCODE_RETURN_TYPED_SCRIPT, \ - &&OPCODE_ITERATE_BEGIN, \ - &&OPCODE_ITERATE_BEGIN_INT, \ - &&OPCODE_ITERATE_BEGIN_FLOAT, \ - &&OPCODE_ITERATE_BEGIN_VECTOR2, \ - &&OPCODE_ITERATE_BEGIN_VECTOR2I, \ - &&OPCODE_ITERATE_BEGIN_VECTOR3, \ - &&OPCODE_ITERATE_BEGIN_VECTOR3I, \ - &&OPCODE_ITERATE_BEGIN_STRING, \ - &&OPCODE_ITERATE_BEGIN_DICTIONARY, \ - &&OPCODE_ITERATE_BEGIN_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_BYTE_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_INT32_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_INT64_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT32_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT64_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_STRING_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR2_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR3_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_COLOR_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_OBJECT, \ - &&OPCODE_ITERATE, \ - &&OPCODE_ITERATE_INT, \ - &&OPCODE_ITERATE_FLOAT, \ - &&OPCODE_ITERATE_VECTOR2, \ - &&OPCODE_ITERATE_VECTOR2I, \ - &&OPCODE_ITERATE_VECTOR3, \ - &&OPCODE_ITERATE_VECTOR3I, \ - &&OPCODE_ITERATE_STRING, \ - &&OPCODE_ITERATE_DICTIONARY, \ - &&OPCODE_ITERATE_ARRAY, \ - &&OPCODE_ITERATE_PACKED_BYTE_ARRAY, \ - &&OPCODE_ITERATE_PACKED_INT32_ARRAY, \ - &&OPCODE_ITERATE_PACKED_INT64_ARRAY, \ - &&OPCODE_ITERATE_PACKED_FLOAT32_ARRAY, \ - &&OPCODE_ITERATE_PACKED_FLOAT64_ARRAY, \ - &&OPCODE_ITERATE_PACKED_STRING_ARRAY, \ - &&OPCODE_ITERATE_PACKED_VECTOR2_ARRAY, \ - &&OPCODE_ITERATE_PACKED_VECTOR3_ARRAY, \ - &&OPCODE_ITERATE_PACKED_COLOR_ARRAY, \ - &&OPCODE_ITERATE_OBJECT, \ - &&OPCODE_STORE_GLOBAL, \ - &&OPCODE_STORE_NAMED_GLOBAL, \ - &&OPCODE_TYPE_ADJUST_BOOL, \ - &&OPCODE_TYPE_ADJUST_INT, \ - &&OPCODE_TYPE_ADJUST_FLOAT, \ - &&OPCODE_TYPE_ADJUST_STRING, \ - &&OPCODE_TYPE_ADJUST_VECTOR2, \ - &&OPCODE_TYPE_ADJUST_VECTOR2I, \ - &&OPCODE_TYPE_ADJUST_RECT2, \ - &&OPCODE_TYPE_ADJUST_RECT2I, \ - &&OPCODE_TYPE_ADJUST_VECTOR3, \ - &&OPCODE_TYPE_ADJUST_VECTOR3I, \ - &&OPCODE_TYPE_ADJUST_TRANSFORM2D, \ - &&OPCODE_TYPE_ADJUST_VECTOR4, \ - &&OPCODE_TYPE_ADJUST_VECTOR4I, \ - &&OPCODE_TYPE_ADJUST_PLANE, \ - &&OPCODE_TYPE_ADJUST_QUATERNION, \ - &&OPCODE_TYPE_ADJUST_AABB, \ - &&OPCODE_TYPE_ADJUST_BASIS, \ - &&OPCODE_TYPE_ADJUST_TRANSFORM3D, \ - &&OPCODE_TYPE_ADJUST_PROJECTION, \ - &&OPCODE_TYPE_ADJUST_COLOR, \ - &&OPCODE_TYPE_ADJUST_STRING_NAME, \ - &&OPCODE_TYPE_ADJUST_NODE_PATH, \ - &&OPCODE_TYPE_ADJUST_RID, \ - &&OPCODE_TYPE_ADJUST_OBJECT, \ - &&OPCODE_TYPE_ADJUST_CALLABLE, \ - &&OPCODE_TYPE_ADJUST_SIGNAL, \ - &&OPCODE_TYPE_ADJUST_DICTIONARY, \ - &&OPCODE_TYPE_ADJUST_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_BYTE_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_INT32_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_INT64_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_FLOAT32_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_FLOAT64_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_STRING_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_VECTOR2_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_VECTOR3_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_COLOR_ARRAY, \ - &&OPCODE_ASSERT, \ - &&OPCODE_BREAKPOINT, \ - &&OPCODE_LINE, \ - &&OPCODE_END \ - }; \ +#define OPCODES_TABLE \ + static const void *switch_table_ops[] = { \ + &&OPCODE_OPERATOR, \ + &&OPCODE_OPERATOR_VALIDATED, \ + &&OPCODE_TYPE_TEST_BUILTIN, \ + &&OPCODE_TYPE_TEST_ARRAY, \ + &&OPCODE_TYPE_TEST_NATIVE, \ + &&OPCODE_TYPE_TEST_SCRIPT, \ + &&OPCODE_SET_KEYED, \ + &&OPCODE_SET_KEYED_VALIDATED, \ + &&OPCODE_SET_INDEXED_VALIDATED, \ + &&OPCODE_GET_KEYED, \ + &&OPCODE_GET_KEYED_VALIDATED, \ + &&OPCODE_GET_INDEXED_VALIDATED, \ + &&OPCODE_SET_NAMED, \ + &&OPCODE_SET_NAMED_VALIDATED, \ + &&OPCODE_GET_NAMED, \ + &&OPCODE_GET_NAMED_VALIDATED, \ + &&OPCODE_SET_MEMBER, \ + &&OPCODE_GET_MEMBER, \ + &&OPCODE_SET_STATIC_VARIABLE, \ + &&OPCODE_GET_STATIC_VARIABLE, \ + &&OPCODE_ASSIGN, \ + &&OPCODE_ASSIGN_NULL, \ + &&OPCODE_ASSIGN_TRUE, \ + &&OPCODE_ASSIGN_FALSE, \ + &&OPCODE_ASSIGN_TYPED_BUILTIN, \ + &&OPCODE_ASSIGN_TYPED_ARRAY, \ + &&OPCODE_ASSIGN_TYPED_NATIVE, \ + &&OPCODE_ASSIGN_TYPED_SCRIPT, \ + &&OPCODE_CAST_TO_BUILTIN, \ + &&OPCODE_CAST_TO_NATIVE, \ + &&OPCODE_CAST_TO_SCRIPT, \ + &&OPCODE_CONSTRUCT, \ + &&OPCODE_CONSTRUCT_VALIDATED, \ + &&OPCODE_CONSTRUCT_ARRAY, \ + &&OPCODE_CONSTRUCT_TYPED_ARRAY, \ + &&OPCODE_CONSTRUCT_DICTIONARY, \ + &&OPCODE_CALL, \ + &&OPCODE_CALL_RETURN, \ + &&OPCODE_CALL_ASYNC, \ + &&OPCODE_CALL_UTILITY, \ + &&OPCODE_CALL_UTILITY_VALIDATED, \ + &&OPCODE_CALL_GDSCRIPT_UTILITY, \ + &&OPCODE_CALL_BUILTIN_TYPE_VALIDATED, \ + &&OPCODE_CALL_SELF_BASE, \ + &&OPCODE_CALL_METHOD_BIND, \ + &&OPCODE_CALL_METHOD_BIND_RET, \ + &&OPCODE_CALL_BUILTIN_STATIC, \ + &&OPCODE_CALL_NATIVE_STATIC, \ + &&OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN, \ + &&OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN, \ + &&OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN, \ + &&OPCODE_CALL_METHOD_BIND_VALIDATED_NO_RETURN, \ + &&OPCODE_AWAIT, \ + &&OPCODE_AWAIT_RESUME, \ + &&OPCODE_CREATE_LAMBDA, \ + &&OPCODE_CREATE_SELF_LAMBDA, \ + &&OPCODE_JUMP, \ + &&OPCODE_JUMP_IF, \ + &&OPCODE_JUMP_IF_NOT, \ + &&OPCODE_JUMP_TO_DEF_ARGUMENT, \ + &&OPCODE_JUMP_IF_SHARED, \ + &&OPCODE_RETURN, \ + &&OPCODE_RETURN_TYPED_BUILTIN, \ + &&OPCODE_RETURN_TYPED_ARRAY, \ + &&OPCODE_RETURN_TYPED_NATIVE, \ + &&OPCODE_RETURN_TYPED_SCRIPT, \ + &&OPCODE_ITERATE_BEGIN, \ + &&OPCODE_ITERATE_BEGIN_INT, \ + &&OPCODE_ITERATE_BEGIN_FLOAT, \ + &&OPCODE_ITERATE_BEGIN_VECTOR2, \ + &&OPCODE_ITERATE_BEGIN_VECTOR2I, \ + &&OPCODE_ITERATE_BEGIN_VECTOR3, \ + &&OPCODE_ITERATE_BEGIN_VECTOR3I, \ + &&OPCODE_ITERATE_BEGIN_STRING, \ + &&OPCODE_ITERATE_BEGIN_DICTIONARY, \ + &&OPCODE_ITERATE_BEGIN_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_BYTE_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_INT32_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_INT64_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT32_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT64_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_STRING_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR2_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR3_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_COLOR_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_OBJECT, \ + &&OPCODE_ITERATE, \ + &&OPCODE_ITERATE_INT, \ + &&OPCODE_ITERATE_FLOAT, \ + &&OPCODE_ITERATE_VECTOR2, \ + &&OPCODE_ITERATE_VECTOR2I, \ + &&OPCODE_ITERATE_VECTOR3, \ + &&OPCODE_ITERATE_VECTOR3I, \ + &&OPCODE_ITERATE_STRING, \ + &&OPCODE_ITERATE_DICTIONARY, \ + &&OPCODE_ITERATE_ARRAY, \ + &&OPCODE_ITERATE_PACKED_BYTE_ARRAY, \ + &&OPCODE_ITERATE_PACKED_INT32_ARRAY, \ + &&OPCODE_ITERATE_PACKED_INT64_ARRAY, \ + &&OPCODE_ITERATE_PACKED_FLOAT32_ARRAY, \ + &&OPCODE_ITERATE_PACKED_FLOAT64_ARRAY, \ + &&OPCODE_ITERATE_PACKED_STRING_ARRAY, \ + &&OPCODE_ITERATE_PACKED_VECTOR2_ARRAY, \ + &&OPCODE_ITERATE_PACKED_VECTOR3_ARRAY, \ + &&OPCODE_ITERATE_PACKED_COLOR_ARRAY, \ + &&OPCODE_ITERATE_OBJECT, \ + &&OPCODE_STORE_GLOBAL, \ + &&OPCODE_STORE_NAMED_GLOBAL, \ + &&OPCODE_TYPE_ADJUST_BOOL, \ + &&OPCODE_TYPE_ADJUST_INT, \ + &&OPCODE_TYPE_ADJUST_FLOAT, \ + &&OPCODE_TYPE_ADJUST_STRING, \ + &&OPCODE_TYPE_ADJUST_VECTOR2, \ + &&OPCODE_TYPE_ADJUST_VECTOR2I, \ + &&OPCODE_TYPE_ADJUST_RECT2, \ + &&OPCODE_TYPE_ADJUST_RECT2I, \ + &&OPCODE_TYPE_ADJUST_VECTOR3, \ + &&OPCODE_TYPE_ADJUST_VECTOR3I, \ + &&OPCODE_TYPE_ADJUST_TRANSFORM2D, \ + &&OPCODE_TYPE_ADJUST_VECTOR4, \ + &&OPCODE_TYPE_ADJUST_VECTOR4I, \ + &&OPCODE_TYPE_ADJUST_PLANE, \ + &&OPCODE_TYPE_ADJUST_QUATERNION, \ + &&OPCODE_TYPE_ADJUST_AABB, \ + &&OPCODE_TYPE_ADJUST_BASIS, \ + &&OPCODE_TYPE_ADJUST_TRANSFORM3D, \ + &&OPCODE_TYPE_ADJUST_PROJECTION, \ + &&OPCODE_TYPE_ADJUST_COLOR, \ + &&OPCODE_TYPE_ADJUST_STRING_NAME, \ + &&OPCODE_TYPE_ADJUST_NODE_PATH, \ + &&OPCODE_TYPE_ADJUST_RID, \ + &&OPCODE_TYPE_ADJUST_OBJECT, \ + &&OPCODE_TYPE_ADJUST_CALLABLE, \ + &&OPCODE_TYPE_ADJUST_SIGNAL, \ + &&OPCODE_TYPE_ADJUST_DICTIONARY, \ + &&OPCODE_TYPE_ADJUST_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_BYTE_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_INT32_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_INT64_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_FLOAT32_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_FLOAT64_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_STRING_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_VECTOR2_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_VECTOR3_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_COLOR_ARRAY, \ + &&OPCODE_ASSERT, \ + &&OPCODE_BREAKPOINT, \ + &&OPCODE_LINE, \ + &&OPCODE_END \ + }; \ static_assert((sizeof(switch_table_ops) / sizeof(switch_table_ops[0]) == (OPCODE_END + 1)), "Opcodes in jump table aren't the same as opcodes in enum."); #define OPCODE(m_op) \ @@ -882,23 +884,27 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #endif #ifdef DEBUG_ENABLED if (!valid) { - Object *obj = dst->get_validated_object(); - String v = index->operator String(); - bool read_only_property = false; - if (obj) { - read_only_property = ClassDB::has_property(obj->get_class_name(), v) && (ClassDB::get_property_setter(obj->get_class_name(), v) == StringName()); - } - if (read_only_property) { - err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", v, _get_var_type(dst)); + if (dst->is_read_only()) { + err_text = "Invalid assignment on read-only value (on base: '" + _get_var_type(dst) + "')."; } else { - if (!v.is_empty()) { - v = "'" + v + "'"; - } else { - v = "of type '" + _get_var_type(index) + "'"; + Object *obj = dst->get_validated_object(); + String v = index->operator String(); + bool read_only_property = false; + if (obj) { + read_only_property = ClassDB::has_property(obj->get_class_name(), v) && (ClassDB::get_property_setter(obj->get_class_name(), v) == StringName()); } - err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'."; - if (err_code == Variant::VariantSetError::SET_INDEXED_ERR) { - err_text = "Invalid assignment of index " + v + " (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'."; + if (read_only_property) { + err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", v, _get_var_type(dst)); + } else { + if (!v.is_empty()) { + v = "'" + v + "'"; + } else { + v = "of type '" + _get_var_type(index) + "'"; + } + err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'."; + if (err_code == Variant::VariantSetError::SET_INDEXED_ERR) { + err_text = "Invalid assignment of index " + v + " (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'."; + } } } OPCODE_BREAK; @@ -924,13 +930,17 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED if (!valid) { - String v = index->operator String(); - if (!v.is_empty()) { - v = "'" + v + "'"; + if (dst->is_read_only()) { + err_text = "Invalid assignment on read-only value (on base: '" + _get_var_type(dst) + "')."; } else { - v = "of type '" + _get_var_type(index) + "'"; + String v = index->operator String(); + if (!v.is_empty()) { + v = "'" + v + "'"; + } else { + v = "of type '" + _get_var_type(index) + "'"; + } + err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'."; } - err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'."; OPCODE_BREAK; } #endif @@ -956,13 +966,17 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED if (oob) { - String v = index->operator String(); - if (!v.is_empty()) { - v = "'" + v + "'"; + if (dst->is_read_only()) { + err_text = "Invalid assignment on read-only value (on base: '" + _get_var_type(dst) + "')."; } else { - v = "of type '" + _get_var_type(index) + "'"; + String v = index->operator String(); + if (!v.is_empty()) { + v = "'" + v + "'"; + } else { + v = "of type '" + _get_var_type(index) + "'"; + } + err_text = "Out of bounds set index " + v + " (on base: '" + _get_var_type(dst) + "')"; } - err_text = "Out of bounds set index " + v + " (on base: '" + _get_var_type(dst) + "')"; OPCODE_BREAK; } #endif @@ -1090,15 +1104,19 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED if (!valid) { - Object *obj = dst->get_validated_object(); - bool read_only_property = false; - if (obj) { - read_only_property = ClassDB::has_property(obj->get_class_name(), *index) && (ClassDB::get_property_setter(obj->get_class_name(), *index) == StringName()); - } - if (read_only_property) { - err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", String(*index), _get_var_type(dst)); + if (dst->is_read_only()) { + err_text = "Invalid assignment on read-only value (on base: '" + _get_var_type(dst) + "')."; } else { - err_text = "Invalid assignment of property or key '" + String(*index) + "' with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'."; + Object *obj = dst->get_validated_object(); + bool read_only_property = false; + if (obj) { + read_only_property = ClassDB::has_property(obj->get_class_name(), *index) && (ClassDB::get_property_setter(obj->get_class_name(), *index) == StringName()); + } + if (read_only_property) { + err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", String(*index), _get_var_type(dst)); + } else { + err_text = "Invalid assignment of property or key '" + String(*index) + "' with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'."; + } } OPCODE_BREAK; } @@ -1956,6 +1974,78 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } DISPATCH_OPCODE; + OPCODE(OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN) { + LOAD_INSTRUCTION_ARGS + CHECK_SPACE(3 + instr_arg_count); + + ip += instr_arg_count; + + int argc = _code_ptr[ip + 1]; + GD_ERR_BREAK(argc < 0); + + GD_ERR_BREAK(_code_ptr[ip + 2] < 0 || _code_ptr[ip + 2] >= _methods_count); + MethodBind *method = _methods_ptr[_code_ptr[ip + 2]]; + + Variant **argptrs = instruction_args; + +#ifdef DEBUG_ENABLED + uint64_t call_time = 0; + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + call_time = OS::get_singleton()->get_ticks_usec(); + } +#endif + + GET_INSTRUCTION_ARG(ret, argc); + method->validated_call(nullptr, (const Variant **)argptrs, ret); + +#ifdef DEBUG_ENABLED + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time; + _profile_native_call(t_taken, method->get_name(), method->get_instance_class()); + function_call_time += t_taken; + } +#endif + + ip += 3; + } + DISPATCH_OPCODE; + + OPCODE(OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN) { + LOAD_INSTRUCTION_ARGS + CHECK_SPACE(3 + instr_arg_count); + + ip += instr_arg_count; + + int argc = _code_ptr[ip + 1]; + GD_ERR_BREAK(argc < 0); + + GD_ERR_BREAK(_code_ptr[ip + 2] < 0 || _code_ptr[ip + 2] >= _methods_count); + MethodBind *method = _methods_ptr[_code_ptr[ip + 2]]; + + Variant **argptrs = instruction_args; +#ifdef DEBUG_ENABLED + uint64_t call_time = 0; + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + call_time = OS::get_singleton()->get_ticks_usec(); + } +#endif + + GET_INSTRUCTION_ARG(ret, argc); + VariantInternal::initialize(ret, Variant::NIL); + method->validated_call(nullptr, (const Variant **)argptrs, nullptr); + +#ifdef DEBUG_ENABLED + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time; + _profile_native_call(t_taken, method->get_name(), method->get_instance_class()); + function_call_time += t_taken; + } +#endif + + ip += 3; + } + DISPATCH_OPCODE; + OPCODE(OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN) { LOAD_INSTRUCTION_ARGS CHECK_SPACE(3 + instr_arg_count); @@ -2660,6 +2750,8 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a GET_VARIANT_PTR(counter, 0); GET_VARIANT_PTR(container, 1); + *counter = Variant(); + bool valid; if (!container->iter_init(*counter, valid)) { #ifdef DEBUG_ENABLED @@ -2987,20 +3079,22 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #else Object *obj = *VariantInternal::get_object(container); #endif + + *counter = Variant(); + Array ref; ref.push_back(*counter); Variant vref; VariantInternal::initialize(&vref, Variant::ARRAY); *VariantInternal::get_array(&vref) = ref; - Variant **args = instruction_args; // Overriding an instruction argument, but we don't need access to that anymore. - args[0] = &vref; + const Variant *args[] = { &vref }; Callable::CallError ce; - Variant has_next = obj->callp(CoreStringNames::get_singleton()->_iter_init, (const Variant **)args, 1, ce); + Variant has_next = obj->callp(CoreStringNames::get_singleton()->_iter_init, args, 1, ce); #ifdef DEBUG_ENABLED - if (ce.error != Callable::CallError::CALL_OK) { + if (ref.size() != 1 || ce.error != Callable::CallError::CALL_OK) { err_text = vformat(R"(There was an error calling "_iter_next" on iterator object of type %s.)", *container); OPCODE_BREAK; } @@ -3010,8 +3104,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a GD_ERR_BREAK(jumpto < 0 || jumpto > _code_size); ip = jumpto; } else { + *counter = ref[0]; + GET_VARIANT_PTR(iterator, 2); - *iterator = obj->callp(CoreStringNames::get_singleton()->_iter_get, (const Variant **)args, 1, ce); + *iterator = obj->callp(CoreStringNames::get_singleton()->_iter_get, (const Variant **)&counter, 1, ce); #ifdef DEBUG_ENABLED if (ce.error != Callable::CallError::CALL_OK) { err_text = vformat(R"(There was an error calling "_iter_get" on iterator object of type %s.)", *container); @@ -3318,20 +3414,20 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #else Object *obj = *VariantInternal::get_object(container); #endif + Array ref; ref.push_back(*counter); Variant vref; VariantInternal::initialize(&vref, Variant::ARRAY); *VariantInternal::get_array(&vref) = ref; - Variant **args = instruction_args; // Overriding an instruction argument, but we don't need access to that anymore. - args[0] = &vref; + const Variant *args[] = { &vref }; Callable::CallError ce; - Variant has_next = obj->callp(CoreStringNames::get_singleton()->_iter_next, (const Variant **)args, 1, ce); + Variant has_next = obj->callp(CoreStringNames::get_singleton()->_iter_next, args, 1, ce); #ifdef DEBUG_ENABLED - if (ce.error != Callable::CallError::CALL_OK) { + if (ref.size() != 1 || ce.error != Callable::CallError::CALL_OK) { err_text = vformat(R"(There was an error calling "_iter_next" on iterator object of type %s.)", *container); OPCODE_BREAK; } @@ -3341,8 +3437,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a GD_ERR_BREAK(jumpto < 0 || jumpto > _code_size); ip = jumpto; } else { + *counter = ref[0]; + GET_VARIANT_PTR(iterator, 2); - *iterator = obj->callp(CoreStringNames::get_singleton()->_iter_get, (const Variant **)args, 1, ce); + *iterator = obj->callp(CoreStringNames::get_singleton()->_iter_get, (const Variant **)&counter, 1, ce); #ifdef DEBUG_ENABLED if (ce.error != Callable::CallError::CALL_OK) { err_text = vformat(R"(There was an error calling "_iter_get" on iterator object of type %s.)", *container); diff --git a/modules/gdscript/language_server/gdscript_language_server.h b/modules/gdscript/language_server/gdscript_language_server.h index 2ace5ca446..4ae5ab6cbf 100644 --- a/modules/gdscript/language_server/gdscript_language_server.h +++ b/modules/gdscript/language_server/gdscript_language_server.h @@ -34,7 +34,7 @@ #include "../gdscript_parser.h" #include "gdscript_language_protocol.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class GDScriptLanguageServer : public EditorPlugin { GDCLASS(GDScriptLanguageServer, EditorPlugin); diff --git a/modules/gdscript/language_server/gdscript_workspace.cpp b/modules/gdscript/language_server/gdscript_workspace.cpp index 853a8e0f19..a63d32ef30 100644 --- a/modules/gdscript/language_server/gdscript_workspace.cpp +++ b/modules/gdscript/language_server/gdscript_workspace.cpp @@ -233,18 +233,25 @@ void GDScriptWorkspace::reload_all_workspace_scripts() { void GDScriptWorkspace::list_script_files(const String &p_root_dir, List<String> &r_files) { Error err; Ref<DirAccess> dir = DirAccess::open(p_root_dir, &err); - if (OK == err) { - dir->list_dir_begin(); - String file_name = dir->get_next(); - while (file_name.length()) { - if (dir->current_is_dir() && file_name != "." && file_name != ".." && file_name != "./") { - list_script_files(p_root_dir.path_join(file_name), r_files); - } else if (file_name.ends_with(".gd")) { - String script_file = p_root_dir.path_join(file_name); - r_files.push_back(script_file); - } - file_name = dir->get_next(); + if (OK != err) { + return; + } + + // Ignore scripts in directories with a .gdignore file. + if (dir->file_exists(".gdignore")) { + return; + } + + dir->list_dir_begin(); + String file_name = dir->get_next(); + while (file_name.length()) { + if (dir->current_is_dir() && file_name != "." && file_name != ".." && file_name != "./") { + list_script_files(p_root_dir.path_join(file_name), r_files); + } else if (file_name.ends_with(".gd")) { + String script_file = p_root_dir.path_join(file_name); + r_files.push_back(script_file); } + file_name = dir->get_next(); } } diff --git a/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out b/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out index c524a1ae6b..350d5d1d45 100644 --- a/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out +++ b/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out @@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR >> on function: test() >> runtime/errors/constant_array_is_deep.gd >> 6 ->> Invalid assignment of property or key '0' with value of type 'int' on a base object of type 'Dictionary'. +>> Invalid assignment on read-only value (on base: 'Dictionary'). diff --git a/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out b/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out index cf51b0262d..5f1f372b0a 100644 --- a/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out +++ b/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out @@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR >> on function: test() >> runtime/errors/constant_dictionary_is_deep.gd >> 6 ->> Invalid assignment of index '0' (on base: 'Array') with value of type 'int'. +>> Invalid assignment on read-only value (on base: 'Array'). diff --git a/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd new file mode 100644 index 0000000000..2f31ecc52f --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd @@ -0,0 +1,4 @@ +func test(): + var dictionary := { "a": 0 } + dictionary.make_read_only() + dictionary.a = 1 diff --git a/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out new file mode 100644 index 0000000000..f7d531e119 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out @@ -0,0 +1,6 @@ +GDTEST_RUNTIME_ERROR +>> SCRIPT ERROR +>> on function: test() +>> runtime/errors/read_only_dictionary.gd +>> 4 +>> Invalid assignment on read-only value (on base: 'Dictionary'). diff --git a/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd new file mode 100644 index 0000000000..35e4dbd6a0 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd @@ -0,0 +1,7 @@ +func test(): + # Validated native static call with return value. + print(FileAccess.file_exists("some_file")) + + # Validated native static call without return value. + Node.print_orphan_nodes() + diff --git a/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out new file mode 100644 index 0000000000..44302c8137 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out @@ -0,0 +1,2 @@ +GDTEST_OK +false diff --git a/modules/gdscript/tests/scripts/runtime/features/object_iterators.gd b/modules/gdscript/tests/scripts/runtime/features/object_iterators.gd new file mode 100644 index 0000000000..6fe28c6f78 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/object_iterators.gd @@ -0,0 +1,49 @@ +class MyIterator: + var count: int + + func _init(p_count: int) -> void: + count = p_count + + func _iter_init(arg: Array) -> bool: + prints("_iter_init", arg) + arg[0] = 0 + return arg[0] < count + + func _iter_next(arg: Array) -> bool: + prints("_iter_next", arg) + arg[0] += 1 + return arg[0] < count + + func _iter_get(arg: Variant) -> Variant: + prints("_iter_get", arg) + return arg + +func test(): + var container := PackedDataContainer.new() + var _err := container.pack([{ + id = 123, + node_path = ^"/some/path", + data = PackedByteArray(), + }]) + + for ref: PackedDataContainerRef in container: + for key: String in ref: + print(key) + + print("===") + + for ref: Variant in container: + for key: String in ref: + print(key) + + print("===") + + var hard_custom := MyIterator.new(3) + for x in hard_custom: + print(x) + + print("===") + + var weak_custom: Variant = MyIterator.new(3) + for x in weak_custom: + print(x) diff --git a/modules/gdscript/tests/scripts/runtime/features/object_iterators.out b/modules/gdscript/tests/scripts/runtime/features/object_iterators.out new file mode 100644 index 0000000000..942a2c9dd8 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/object_iterators.out @@ -0,0 +1,30 @@ +GDTEST_OK +id +node_path +data +=== +id +node_path +data +=== +_iter_init [<null>] +_iter_get 0 +0 +_iter_next [0] +_iter_get 1 +1 +_iter_next [1] +_iter_get 2 +2 +_iter_next [2] +=== +_iter_init [<null>] +_iter_get 0 +0 +_iter_next [0] +_iter_get 1 +1 +_iter_next [1] +_iter_get 2 +2 +_iter_next [2] diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h index 683ff6d4f6..6bc6160571 100644 --- a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h +++ b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h @@ -36,7 +36,7 @@ #include "../gltf_document.h" #include "editor_scene_exporter_gltf_settings.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class EditorFileDialog; class EditorInspector; diff --git a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp index 352b439332..7e52cde059 100644 --- a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp +++ b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp @@ -127,6 +127,11 @@ Error GLTFDocumentExtensionPhysics::parse_node_extensions(Ref<GLTFState> p_state trigger_body->set_body_type("trigger"); p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), trigger_body); } + // If this node defines explicit member shape nodes, save this information. + if (node_trigger.has("nodes")) { + Array node_trigger_nodes = node_trigger["nodes"]; + p_gltf_node->set_additional_data(StringName("GLTFPhysicsCompoundTriggerNodes"), node_trigger_nodes); + } } if (physics_body_ext.has("motion") || physics_body_ext.has("type")) { p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), GLTFPhysicsBody::from_dictionary(physics_body_ext)); @@ -241,6 +246,19 @@ Node3D *_add_physics_node_to_given_node(Node3D *p_current_node, Node3D *p_child, return p_current_node; } +Array _get_ancestor_compound_trigger_nodes(Ref<GLTFState> p_state, TypedArray<GLTFNode> p_state_nodes, CollisionObject3D *p_ancestor_col_obj) { + GLTFNodeIndex ancestor_index = p_state->get_node_index(p_ancestor_col_obj); + ERR_FAIL_INDEX_V(ancestor_index, p_state_nodes.size(), Array()); + Ref<GLTFNode> ancestor_gltf_node = p_state_nodes[ancestor_index]; + Variant compound_trigger_nodes = ancestor_gltf_node->get_additional_data(StringName("GLTFPhysicsCompoundTriggerNodes")); + if (compound_trigger_nodes.is_array()) { + return compound_trigger_nodes; + } + Array ret; + ancestor_gltf_node->set_additional_data(StringName("GLTFPhysicsCompoundTriggerNodes"), ret); + return ret; +} + Node3D *GLTFDocumentExtensionPhysics::generate_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent) { Ref<GLTFPhysicsBody> gltf_physics_body = p_gltf_node->get_additional_data(StringName("GLTFPhysicsBody")); #ifndef DISABLE_DEPRECATED @@ -269,12 +287,27 @@ Node3D *GLTFDocumentExtensionPhysics::generate_scene_node(Ref<GLTFState> p_state #endif // DISABLE_DEPRECATED Node3D *ret = nullptr; CollisionObject3D *ancestor_col_obj = nullptr; + Ref<GLTFPhysicsShape> gltf_physics_collider_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape")); + Ref<GLTFPhysicsShape> gltf_physics_trigger_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShape")); if (gltf_physics_body.is_valid()) { ancestor_col_obj = gltf_physics_body->to_node(); ret = ancestor_col_obj; } else { ancestor_col_obj = _get_ancestor_collision_object(p_scene_parent); - if (!Object::cast_to<PhysicsBody3D>(ancestor_col_obj)) { + if (Object::cast_to<Area3D>(ancestor_col_obj) && gltf_physics_trigger_shape.is_valid()) { + // At this point, we found an ancestor Area3D node. But do we want to use it for this trigger shape? + TypedArray<GLTFNode> state_nodes = p_state->get_nodes(); + GLTFNodeIndex self_index = state_nodes.find(p_gltf_node); + Array compound_trigger_nodes = _get_ancestor_compound_trigger_nodes(p_state, state_nodes, ancestor_col_obj); + // Check if the ancestor specifies compound trigger nodes, and if this node is in there. + // Remember that JSON does not have integers, only "number", aka double-precision floats. + if (compound_trigger_nodes.size() > 0 && !compound_trigger_nodes.has(double(self_index))) { + // If the compound trigger we found is not the intended user of + // this shape node, then we need to create a new Area3D node. + ancestor_col_obj = memnew(Area3D); + ret = ancestor_col_obj; + } + } else if (!Object::cast_to<PhysicsBody3D>(ancestor_col_obj)) { if (p_gltf_node->get_additional_data(StringName("GLTFPhysicsCompoundCollider"))) { // If the GLTF file wants this node to group solid shapes together, // and there is no parent body, we need to create a static body. @@ -288,8 +321,6 @@ Node3D *GLTFDocumentExtensionPhysics::generate_scene_node(Ref<GLTFState> p_state // set above. If there is no ancestor body, we will either generate an // Area3D or StaticBody3D implicitly, so prefer an Area3D as the base // node for best compatibility with signal connections to this node. - Ref<GLTFPhysicsShape> gltf_physics_collider_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape")); - Ref<GLTFPhysicsShape> gltf_physics_trigger_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShape")); bool is_ancestor_col_obj_solid = Object::cast_to<PhysicsBody3D>(ancestor_col_obj); if (is_ancestor_col_obj_solid && gltf_physics_collider_shape.is_valid()) { Node3D *child = _generate_shape_node_and_body_if_needed(p_state, p_gltf_node, gltf_physics_collider_shape, ancestor_col_obj, false); @@ -362,8 +393,14 @@ void GLTFDocumentExtensionPhysics::convert_scene_node(Ref<GLTFState> p_state, Re gltf_shape->set_mesh_index(_get_or_insert_mesh_in_state(p_state, importer_mesh)); } } - if (cast_to<Area3D>(_get_ancestor_collision_object(p_scene_node->get_parent()))) { + CollisionObject3D *ancestor_col_obj = _get_ancestor_collision_object(p_scene_node->get_parent()); + if (cast_to<Area3D>(ancestor_col_obj)) { p_gltf_node->set_additional_data(StringName("GLTFPhysicsTriggerShape"), gltf_shape); + // Write explicit member shape nodes to the ancestor compound trigger node. + TypedArray<GLTFNode> state_nodes = p_state->get_nodes(); + GLTFNodeIndex self_index = state_nodes.size(); // The current p_gltf_node will be inserted next. + Array compound_trigger_nodes = _get_ancestor_compound_trigger_nodes(p_state, p_state->get_nodes(), ancestor_col_obj); + compound_trigger_nodes.push_back(double(self_index)); } else { p_gltf_node->set_additional_data(StringName("GLTFPhysicsColliderShape"), gltf_shape); } @@ -422,6 +459,11 @@ Error GLTFDocumentExtensionPhysics::export_node(Ref<GLTFState> p_state, Ref<GLTF Ref<GLTFPhysicsBody> physics_body = p_gltf_node->get_additional_data(StringName("GLTFPhysicsBody")); if (physics_body.is_valid()) { physics_body_ext = physics_body->to_dictionary(); + Variant compound_trigger_nodes = p_gltf_node->get_additional_data(StringName("GLTFPhysicsCompoundTriggerNodes")); + if (compound_trigger_nodes.is_array()) { + Dictionary trigger_property = physics_body_ext.get_or_add("trigger", {}); + trigger_property["nodes"] = compound_trigger_nodes; + } } Ref<GLTFPhysicsShape> collider_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape")); if (collider_shape.is_valid()) { diff --git a/modules/gridmap/editor/grid_map_editor_plugin.h b/modules/gridmap/editor/grid_map_editor_plugin.h index 924e21aef5..cfa0f0c35c 100644 --- a/modules/gridmap/editor/grid_map_editor_plugin.h +++ b/modules/gridmap/editor/grid_map_editor_plugin.h @@ -35,7 +35,7 @@ #include "../grid_map.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/box_container.h" #include "scene/gui/item_list.h" #include "scene/gui/slider.h" diff --git a/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.h b/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.h index 730d1ca83b..3c50b0d5cc 100644 --- a/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.h +++ b/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.h @@ -32,7 +32,7 @@ #define AUDIO_STREAM_INTERACTIVE_EDITOR_PLUGIN_H #include "editor/editor_inspector.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/dialogs.h" class CheckBox; diff --git a/modules/mobile_vr/mobile_vr_interface.cpp b/modules/mobile_vr/mobile_vr_interface.cpp index 94a3f0777e..bba56f6468 100644 --- a/modules/mobile_vr/mobile_vr_interface.cpp +++ b/modules/mobile_vr/mobile_vr_interface.cpp @@ -126,7 +126,7 @@ void MobileVRInterface::set_position_from_sensors() { // 9dof is a misleading marketing term coming from 3 accelerometer axis + 3 gyro axis + 3 magnetometer axis = 9 axis // but in reality this only offers 3 dof (yaw, pitch, roll) orientation - Basis orientation; + Basis orientation = head_transform.basis; uint64_t ticks = OS::get_singleton()->get_ticks_usec(); uint64_t ticks_elapsed = ticks - last_ticks; diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index e3f39c50f4..17df3988ee 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -41,7 +41,7 @@ #include "core/templates/self_list.h" #ifdef TOOLS_ENABLED -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #endif class CSharpScript; diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj index ccef90c911..b396a5b0c7 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -14,6 +14,7 @@ <PackageType>MSBuildSdk</PackageType> <PackageTags>MSBuildSdk</PackageTags> <PackageLicenseExpression>MIT</PackageLicenseExpression> + <Copyright>Copyright (c) Godot Engine contributors</Copyright> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Exclude target framework from the package dependencies as we don't include the build output --> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs index 462da31d66..2b5eecab8a 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs @@ -66,6 +66,12 @@ public class MustBeVariantGD0301 Method<Rid[]>(); } + public void MethodCallDynamic() + { + dynamic self = this; + self.Method<object>(); + } + public void Method<[MustBeVariant] T>() { } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj index 8a3b17ac49..fbabed50d0 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj @@ -14,6 +14,7 @@ <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators</RepositoryUrl> <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> <PackageLicenseExpression>MIT</PackageLicenseExpression> + <Copyright>Copyright (c) Godot Engine contributors</Copyright> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Do not include the generator as a lib dependency --> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs index 95eaca4d3d..e894e7a86c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs @@ -50,8 +50,18 @@ namespace Godot.SourceGenerators var typeSymbol = sm.GetSymbolInfo(typeSyntax).Symbol as ITypeSymbol; Helper.ThrowIfNull(typeSymbol); - var parentSymbol = sm.GetSymbolInfo(parentSyntax).Symbol; - Helper.ThrowIfNull(parentSymbol); + var parentSymbolInfo = sm.GetSymbolInfo(parentSyntax); + var parentSymbol = parentSymbolInfo.Symbol; + if (parentSymbol == null) + { + if (parentSymbolInfo.CandidateReason == CandidateReason.LateBound) + { + // Invocations on dynamic are late bound so we can't retrieve the symbol. + continue; + } + + Helper.ThrowIfNull(parentSymbol); + } if (!ShouldCheckTypeArgument(context, parentSyntax, parentSymbol, typeSyntax, typeSymbol, i)) { diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj index be6af398e2..f681228892 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj @@ -12,6 +12,7 @@ <PackageTags>godot</PackageTags> <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/GodotTools/GodotTools.IdeMessaging</RepositoryUrl> <PackageLicenseExpression>MIT</PackageLicenseExpression> + <Copyright>Copyright (c) Godot Engine contributors</Copyright> <Description> This library enables communication with the Godot Engine editor (the version with .NET support). It's intended for use in IDEs/editors plugins for a better experience working with Godot C# projects. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs index 856fd54352..16de028045 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs @@ -192,6 +192,23 @@ namespace Godot } /// <summary> + /// Returns a new vector with all components clamped between the + /// <paramref name="min"/> and <paramref name="max"/> using + /// <see cref="Mathf.Clamp(real_t, real_t, real_t)"/>. + /// </summary> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The vector with all components clamped.</returns> + public readonly Vector2 Clamp(real_t min, real_t max) + { + return new Vector2 + ( + Mathf.Clamp(X, min, max), + Mathf.Clamp(Y, min, max) + ); + } + + /// <summary> /// Returns the cross product of this vector and <paramref name="with"/>. /// </summary> /// <param name="with">The other vector.</param> @@ -600,7 +617,7 @@ namespace Godot } /// <summary> - /// Returns this vector with each component snapped to the nearest multiple of <paramref name="step"/>. + /// Returns a new vector with each component snapped to the nearest multiple of the corresponding component in <paramref name="step"/>. /// This can also be used to round to an arbitrary number of decimals. /// </summary> /// <param name="step">A vector value representing the step size to snap to.</param> @@ -611,6 +628,17 @@ namespace Godot } /// <summary> + /// Returns a new vector with each component snapped to the nearest multiple of <paramref name="step"/>. + /// This can also be used to round to an arbitrary number of decimals. + /// </summary> + /// <param name="step">The step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public readonly Vector2 Snapped(real_t step) + { + return new Vector2(Mathf.Snapped(X, step), Mathf.Snapped(Y, step)); + } + + /// <summary> /// Returns a perpendicular vector rotated 90 degrees counter-clockwise /// compared to the original, with the same length. /// </summary> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs index 511cc7971c..3d11a15e28 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs @@ -125,6 +125,23 @@ namespace Godot } /// <summary> + /// Returns a new vector with all components clamped between the + /// <paramref name="min"/> and <paramref name="max"/> using + /// <see cref="Mathf.Clamp(int, int, int)"/>. + /// </summary> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The vector with all components clamped.</returns> + public readonly Vector2I Clamp(int min, int max) + { + return new Vector2I + ( + Mathf.Clamp(X, min, max), + Mathf.Clamp(Y, min, max) + ); + } + + /// <summary> /// Returns the squared distance between this vector and <paramref name="to"/>. /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if /// you need to compare vectors or need the squared distance for some formula. @@ -208,6 +225,34 @@ namespace Godot return v; } + /// <summary> + /// Returns a new vector with each component snapped to the closest multiple of the corresponding component in <paramref name="step"/>. + /// </summary> + /// <param name="step">A vector value representing the step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public readonly Vector2I Snapped(Vector2I step) + { + return new Vector2I + ( + (int)Mathf.Snapped((double)X, (double)step.X), + (int)Mathf.Snapped((double)Y, (double)step.Y) + ); + } + + /// <summary> + /// Returns a new vector with each component snapped to the closest multiple of <paramref name="step"/>. + /// </summary> + /// <param name="step">The step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public readonly Vector2I Snapped(int step) + { + return new Vector2I + ( + (int)Mathf.Snapped((double)X, (double)step), + (int)Mathf.Snapped((double)Y, (double)step) + ); + } + // Constants private static readonly Vector2I _minValue = new Vector2I(int.MinValue, int.MinValue); private static readonly Vector2I _maxValue = new Vector2I(int.MaxValue, int.MaxValue); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs index 6300705107..a2d5c012c0 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs @@ -179,6 +179,24 @@ namespace Godot } /// <summary> + /// Returns a new vector with all components clamped between the + /// <paramref name="min"/> and <paramref name="max"/> using + /// <see cref="Mathf.Clamp(real_t, real_t, real_t)"/>. + /// </summary> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The vector with all components clamped.</returns> + public readonly Vector3 Clamp(real_t min, real_t max) + { + return new Vector3 + ( + Mathf.Clamp(X, min, max), + Mathf.Clamp(Y, min, max), + Mathf.Clamp(Z, min, max) + ); + } + + /// <summary> /// Returns the cross product of this vector and <paramref name="with"/>. /// </summary> /// <param name="with">The other vector.</param> @@ -643,7 +661,7 @@ namespace Godot } /// <summary> - /// Returns this vector with each component snapped to the nearest multiple of <paramref name="step"/>. + /// Returns a new vector with each component snapped to the nearest multiple of the corresponding component in <paramref name="step"/>. /// This can also be used to round to an arbitrary number of decimals. /// </summary> /// <param name="step">A vector value representing the step size to snap to.</param> @@ -658,6 +676,22 @@ namespace Godot ); } + /// <summary> + /// Returns a new vector with each component snapped to the nearest multiple of <paramref name="step"/>. + /// This can also be used to round to an arbitrary number of decimals. + /// </summary> + /// <param name="step">The step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public readonly Vector3 Snapped(real_t step) + { + return new Vector3 + ( + Mathf.Snapped(X, step), + Mathf.Snapped(Y, step), + Mathf.Snapped(Z, step) + ); + } + // Constants private static readonly Vector3 _zero = new Vector3(0, 0, 0); private static readonly Vector3 _one = new Vector3(1, 1, 1); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs index aea46efc5b..642c73a8ec 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs @@ -133,6 +133,24 @@ namespace Godot } /// <summary> + /// Returns a new vector with all components clamped between the + /// <paramref name="min"/> and <paramref name="max"/> using + /// <see cref="Mathf.Clamp(int, int, int)"/>. + /// </summary> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The vector with all components clamped.</returns> + public readonly Vector3I Clamp(int min, int max) + { + return new Vector3I + ( + Mathf.Clamp(X, min, max), + Mathf.Clamp(Y, min, max), + Mathf.Clamp(Z, min, max) + ); + } + + /// <summary> /// Returns the squared distance between this vector and <paramref name="to"/>. /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if /// you need to compare vectors or need the squared distance for some formula. @@ -219,6 +237,36 @@ namespace Godot return v; } + /// <summary> + /// Returns a new vector with each component snapped to the closest multiple of the corresponding component in <paramref name="step"/>. + /// </summary> + /// <param name="step">A vector value representing the step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public readonly Vector3I Snapped(Vector3I step) + { + return new Vector3I + ( + (int)Mathf.Snapped((double)X, (double)step.X), + (int)Mathf.Snapped((double)Y, (double)step.Y), + (int)Mathf.Snapped((double)Z, (double)step.Z) + ); + } + + /// <summary> + /// Returns a new vector with each component snapped to the closest multiple of <paramref name="step"/>. + /// </summary> + /// <param name="step">The step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public readonly Vector3I Snapped(int step) + { + return new Vector3I + ( + (int)Mathf.Snapped((double)X, (double)step), + (int)Mathf.Snapped((double)Y, (double)step), + (int)Mathf.Snapped((double)Z, (double)step) + ); + } + // Constants private static readonly Vector3I _minValue = new Vector3I(int.MinValue, int.MinValue, int.MinValue); private static readonly Vector3I _maxValue = new Vector3I(int.MaxValue, int.MaxValue, int.MaxValue); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs index 7c4832943c..9862dfdab6 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs @@ -177,6 +177,25 @@ namespace Godot } /// <summary> + /// Returns a new vector with all components clamped between the + /// <paramref name="min"/> and <paramref name="max"/> using + /// <see cref="Mathf.Clamp(real_t, real_t, real_t)"/>. + /// </summary> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The vector with all components clamped.</returns> + public readonly Vector4 Clamp(real_t min, real_t max) + { + return new Vector4 + ( + Mathf.Clamp(X, min, max), + Mathf.Clamp(Y, min, max), + Mathf.Clamp(Z, min, max), + Mathf.Clamp(W, min, max) + ); + } + + /// <summary> /// Performs a cubic interpolation between vectors <paramref name="preA"/>, this vector, /// <paramref name="b"/>, and <paramref name="postB"/>, by the given amount <paramref name="weight"/>. /// </summary> @@ -465,7 +484,7 @@ namespace Godot } /// <summary> - /// Returns this vector with each component snapped to the nearest multiple of <paramref name="step"/>. + /// Returns a new vector with each component snapped to the nearest multiple of the corresponding component in <paramref name="step"/>. /// This can also be used to round to an arbitrary number of decimals. /// </summary> /// <param name="step">A vector value representing the step size to snap to.</param> @@ -480,6 +499,22 @@ namespace Godot ); } + /// <summary> + /// Returns a new vector with each component snapped to the nearest multiple of <paramref name="step"/>. + /// This can also be used to round to an arbitrary number of decimals. + /// </summary> + /// <param name="step">The step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public readonly Vector4 Snapped(real_t step) + { + return new Vector4( + Mathf.Snapped(X, step), + Mathf.Snapped(Y, step), + Mathf.Snapped(Z, step), + Mathf.Snapped(W, step) + ); + } + // Constants private static readonly Vector4 _zero = new Vector4(0, 0, 0, 0); private static readonly Vector4 _one = new Vector4(1, 1, 1, 1); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs index 27aa86b7e4..321f985209 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs @@ -150,6 +150,25 @@ namespace Godot } /// <summary> + /// Returns a new vector with all components clamped between + /// <paramref name="min"/> and <paramref name="max"/> using + /// <see cref="Mathf.Clamp(int, int, int)"/>. + /// </summary> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The vector with all components clamped.</returns> + public readonly Vector4I Clamp(int min, int max) + { + return new Vector4I + ( + Mathf.Clamp(X, min, max), + Mathf.Clamp(Y, min, max), + Mathf.Clamp(Z, min, max), + Mathf.Clamp(W, min, max) + ); + } + + /// <summary> /// Returns the squared distance between this vector and <paramref name="to"/>. /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if /// you need to compare vectors or need the squared distance for some formula. @@ -254,6 +273,36 @@ namespace Godot return new Vector4I(Mathf.Sign(X), Mathf.Sign(Y), Mathf.Sign(Z), Mathf.Sign(W)); } + /// <summary> + /// Returns a new vector with each component snapped to the closest multiple of the corresponding component in <paramref name="step"/>. + /// </summary> + /// <param name="step">A vector value representing the step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public readonly Vector4I Snapped(Vector4I step) + { + return new Vector4I( + (int)Mathf.Snapped((double)X, (double)step.X), + (int)Mathf.Snapped((double)Y, (double)step.Y), + (int)Mathf.Snapped((double)Z, (double)step.Z), + (int)Mathf.Snapped((double)W, (double)step.W) + ); + } + + /// <summary> + /// Returns a new vector with each component snapped to the closest multiple of <paramref name="step"/>. + /// </summary> + /// <param name="step">The step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public readonly Vector4I Snapped(int step) + { + return new Vector4I( + (int)Mathf.Snapped((double)X, (double)step), + (int)Mathf.Snapped((double)Y, (double)step), + (int)Mathf.Snapped((double)Z, (double)step), + (int)Mathf.Snapped((double)W, (double)step) + ); + } + // Constants private static readonly Vector4I _minValue = new Vector4I(int.MinValue, int.MinValue, int.MinValue, int.MinValue); private static readonly Vector4I _maxValue = new Vector4I(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index d4c11da963..67282416ed 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -22,6 +22,7 @@ <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/glue/GodotSharp/GodotSharp</RepositoryUrl> <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> <PackageLicenseExpression>MIT</PackageLicenseExpression> + <Copyright>Copyright (c) Godot Engine contributors</Copyright> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <IncludeSymbols>true</IncludeSymbols> diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj index c32cbcd3d1..8373edb9bf 100644 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj @@ -20,6 +20,7 @@ <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/glue/GodotSharp/GodotSharpEditor</RepositoryUrl> <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> <PackageLicenseExpression>MIT</PackageLicenseExpression> + <Copyright>Copyright (c) Godot Engine contributors</Copyright> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <IncludeSymbols>true</IncludeSymbols> diff --git a/modules/multiplayer/editor/multiplayer_editor_plugin.h b/modules/multiplayer/editor/multiplayer_editor_plugin.h index a22144cdcf..e8ade539a7 100644 --- a/modules/multiplayer/editor/multiplayer_editor_plugin.h +++ b/modules/multiplayer/editor/multiplayer_editor_plugin.h @@ -31,8 +31,8 @@ #ifndef MULTIPLAYER_EDITOR_PLUGIN_H #define MULTIPLAYER_EDITOR_PLUGIN_H -#include "editor/editor_plugin.h" #include "editor/plugins/editor_debugger_plugin.h" +#include "editor/plugins/editor_plugin.h" class EditorNetworkProfiler; class MultiplayerEditorDebugger : public EditorDebuggerPlugin { diff --git a/modules/multiplayer/editor/replication_editor.h b/modules/multiplayer/editor/replication_editor.h index 80c1892ec3..8f11774292 100644 --- a/modules/multiplayer/editor/replication_editor.h +++ b/modules/multiplayer/editor/replication_editor.h @@ -33,7 +33,7 @@ #include "../scene_replication_config.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/box_container.h" class ConfirmationDialog; diff --git a/modules/navigation/editor/navigation_mesh_editor_plugin.h b/modules/navigation/editor/navigation_mesh_editor_plugin.h index b73d8d2e69..6114c62ebf 100644 --- a/modules/navigation/editor/navigation_mesh_editor_plugin.h +++ b/modules/navigation/editor/navigation_mesh_editor_plugin.h @@ -33,7 +33,7 @@ #ifdef TOOLS_ENABLED -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class AcceptDialog; class Button; diff --git a/modules/noise/editor/noise_editor_plugin.h b/modules/noise/editor/noise_editor_plugin.h index 948ccba29b..aa94cf4d23 100644 --- a/modules/noise/editor/noise_editor_plugin.h +++ b/modules/noise/editor/noise_editor_plugin.h @@ -33,7 +33,7 @@ #ifdef TOOLS_ENABLED -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class NoiseEditorPlugin : public EditorPlugin { GDCLASS(NoiseEditorPlugin, EditorPlugin) diff --git a/modules/noise/register_types.cpp b/modules/noise/register_types.cpp index 29eb42522f..363b7bdc31 100644 --- a/modules/noise/register_types.cpp +++ b/modules/noise/register_types.cpp @@ -40,7 +40,7 @@ #endif #ifdef TOOLS_ENABLED -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" #endif void initialize_noise_module(ModuleInitializationLevel p_level) { diff --git a/modules/openxr/action_map/openxr_action_map.cpp b/modules/openxr/action_map/openxr_action_map.cpp index bbcb63a7e6..ba0e4f6cdd 100644 --- a/modules/openxr/action_map/openxr_action_map.cpp +++ b/modules/openxr/action_map/openxr_action_map.cpp @@ -167,11 +167,11 @@ void OpenXRActionMap::create_default_action_sets() { // we still want it to be part of our action map as we may deploy the same game to platforms that do and don't support it. // - the same applies for interaction profiles that are only supported if the relevant extension is supported. - // Create our Godot action set + // Create our Godot action set. Ref<OpenXRActionSet> action_set = OpenXRActionSet::new_action_set("godot", "Godot action set"); add_action_set(action_set); - // Create our actions + // Create our actions. Ref<OpenXRAction> trigger = action_set->add_new_action("trigger", "Trigger", OpenXRAction::OPENXR_ACTION_FLOAT, "/user/hand/left,/user/hand/right"); Ref<OpenXRAction> trigger_click = action_set->add_new_action("trigger_click", "Trigger click", OpenXRAction::OPENXR_ACTION_BOOL, "/user/hand/left,/user/hand/right"); Ref<OpenXRAction> trigger_touch = action_set->add_new_action("trigger_touch", "Trigger touching", OpenXRAction::OPENXR_ACTION_BOOL, "/user/hand/left,/user/hand/right"); @@ -193,7 +193,7 @@ void OpenXRActionMap::create_default_action_sets() { Ref<OpenXRAction> default_pose = action_set->add_new_action("default_pose", "Default pose", OpenXRAction::OPENXR_ACTION_POSE, "/user/hand/left," "/user/hand/right," - // "/user/vive_tracker_htcx/role/handheld_object," <-- getting errors on this one + // "/user/vive_tracker_htcx/role/handheld_object," <-- getting errors on this one. "/user/vive_tracker_htcx/role/left_foot," "/user/vive_tracker_htcx/role/right_foot," "/user/vive_tracker_htcx/role/left_shoulder," @@ -213,7 +213,7 @@ void OpenXRActionMap::create_default_action_sets() { Ref<OpenXRAction> haptic = action_set->add_new_action("haptic", "Haptic", OpenXRAction::OPENXR_ACTION_HAPTIC, "/user/hand/left," "/user/hand/right," - // "/user/vive_tracker_htcx/role/handheld_object," <-- getting errors on this one + // "/user/vive_tracker_htcx/role/handheld_object," <-- getting errors on this one. "/user/vive_tracker_htcx/role/left_foot," "/user/vive_tracker_htcx/role/right_foot," "/user/vive_tracker_htcx/role/left_shoulder," @@ -227,7 +227,7 @@ void OpenXRActionMap::create_default_action_sets() { "/user/vive_tracker_htcx/role/camera," "/user/vive_tracker_htcx/role/keyboard"); - // Create our interaction profiles + // Create our interaction profiles. Ref<OpenXRInteractionProfile> profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/khr/simple_controller"); profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); @@ -235,11 +235,11 @@ void OpenXRActionMap::create_default_action_sets() { profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click"); profile->add_new_binding(select_button, "/user/hand/left/input/select/click,/user/hand/right/input/select/click"); - // generic has no support for triggers, grip, A/B buttons, nor joystick/trackpad inputs + // generic has no support for triggers, grip, A/B buttons, nor joystick/trackpad inputs. profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); - // Create our Vive controller profile + // Create our Vive controller profile. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_controller"); profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); @@ -247,64 +247,64 @@ void OpenXRActionMap::create_default_action_sets() { profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click"); profile->add_new_binding(select_button, "/user/hand/left/input/system/click,/user/hand/right/input/system/click"); - // wmr controller has no a/b/x/y buttons + // wmr controller has no a/b/x/y buttons. profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click"); - profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); // OpenXR will convert bool to float + profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); // OpenXR will convert bool to float. profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); - // primary on our vive controller is our trackpad + // primary on our vive controller is our trackpad. profile->add_new_binding(primary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad"); profile->add_new_binding(primary_click, "/user/hand/left/input/trackpad/click,/user/hand/right/input/trackpad/click"); profile->add_new_binding(primary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch"); - // vive controllers have no secondary input + // vive controllers have no secondary input. profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); - // Create our WMR controller profile + // Create our WMR controller profile. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/microsoft/motion_controller"); profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); - // wmr controllers have no select button we can use + // wmr controllers have no select button we can use. profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click"); - // wmr controller has no a/b/x/y buttons + // wmr controller has no a/b/x/y buttons. profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); - profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // OpenXR will convert float to bool - profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); // OpenXR will convert bool to float + profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // OpenXR will convert float to bool. + profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); // OpenXR will convert bool to float. profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); - // primary on our wmr controller is our thumbstick, no touch + // primary on our wmr controller is our thumbstick, no touch. profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick"); profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click"); - // secondary on our wmr controller is our trackpad + // secondary on our wmr controller is our trackpad. profile->add_new_binding(secondary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad"); profile->add_new_binding(secondary_click, "/user/hand/left/input/trackpad/click,/user/hand/right/input/trackpad/click"); profile->add_new_binding(secondary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch"); profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); - // Create our Meta touch controller profile + // Create our Meta touch controller profile. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/oculus/touch_controller"); profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); - // touch controllers have no select button we can use - profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/system/click"); // right hand system click may not be available - profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand + // touch controllers have no select button we can use. + profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/system/click"); // right hand system click may not be available. + profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand. profile->add_new_binding(ax_touch, "/user/hand/left/input/x/touch,/user/hand/right/input/a/touch"); - profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand + profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand. profile->add_new_binding(by_touch, "/user/hand/left/input/y/touch,/user/hand/right/input/b/touch"); profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); - profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // should be converted to boolean + profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // should be converted to boolean. profile->add_new_binding(trigger_touch, "/user/hand/left/input/trigger/touch,/user/hand/right/input/trigger/touch"); - profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // should be converted to boolean + profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // should be converted to boolean. profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); - // primary on our touch controller is our thumbstick + // primary on our touch controller is our thumbstick. profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick"); profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click"); profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch"); - // touch controller has no secondary input + // touch controller has no secondary input. profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); @@ -314,73 +314,73 @@ void OpenXRActionMap::create_default_action_sets() { profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); - profile->add_new_binding(select_button, "/user/hand/left/input/system/click,/user/hand/right/input/system/click"); // system click may not be available + profile->add_new_binding(select_button, "/user/hand/left/input/system/click,/user/hand/right/input/system/click"); // system click may not be available. profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click"); - profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand + profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand. profile->add_new_binding(ax_touch, "/user/hand/left/input/x/touch,/user/hand/right/input/a/touch"); - profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand + profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand. profile->add_new_binding(by_touch, "/user/hand/left/input/y/touch,/user/hand/right/input/b/touch"); profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); - profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // should be converted to boolean + profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // should be converted to boolean. profile->add_new_binding(trigger_touch, "/user/hand/left/input/trigger/touch,/user/hand/right/input/trigger/touch"); - profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // should be converted to boolean + profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // should be converted to boolean. profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); - // primary on our pico controller is our thumbstick + // primary on our pico controller is our thumbstick. profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick"); profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click"); profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch"); - // pico controller has no secondary input + // pico controller has no secondary input. profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); - // Create our Valve index controller profile + // Create our Valve index controller profile. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/valve/index_controller"); profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); - // index controllers have no select button we can use + // index controllers have no select button we can use. profile->add_new_binding(menu_button, "/user/hand/left/input/system/click,/user/hand/right/input/system/click"); - profile->add_new_binding(ax_button, "/user/hand/left/input/a/click,/user/hand/right/input/a/click"); // a on both controllers + profile->add_new_binding(ax_button, "/user/hand/left/input/a/click,/user/hand/right/input/a/click"); // a on both controllers. profile->add_new_binding(ax_touch, "/user/hand/left/input/a/touch,/user/hand/right/input/a/touch"); - profile->add_new_binding(by_button, "/user/hand/left/input/b/click,/user/hand/right/input/b/click"); // b on both controllers + profile->add_new_binding(by_button, "/user/hand/left/input/b/click,/user/hand/right/input/b/click"); // b on both controllers. profile->add_new_binding(by_touch, "/user/hand/left/input/b/touch,/user/hand/right/input/b/touch"); profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click"); profile->add_new_binding(trigger_touch, "/user/hand/left/input/trigger/touch,/user/hand/right/input/trigger/touch"); profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); - profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // this should do a float to bool conversion - profile->add_new_binding(grip_force, "/user/hand/left/input/squeeze/force,/user/hand/right/input/squeeze/force"); // grip force seems to be unique to the Valve Index - // primary on our index controller is our thumbstick + profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // this should do a float to bool conversion. + profile->add_new_binding(grip_force, "/user/hand/left/input/squeeze/force,/user/hand/right/input/squeeze/force"); // grip force seems to be unique to the Valve Index. + // primary on our index controller is our thumbstick. profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick"); profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click"); profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch"); - // secondary on our index controller is our trackpad + // secondary on our index controller is our trackpad. profile->add_new_binding(secondary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad"); profile->add_new_binding(secondary_click, "/user/hand/left/input/trackpad/force,/user/hand/right/input/trackpad/force"); // not sure if this will work but doesn't seem to support click... profile->add_new_binding(secondary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch"); profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); - // Create our HP MR controller profile + // Create our HP MR controller profile. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/hp/mixed_reality_controller"); profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); - // hpmr controllers have no select button we can use + // hpmr controllers have no select button we can use. profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click"); - // hpmr controllers only register click, not touch, on our a/b/x/y buttons - profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand - profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand + // hpmr controllers only register click, not touch, on our a/b/x/y buttons. + profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand. + profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand. profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); - // primary on our hpmr controller is our thumbstick + // primary on our hpmr controller is our thumbstick. profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick"); profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click"); - // No secondary on our hpmr controller + // No secondary on our hpmr controller. profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); @@ -391,72 +391,72 @@ void OpenXRActionMap::create_default_action_sets() { profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); - // Odyssey controllers have no select button we can use + // Odyssey controllers have no select button we can use. profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click"); - // Odyssey controller has no a/b/x/y buttons + // Odyssey controller has no a/b/x/y buttons. profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); - // primary on our Odyssey controller is our thumbstick, no touch + // primary on our Odyssey controller is our thumbstick, no touch. profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick"); profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click"); - // secondary on our Odyssey controller is our trackpad + // secondary on our Odyssey controller is our trackpad. profile->add_new_binding(secondary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad"); profile->add_new_binding(secondary_click, "/user/hand/left/input/trackpad/click,/user/hand/right/input/trackpad/click"); profile->add_new_binding(secondary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch"); profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); - // Create our Vive Cosmos controller + // Create our Vive Cosmos controller. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_cosmos_controller"); profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click"); - profile->add_new_binding(select_button, "/user/hand/right/input/system/click"); // we'll map system to select - profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand - profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand + profile->add_new_binding(select_button, "/user/hand/right/input/system/click"); // we'll map system to select. + profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand. + profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand. profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click"); profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); - // primary on our Cosmos controller is our thumbstick + // primary on our Cosmos controller is our thumbstick. profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick"); profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click"); profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch"); - // No secondary on our cosmos controller + // No secondary on our cosmos controller. profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); - // Create our Vive Focus 3 controller + // Create our Vive Focus 3 controller. // Note, Vive Focus 3 currently is not yet supported as a stand alone device - // however HTC currently has a beta OpenXR runtime in testing we may support in the near future + // however HTC currently has a beta OpenXR runtime in testing we may support in the near future. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_focus3_controller"); profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click"); - profile->add_new_binding(select_button, "/user/hand/right/input/system/click"); // we'll map system to select - profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand - profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand + profile->add_new_binding(select_button, "/user/hand/right/input/system/click"); // we'll map system to select. + profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand. + profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand. profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click"); profile->add_new_binding(trigger_touch, "/user/hand/left/input/trigger/touch,/user/hand/right/input/trigger/touch"); profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); - // primary on our Focus 3 controller is our thumbstick + // primary on our Focus 3 controller is our thumbstick. profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick"); profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click"); profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch"); - // We only have a thumb rest + // We only have a thumb rest. profile->add_new_binding(secondary_touch, "/user/hand/left/input/thumbrest/touch,/user/hand/right/input/thumbrest/touch"); profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); - // Create our Huawei controller + // Create our Huawei controller. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/huawei/controller"); profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); @@ -465,17 +465,17 @@ void OpenXRActionMap::create_default_action_sets() { profile->add_new_binding(menu_button, "/user/hand/left/input/home/click,/user/hand/right/input/home/click"); profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click"); - // primary on our Huawei controller is our trackpad + // primary on our Huawei controller is our trackpad. profile->add_new_binding(primary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad"); profile->add_new_binding(primary_click, "/user/hand/left/input/trackpad/click,/user/hand/right/input/trackpad/click"); profile->add_new_binding(primary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch"); profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic"); add_interaction_profile(profile); - // Create our HTC Vive tracker profile + // Create our HTC Vive tracker profile. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_tracker_htcx"); profile->add_new_binding(default_pose, - // "/user/vive_tracker_htcx/role/handheld_object/input/grip/pose," <-- getting errors on this one + // "/user/vive_tracker_htcx/role/handheld_object/input/grip/pose," <-- getting errors on this one. "/user/vive_tracker_htcx/role/left_foot/input/grip/pose," "/user/vive_tracker_htcx/role/right_foot/input/grip/pose," "/user/vive_tracker_htcx/role/left_shoulder/input/grip/pose," @@ -489,7 +489,7 @@ void OpenXRActionMap::create_default_action_sets() { "/user/vive_tracker_htcx/role/camera/input/grip/pose," "/user/vive_tracker_htcx/role/keyboard/input/grip/pose"); profile->add_new_binding(haptic, - // "/user/vive_tracker_htcx/role/handheld_object/output/haptic," <-- getting errors on this one + // "/user/vive_tracker_htcx/role/handheld_object/output/haptic," <-- getting errors on this one. "/user/vive_tracker_htcx/role/left_foot/output/haptic," "/user/vive_tracker_htcx/role/right_foot/output/haptic," "/user/vive_tracker_htcx/role/left_shoulder/output/haptic," @@ -504,10 +504,30 @@ void OpenXRActionMap::create_default_action_sets() { "/user/vive_tracker_htcx/role/keyboard/output/haptic"); add_interaction_profile(profile); - // Create our eye gaze interaction profile + // Create our eye gaze interaction profile. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/ext/eye_gaze_interaction"); profile->add_new_binding(default_pose, "/user/eyes_ext/input/gaze_ext/pose"); add_interaction_profile(profile); + + // Create our hand interaction profile. + profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/ext/hand_interaction_ext"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); + profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); + profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); + + // Use pinch as primary. + profile->add_new_binding(primary, "/user/hand/left/input/pinch_ext/value,/user/hand/right/input/pinch_ext/value"); + profile->add_new_binding(primary_click, "/user/hand/left/input/pinch_ext/ready_ext,/user/hand/right/input/pinch_ext/ready_ext"); + + // Use activation as secondary. + profile->add_new_binding(secondary, "/user/hand/left/input/aim_activate_ext/value,/user/hand/right/input/aim_activate_ext/value"); + profile->add_new_binding(secondary_click, "/user/hand/left/input/aim_activate_ext/ready_ext,/user/hand/right/input/aim_activate_ext/ready_ext"); + + // We link grasp to our grip. + profile->add_new_binding(grip, "/user/hand/left/input/grasp_ext/value,/user/hand/right/input/grasp_ext/value"); + profile->add_new_binding(grip_click, "/user/hand/left/input/grasp_ext/ready_ext,/user/hand/right/input/grasp_ext/ready_ext"); + add_interaction_profile(profile); } void OpenXRActionMap::create_editor_action_sets() { diff --git a/modules/openxr/doc_classes/OpenXRAPIExtension.xml b/modules/openxr/doc_classes/OpenXRAPIExtension.xml index f737f3b642..4419d24dd3 100644 --- a/modules/openxr/doc_classes/OpenXRAPIExtension.xml +++ b/modules/openxr/doc_classes/OpenXRAPIExtension.xml @@ -54,7 +54,7 @@ <method name="get_next_frame_time"> <return type="int" /> <description> - Returns the timing for the next frame. + Returns the predicted display timing for the next frame. </description> </method> <method name="get_play_space"> @@ -63,6 +63,12 @@ Returns the play space, which is an [url=https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrSpace.html]XrSpace[/url] cast to an integer. </description> </method> + <method name="get_predicted_display_time"> + <return type="int" /> + <description> + Returns the predicted display timing for the current frame. + </description> + </method> <method name="get_session"> <return type="int" /> <description> diff --git a/modules/openxr/doc_classes/OpenXRInterface.xml b/modules/openxr/doc_classes/OpenXRInterface.xml index 05dff7d6ae..86ba1416c8 100644 --- a/modules/openxr/doc_classes/OpenXRInterface.xml +++ b/modules/openxr/doc_classes/OpenXRInterface.xml @@ -106,6 +106,13 @@ [b]Note:[/b] This feature is only available on the compatibility renderer and currently only available on some stand alone headsets. For Vulkan set [member Viewport.vrs_mode] to [code]VRS_XR[/code] on desktop. </description> </method> + <method name="is_hand_interaction_supported" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if OpenXR's hand interaction profile is supported and enabled. + [b]Note:[/b] This only returns a valid value after OpenXR has been initialized. + </description> + </method> <method name="is_hand_tracking_supported"> <return type="bool" /> <description> @@ -147,6 +154,11 @@ </member> </members> <signals> + <signal name="instance_exiting"> + <description> + Informs our OpenXR instance is exiting. + </description> + </signal> <signal name="pose_recentered"> <description> Informs the user queued a recenter of the player position. @@ -169,6 +181,11 @@ Informs our OpenXR session now has focus. </description> </signal> + <signal name="session_loss_pending"> + <description> + Informs our OpenXR session is in the process of being lost. + </description> + </signal> <signal name="session_stopping"> <description> Informs our OpenXR session is stopping. diff --git a/modules/openxr/editor/openxr_action_map_editor.h b/modules/openxr/editor/openxr_action_map_editor.h index 22e8853c8c..cfe5fed095 100644 --- a/modules/openxr/editor/openxr_action_map_editor.h +++ b/modules/openxr/editor/openxr_action_map_editor.h @@ -36,8 +36,8 @@ #include "openxr_interaction_profile_editor.h" #include "openxr_select_interaction_profile_dialog.h" -#include "editor/editor_plugin.h" #include "editor/editor_undo_redo_manager.h" +#include "editor/plugins/editor_plugin.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/label.h" diff --git a/modules/openxr/editor/openxr_editor_plugin.h b/modules/openxr/editor/openxr_editor_plugin.h index b80f20d049..672df0de28 100644 --- a/modules/openxr/editor/openxr_editor_plugin.h +++ b/modules/openxr/editor/openxr_editor_plugin.h @@ -34,7 +34,7 @@ #include "openxr_action_map_editor.h" #include "openxr_select_runtime.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class OpenXREditorPlugin : public EditorPlugin { GDCLASS(OpenXREditorPlugin, EditorPlugin); diff --git a/modules/openxr/extensions/openxr_composition_layer_extension.cpp b/modules/openxr/extensions/openxr_composition_layer_extension.cpp index 1fba8e5f8b..51f4a03d52 100644 --- a/modules/openxr/extensions/openxr_composition_layer_extension.cpp +++ b/modules/openxr/extensions/openxr_composition_layer_extension.cpp @@ -274,7 +274,7 @@ bool OpenXRViewportCompositionLayerProvider::update_and_acquire_swapchain(bool p if (swapchain_size == viewport_size && !p_static_image && !static_image) { // We're all good! Just acquire it. // We can ignore should_render here, return will be false. - XrBool32 should_render = true; + bool should_render = true; return swapchain_info.acquire(should_render); } @@ -296,7 +296,7 @@ bool OpenXRViewportCompositionLayerProvider::update_and_acquire_swapchain(bool p // Acquire our image so we can start rendering into it, // we can ignore should_render here, ret will be false. - XrBool32 should_render = true; + bool should_render = true; bool ret = swapchain_info.acquire(should_render); swapchain_size = viewport_size; diff --git a/modules/openxr/extensions/openxr_hand_interaction_extension.cpp b/modules/openxr/extensions/openxr_hand_interaction_extension.cpp new file mode 100644 index 0000000000..65de4b23c4 --- /dev/null +++ b/modules/openxr/extensions/openxr_hand_interaction_extension.cpp @@ -0,0 +1,97 @@ +/**************************************************************************/ +/* openxr_hand_interaction_extension.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "openxr_hand_interaction_extension.h" + +#include "../action_map/openxr_interaction_profile_metadata.h" +#include "core/config/project_settings.h" + +OpenXRHandInteractionExtension *OpenXRHandInteractionExtension::singleton = nullptr; + +OpenXRHandInteractionExtension *OpenXRHandInteractionExtension::get_singleton() { + return singleton; +} + +OpenXRHandInteractionExtension::OpenXRHandInteractionExtension() { + singleton = this; +} + +OpenXRHandInteractionExtension::~OpenXRHandInteractionExtension() { + singleton = nullptr; +} + +HashMap<String, bool *> OpenXRHandInteractionExtension::get_requested_extensions() { + HashMap<String, bool *> request_extensions; + + // Only enable this extension when requested. + // We still register our meta data or the action map editor will fail. + if (GLOBAL_GET("xr/openxr/extensions/hand_interaction_profile")) { + request_extensions[XR_EXT_HAND_INTERACTION_EXTENSION_NAME] = &available; + } + + return request_extensions; +} + +bool OpenXRHandInteractionExtension::is_available() { + return available; +} + +void OpenXRHandInteractionExtension::on_register_metadata() { + OpenXRInteractionProfileMetadata *metadata = OpenXRInteractionProfileMetadata::get_singleton(); + ERR_FAIL_NULL(metadata); + + // Hand interaction profile + metadata->register_interaction_profile("Hand interaction", "/interaction_profiles/ext/hand_interaction_ext", XR_EXT_HAND_INTERACTION_EXTENSION_NAME); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grip pose", "/user/hand/left", "/user/hand/left/input/grip/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grip pose", "/user/hand/right", "/user/hand/right/input/grip/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim pose", "/user/hand/left", "/user/hand/left/input/aim/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim pose", "/user/hand/right", "/user/hand/right/input/aim/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch pose", "/user/hand/left", "/user/hand/left/input/pinch_ext/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch pose", "/user/hand/right", "/user/hand/right/input/pinch_ext/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Poke pose", "/user/hand/left", "/user/hand/left/input/poke_ext/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Poke pose", "/user/hand/right", "/user/hand/right/input/poke_ext/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Palm pose", "/user/hand/left", "/user/hand/left/input/palm_ext/pose", XR_EXT_PALM_POSE_EXTENSION_NAME, OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Palm pose", "/user/hand/right", "/user/hand/right/input/palm_ext/pose", XR_EXT_PALM_POSE_EXTENSION_NAME, OpenXRAction::OPENXR_ACTION_POSE); + + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch", "/user/hand/left", "/user/hand/left/input/pinch_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch", "/user/hand/right", "/user/hand/right/input/pinch_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch ready", "/user/hand/left", "/user/hand/left/input/pinch_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch ready", "/user/hand/right", "/user/hand/right/input/pinch_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim activate", "/user/hand/left", "/user/hand/left/input/aim_activate_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim activate", "/user/hand/right", "/user/hand/right/input/aim_activate_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim activate ready", "/user/hand/left", "/user/hand/left/input/aim_activate_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim activate ready", "/user/hand/right", "/user/hand/right/input/aim_activate_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grasp", "/user/hand/left", "/user/hand/left/input/grasp_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grasp", "/user/hand/right", "/user/hand/right/input/grasp_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grasp ready", "/user/hand/left", "/user/hand/left/input/grasp_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grasp ready", "/user/hand/right", "/user/hand/right/input/grasp_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL); +} diff --git a/modules/openxr/extensions/openxr_hand_interaction_extension.h b/modules/openxr/extensions/openxr_hand_interaction_extension.h new file mode 100644 index 0000000000..789e300c0b --- /dev/null +++ b/modules/openxr/extensions/openxr_hand_interaction_extension.h @@ -0,0 +1,72 @@ +/**************************************************************************/ +/* openxr_hand_interaction_extension.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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 OPENXR_HAND_INTERACTION_EXTENSION_H +#define OPENXR_HAND_INTERACTION_EXTENSION_H + +#include "openxr_extension_wrapper.h" + +// When supported the hand interaction extension introduces an interaction +// profile that becomes active when the user either lets go of their +// controllers or isn't using controllers at all. +// +// The OpenXR specification states that all XR runtimes that support this +// interaction profile should also allow it's controller to use this +// interaction profile. +// This means that if you only supply this interaction profile in your +// action map, it should work both when the player is holding a controller +// or using visual hand tracking. +// +// This allows easier portability between games that use controller +// tracking or hand tracking. +// +// See: https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XR_EXT_hand_interaction +// for more information. + +class OpenXRHandInteractionExtension : public OpenXRExtensionWrapper { +public: + static OpenXRHandInteractionExtension *get_singleton(); + + OpenXRHandInteractionExtension(); + virtual ~OpenXRHandInteractionExtension() override; + + virtual HashMap<String, bool *> get_requested_extensions() override; + + bool is_available(); + + virtual void on_register_metadata() override; + +private: + static OpenXRHandInteractionExtension *singleton; + + bool available = false; +}; + +#endif // OPENXR_HAND_INTERACTION_EXTENSION_H diff --git a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp index f8cc3d1d8c..b8a2f58935 100644 --- a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp +++ b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp @@ -128,7 +128,7 @@ void OpenXRHandTrackingExtension::on_process() { } // process our hands - const XrTime time = OpenXRAPI::get_singleton()->get_next_frame_time(); // This data will be used for the next frame we render + const XrTime time = OpenXRAPI::get_singleton()->get_predicted_display_time(); if (time == 0) { // we don't have timing info yet, or we're skipping a frame... return; @@ -195,7 +195,7 @@ void OpenXRHandTrackingExtension::on_process() { Ref<XRHandTracker> godot_tracker; godot_tracker.instantiate(); - godot_tracker->set_hand(i == 0 ? XRHandTracker::HAND_LEFT : XRHandTracker::HAND_RIGHT); + godot_tracker->set_tracker_hand(i == 0 ? XRPositionalTracker::TRACKER_HAND_LEFT : XRPositionalTracker::TRACKER_HAND_RIGHT); godot_tracker->set_tracker_name(i == 0 ? "/user/hand_tracker/left" : "/user/hand_tracker/right"); XRServer::get_singleton()->add_tracker(godot_tracker); hand_trackers[i].godot_tracker = godot_tracker; diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index 1fe402341b..40e3ecfefc 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -160,7 +160,7 @@ void OpenXRAPI::OpenXRSwapChainInfo::free() { } } -bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) { +bool OpenXRAPI::OpenXRSwapChainInfo::acquire(bool &p_should_render) { ERR_FAIL_COND_V(image_acquired, true); // This was not released when it should be, error out and reuse... OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); @@ -193,10 +193,18 @@ bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) { XrSwapchainImageWaitInfo swapchain_image_wait_info = { XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, // type nullptr, // next - 17000000 // timeout in nanoseconds + 1000000000 // 1s timeout in nanoseconds }; - result = openxr_api->xrWaitSwapchainImage(swapchain, &swapchain_image_wait_info); + // Wait for a maximum of 10 seconds before calling it a critical failure... + for (int retry = 0; retry < 10; retry++) { + result = openxr_api->xrWaitSwapchainImage(swapchain, &swapchain_image_wait_info); + if (result != XR_TIMEOUT_EXPIRED) { + break; + } + WARN_PRINT("OpenXR: timed out waiting for swapchain image."); + } + if (!XR_UNQUALIFIED_SUCCESS(result)) { // Make sure end_frame knows we need to submit an empty frame p_should_render = false; @@ -206,6 +214,8 @@ bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) { print_line("OpenXR: failed to wait for swapchain image [", openxr_api->get_error_string(result), "]"); return false; } else { + WARN_PRINT("OpenXR: couldn't to wait for swapchain but not a complete error [" + openxr_api->get_error_string(result) + "]"); + // Make sure to skip trying to acquire the swapchain image in the next frame skip_acquire_swapchain = true; return false; @@ -760,21 +770,6 @@ bool OpenXRAPI::load_supported_view_configuration_views(XrViewConfigurationType print_verbose(String(" - recommended render sample count: ") + itos(view_configuration_views[i].recommendedSwapchainSampleCount)); } - // Allocate buffers we'll be populating with view information. - views = (XrView *)memalloc(sizeof(XrView) * view_count); - ERR_FAIL_NULL_V_MSG(views, false, "OpenXR Couldn't allocate memory for views"); - memset(views, 0, sizeof(XrView) * view_count); - - projection_views = (XrCompositionLayerProjectionView *)memalloc(sizeof(XrCompositionLayerProjectionView) * view_count); - ERR_FAIL_NULL_V_MSG(projection_views, false, "OpenXR Couldn't allocate memory for projection views"); - memset(projection_views, 0, sizeof(XrCompositionLayerProjectionView) * view_count); - - if (submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) { - depth_views = (XrCompositionLayerDepthInfoKHR *)memalloc(sizeof(XrCompositionLayerDepthInfoKHR) * view_count); - ERR_FAIL_NULL_V_MSG(depth_views, false, "OpenXR Couldn't allocate memory for depth views"); - memset(depth_views, 0, sizeof(XrCompositionLayerDepthInfoKHR) * view_count); - } - return true; } @@ -927,6 +922,9 @@ bool OpenXRAPI::setup_play_space() { // If we've previously created a play space, clean it up first. if (play_space != XR_NULL_HANDLE) { + // TODO Investigate if destroying our play space here is safe, + // it may still be used in the rendering thread. + xrDestroySpace(play_space); } play_space = new_play_space; @@ -936,7 +934,11 @@ bool OpenXRAPI::setup_play_space() { if (emulating_local_floor) { // We'll use the STAGE space to get the floor height, but we can't do that until // after xrWaitFrame(), so just set this flag for now. + // Render state will be updated then. should_reset_emulated_floor_height = true; + } else { + // Update render state so this play space is used rendering the upcoming frame. + set_render_play_space(play_space); } return true; @@ -1016,7 +1018,7 @@ bool OpenXRAPI::reset_emulated_floor_height() { identityPose, // pose }; - result = xrLocateSpace(stage_space, local_space, get_next_frame_time(), &stage_location); + result = xrLocateSpace(stage_space, local_space, get_predicted_display_time(), &stage_location); xrDestroySpace(local_space); xrDestroySpace(stage_space); @@ -1042,6 +1044,9 @@ bool OpenXRAPI::reset_emulated_floor_height() { // report that as the reference space to the outside world. reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR_EXT; + // Update render state so this play space is used rendering the upcoming frame. + set_render_play_space(play_space); + return true; } @@ -1136,6 +1141,7 @@ bool OpenXRAPI::obtain_swapchain_formats() { } bool OpenXRAPI::create_main_swapchains(Size2i p_size) { + ERR_NOT_ON_RENDER_THREAD_V(false); ERR_FAIL_NULL_V(graphics_extension, false); ERR_FAIL_COND_V(session == XR_NULL_HANDLE, false); @@ -1154,12 +1160,12 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { as we render 3D content into internal buffers that are copied into the swapchain, we do now have (basic) VRS support */ - main_swapchain_size = p_size; + render_state.main_swapchain_size = p_size; uint32_t sample_count = 1; // We start with our color swapchain... if (color_swapchain_format != 0) { - if (!main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, main_swapchain_size.width, main_swapchain_size.height, sample_count, view_count)) { + if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_count)) { return false; } } @@ -1169,7 +1175,7 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { // - we support our depth layer extension // - we have our spacewarp extension (not yet implemented) if (depth_swapchain_format != 0 && submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) { - if (!main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, main_swapchain_size.width, main_swapchain_size.height, sample_count, view_count)) { + if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_count)) { return false; } } @@ -1180,36 +1186,36 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { // TBD } - for (uint32_t i = 0; i < view_count; i++) { - views[i].type = XR_TYPE_VIEW; - views[i].next = nullptr; - - projection_views[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; - projection_views[i].next = nullptr; - projection_views[i].subImage.swapchain = main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain(); - projection_views[i].subImage.imageArrayIndex = i; - projection_views[i].subImage.imageRect.offset.x = 0; - projection_views[i].subImage.imageRect.offset.y = 0; - projection_views[i].subImage.imageRect.extent.width = main_swapchain_size.width; - projection_views[i].subImage.imageRect.extent.height = main_swapchain_size.height; - - if (submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available() && depth_views) { - projection_views[i].next = &depth_views[i]; - - depth_views[i].type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR; - depth_views[i].next = nullptr; - depth_views[i].subImage.swapchain = main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_swapchain(); - depth_views[i].subImage.imageArrayIndex = i; - depth_views[i].subImage.imageRect.offset.x = 0; - depth_views[i].subImage.imageRect.offset.y = 0; - depth_views[i].subImage.imageRect.extent.width = main_swapchain_size.width; - depth_views[i].subImage.imageRect.extent.height = main_swapchain_size.height; + for (uint32_t i = 0; i < render_state.view_count; i++) { + render_state.views[i].type = XR_TYPE_VIEW; + render_state.views[i].next = nullptr; + + render_state.projection_views[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; + render_state.projection_views[i].next = nullptr; + render_state.projection_views[i].subImage.swapchain = render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain(); + render_state.projection_views[i].subImage.imageArrayIndex = i; + render_state.projection_views[i].subImage.imageRect.offset.x = 0; + render_state.projection_views[i].subImage.imageRect.offset.y = 0; + render_state.projection_views[i].subImage.imageRect.extent.width = render_state.main_swapchain_size.width; + render_state.projection_views[i].subImage.imageRect.extent.height = render_state.main_swapchain_size.height; + + if (render_state.submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available() && render_state.depth_views) { + render_state.projection_views[i].next = &render_state.depth_views[i]; + + render_state.depth_views[i].type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR; + render_state.depth_views[i].next = nullptr; + render_state.depth_views[i].subImage.swapchain = render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_swapchain(); + render_state.depth_views[i].subImage.imageArrayIndex = i; + render_state.depth_views[i].subImage.imageRect.offset.x = 0; + render_state.depth_views[i].subImage.imageRect.offset.y = 0; + render_state.depth_views[i].subImage.imageRect.extent.width = render_state.main_swapchain_size.width; + render_state.depth_views[i].subImage.imageRect.extent.height = render_state.main_swapchain_size.height; // OpenXR spec says that: minDepth < maxDepth. - depth_views[i].minDepth = 0.0; - depth_views[i].maxDepth = 1.0; + render_state.depth_views[i].minDepth = 0.0; + render_state.depth_views[i].maxDepth = 1.0; // But we can reverse near and far for reverse-Z. - depth_views[i].nearZ = 100.0; // Near and far Z will be set to the correct values in fill_projection_matrix - depth_views[i].farZ = 0.01; + render_state.depth_views[i].nearZ = 100.0; // Near and far Z will be set to the correct values in fill_projection_matrix + render_state.depth_views[i].farZ = 0.01; } }; @@ -1217,23 +1223,33 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { }; void OpenXRAPI::destroy_session() { - if (running && session != XR_NULL_HANDLE) { - xrEndSession(session); + // TODO need to figure out if we're still rendering our current frame + // in a separate rendering thread and if so, + // if we need to wait for completion. + // We could be pulling the rug from underneath rendering... + + if (running) { + if (session != XR_NULL_HANDLE) { + xrEndSession(session); + } + + running = false; + render_state.running = false; } - if (views != nullptr) { - memfree(views); - views = nullptr; + if (render_state.views != nullptr) { + memfree(render_state.views); + render_state.views = nullptr; } - if (projection_views != nullptr) { - memfree(projection_views); - projection_views = nullptr; + if (render_state.projection_views != nullptr) { + memfree(render_state.projection_views); + render_state.projection_views = nullptr; } - if (depth_views != nullptr) { - memfree(depth_views); - depth_views = nullptr; + if (render_state.depth_views != nullptr) { + memfree(render_state.depth_views); + render_state.depth_views = nullptr; } free_main_swapchains(); @@ -1248,6 +1264,7 @@ void OpenXRAPI::destroy_session() { if (play_space != XR_NULL_HANDLE) { xrDestroySpace(play_space); play_space = XR_NULL_HANDLE; + render_state.play_space = XR_NULL_HANDLE; } if (view_space != XR_NULL_HANDLE) { xrDestroySpace(view_space); @@ -1298,6 +1315,7 @@ bool OpenXRAPI::on_state_ready() { // we're running running = true; + set_render_session_running(true); for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_state_ready(); @@ -1374,34 +1392,37 @@ bool OpenXRAPI::on_state_stopping() { } running = false; + set_render_session_running(false); } - // TODO further cleanup - return true; } bool OpenXRAPI::on_state_loss_pending() { print_verbose("On state loss pending"); + if (xr_interface) { + xr_interface->on_state_loss_pending(); + } + for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_state_loss_pending(); } - // TODO need to look into the correct action here, read up on the spec but we may need to signal Godot to exit (if it's not already exiting) - return true; } bool OpenXRAPI::on_state_exiting() { print_verbose("On state existing"); + if (xr_interface) { + xr_interface->on_state_exiting(); + } + for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_state_exiting(); } - // TODO need to look into the correct action here, read up on the spec but we may need to signal Godot to exit (if it's not already exiting) - return true; } @@ -1419,10 +1440,7 @@ void OpenXRAPI::set_view_configuration(XrViewConfigurationType p_view_configurat bool OpenXRAPI::set_requested_reference_space(XrReferenceSpaceType p_requested_reference_space) { requested_reference_space = p_requested_reference_space; - - if (is_initialized()) { - return setup_play_space(); - } + play_space_is_dirty = true; return true; } @@ -1625,11 +1643,6 @@ bool OpenXRAPI::initialize_session() { return false; } - if (!setup_play_space()) { - destroy_session(); - return false; - } - if (!setup_view_space()) { destroy_session(); return false; @@ -1645,6 +1658,8 @@ bool OpenXRAPI::initialize_session() { return false; } + allocate_view_buffers(view_count, submit_depth_buffer); + return true; } @@ -1696,12 +1711,18 @@ XrHandTrackerEXT OpenXRAPI::get_hand_tracker(int p_hand_index) { } Size2 OpenXRAPI::get_recommended_target_size() { + RenderingServer *rendering_server = RenderingServer::get_singleton(); ERR_FAIL_NULL_V(view_configuration_views, Size2()); Size2 target_size; - target_size.width = view_configuration_views[0].recommendedImageRectWidth * render_target_size_multiplier; - target_size.height = view_configuration_views[0].recommendedImageRectHeight * render_target_size_multiplier; + if (rendering_server && rendering_server->is_on_render_thread()) { + target_size.width = view_configuration_views[0].recommendedImageRectWidth * render_state.render_target_size_multiplier; + target_size.height = view_configuration_views[0].recommendedImageRectHeight * render_state.render_target_size_multiplier; + } else { + target_size.width = view_configuration_views[0].recommendedImageRectWidth * render_target_size_multiplier; + target_size.height = view_configuration_views[0].recommendedImageRectHeight * render_target_size_multiplier; + } return target_size; } @@ -1713,14 +1734,12 @@ XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform, return XRPose::XR_TRACKING_CONFIDENCE_NONE; } - // xrWaitFrame not run yet - if (frame_state.predictedDisplayTime == 0) { + // Get display time + XrTime display_time = get_predicted_display_time(); + if (display_time == 0) { return XRPose::XR_TRACKING_CONFIDENCE_NONE; } - // Get timing for the next frame, as that is the current frame we're processing - XrTime display_time = get_next_frame_time(); - XrSpaceVelocity velocity = { XR_TYPE_SPACE_VELOCITY, // type nullptr, // next @@ -1764,54 +1783,47 @@ XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform, } bool OpenXRAPI::get_view_transform(uint32_t p_view, Transform3D &r_transform) { - if (!running) { - return false; - } + ERR_NOT_ON_RENDER_THREAD_V(false); - // xrWaitFrame not run yet - if (frame_state.predictedDisplayTime == 0) { + if (!render_state.running) { return false; } // we don't have valid view info - if (views == nullptr || !view_pose_valid) { + if (render_state.views == nullptr || !render_state.view_pose_valid) { return false; } // Note, the timing of this is set right before rendering, which is what we need here. - r_transform = transform_from_pose(views[p_view].pose); + r_transform = transform_from_pose(render_state.views[p_view].pose); return true; } bool OpenXRAPI::get_view_projection(uint32_t p_view, double p_z_near, double p_z_far, Projection &p_camera_matrix) { + ERR_NOT_ON_RENDER_THREAD_V(false); ERR_FAIL_NULL_V(graphics_extension, false); - if (!running) { - return false; - } - - // xrWaitFrame not run yet - if (frame_state.predictedDisplayTime == 0) { + if (!render_state.running) { return false; } // we don't have valid view info - if (views == nullptr || !view_pose_valid) { + if (render_state.views == nullptr || !render_state.view_pose_valid) { return false; } // if we're using depth views, make sure we update our near and far there... - if (depth_views != nullptr) { - for (uint32_t i = 0; i < view_count; i++) { + if (render_state.depth_views != nullptr) { + for (uint32_t i = 0; i < render_state.view_count; i++) { // As we are using reverse-Z these need to be flipped. - depth_views[i].nearZ = p_z_far; - depth_views[i].farZ = p_z_near; + render_state.depth_views[i].nearZ = p_z_far; + render_state.depth_views[i].farZ = p_z_near; } } // now update our projection - return graphics_extension->create_projection_fov(views[p_view].fov, p_z_near, p_z_far, p_camera_matrix); + return graphics_extension->create_projection_fov(render_state.views[p_view].fov, p_z_near, p_z_far, p_camera_matrix); } bool OpenXRAPI::poll_events() { @@ -1934,53 +1946,85 @@ bool OpenXRAPI::poll_events() { } } -bool OpenXRAPI::process() { - ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, false); +void OpenXRAPI::_allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; - if (!poll_events()) { - return false; - } + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); - if (!running) { - return false; - } + openxr_api->render_state.view_count = p_view_count; + openxr_api->render_state.submit_depth_buffer = p_submit_depth_buffer; - for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { - wrapper->on_process(); + // Allocate buffers we'll be populating with view information. + openxr_api->render_state.views = (XrView *)memalloc(sizeof(XrView) * p_view_count); + ERR_FAIL_NULL_MSG(openxr_api->render_state.views, "OpenXR Couldn't allocate memory for views"); + memset(openxr_api->render_state.views, 0, sizeof(XrView) * p_view_count); + + openxr_api->render_state.projection_views = (XrCompositionLayerProjectionView *)memalloc(sizeof(XrCompositionLayerProjectionView) * p_view_count); + ERR_FAIL_NULL_MSG(openxr_api->render_state.projection_views, "OpenXR Couldn't allocate memory for projection views"); + memset(openxr_api->render_state.projection_views, 0, sizeof(XrCompositionLayerProjectionView) * p_view_count); + + if (p_submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) { + openxr_api->render_state.depth_views = (XrCompositionLayerDepthInfoKHR *)memalloc(sizeof(XrCompositionLayerDepthInfoKHR) * p_view_count); + ERR_FAIL_NULL_MSG(openxr_api->render_state.depth_views, "OpenXR Couldn't allocate memory for depth views"); + memset(openxr_api->render_state.depth_views, 0, sizeof(XrCompositionLayerDepthInfoKHR) * p_view_count); } +} - return true; +void OpenXRAPI::_set_render_session_running(bool p_is_running) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + openxr_api->render_state.running = p_is_running; } -void OpenXRAPI::free_main_swapchains() { - for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) { - main_swapchains[i].queue_free(); - } +void OpenXRAPI::_set_render_display_info(XrTime p_predicted_display_time, bool p_should_render) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + openxr_api->render_state.predicted_display_time = p_predicted_display_time; + openxr_api->render_state.should_render = p_should_render; } -void OpenXRAPI::pre_render() { - ERR_FAIL_COND(instance == XR_NULL_HANDLE); +void OpenXRAPI::_set_render_play_space(uint64_t p_play_space) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; - if (!running) { - return; - } + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + openxr_api->render_state.play_space = XrSpace(p_play_space); +} - // Process any swapchains that were queued to be freed - OpenXRSwapChainInfo::free_queued(); +void OpenXRAPI::_set_render_state_multiplier(double p_render_target_size_multiplier) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; - Size2i swapchain_size = get_recommended_target_size(); - if (swapchain_size != main_swapchain_size) { - // Out with the old. - free_main_swapchains(); + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + openxr_api->render_state.render_target_size_multiplier = p_render_target_size_multiplier; +} - // In with the new. - create_main_swapchains(swapchain_size); +bool OpenXRAPI::process() { + ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, false); + + if (!poll_events()) { + return false; } - // Waitframe does 2 important things in our process: - // 1) It provides us with predictive timing, telling us when OpenXR expects to display the frame we're about to commit - // 2) It will use the previous timing to pause our thread so that rendering starts as close to displaying as possible - // This must thus be called as close to when we start rendering as possible + if (!running) { + return false; + } + + // We call xrWaitFrame as early as possible, this will allow OpenXR to get + // proper timing info between this point, and when we're ready to start rendering. + // As the name suggests, OpenXR can pause the thread to minimize the time between + // retrieving tracking data and using that tracking data to render. + // OpenXR thus works best if rendering is performed on a separate thread. XrFrameWaitInfo frame_wait_info = { XR_TYPE_FRAME_WAIT_INFO, nullptr }; frame_state.predictedDisplayTime = 0; frame_state.predictedDisplayPeriod = 0; @@ -1995,7 +2039,9 @@ void OpenXRAPI::pre_render() { frame_state.predictedDisplayPeriod = 0; frame_state.shouldRender = false; - return; + set_render_display_info(0, false); + + return false; } if (frame_state.predictedDisplayPeriod > 500000000) { @@ -2004,12 +2050,54 @@ void OpenXRAPI::pre_render() { frame_state.predictedDisplayPeriod = 0; } + set_render_display_info(frame_state.predictedDisplayTime, frame_state.shouldRender); + + if (unlikely(play_space_is_dirty)) { + setup_play_space(); + play_space_is_dirty = false; + } + if (unlikely(should_reset_emulated_floor_height)) { reset_emulated_floor_height(); should_reset_emulated_floor_height = false; } for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { + wrapper->on_process(); + } + + return true; +} + +void OpenXRAPI::free_main_swapchains() { + for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) { + render_state.main_swapchains[i].queue_free(); + } +} + +void OpenXRAPI::pre_render() { + ERR_FAIL_COND(session == XR_NULL_HANDLE); + + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + if (!render_state.running) { + return; + } + + // Process any swapchains that were queued to be freed + OpenXRSwapChainInfo::free_queued(); + + Size2i swapchain_size = get_recommended_target_size(); + if (swapchain_size != render_state.main_swapchain_size) { + // Out with the old. + free_main_swapchains(); + + // In with the new. + create_main_swapchains(swapchain_size); + } + + for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_pre_render(); } @@ -2028,8 +2116,8 @@ void OpenXRAPI::pre_render() { XR_TYPE_VIEW_LOCATE_INFO, // type nullptr, // next view_configuration, // viewConfigurationType - frame_state.predictedDisplayTime, // displayTime - play_space // space + render_state.predicted_display_time, // displayTime + render_state.play_space // space }; XrViewState view_state = { XR_TYPE_VIEW_STATE, // type @@ -2037,7 +2125,7 @@ void OpenXRAPI::pre_render() { 0 // viewStateFlags }; uint32_t view_count_output; - result = xrLocateViews(session, &view_locate_info, &view_state, view_count, &view_count_output, views); + XrResult result = xrLocateViews(session, &view_locate_info, &view_state, render_state.view_count, &view_count_output, render_state.views); if (XR_FAILED(result)) { print_line("OpenXR: Couldn't locate views [", get_error_string(result), "]"); return; @@ -2050,9 +2138,9 @@ void OpenXRAPI::pre_render() { pose_valid = false; } } - if (view_pose_valid != pose_valid) { - view_pose_valid = pose_valid; - if (!view_pose_valid) { + if (render_state.view_pose_valid != pose_valid) { + render_state.view_pose_valid = pose_valid; + if (!render_state.view_pose_valid) { print_verbose("OpenXR View pose became invalid"); } else { print_verbose("OpenXR View pose became valid"); @@ -2071,23 +2159,24 @@ void OpenXRAPI::pre_render() { } // Reset this, we haven't found a viewport for output yet - has_xr_viewport = false; + render_state.has_xr_viewport = false; } bool OpenXRAPI::pre_draw_viewport(RID p_render_target) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD_V(false); + // We found an XR viewport! - has_xr_viewport = true; + render_state.has_xr_viewport = true; - if (!can_render()) { + if (instance == XR_NULL_HANDLE || session == XR_NULL_HANDLE || !render_state.running || !render_state.view_pose_valid || !render_state.should_render) { return false; } - // TODO: at some point in time we may support multiple viewports in which case we need to handle that... - // Acquire our images for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) { - if (!main_swapchains[i].is_image_acquired() && main_swapchains[i].get_swapchain() != XR_NULL_HANDLE) { - if (!main_swapchains[i].acquire(frame_state.shouldRender)) { + if (!render_state.main_swapchains[i].is_image_acquired() && render_state.main_swapchains[i].get_swapchain() != XR_NULL_HANDLE) { + if (!render_state.main_swapchains[i].acquire(render_state.should_render)) { return false; } } @@ -2101,24 +2190,33 @@ bool OpenXRAPI::pre_draw_viewport(RID p_render_target) { } XrSwapchain OpenXRAPI::get_color_swapchain() { - return main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain(); + ERR_NOT_ON_RENDER_THREAD_V(XR_NULL_HANDLE); + + return render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain(); } RID OpenXRAPI::get_color_texture() { - return main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_image(); + ERR_NOT_ON_RENDER_THREAD_V(RID()); + + return render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_image(); } RID OpenXRAPI::get_depth_texture() { + ERR_NOT_ON_RENDER_THREAD_V(RID()); + // Note, image will not be acquired if we didn't have a suitable swap chain format. - if (submit_depth_buffer) { - return main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_image(); + if (render_state.submit_depth_buffer && render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].is_image_acquired()) { + return render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_image(); } else { return RID(); } } void OpenXRAPI::post_draw_viewport(RID p_render_target) { - if (!can_render()) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + if (instance == XR_NULL_HANDLE || session == XR_NULL_HANDLE || !render_state.running || !render_state.view_pose_valid || !render_state.should_render) { return; } @@ -2130,30 +2228,33 @@ void OpenXRAPI::post_draw_viewport(RID p_render_target) { void OpenXRAPI::end_frame() { XrResult result; - ERR_FAIL_COND(instance == XR_NULL_HANDLE); + ERR_FAIL_COND(session == XR_NULL_HANDLE); - if (!running) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + if (!render_state.running) { return; } - if (frame_state.shouldRender && view_pose_valid) { - if (!has_xr_viewport) { + if (render_state.should_render && render_state.view_pose_valid) { + if (!render_state.has_xr_viewport) { print_line("OpenXR: No viewport was marked with use_xr, there is no rendered output!"); - } else if (!main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) { + } else if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) { print_line("OpenXR: No swapchain could be acquired to render to!"); } } // must have: - // - shouldRender set to true + // - should_render set to true // - a valid view pose for projection_views[eye].pose to submit layer // - an image to render - if (!frame_state.shouldRender || !view_pose_valid || !main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) { + if (!render_state.should_render || !render_state.view_pose_valid || !render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) { // submit 0 layers when we shouldn't render XrFrameEndInfo frame_end_info = { XR_TYPE_FRAME_END_INFO, // type nullptr, // next - frame_state.predictedDisplayTime, // displayTime + render_state.predicted_display_time, // displayTime environment_blend_mode, // environmentBlendMode 0, // layerCount nullptr // layers @@ -2170,14 +2271,14 @@ void OpenXRAPI::end_frame() { // release our swapchain image if we acquired it for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) { - if (main_swapchains[i].is_image_acquired()) { - main_swapchains[i].release(); + if (render_state.main_swapchains[i].is_image_acquired()) { + render_state.main_swapchains[i].release(); } } - for (uint32_t eye = 0; eye < view_count; eye++) { - projection_views[eye].fov = views[eye].fov; - projection_views[eye].pose = views[eye].pose; + for (uint32_t eye = 0; eye < render_state.view_count; eye++) { + render_state.projection_views[eye].fov = render_state.views[eye].fov; + render_state.projection_views[eye].pose = render_state.views[eye].pose; } Vector<OrderedCompositionLayer> ordered_layers_list; @@ -2210,9 +2311,9 @@ void OpenXRAPI::end_frame() { XR_TYPE_COMPOSITION_LAYER_PROJECTION, // type nullptr, // next layer_flags, // layerFlags - play_space, // space - view_count, // viewCount - projection_views, // views + render_state.play_space, // space + render_state.view_count, // viewCount + render_state.projection_views, // views }; ordered_layers_list.push_back({ (const XrCompositionLayerBaseHeader *)&projection_layer, 0 }); @@ -2228,7 +2329,7 @@ void OpenXRAPI::end_frame() { XrFrameEndInfo frame_end_info = { XR_TYPE_FRAME_END_INFO, // type nullptr, // next - frame_state.predictedDisplayTime, // displayTime + render_state.predicted_display_time, // displayTime environment_blend_mode, // environmentBlendMode static_cast<uint32_t>(layers_list.size()), // layerCount layers_list.ptr() // layers @@ -2271,6 +2372,7 @@ double OpenXRAPI::get_render_target_size_multiplier() const { void OpenXRAPI::set_render_target_size_multiplier(double multiplier) { render_target_size_multiplier = multiplier; + set_render_state_multiplier(multiplier); } bool OpenXRAPI::is_foveation_supported() const { @@ -2414,10 +2516,6 @@ OpenXRAPI::OpenXRAPI() { submit_depth_buffer = GLOBAL_GET("xr/openxr/submit_depth_buffer"); } - - // Reset a few things that can't be done in our class definition. - frame_state.predictedDisplayTime = 0; - frame_state.predictedDisplayPeriod = 0; } OpenXRAPI::~OpenXRAPI() { @@ -3132,7 +3230,7 @@ XRPose::TrackingConfidence OpenXRAPI::get_action_pose(RID p_action, RID p_tracke return XRPose::XR_TRACKING_CONFIDENCE_NONE; } - XrTime display_time = get_next_frame_time(); + XrTime display_time = get_predicted_display_time(); if (display_time == 0) { return XRPose::XR_TRACKING_CONFIDENCE_NONE; } diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index e835366200..c95867810c 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -46,13 +46,11 @@ #include "core/templates/rb_map.h" #include "core/templates/rid_owner.h" #include "core/templates/vector.h" +#include "servers/rendering_server.h" #include "servers/xr/xr_pose.h" #include <openxr/openxr.h> -// Note, OpenXR code that we wrote for our plugin makes use of C++20 notation for initializing structs which ensures zeroing out unspecified members. -// Godot is currently restricted to C++17 which doesn't allow this notation. Make sure critical fields are set. - // forward declarations, we don't want to include these fully class OpenXRInterface; @@ -77,7 +75,7 @@ public: static void free_queued(); void free(); - bool acquire(XrBool32 &p_should_render); + bool acquire(bool &p_should_render); bool release(); RID get_image(); }; @@ -151,9 +149,6 @@ private: uint32_t view_count = 0; XrViewConfigurationView *view_configuration_views = nullptr; - XrView *views = nullptr; - XrCompositionLayerProjectionView *projection_views = nullptr; - XrCompositionLayerDepthInfoKHR *depth_views = nullptr; // Only used by Composition Layer Depth Extension if available enum OpenXRSwapChainTypes { OPENXR_SWAPCHAIN_COLOR, @@ -164,14 +159,11 @@ private: int64_t color_swapchain_format = 0; int64_t depth_swapchain_format = 0; - Size2i main_swapchain_size = { 0, 0 }; - OpenXRSwapChainInfo main_swapchains[OPENXR_SWAPCHAIN_MAX]; + bool play_space_is_dirty = true; XrSpace play_space = XR_NULL_HANDLE; XrSpace view_space = XR_NULL_HANDLE; - bool view_pose_valid = false; XRPose::TrackingConfidence head_pose_confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE; - bool has_xr_viewport = false; bool emulating_local_floor = false; bool should_reset_emulated_floor_height = false; @@ -328,6 +320,72 @@ private: // convenience void copy_string_to_char_buffer(const String p_string, char *p_buffer, int p_buffer_len); + // Render state, Only accessible in rendering thread + struct RenderState { + bool running = false; + bool should_render = false; + bool has_xr_viewport = false; + XrTime predicted_display_time = 0; + XrSpace play_space = XR_NULL_HANDLE; + double render_target_size_multiplier = 1.0; + + uint32_t view_count = 0; + XrView *views = nullptr; + XrCompositionLayerProjectionView *projection_views = nullptr; + XrCompositionLayerDepthInfoKHR *depth_views = nullptr; // Only used by Composition Layer Depth Extension if available + bool submit_depth_buffer = false; // if set to true we submit depth buffers to OpenXR if a suitable extension is enabled. + bool view_pose_valid = false; + + Size2i main_swapchain_size; + OpenXRSwapChainInfo main_swapchains[OPENXR_SWAPCHAIN_MAX]; + } render_state; + + static void _allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer); + static void _set_render_session_running(bool p_is_running); + static void _set_render_display_info(XrTime p_predicted_display_time, bool p_should_render); + static void _set_render_play_space(uint64_t p_play_space); + static void _set_render_state_multiplier(double p_render_target_size_multiplier); + + _FORCE_INLINE_ void allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_allocate_view_buffers).bind(p_view_count, p_submit_depth_buffer)); + } + + _FORCE_INLINE_ void set_render_session_running(bool p_is_running) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_session_running).bind(p_is_running)); + } + + _FORCE_INLINE_ void set_render_display_info(XrTime p_predicted_display_time, bool p_should_render) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_display_info).bind(p_predicted_display_time, p_should_render)); + } + + _FORCE_INLINE_ void set_render_play_space(XrSpace p_play_space) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_play_space).bind(uint64_t(p_play_space))); + } + + _FORCE_INLINE_ void set_render_state_multiplier(double p_render_target_size_multiplier) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_state_multiplier).bind(p_render_target_size_multiplier)); + } + public: XrInstance get_instance() const { return instance; }; XrSystemId get_system_id() const { return system_id; }; @@ -384,9 +442,13 @@ public: bool initialize_session(); void finish(); - XrSpace get_play_space() const { return play_space; } - XrTime get_next_frame_time() { return frame_state.predictedDisplayTime + frame_state.predictedDisplayPeriod; } - bool can_render() { return instance != XR_NULL_HANDLE && session != XR_NULL_HANDLE && running && view_pose_valid && frame_state.shouldRender; } + _FORCE_INLINE_ XrSpace get_play_space() const { return play_space; } + _FORCE_INLINE_ XrTime get_predicted_display_time() { return frame_state.predictedDisplayTime; } + _FORCE_INLINE_ XrTime get_next_frame_time() { return frame_state.predictedDisplayTime + frame_state.predictedDisplayPeriod; } + _FORCE_INLINE_ bool can_render() { + ERR_ON_RENDER_THREAD_V(false); + return instance != XR_NULL_HANDLE && session != XR_NULL_HANDLE && running && frame_state.shouldRender; + } XrHandTrackerEXT get_hand_tracker(int p_hand_index); diff --git a/modules/openxr/openxr_api_extension.cpp b/modules/openxr/openxr_api_extension.cpp index fae0fc13d3..a1744fa1db 100644 --- a/modules/openxr/openxr_api_extension.cpp +++ b/modules/openxr/openxr_api_extension.cpp @@ -48,6 +48,7 @@ void OpenXRAPIExtension::_bind_methods() { ClassDB::bind_method(D_METHOD("is_running"), &OpenXRAPIExtension::is_running); ClassDB::bind_method(D_METHOD("get_play_space"), &OpenXRAPIExtension::get_play_space); + ClassDB::bind_method(D_METHOD("get_predicted_display_time"), &OpenXRAPIExtension::get_predicted_display_time); ClassDB::bind_method(D_METHOD("get_next_frame_time"), &OpenXRAPIExtension::get_next_frame_time); ClassDB::bind_method(D_METHOD("can_render"), &OpenXRAPIExtension::can_render); @@ -130,8 +131,17 @@ uint64_t OpenXRAPIExtension::get_play_space() { return (uint64_t)OpenXRAPI::get_singleton()->get_play_space(); } +int64_t OpenXRAPIExtension::get_predicted_display_time() { + ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), 0); + return (XrTime)OpenXRAPI::get_singleton()->get_predicted_display_time(); +} + int64_t OpenXRAPIExtension::get_next_frame_time() { ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), 0); + + // In the past we needed to look a frame ahead, may be calling this unintentionally so lets warn the dev. + WARN_PRINT_ONCE("OpenXR: Next frame timing called, verify this is intended."); + return (XrTime)OpenXRAPI::get_singleton()->get_next_frame_time(); } diff --git a/modules/openxr/openxr_api_extension.h b/modules/openxr/openxr_api_extension.h index 576e497798..cff2c4738e 100644 --- a/modules/openxr/openxr_api_extension.h +++ b/modules/openxr/openxr_api_extension.h @@ -69,6 +69,7 @@ public: bool is_running(); uint64_t get_play_space(); + int64_t get_predicted_display_time(); int64_t get_next_frame_time(); bool can_render(); diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp index aa68441f03..39a61d1b4d 100644 --- a/modules/openxr/openxr_interface.cpp +++ b/modules/openxr/openxr_interface.cpp @@ -35,6 +35,7 @@ #include "servers/rendering/rendering_server_globals.h" #include "extensions/openxr_eye_gaze_interaction.h" +#include "extensions/openxr_hand_interaction_extension.h" #include "thirdparty/openxr/include/openxr/openxr.h" void OpenXRInterface::_bind_methods() { @@ -43,6 +44,8 @@ void OpenXRInterface::_bind_methods() { ADD_SIGNAL(MethodInfo("session_stopping")); ADD_SIGNAL(MethodInfo("session_focussed")); ADD_SIGNAL(MethodInfo("session_visible")); + ADD_SIGNAL(MethodInfo("session_loss_pending")); + ADD_SIGNAL(MethodInfo("instance_exiting")); ADD_SIGNAL(MethodInfo("pose_recentered")); ADD_SIGNAL(MethodInfo("refresh_rate_changed", PropertyInfo(Variant::FLOAT, "refresh_rate"))); @@ -91,6 +94,7 @@ void OpenXRInterface::_bind_methods() { ClassDB::bind_method(D_METHOD("get_hand_joint_angular_velocity", "hand", "joint"), &OpenXRInterface::get_hand_joint_angular_velocity); ClassDB::bind_method(D_METHOD("is_hand_tracking_supported"), &OpenXRInterface::is_hand_tracking_supported); + ClassDB::bind_method(D_METHOD("is_hand_interaction_supported"), &OpenXRInterface::is_hand_interaction_supported); ClassDB::bind_method(D_METHOD("is_eye_gaze_interaction_supported"), &OpenXRInterface::is_eye_gaze_interaction_supported); BIND_ENUM_CONSTANT(HAND_LEFT); @@ -806,6 +810,21 @@ bool OpenXRInterface::is_hand_tracking_supported() { } } +bool OpenXRInterface::is_hand_interaction_supported() const { + if (openxr_api == nullptr) { + return false; + } else if (!openxr_api->is_initialized()) { + return false; + } else { + OpenXRHandInteractionExtension *hand_interaction_ext = OpenXRHandInteractionExtension::get_singleton(); + if (hand_interaction_ext == nullptr) { + return false; + } else { + return hand_interaction_ext->is_available(); + } + } +} + bool OpenXRInterface::is_eye_gaze_interaction_supported() { if (openxr_api == nullptr) { return false; @@ -1258,6 +1277,14 @@ void OpenXRInterface::on_state_stopping() { emit_signal(SNAME("session_stopping")); } +void OpenXRInterface::on_state_loss_pending() { + emit_signal(SNAME("session_loss_pending")); +} + +void OpenXRInterface::on_state_exiting() { + emit_signal(SNAME("instance_exiting")); +} + void OpenXRInterface::on_pose_recentered() { emit_signal(SNAME("pose_recentered")); } diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h index e916c7dac2..ac33304757 100644 --- a/modules/openxr/openxr_interface.h +++ b/modules/openxr/openxr_interface.h @@ -31,6 +31,29 @@ #ifndef OPENXR_INTERFACE_H #define OPENXR_INTERFACE_H +// A note on multithreading and thread safety in OpenXR. +// +// Most entry points will be called from the main thread in Godot +// however a number of entry points will be called from the +// rendering thread, potentially while we're already processing +// the next frame on the main thread. +// +// OpenXR itself has been designed with threading in mind including +// a high likelihood that the XR runtime runs in separate threads +// as well. +// Hence all the frame timing information, use of swapchains and +// sync functions. +// Do note that repeated calls to tracking APIs will provide +// increasingly more accurate data for the same timestamp as +// tracking data is continuously updated. +// +// For our code we mostly implement this in our OpenXRAPI class. +// We store data accessed from the rendering thread in a separate +// struct, setting values through our renderer command queue. +// +// As some data is setup before we start rendering, and cleaned up +// after we've stopped, that is accessed directly from both threads. + #include "action_map/openxr_action_map.h" #include "extensions/openxr_hand_tracking_extension.h" #include "openxr_api.h" @@ -110,6 +133,7 @@ public: virtual TrackingStatus get_tracking_status() const override; bool is_hand_tracking_supported(); + bool is_hand_interaction_supported() const; bool is_eye_gaze_interaction_supported(); bool initialize_on_startup() const; @@ -173,6 +197,8 @@ public: void on_state_visible(); void on_state_focused(); void on_state_stopping(); + void on_state_loss_pending(); + void on_state_exiting(); void on_pose_recentered(); void on_refresh_rate_changes(float p_new_rate); void tracker_profile_changed(RID p_tracker, RID p_interaction_profile); diff --git a/modules/openxr/register_types.cpp b/modules/openxr/register_types.cpp index eb0527f07c..85514737f2 100644 --- a/modules/openxr/register_types.cpp +++ b/modules/openxr/register_types.cpp @@ -49,6 +49,7 @@ #include "extensions/openxr_composition_layer_extension.h" #include "extensions/openxr_eye_gaze_interaction.h" #include "extensions/openxr_fb_display_refresh_rate_extension.h" +#include "extensions/openxr_hand_interaction_extension.h" #include "extensions/openxr_hand_tracking_extension.h" #include "extensions/openxr_htc_controller_extension.h" #include "extensions/openxr_htc_vive_tracker_extension.h" @@ -124,6 +125,7 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) { OpenXRAPI::register_extension_wrapper(memnew(OpenXRML2ControllerExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRMetaControllerExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXREyeGazeInteractionExtension)); + OpenXRAPI::register_extension_wrapper(memnew(OpenXRHandInteractionExtension)); // register gated extensions if (GLOBAL_GET("xr/openxr/extensions/hand_tracking")) { diff --git a/modules/raycast/config.py b/modules/raycast/config.py index 26329d813a..0fd35af528 100644 --- a/modules/raycast/config.py +++ b/modules/raycast/config.py @@ -1,8 +1,9 @@ def can_build(env, platform): - # Supported architectures depend on the Embree library. + # Supported architectures and platforms depend on the Embree library. + if env["arch"] == "arm64" and platform == "windows": + return False if env["arch"] in ["x86_64", "arm64", "wasm32"]: return True - # x86_32 only seems supported on Windows for now. if env["arch"] == "x86_32" and platform == "windows": return True return False diff --git a/modules/text_server_adv/gdextension_build/SConstruct b/modules/text_server_adv/gdextension_build/SConstruct index 1d9d36fbbf..fcf3f64315 100644 --- a/modules/text_server_adv/gdextension_build/SConstruct +++ b/modules/text_server_adv/gdextension_build/SConstruct @@ -1,10 +1,20 @@ #!/usr/bin/env python import atexit -import os import sys import methods import time +# Enable ANSI escape code support on Windows 10 and later (for colored console output). +# <https://github.com/python/cpython/issues/73245> +if sys.platform == "win32": + from ctypes import windll, c_int, byref + + stdout_handle = windll.kernel32.GetStdHandle(c_int(-11)) + mode = c_int(0) + windll.kernel32.GetConsoleMode(c_int(stdout_handle), byref(mode)) + mode = c_int(mode.value | 4) + windll.kernel32.SetConsoleMode(c_int(stdout_handle), mode) + # For the reference: # - CCFLAGS are compilation flags shared between C and C++ # - CFLAGS are for C-specific compilation flags @@ -30,7 +40,7 @@ opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", Fa opts.Update(env) if not env["verbose"]: - methods.no_verbose(sys, env) + methods.no_verbose(env) if env["platform"] == "windows" and not env["use_mingw"]: env.AppendUnique(CCFLAGS=["/utf-8"]) # Force to use Unicode encoding. @@ -764,9 +774,16 @@ Default(library) def print_elapsed_time(): - elapsed_time_sec = round(time.time() - time_at_start, 3) - time_ms = round((elapsed_time_sec % 1) * 1000) - print("[Time elapsed: {}.{:03}]".format(time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)), time_ms)) + elapsed_time_sec = round(time.time() - time_at_start, 2) + time_centiseconds = round((elapsed_time_sec % 1) * 100) + print( + "{}[Time elapsed: {}.{:02}]{}".format( + methods.ANSI.GRAY, + time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)), + time_centiseconds, + methods.ANSI.RESET, + ) + ) atexit.register(print_elapsed_time) diff --git a/modules/text_server_adv/gdextension_build/methods.py b/modules/text_server_adv/gdextension_build/methods.py index 32dbc59fd4..3453c3e8f0 100644 --- a/modules/text_server_adv/gdextension_build/methods.py +++ b/modules/text_server_adv/gdextension_build/methods.py @@ -1,66 +1,75 @@ import os import sys +from enum import Enum +# Colors are disabled in non-TTY environments such as pipes. This means +# that if output is redirected to a file, it won't contain color codes. +# Colors are always enabled on continuous integration. +_colorize = bool(sys.stdout.isatty() or os.environ.get("CI")) -def no_verbose(sys, env): - colors = {} - # Colors are disabled in non-TTY environments such as pipes. This means - # that if output is redirected to a file, it will not contain color codes - if sys.stdout.isatty(): - colors["blue"] = "\033[0;94m" - colors["bold_blue"] = "\033[1;94m" - colors["reset"] = "\033[0m" - else: - colors["blue"] = "" - colors["bold_blue"] = "" - colors["reset"] = "" +class ANSI(Enum): + """ + Enum class for adding ansi colorcodes directly into strings. + Automatically converts values to strings representing their + internal value, or an empty string in a non-colorized scope. + """ + + RESET = "\x1b[0m" + + BOLD = "\x1b[1m" + ITALIC = "\x1b[3m" + UNDERLINE = "\x1b[4m" + STRIKETHROUGH = "\x1b[9m" + REGULAR = "\x1b[22;23;24;29m" + + BLACK = "\x1b[30m" + RED = "\x1b[31m" + GREEN = "\x1b[32m" + YELLOW = "\x1b[33m" + BLUE = "\x1b[34m" + MAGENTA = "\x1b[35m" + CYAN = "\x1b[36m" + WHITE = "\x1b[37m" + + PURPLE = "\x1b[38;5;93m" + PINK = "\x1b[38;5;206m" + ORANGE = "\x1b[38;5;214m" + GRAY = "\x1b[38;5;244m" + + def __str__(self) -> str: + global _colorize + return str(self.value) if _colorize else "" + + +def no_verbose(env): + colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET] # There is a space before "..." to ensure that source file names can be # Ctrl + clicked in the VS Code terminal. - compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - link_program_message = "{}Linking Program {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - generated_file_message = "{}Generating {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - - env.Append(CXXCOMSTR=[compile_source_message]) - env.Append(CCCOMSTR=[compile_source_message]) - env.Append(SHCCCOMSTR=[compile_shared_source_message]) - env.Append(SHCXXCOMSTR=[compile_shared_source_message]) - env.Append(ARCOMSTR=[link_library_message]) - env.Append(RANLIBCOMSTR=[ranlib_library_message]) - env.Append(SHLINKCOMSTR=[link_shared_library_message]) - env.Append(LINKCOMSTR=[link_program_message]) - env.Append(JARCOMSTR=[java_library_message]) - env.Append(JAVACCOMSTR=[java_compile_source_message]) - env.Append(RCCOMSTR=[compiled_resource_message]) - env.Append(GENCOMSTR=[generated_file_message]) + compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors) + link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors) + link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors) + ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors) + link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors) + java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors) + compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors) + generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors) + + env["CXXCOMSTR"] = compile_source_message + env["CCCOMSTR"] = compile_source_message + env["SHCCCOMSTR"] = compile_shared_source_message + env["SHCXXCOMSTR"] = compile_shared_source_message + env["ARCOMSTR"] = link_library_message + env["RANLIBCOMSTR"] = ranlib_library_message + env["SHLINKCOMSTR"] = link_shared_library_message + env["LINKCOMSTR"] = link_program_message + env["JARCOMSTR"] = java_library_message + env["JAVACCOMSTR"] = java_compile_source_message + env["RCCOMSTR"] = compiled_resource_message + env["GENCOMSTR"] = generated_file_message def disable_warnings(self): diff --git a/modules/text_server_fb/gdextension_build/SConstruct b/modules/text_server_fb/gdextension_build/SConstruct index 29801ede8e..07940719eb 100644 --- a/modules/text_server_fb/gdextension_build/SConstruct +++ b/modules/text_server_fb/gdextension_build/SConstruct @@ -1,10 +1,20 @@ #!/usr/bin/env python import atexit -import os import sys import methods import time +# Enable ANSI escape code support on Windows 10 and later (for colored console output). +# <https://github.com/python/cpython/issues/73245> +if sys.platform == "win32": + from ctypes import windll, c_int, byref + + stdout_handle = windll.kernel32.GetStdHandle(c_int(-11)) + mode = c_int(0) + windll.kernel32.GetConsoleMode(c_int(stdout_handle), byref(mode)) + mode = c_int(mode.value | 4) + windll.kernel32.SetConsoleMode(c_int(stdout_handle), mode) + # For the reference: # - CCFLAGS are compilation flags shared between C and C++ # - CFLAGS are for C-specific compilation flags @@ -28,7 +38,7 @@ opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", Fa opts.Update(env) if not env["verbose"]: - methods.no_verbose(sys, env) + methods.no_verbose(env) # ThorVG if env["thorvg_enabled"] and env["freetype_enabled"]: @@ -311,9 +321,16 @@ Default(library) def print_elapsed_time(): - elapsed_time_sec = round(time.time() - time_at_start, 3) - time_ms = round((elapsed_time_sec % 1) * 1000) - print("[Time elapsed: {}.{:03}]".format(time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)), time_ms)) + elapsed_time_sec = round(time.time() - time_at_start, 2) + time_centiseconds = round((elapsed_time_sec % 1) * 100) + print( + "{}[Time elapsed: {}.{:02}]{}".format( + methods.ANSI.GRAY, + time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)), + time_centiseconds, + methods.ANSI.RESET, + ) + ) atexit.register(print_elapsed_time) diff --git a/modules/text_server_fb/gdextension_build/methods.py b/modules/text_server_fb/gdextension_build/methods.py index 32dbc59fd4..3453c3e8f0 100644 --- a/modules/text_server_fb/gdextension_build/methods.py +++ b/modules/text_server_fb/gdextension_build/methods.py @@ -1,66 +1,75 @@ import os import sys +from enum import Enum +# Colors are disabled in non-TTY environments such as pipes. This means +# that if output is redirected to a file, it won't contain color codes. +# Colors are always enabled on continuous integration. +_colorize = bool(sys.stdout.isatty() or os.environ.get("CI")) -def no_verbose(sys, env): - colors = {} - # Colors are disabled in non-TTY environments such as pipes. This means - # that if output is redirected to a file, it will not contain color codes - if sys.stdout.isatty(): - colors["blue"] = "\033[0;94m" - colors["bold_blue"] = "\033[1;94m" - colors["reset"] = "\033[0m" - else: - colors["blue"] = "" - colors["bold_blue"] = "" - colors["reset"] = "" +class ANSI(Enum): + """ + Enum class for adding ansi colorcodes directly into strings. + Automatically converts values to strings representing their + internal value, or an empty string in a non-colorized scope. + """ + + RESET = "\x1b[0m" + + BOLD = "\x1b[1m" + ITALIC = "\x1b[3m" + UNDERLINE = "\x1b[4m" + STRIKETHROUGH = "\x1b[9m" + REGULAR = "\x1b[22;23;24;29m" + + BLACK = "\x1b[30m" + RED = "\x1b[31m" + GREEN = "\x1b[32m" + YELLOW = "\x1b[33m" + BLUE = "\x1b[34m" + MAGENTA = "\x1b[35m" + CYAN = "\x1b[36m" + WHITE = "\x1b[37m" + + PURPLE = "\x1b[38;5;93m" + PINK = "\x1b[38;5;206m" + ORANGE = "\x1b[38;5;214m" + GRAY = "\x1b[38;5;244m" + + def __str__(self) -> str: + global _colorize + return str(self.value) if _colorize else "" + + +def no_verbose(env): + colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET] # There is a space before "..." to ensure that source file names can be # Ctrl + clicked in the VS Code terminal. - compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - link_program_message = "{}Linking Program {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - generated_file_message = "{}Generating {}$TARGET{} ...{}".format( - colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"] - ) - - env.Append(CXXCOMSTR=[compile_source_message]) - env.Append(CCCOMSTR=[compile_source_message]) - env.Append(SHCCCOMSTR=[compile_shared_source_message]) - env.Append(SHCXXCOMSTR=[compile_shared_source_message]) - env.Append(ARCOMSTR=[link_library_message]) - env.Append(RANLIBCOMSTR=[ranlib_library_message]) - env.Append(SHLINKCOMSTR=[link_shared_library_message]) - env.Append(LINKCOMSTR=[link_program_message]) - env.Append(JARCOMSTR=[java_library_message]) - env.Append(JAVACCOMSTR=[java_compile_source_message]) - env.Append(RCCOMSTR=[compiled_resource_message]) - env.Append(GENCOMSTR=[generated_file_message]) + compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors) + link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors) + link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors) + ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors) + link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors) + java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors) + compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors) + generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors) + + env["CXXCOMSTR"] = compile_source_message + env["CCCOMSTR"] = compile_source_message + env["SHCCCOMSTR"] = compile_shared_source_message + env["SHCXXCOMSTR"] = compile_shared_source_message + env["ARCOMSTR"] = link_library_message + env["RANLIBCOMSTR"] = ranlib_library_message + env["SHLINKCOMSTR"] = link_shared_library_message + env["LINKCOMSTR"] = link_program_message + env["JARCOMSTR"] = java_library_message + env["JAVACCOMSTR"] = java_compile_source_message + env["RCCOMSTR"] = compiled_resource_message + env["GENCOMSTR"] = generated_file_message def disable_warnings(self): diff --git a/modules/webxr/webxr_interface_js.cpp b/modules/webxr/webxr_interface_js.cpp index 535d464d6f..45c1d8ec06 100644 --- a/modules/webxr/webxr_interface_js.cpp +++ b/modules/webxr/webxr_interface_js.cpp @@ -713,7 +713,7 @@ void WebXRInterfaceJS::_update_input_source(int p_input_source_id) { if (unlikely(hand_tracker.is_null())) { hand_tracker.instantiate(); - hand_tracker->set_hand(p_input_source_id == 0 ? XRHandTracker::HAND_LEFT : XRHandTracker::HAND_RIGHT); + hand_tracker->set_tracker_hand(p_input_source_id == 0 ? XRPositionalTracker::TRACKER_HAND_LEFT : XRPositionalTracker::TRACKER_HAND_RIGHT); hand_tracker->set_tracker_name(p_input_source_id == 0 ? "/user/hand_tracker/left" : "/user/hand_tracker/right"); // These flags always apply, since WebXR doesn't give us enough insight to be more fine grained. diff --git a/platform/android/SCsub b/platform/android/SCsub index 31bc7c25b0..4d76ffb180 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -1,6 +1,8 @@ #!/usr/bin/env python +import sys import subprocess +from methods import print_warning Import("env") @@ -52,7 +54,7 @@ elif env["arch"] == "x86_32": elif env["arch"] == "x86_64": lib_arch_dir = "x86_64" else: - print("WARN: Architecture not suitable for embedding into APK; keeping .so at \\bin") + print_warning("Architecture not suitable for embedding into APK; keeping .so at \\bin") if lib_arch_dir != "": if env.dev_build: @@ -81,10 +83,21 @@ if lib_arch_dir != "": env_android.Command(out_dir + "/libc++_shared.so", stl_lib_path, Copy("$TARGET", "$SOURCE")) def generate_apk(target, source, env): + gradle_process = [] + + if sys.platform.startswith("win"): + gradle_process = [ + "cmd", + "/c", + "gradlew.bat", + ] + else: + gradle_process = ["./gradlew"] + if env["target"] != "editor" and env["dev_build"]: subprocess.run( - [ - "./gradlew", + gradle_process + + [ "generateDevTemplate", "--quiet", ], @@ -93,8 +106,8 @@ if lib_arch_dir != "": else: # Android editor with `dev_build=yes` is handled by the `generateGodotEditor` task. subprocess.run( - [ - "./gradlew", + gradle_process + + [ "generateGodotEditor" if env["target"] == "editor" else "generateGodotTemplates", "--quiet", ], diff --git a/platform/android/detect.py b/platform/android/detect.py index fea8ec3287..cbd6144182 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -2,7 +2,7 @@ import os import sys import platform import subprocess - +from methods import print_warning, print_error from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -76,7 +76,6 @@ def get_flags(): # Check if Android NDK version is installed # If not, install it. def install_ndk_if_needed(env: "SConsEnvironment"): - print("Checking for Android NDK...") sdk_root = env["ANDROID_HOME"] if not os.path.exists(get_android_ndk_root(env)): extension = ".bat" if os.name == "nt" else "" @@ -87,13 +86,11 @@ def install_ndk_if_needed(env: "SConsEnvironment"): ndk_download_args = "ndk;" + get_ndk_version() subprocess.check_call([sdkmanager, ndk_download_args]) else: - print("Cannot find " + sdkmanager) - print( - "Please ensure ANDROID_HOME is correct and cmdline-tools are installed, or install NDK version " - + get_ndk_version() - + " manually." + print_error( + f'Cannot find "{sdkmanager}". Please ensure ANDROID_HOME is correct and cmdline-tools' + f'are installed, or install NDK version "{get_ndk_version()}" manually.' ) - sys.exit() + sys.exit(255) env["ANDROID_NDK_ROOT"] = get_android_ndk_root(env) @@ -101,15 +98,15 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for Android. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) if get_min_sdk_version(env["ndk_platform"]) < get_min_target_api(): - print( - "WARNING: minimum supported Android target api is %d. Forcing target api %d." + print_warning( + "Minimum supported Android target api is %d. Forcing target api %d." % (get_min_target_api(), get_min_target_api()) ) env["ndk_platform"] = "android-" + str(get_min_target_api()) diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index c6f2f82117..9869756be1 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -326,7 +326,7 @@ void DisplayServerAndroid::window_set_drop_files_callback(const Callable &p_call } void DisplayServerAndroid::_window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred) const { - if (!p_callable.is_null()) { + if (p_callable.is_valid()) { if (p_deferred) { p_callable.call_deferred(p_arg); } else { diff --git a/platform/ios/detect.py b/platform/ios/detect.py index 0c9b7b3204..e3bac4ec5c 100644 --- a/platform/ios/detect.py +++ b/platform/ios/detect.py @@ -1,6 +1,6 @@ import os import sys -from methods import detect_darwin_sdk_path +from methods import print_error, detect_darwin_sdk_path from typing import TYPE_CHECKING @@ -60,11 +60,11 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_64", "arm64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for iOS. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) ## LTO @@ -118,7 +118,7 @@ def configure(env: "SConsEnvironment"): if env["arch"] == "x86_64": if not env["ios_simulator"]: - print("ERROR: Building for iOS with 'arch=x86_64' requires 'ios_simulator=yes'.") + print_error("Building for iOS with 'arch=x86_64' requires 'ios_simulator=yes'.") sys.exit(255) env["ENV"]["MACOSX_DEPLOYMENT_TARGET"] = "10.9" diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index cd6f855d77..62bc55dce8 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -218,7 +218,7 @@ void DisplayServerIOS::send_window_event(DisplayServer::WindowEvent p_event) con } void DisplayServerIOS::_window_callback(const Callable &p_callable, const Variant &p_arg) const { - if (!p_callable.is_null()) { + if (p_callable.is_valid()) { p_callable.call(p_arg); } } diff --git a/platform/ios/doc_classes/EditorExportPlatformIOS.xml b/platform/ios/doc_classes/EditorExportPlatformIOS.xml index 0c0ded5fea..20c1647843 100644 --- a/platform/ios/doc_classes/EditorExportPlatformIOS.xml +++ b/platform/ios/doc_classes/EditorExportPlatformIOS.xml @@ -29,6 +29,9 @@ <member name="application/code_sign_identity_release" type="String" setter="" getter=""> The "Full Name", "Common Name" or SHA-1 hash of the signing identity used for release export. </member> + <member name="application/delete_old_export_files_unconditionally" type="bool" setter="" getter=""> + If [code]true[/code], existing "project name" and "project name.xcodeproj" in the export destination directory will be unconditionally deleted during export. + </member> <member name="application/export_method_debug" type="int" setter="" getter=""> Application distribution target (debug export). </member> @@ -123,12 +126,441 @@ <member name="icons/spotlight_80x80" type="String" setter="" getter=""> Spotlight icon file on iPad and iPhone (2x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. </member> + <member name="privacy/active_keyboard_access_reasons" type="int" setter="" getter=""> + The reasons your app use active keyboard API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> <member name="privacy/camera_usage_description" type="String" setter="" getter=""> A message displayed when requesting access to the device's camera (in English). </member> <member name="privacy/camera_usage_description_localized" type="Dictionary" setter="" getter=""> A message displayed when requesting access to the device's camera (localized). </member> + <member name="privacy/collected_data/advertising_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects advertising data. + </member> + <member name="privacy/collected_data/advertising_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects advertising data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/advertising_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links advertising data to the user's identity. + </member> + <member name="privacy/collected_data/advertising_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses advertising data for tracking. + </member> + <member name="privacy/collected_data/audio_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects audio data data. + </member> + <member name="privacy/collected_data/audio_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects audio data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/audio_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links audio data data to the user's identity. + </member> + <member name="privacy/collected_data/audio_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses audio data data for tracking. + </member> + <member name="privacy/collected_data/browsing_history/collected" type="bool" setter="" getter=""> + Indicates whether your app collects browsing history. + </member> + <member name="privacy/collected_data/browsing_history/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects browsing history. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/browsing_history/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links browsing history to the user's identity. + </member> + <member name="privacy/collected_data/browsing_history/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses browsing history for tracking. + </member> + <member name="privacy/collected_data/coarse_location/collected" type="bool" setter="" getter=""> + Indicates whether your app collects coarse location data. + </member> + <member name="privacy/collected_data/coarse_location/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects coarse location data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/coarse_location/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links coarse location data to the user's identity. + </member> + <member name="privacy/collected_data/coarse_location/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses coarse location data for tracking. + </member> + <member name="privacy/collected_data/contacts/collected" type="bool" setter="" getter=""> + Indicates whether your app collects contacts. + </member> + <member name="privacy/collected_data/contacts/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects contacts. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/contacts/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links contacts to the user's identity. + </member> + <member name="privacy/collected_data/contacts/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses contacts for tracking. + </member> + <member name="privacy/collected_data/crash_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects crash data. + </member> + <member name="privacy/collected_data/crash_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects crash data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/crash_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links crash data to the user's identity. + </member> + <member name="privacy/collected_data/crash_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses crash data for tracking. + </member> + <member name="privacy/collected_data/credit_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects credit information. + </member> + <member name="privacy/collected_data/credit_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects credit information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/credit_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links credit information to the user's identity. + </member> + <member name="privacy/collected_data/credit_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses credit information for tracking. + </member> + <member name="privacy/collected_data/customer_support/collected" type="bool" setter="" getter=""> + Indicates whether your app collects customer support data. + </member> + <member name="privacy/collected_data/customer_support/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects customer support data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/customer_support/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links customer support data to the user's identity. + </member> + <member name="privacy/collected_data/customer_support/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses customer support data for tracking. + </member> + <member name="privacy/collected_data/device_id/collected" type="bool" setter="" getter=""> + Indicates whether your app collects device IDs. + </member> + <member name="privacy/collected_data/device_id/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects device IDs. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/device_id/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links device IDs to the user's identity. + </member> + <member name="privacy/collected_data/device_id/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses device IDs for tracking. + </member> + <member name="privacy/collected_data/email_address/collected" type="bool" setter="" getter=""> + Indicates whether your app collects email address. + </member> + <member name="privacy/collected_data/email_address/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects email address. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/email_address/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links email address to the user's identity. + </member> + <member name="privacy/collected_data/email_address/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses email address for tracking. + </member> + <member name="privacy/collected_data/emails_or_text_messages/collected" type="bool" setter="" getter=""> + Indicates whether your app collects emails or text messages. + </member> + <member name="privacy/collected_data/emails_or_text_messages/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects emails or text messages. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/emails_or_text_messages/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links emails or text messages to the user's identity. + </member> + <member name="privacy/collected_data/emails_or_text_messages/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses emails or text messages for tracking. + </member> + <member name="privacy/collected_data/environment_scanning/collected" type="bool" setter="" getter=""> + Indicates whether your app collects environment scanning data. + </member> + <member name="privacy/collected_data/environment_scanning/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects environment scanning data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/environment_scanning/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links environment scanning data to the user's identity. + </member> + <member name="privacy/collected_data/environment_scanning/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses environment scanning data for tracking. + </member> + <member name="privacy/collected_data/fitness/collected" type="bool" setter="" getter=""> + Indicates whether your app collects fitness and exercise data. + </member> + <member name="privacy/collected_data/fitness/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects fitness and exercise data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/fitness/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links fitness and exercise data to the user's identity. + </member> + <member name="privacy/collected_data/fitness/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses fitness and exercise data for tracking. + </member> + <member name="privacy/collected_data/gameplay_content/collected" type="bool" setter="" getter=""> + Indicates whether your app collects gameplay content. + </member> + <member name="privacy/collected_data/gameplay_content/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects gameplay content. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/gameplay_content/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links gameplay content to the user's identity. + </member> + <member name="privacy/collected_data/gameplay_content/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses gameplay content for tracking. + </member> + <member name="privacy/collected_data/hands/collected" type="bool" setter="" getter=""> + Indicates whether your app collects user's hand structure and hand movements. + </member> + <member name="privacy/collected_data/hands/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects user's hand structure and hand movements. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/hands/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links user's hand structure and hand movements to the user's identity. + </member> + <member name="privacy/collected_data/hands/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses user's hand structure and hand movements for tracking. + </member> + <member name="privacy/collected_data/head/collected" type="bool" setter="" getter=""> + Indicates whether your app collects user's head movement. + </member> + <member name="privacy/collected_data/head/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects user's head movement. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/head/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links user's head movement to the user's identity. + </member> + <member name="privacy/collected_data/head/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses user's head movement for tracking. + </member> + <member name="privacy/collected_data/health/collected" type="bool" setter="" getter=""> + Indicates whether your app collects health and medical data. + </member> + <member name="privacy/collected_data/health/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects health and medical data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/health/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links health and medical data to the user's identity. + </member> + <member name="privacy/collected_data/health/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses health and medical data for tracking. + </member> + <member name="privacy/collected_data/name/collected" type="bool" setter="" getter=""> + Indicates whether your app collects user's name. + </member> + <member name="privacy/collected_data/name/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects user's name. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/name/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links user's name to the user's identity. + </member> + <member name="privacy/collected_data/name/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses user's name for tracking. + </member> + <member name="privacy/collected_data/other_contact_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other contact information. + </member> + <member name="privacy/collected_data/other_contact_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other contact information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_contact_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other contact information to the user's identity. + </member> + <member name="privacy/collected_data/other_contact_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other contact information for tracking. + </member> + <member name="privacy/collected_data/other_data_types/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other data. + </member> + <member name="privacy/collected_data/other_data_types/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_data_types/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other data to the user's identity. + </member> + <member name="privacy/collected_data/other_data_types/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other data for tracking. + </member> + <member name="privacy/collected_data/other_diagnostic_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other diagnostic data. + </member> + <member name="privacy/collected_data/other_diagnostic_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other diagnostic data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_diagnostic_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other diagnostic data to the user's identity. + </member> + <member name="privacy/collected_data/other_diagnostic_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other diagnostic data for tracking. + </member> + <member name="privacy/collected_data/other_financial_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other financial information. + </member> + <member name="privacy/collected_data/other_financial_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other financial information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_financial_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other financial information to the user's identity. + </member> + <member name="privacy/collected_data/other_financial_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other financial information for tracking. + </member> + <member name="privacy/collected_data/other_usage_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other usage data. + </member> + <member name="privacy/collected_data/other_usage_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other usage data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_usage_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other usage data to the user's identity. + </member> + <member name="privacy/collected_data/other_usage_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other usage data for tracking. + </member> + <member name="privacy/collected_data/other_user_content/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other user generated content. + </member> + <member name="privacy/collected_data/other_user_content/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other user generated content. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_user_content/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other user generated content to the user's identity. + </member> + <member name="privacy/collected_data/other_user_content/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other user generated content for tracking. + </member> + <member name="privacy/collected_data/payment_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects payment information. + </member> + <member name="privacy/collected_data/payment_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects payment information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/payment_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links payment information to the user's identity. + </member> + <member name="privacy/collected_data/payment_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses payment information for tracking. + </member> + <member name="privacy/collected_data/performance_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects performance data. + </member> + <member name="privacy/collected_data/performance_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects performance data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/performance_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links performance data to the user's identity. + </member> + <member name="privacy/collected_data/performance_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses performance data for tracking. + </member> + <member name="privacy/collected_data/phone_number/collected" type="bool" setter="" getter=""> + Indicates whether your app collects phone number. + </member> + <member name="privacy/collected_data/phone_number/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects phone number. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/phone_number/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links phone number to the user's identity. + </member> + <member name="privacy/collected_data/phone_number/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses phone number for tracking. + </member> + <member name="privacy/collected_data/photos_or_videos/collected" type="bool" setter="" getter=""> + Indicates whether your app collects photos or videos. + </member> + <member name="privacy/collected_data/photos_or_videos/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects photos or videos. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/photos_or_videos/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links photos or videos to the user's identity. + </member> + <member name="privacy/collected_data/photos_or_videos/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses photos or videos for tracking. + </member> + <member name="privacy/collected_data/physical_address/collected" type="bool" setter="" getter=""> + Indicates whether your app collects physical address. + </member> + <member name="privacy/collected_data/physical_address/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects physical address. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/physical_address/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links physical address to the user's identity. + </member> + <member name="privacy/collected_data/physical_address/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses physical address for tracking. + </member> + <member name="privacy/collected_data/precise_location/collected" type="bool" setter="" getter=""> + Indicates whether your app collects precise location data. + </member> + <member name="privacy/collected_data/precise_location/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects precise location data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/precise_location/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links precise location data to the user's identity. + </member> + <member name="privacy/collected_data/precise_location/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses precise location data for tracking. + </member> + <member name="privacy/collected_data/product_interaction/collected" type="bool" setter="" getter=""> + Indicates whether your app collects product interaction data. + </member> + <member name="privacy/collected_data/product_interaction/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects product interaction data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/product_interaction/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links product interaction data to the user's identity. + </member> + <member name="privacy/collected_data/product_interaction/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses product interaction data for tracking. + </member> + <member name="privacy/collected_data/purchase_history/collected" type="bool" setter="" getter=""> + Indicates whether your app collects purchase history. + </member> + <member name="privacy/collected_data/purchase_history/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects purchase history. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/purchase_history/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links purchase history to the user's identity. + </member> + <member name="privacy/collected_data/purchase_history/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses purchase history for tracking. + </member> + <member name="privacy/collected_data/search_hhistory/collected" type="bool" setter="" getter=""> + Indicates whether your app collects search history. + </member> + <member name="privacy/collected_data/search_hhistory/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects search history. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/search_hhistory/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links search history to the user's identity. + </member> + <member name="privacy/collected_data/search_hhistory/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses search history for tracking. + </member> + <member name="privacy/collected_data/sensitive_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects sensitive user information. + </member> + <member name="privacy/collected_data/sensitive_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects sensitive user information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/sensitive_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links sensitive user information to the user's identity. + </member> + <member name="privacy/collected_data/sensitive_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses sensitive user information for tracking. + </member> + <member name="privacy/collected_data/user_id/collected" type="bool" setter="" getter=""> + Indicates whether your app collects user IDs. + </member> + <member name="privacy/collected_data/user_id/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects user IDs. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/user_id/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links user IDs to the user's identity. + </member> + <member name="privacy/collected_data/user_id/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses user IDs for tracking. + </member> + <member name="privacy/disk_space_access_reasons" type="int" setter="" getter=""> + The reasons your app use free disk space API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> + <member name="privacy/file_timestamp_access_reasons" type="int" setter="" getter=""> + The reasons your app use file timestamp/metadata API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> <member name="privacy/microphone_usage_description" type="String" setter="" getter=""> A message displayed when requesting access to the device's microphone (in English). </member> @@ -141,6 +573,18 @@ <member name="privacy/photolibrary_usage_description_localized" type="Dictionary" setter="" getter=""> A message displayed when requesting access to the user's photo library (localized). </member> + <member name="privacy/system_boot_time_access_reasons" type="int" setter="" getter=""> + The reasons your app use system boot time / absolute time API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> + <member name="privacy/tracking_domains" type="PackedStringArray" setter="" getter=""> + The list of internet domains your app connects to that engage in tracking. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files]Privacy manifest files[/url]. + </member> + <member name="privacy/tracking_enabled" type="bool" setter="" getter=""> + Indicates whether your app uses data for tracking. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files]Privacy manifest files[/url]. + </member> + <member name="privacy/user_defaults_access_reasons" type="int" setter="" getter=""> + The reasons your app use user defaults API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> <member name="storyboard/custom_bg_color" type="Color" setter="" getter=""> A custom background color of the storyboard launch screen. </member> diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index 33389129b7..c35c72d093 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -106,6 +106,94 @@ static const IconInfo icon_infos[] = { { PNAME("icons/notification_60x60"), "iphone", "Icon-60.png", "60", "3x", "20x20", false } }; +struct APIAccessInfo { + String prop_name; + String type_name; + Vector<String> prop_flag_value; + Vector<String> prop_flag_name; + int default_value; +}; + +static const APIAccessInfo api_info[] = { + { "file_timestamp", + "NSPrivacyAccessedAPICategoryFileTimestamp", + { "DDA9.1", "C617.1", "3B52.1" }, + { "Display to user on-device:", "Inside app or group container", "Files provided to app by user" }, + 3 }, + { "system_boot_time", + "NSPrivacyAccessedAPICategorySystemBootTime", + { "35F9.1", "8FFB.1", "3D61.1" }, + { "Measure time on-device", "Calculate absolute event timestamps", "User-initiated bug report" }, + 1 }, + { "disk_space", + "NSPrivacyAccessedAPICategoryDiskSpace", + { "E174.1", "85F4.1", "7D9E.1", "B728.1" }, + { "Write or delete file on-device", "Display to user on-device", "User-initiated bug report", "Health research app" }, + 3 }, + { "active_keyboard", + "NSPrivacyAccessedAPICategoryActiveKeyboards", + { "3EC4.1", "54BD.1" }, + { "Custom keyboard app on-device", "Customize UI on-device:2" }, + 0 }, + { "user_defaults", + "NSPrivacyAccessedAPICategoryUserDefaults", + { "1C8F.1", "AC6B.1", "CA92.1" }, + { "Access info from same App Group", "Access managed app configuration", "Access info from same app" }, + 0 } +}; + +struct DataCollectionInfo { + String prop_name; + String type_name; +}; + +static const DataCollectionInfo data_collect_type_info[] = { + { "name", "NSPrivacyCollectedDataTypeName" }, + { "email_address", "NSPrivacyCollectedDataTypeEmailAddress" }, + { "phone_number", "NSPrivacyCollectedDataTypePhoneNumber" }, + { "physical_address", "NSPrivacyCollectedDataTypePhysicalAddress" }, + { "other_contact_info", "NSPrivacyCollectedDataTypeOtherUserContactInfo" }, + { "health", "NSPrivacyCollectedDataTypeHealth" }, + { "fitness", "NSPrivacyCollectedDataTypeFitness" }, + { "payment_info", "NSPrivacyCollectedDataTypePaymentInfo" }, + { "credit_info", "NSPrivacyCollectedDataTypeCreditInfo" }, + { "other_financial_info", "NSPrivacyCollectedDataTypeOtherFinancialInfo" }, + { "precise_location", "NSPrivacyCollectedDataTypePreciseLocation" }, + { "coarse_location", "NSPrivacyCollectedDataTypeCoarseLocation" }, + { "sensitive_info", "NSPrivacyCollectedDataTypeSensitiveInfo" }, + { "contacts", "NSPrivacyCollectedDataTypeContacts" }, + { "emails_or_text_messages", "NSPrivacyCollectedDataTypeEmailsOrTextMessages" }, + { "photos_or_videos", "NSPrivacyCollectedDataTypePhotosorVideos" }, + { "audio_data", "NSPrivacyCollectedDataTypeAudioData" }, + { "gameplay_content", "NSPrivacyCollectedDataTypeGameplayContent" }, + { "customer_support", "NSPrivacyCollectedDataTypeCustomerSupport" }, + { "other_user_content", "NSPrivacyCollectedDataTypeOtherUserContent" }, + { "browsing_history", "NSPrivacyCollectedDataTypeBrowsingHistory" }, + { "search_hhistory", "NSPrivacyCollectedDataTypeSearchHistory" }, + { "user_id", "NSPrivacyCollectedDataTypeUserID" }, + { "device_id", "NSPrivacyCollectedDataTypeDeviceID" }, + { "purchase_history", "NSPrivacyCollectedDataTypePurchaseHistory" }, + { "product_interaction", "NSPrivacyCollectedDataTypeProductInteraction" }, + { "advertising_data", "NSPrivacyCollectedDataTypeAdvertisingData" }, + { "other_usage_data", "NSPrivacyCollectedDataTypeOtherUsageData" }, + { "crash_data", "NSPrivacyCollectedDataTypeCrashData" }, + { "performance_data", "NSPrivacyCollectedDataTypePerformanceData" }, + { "other_diagnostic_data", "NSPrivacyCollectedDataTypeOtherDiagnosticData" }, + { "environment_scanning", "NSPrivacyCollectedDataTypeEnvironmentScanning" }, + { "hands", "NSPrivacyCollectedDataTypeHands" }, + { "head", "NSPrivacyCollectedDataTypeHead" }, + { "other_data_types", "NSPrivacyCollectedDataTypeOtherDataTypes" }, +}; + +static const DataCollectionInfo data_collect_purpose_info[] = { + { "Analytics", "NSPrivacyCollectedDataTypePurposeAnalytics" }, + { "App Functionality", "NSPrivacyCollectedDataTypePurposeAppFunctionality" }, + { "Developer Advertising", "NSPrivacyCollectedDataTypePurposeDeveloperAdvertising" }, + { "Third-party Advertising", "NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising" }, + { "Product Personalization", "NSPrivacyCollectedDataTypePurposeProductPersonalization" }, + { "Other", "NSPrivacyCollectedDataTypePurposeOther" }, +}; + String EditorExportPlatformIOS::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const { if (p_preset) { if (p_name == "application/app_store_team_id") { @@ -119,6 +207,21 @@ String EditorExportPlatformIOS::get_export_option_warning(const EditorExportPres if (!is_package_name_valid(identifier, &pn_err)) { return TTR("Invalid Identifier:") + " " + pn_err; } + } else if (p_name == "privacy/file_timestamp_access_reasons") { + int access = p_preset->get("privacy/file_timestamp_access_reasons"); + if (access == 0) { + return TTR("At least one file timestamp access reason should be selected."); + } + } else if (p_name == "privacy/disk_space_access_reasons") { + int access = p_preset->get("privacy/disk_space_access_reasons"); + if (access == 0) { + return TTR("At least one disk space access reason should be selected."); + } + } else if (p_name == "privacy/system_boot_time_access_reasons") { + int access = p_preset->get("privacy/system_boot_time_access_reasons"); + if (access == 0) { + return TTR("At least one system boot time access reason should be selected."); + } } } return String(); @@ -140,6 +243,15 @@ bool EditorExportPlatformIOS::get_export_option_visibility(const EditorExportPre return false; } + if (p_preset == nullptr) { + return true; + } + + bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); + if (p_option.begins_with("privacy")) { + return advanced_options_enabled; + } + return true; } @@ -175,6 +287,7 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/icon_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/export_project_only"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/delete_old_export_files_unconditionally"), false)); Vector<PluginConfigIOS> found_plugins = get_plugins(); for (int i = 0; i < found_plugins.size(); i++) { @@ -220,6 +333,37 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photolibrary_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need access to the photo library"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photolibrary_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + for (uint64_t i = 0; i < sizeof(api_info) / sizeof(api_info[0]); ++i) { + String prop_name = vformat("privacy/%s_access_reasons", api_info[i].prop_name); + String hint; + for (int j = 0; j < api_info[i].prop_flag_value.size(); j++) { + if (j != 0) { + hint += ","; + } + hint += vformat("%s - %s:%d", api_info[i].prop_flag_value[j], api_info[i].prop_flag_name[j], (1 << j)); + } + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, prop_name, PROPERTY_HINT_FLAGS, hint), api_info[i].default_value)); + } + + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "privacy/tracking_enabled"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "privacy/tracking_domains"), Vector<String>())); + + { + String hint; + for (uint64_t i = 0; i < sizeof(data_collect_purpose_info) / sizeof(data_collect_purpose_info[0]); ++i) { + if (i != 0) { + hint += ","; + } + hint += vformat("%s:%d", data_collect_purpose_info[i].prop_name, (1 << i)); + } + for (uint64_t i = 0; i < sizeof(data_collect_type_info) / sizeof(data_collect_type_info[0]); ++i) { + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/collected", data_collect_type_info[i].prop_name)), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/linked_to_user", data_collect_type_info[i].prop_name)), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/used_for_tracking", data_collect_type_info[i].prop_name)), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, vformat("privacy/collected_data/%s/collection_purposes", data_collect_type_info[i].prop_name), PROPERTY_HINT_FLAGS, hint), 0)); + } + } + HashSet<String> used_names; for (uint64_t i = 0; i < sizeof(icon_infos) / sizeof(icon_infos[0]); ++i) { if (!used_names.has(icon_infos[i].preset_key)) { @@ -522,6 +666,87 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ } else if (lines[i].find("$swift_runtime_build_phase") != -1) { String value = !p_config.use_swift_runtime ? "" : "90B4C2B62680C7E90039117A /* dummy.swift */,"; strnew += lines[i].replace("$swift_runtime_build_phase", value) + "\n"; + } else if (lines[i].find("$priv_collection") != -1) { + bool section_opened = false; + for (uint64_t j = 0; j < sizeof(data_collect_type_info) / sizeof(data_collect_type_info[0]); ++j) { + bool data_collected = p_preset->get(vformat("privacy/collected_data/%s/collected", data_collect_type_info[j].prop_name)); + bool linked = p_preset->get(vformat("privacy/collected_data/%s/linked_to_user", data_collect_type_info[j].prop_name)); + bool tracking = p_preset->get(vformat("privacy/collected_data/%s/used_for_tracking", data_collect_type_info[j].prop_name)); + int purposes = p_preset->get(vformat("privacy/collected_data/%s/collection_purposes", data_collect_type_info[j].prop_name)); + if (data_collected) { + if (!section_opened) { + section_opened = true; + strnew += "\t<key>NSPrivacyCollectedDataTypes</key>\n"; + strnew += "\t<array>\n"; + } + strnew += "\t\t<dict>\n"; + strnew += "\t\t\t<key>NSPrivacyCollectedDataType</key>\n"; + strnew += vformat("\t\t\t<string>%s</string>\n", data_collect_type_info[j].type_name); + strnew += "\t\t\t\t<key>NSPrivacyCollectedDataTypeLinked</key>\n"; + if (linked) { + strnew += "\t\t\t\t<true/>\n"; + } else { + strnew += "\t\t\t\t<false/>\n"; + } + strnew += "\t\t\t\t<key>NSPrivacyCollectedDataTypeTracking</key>\n"; + if (tracking) { + strnew += "\t\t\t\t<true/>\n"; + } else { + strnew += "\t\t\t\t<false/>\n"; + } + if (purposes != 0) { + strnew += "\t\t\t\t<key>NSPrivacyCollectedDataTypePurposes</key>\n"; + strnew += "\t\t\t\t<array>\n"; + for (uint64_t k = 0; k < sizeof(data_collect_purpose_info) / sizeof(data_collect_purpose_info[0]); ++k) { + if (purposes & (1 << k)) { + strnew += vformat("\t\t\t\t\t<string>%s</string>\n", data_collect_purpose_info[k].type_name); + } + } + strnew += "\t\t\t\t</array>\n"; + } + strnew += "\t\t\t</dict>\n"; + } + } + if (section_opened) { + strnew += "\t</array>\n"; + } + } else if (lines[i].find("$priv_tracking") != -1) { + bool tracking = p_preset->get("privacy/tracking_enabled"); + strnew += "\t<key>NSPrivacyTracking</key>\n"; + if (tracking) { + strnew += "\t<true/>\n"; + } else { + strnew += "\t<false/>\n"; + } + Vector<String> tracking_domains = p_preset->get("privacy/tracking_domains"); + if (!tracking_domains.is_empty()) { + strnew += "\t<key>NSPrivacyTrackingDomains</key>\n"; + strnew += "\t<array>\n"; + for (const String &E : tracking_domains) { + strnew += "\t\t<string>" + E + "</string>\n"; + } + strnew += "\t</array>\n"; + } + } else if (lines[i].find("$priv_api_types") != -1) { + strnew += "\t<array>\n"; + for (uint64_t j = 0; j < sizeof(api_info) / sizeof(api_info[0]); ++j) { + int api_access = p_preset->get(vformat("privacy/%s_access_reasons", api_info[j].prop_name)); + if (api_access != 0) { + strnew += "\t\t<dict>\n"; + strnew += "\t\t\t<key>NSPrivacyAccessedAPITypeReasons</key>\n"; + strnew += "\t\t\t<array>\n"; + for (int k = 0; k < api_info[j].prop_flag_value.size(); k++) { + if (api_access & (1 << k)) { + strnew += vformat("\t\t\t\t<string>%s</string>\n", api_info[j].prop_flag_value[k]); + } + } + strnew += "\t\t\t</array>\n"; + strnew += "\t\t\t<key>NSPrivacyAccessedAPIType</key>\n"; + strnew += vformat("\t\t\t<string>%s</string>\n", api_info[j].type_name); + strnew += "\t\t</dict>\n"; + } + } + strnew += "\t</array>\n"; } else { strnew += lines[i] + "\n"; } @@ -1632,19 +1857,72 @@ Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPres } { + bool delete_old = p_preset->get("application/delete_old_export_files_unconditionally"); Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (da.is_valid()) { String current_dir = da->get_current_dir(); - // remove leftovers from last export so they don't interfere - // in case some files are no longer needed + // Remove leftovers from last export so they don't interfere in case some files are no longer needed. if (da->change_dir(binary_dir + ".xcodeproj") == OK) { - da->erase_contents_recursive(); + // Check directory content before deleting. + int expected_files = 0; + int total_files = 0; + if (!delete_old) { + da->list_dir_begin(); + for (String n = da->get_next(); !n.is_empty(); n = da->get_next()) { + if (!n.begins_with(".")) { // Ignore ".", ".." and hidden files. + if (da->current_is_dir()) { + if (n == "xcshareddata" || n == "project.xcworkspace") { + expected_files++; + } + } else { + if (n == "project.pbxproj") { + expected_files++; + } + } + total_files++; + } + } + da->list_dir_end(); + } + if ((total_files == 0) || (expected_files >= Math::floor(total_files * 0.8))) { + da->erase_contents_recursive(); + } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Unexpected files found in the export destination directory \"%s.xcodeproj\", delete it manually or select another destination."), binary_dir)); + return ERR_CANT_CREATE; + } } + da->change_dir(current_dir); + if (da->change_dir(binary_dir) == OK) { - da->erase_contents_recursive(); + // Check directory content before deleting. + int expected_files = 0; + int total_files = 0; + if (!delete_old) { + da->list_dir_begin(); + for (String n = da->get_next(); !n.is_empty(); n = da->get_next()) { + if (!n.begins_with(".")) { // Ignore ".", ".." and hidden files. + if (da->current_is_dir()) { + if (n == "dylibs" || n == "Images.xcassets" || n.ends_with(".lproj") || n == "godot-publish-dotnet" || n.ends_with(".xcframework") || n.ends_with(".framework")) { + expected_files++; + } + } else { + if (n == binary_name + "-Info.plist" || n == binary_name + ".entitlements" || n == "Launch Screen.storyboard" || n == "export_options.plist" || n.begins_with("dummy.") || n.ends_with(".gdip")) { + expected_files++; + } + } + total_files++; + } + } + da->list_dir_end(); + } + if ((total_files == 0) || (expected_files >= Math::floor(total_files * 0.8))) { + da->erase_contents_recursive(); + } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Unexpected files found in the export destination directory \"%s\", delete it manually or select another destination."), binary_dir)); + return ERR_CANT_CREATE; + } } - da->change_dir(current_dir); if (!da->dir_exists(binary_dir)) { @@ -1694,6 +1972,7 @@ Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPres files_to_parse.insert("godot_ios.xcodeproj/xcshareddata/xcschemes/godot_ios.xcscheme"); files_to_parse.insert("godot_ios/godot_ios.entitlements"); files_to_parse.insert("godot_ios/Launch Screen.storyboard"); + files_to_parse.insert("PrivacyInfo.xcprivacy"); IOSConfigData config_data = { pkg_name, diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 27dec73b65..afc9d25a80 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -1,7 +1,7 @@ import os import platform import sys -from methods import get_compiler_version, using_gcc +from methods import print_warning, print_error, get_compiler_version, using_gcc from platform_methods import detect_arch from typing import TYPE_CHECKING @@ -20,7 +20,7 @@ def can_build(): pkgconf_error = os.system("pkg-config --version > /dev/null") if pkgconf_error: - print("Error: pkg-config not found. Aborting.") + print_error("pkg-config not found. Aborting.") return False return True @@ -75,7 +75,7 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64", "rv64", "ppc32", "ppc64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for Linux / *BSD. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) @@ -128,7 +128,9 @@ def configure(env: "SConsEnvironment"): found_wrapper = True break if not found_wrapper: - print("Couldn't locate mold installation path. Make sure it's installed in /usr or /usr/local.") + print_error( + "Couldn't locate mold installation path. Make sure it's installed in /usr or /usr/local." + ) sys.exit(255) else: env.Append(LINKFLAGS=["-fuse-ld=mold"]) @@ -185,7 +187,7 @@ def configure(env: "SConsEnvironment"): if env["lto"] != "none": if env["lto"] == "thin": if not env["use_llvm"]: - print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") + print_error("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") sys.exit(255) env.Append(CCFLAGS=["-flto=thin"]) env.Append(LINKFLAGS=["-flto=thin"]) @@ -209,7 +211,7 @@ def configure(env: "SConsEnvironment"): if env["wayland"]: if os.system("wayland-scanner -v 2>/dev/null") != 0: - print("wayland-scanner not found. Disabling Wayland support.") + print_warning("wayland-scanner not found. Disabling Wayland support.") env["wayland"] = False if env["touch"]: @@ -227,7 +229,7 @@ def configure(env: "SConsEnvironment"): env["builtin_harfbuzz"], ] if (not all(ft_linked_deps)) and any(ft_linked_deps): # All or nothing. - print( + print_error( "These libraries should be either all builtin, or all system provided:\n" "freetype, libpng, zlib, graphite, harfbuzz.\n" "Please specify `builtin_<name>=no` for all of them, or none." @@ -318,7 +320,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config fontconfig --cflags --libs") env.Append(CPPDEFINES=["FONTCONFIG_ENABLED"]) else: - print("Warning: fontconfig development libraries not found. Disabling the system fonts support.") + print_warning("fontconfig development libraries not found. Disabling the system fonts support.") env["fontconfig"] = False else: env.Append(CPPDEFINES=["FONTCONFIG_ENABLED"]) @@ -329,7 +331,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config alsa --cflags --libs") env.Append(CPPDEFINES=["ALSA_ENABLED", "ALSAMIDI_ENABLED"]) else: - print("Warning: ALSA development libraries not found. Disabling the ALSA audio driver.") + print_warning("ALSA development libraries not found. Disabling the ALSA audio driver.") env["alsa"] = False else: env.Append(CPPDEFINES=["ALSA_ENABLED", "ALSAMIDI_ENABLED"]) @@ -340,7 +342,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config libpulse --cflags --libs") env.Append(CPPDEFINES=["PULSEAUDIO_ENABLED"]) else: - print("Warning: PulseAudio development libraries not found. Disabling the PulseAudio audio driver.") + print_warning("PulseAudio development libraries not found. Disabling the PulseAudio audio driver.") env["pulseaudio"] = False else: env.Append(CPPDEFINES=["PULSEAUDIO_ENABLED", "_REENTRANT"]) @@ -351,7 +353,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config dbus-1 --cflags --libs") env.Append(CPPDEFINES=["DBUS_ENABLED"]) else: - print("Warning: D-Bus development libraries not found. Disabling screensaver prevention.") + print_warning("D-Bus development libraries not found. Disabling screensaver prevention.") env["dbus"] = False else: env.Append(CPPDEFINES=["DBUS_ENABLED"]) @@ -362,7 +364,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config speech-dispatcher --cflags --libs") env.Append(CPPDEFINES=["SPEECHD_ENABLED"]) else: - print("Warning: speech-dispatcher development libraries not found. Disabling text to speech support.") + print_warning("speech-dispatcher development libraries not found. Disabling text to speech support.") env["speechd"] = False else: env.Append(CPPDEFINES=["SPEECHD_ENABLED"]) @@ -373,11 +375,11 @@ def configure(env: "SConsEnvironment"): env.Append(CPPDEFINES=["XKB_ENABLED"]) else: if env["wayland"]: - print("Error: libxkbcommon development libraries required by Wayland not found. Aborting.") + print_error("libxkbcommon development libraries required by Wayland not found. Aborting.") sys.exit(255) else: - print( - "Warning: libxkbcommon development libraries not found. Disabling dead key composition and key label support." + print_warning( + "libxkbcommon development libraries not found. Disabling dead key composition and key label support." ) else: env.Append(CPPDEFINES=["XKB_ENABLED"]) @@ -390,7 +392,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config libudev --cflags --libs") env.Append(CPPDEFINES=["UDEV_ENABLED"]) else: - print("Warning: libudev development libraries not found. Disabling controller hotplugging support.") + print_warning("libudev development libraries not found. Disabling controller hotplugging support.") env["udev"] = False else: env.Append(CPPDEFINES=["UDEV_ENABLED"]) @@ -416,31 +418,31 @@ def configure(env: "SConsEnvironment"): if env["x11"]: if not env["use_sowrap"]: if os.system("pkg-config --exists x11"): - print("Error: X11 libraries not found. Aborting.") + print_error("X11 libraries not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config x11 --cflags --libs") if os.system("pkg-config --exists xcursor"): - print("Error: Xcursor library not found. Aborting.") + print_error("Xcursor library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xcursor --cflags --libs") if os.system("pkg-config --exists xinerama"): - print("Error: Xinerama library not found. Aborting.") + print_error("Xinerama library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xinerama --cflags --libs") if os.system("pkg-config --exists xext"): - print("Error: Xext library not found. Aborting.") + print_error("Xext library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xext --cflags --libs") if os.system("pkg-config --exists xrandr"): - print("Error: XrandR library not found. Aborting.") + print_error("XrandR library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xrandr --cflags --libs") if os.system("pkg-config --exists xrender"): - print("Error: XRender library not found. Aborting.") + print_error("XRender library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xrender --cflags --libs") if os.system("pkg-config --exists xi"): - print("Error: Xi library not found. Aborting.") + print_error("Xi library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xi --cflags --libs") env.Append(CPPDEFINES=["X11_ENABLED"]) @@ -448,20 +450,20 @@ def configure(env: "SConsEnvironment"): if env["wayland"]: if not env["use_sowrap"]: if os.system("pkg-config --exists libdecor-0"): - print("Warning: libdecor development libraries not found. Disabling client-side decorations.") + print_warning("libdecor development libraries not found. Disabling client-side decorations.") env["libdecor"] = False else: env.ParseConfig("pkg-config libdecor-0 --cflags --libs") if os.system("pkg-config --exists wayland-client"): - print("Error: Wayland client library not found. Aborting.") + print_error("Wayland client library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config wayland-client --cflags --libs") if os.system("pkg-config --exists wayland-cursor"): - print("Error: Wayland cursor library not found. Aborting.") + print_error("Wayland cursor library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config wayland-cursor --cflags --libs") if os.system("pkg-config --exists wayland-egl"): - print("Error: Wayland EGL library not found. Aborting.") + print_error("Wayland EGL library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config wayland-egl --cflags --libs") diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index 7f9008e952..1701aa650d 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -371,28 +371,22 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re } if (strcmp(interface, zxdg_exporter_v1_interface.name) == 0) { - registry->wl_exporter = (struct zxdg_exporter_v1 *)wl_registry_bind(wl_registry, name, &zxdg_exporter_v1_interface, 1); - registry->wl_exporter_name = name; + registry->xdg_exporter = (struct zxdg_exporter_v1 *)wl_registry_bind(wl_registry, name, &zxdg_exporter_v1_interface, 1); + registry->xdg_exporter_name = name; return; } if (strcmp(interface, wl_compositor_interface.name) == 0) { - registry->wl_compositor = (struct wl_compositor *)wl_registry_bind(wl_registry, name, &wl_compositor_interface, 4); + registry->wl_compositor = (struct wl_compositor *)wl_registry_bind(wl_registry, name, &wl_compositor_interface, CLAMP((int)version, 1, 6)); registry->wl_compositor_name = name; return; } - if (strcmp(interface, wl_subcompositor_interface.name) == 0) { - registry->wl_subcompositor = (struct wl_subcompositor *)wl_registry_bind(wl_registry, name, &wl_subcompositor_interface, 1); - registry->wl_subcompositor_name = name; - return; - } - if (strcmp(interface, wl_data_device_manager_interface.name) == 0) { - registry->wl_data_device_manager = (struct wl_data_device_manager *)wl_registry_bind(wl_registry, name, &wl_data_device_manager_interface, 3); + registry->wl_data_device_manager = (struct wl_data_device_manager *)wl_registry_bind(wl_registry, name, &wl_data_device_manager_interface, CLAMP((int)version, 1, 3)); registry->wl_data_device_manager_name = name; - // This global creates some seats data. Let's do that for the ones already available. + // This global creates some seat data. Let's do that for the ones already available. for (struct wl_seat *wl_seat : registry->wl_seats) { SeatState *ss = wl_seat_get_seat_state(wl_seat); ERR_FAIL_NULL(ss); @@ -406,7 +400,7 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re } if (strcmp(interface, wl_output_interface.name) == 0) { - struct wl_output *wl_output = (struct wl_output *)wl_registry_bind(wl_registry, name, &wl_output_interface, 2); + struct wl_output *wl_output = (struct wl_output *)wl_registry_bind(wl_registry, name, &wl_output_interface, CLAMP((int)version, 1, 4)); wl_proxy_tag_godot((struct wl_proxy *)wl_output); registry->wl_outputs.push_back(wl_output); @@ -421,7 +415,7 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re } if (strcmp(interface, wl_seat_interface.name) == 0) { - struct wl_seat *wl_seat = (struct wl_seat *)wl_registry_bind(wl_registry, name, &wl_seat_interface, 5); + struct wl_seat *wl_seat = (struct wl_seat *)wl_registry_bind(wl_registry, name, &wl_seat_interface, CLAMP((int)version, 1, 9)); wl_proxy_tag_godot((struct wl_proxy *)wl_seat); SeatState *ss = memnew(SeatState); @@ -466,7 +460,7 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re } if (strcmp(interface, xdg_wm_base_interface.name) == 0) { - registry->xdg_wm_base = (struct xdg_wm_base *)wl_registry_bind(wl_registry, name, &xdg_wm_base_interface, MAX(2, MIN(6, (int)version))); + registry->xdg_wm_base = (struct xdg_wm_base *)wl_registry_bind(wl_registry, name, &xdg_wm_base_interface, CLAMP((int)version, 1, 6)); registry->xdg_wm_base_name = name; xdg_wm_base_add_listener(registry->xdg_wm_base, &xdg_wm_base_listener, nullptr); @@ -502,7 +496,7 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re if (strcmp(interface, zwp_primary_selection_device_manager_v1_interface.name) == 0) { registry->wp_primary_selection_device_manager = (struct zwp_primary_selection_device_manager_v1 *)wl_registry_bind(wl_registry, name, &zwp_primary_selection_device_manager_v1_interface, 1); - // This global creates some seats data. Let's do that for the ones already available. + // This global creates some seat data. Let's do that for the ones already available. for (struct wl_seat *wl_seat : registry->wl_seats) { SeatState *ss = wl_seat_get_seat_state(wl_seat); ERR_FAIL_NULL(ss); @@ -570,13 +564,13 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry return; } - if (name == registry->wl_exporter_name) { - if (registry->wl_exporter) { - zxdg_exporter_v1_destroy(registry->wl_exporter); - registry->wl_exporter = nullptr; + if (name == registry->xdg_exporter_name) { + if (registry->xdg_exporter) { + zxdg_exporter_v1_destroy(registry->xdg_exporter); + registry->xdg_exporter = nullptr; } - registry->wl_exporter_name = 0; + registry->xdg_exporter_name = 0; return; } @@ -592,17 +586,6 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry return; } - if (name == registry->wl_subcompositor_name) { - if (registry->wl_subcompositor) { - wl_subcompositor_destroy(registry->wl_subcompositor); - registry->wl_subcompositor = nullptr; - } - - registry->wl_subcompositor_name = 0; - - return; - } - if (name == registry->wl_data_device_manager_name) { if (registry->wl_data_device_manager) { wl_data_device_manager_destroy(registry->wl_data_device_manager); @@ -1000,6 +983,12 @@ void WaylandThread::_wl_output_on_geometry(void *data, struct wl_output *wl_outp ss->pending_data.make.parse_utf8(make); ss->pending_data.model.parse_utf8(model); + + // `wl_output::done` is a version 2 addition. We'll directly update the data + // for compatibility. + if (wl_output_get_version(wl_output) == 1) { + ss->data = ss->pending_data; + } } void WaylandThread::_wl_output_on_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { @@ -1010,8 +999,17 @@ void WaylandThread::_wl_output_on_mode(void *data, struct wl_output *wl_output, ss->pending_data.size.height = height; ss->pending_data.refresh_rate = refresh ? refresh / 1000.0f : -1; + + // `wl_output::done` is a version 2 addition. We'll directly update the data + // for compatibility. + if (wl_output_get_version(wl_output) == 1) { + ss->data = ss->pending_data; + } } +// NOTE: The following `wl_output` events are only for version 2 onwards, so we +// can assume that they're "atomic" (i.e. rely on the `wl_output::done` event). + void WaylandThread::_wl_output_on_done(void *data, struct wl_output *wl_output) { ScreenState *ss = (ScreenState *)data; ERR_FAIL_NULL(ss); @@ -1523,7 +1521,7 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point wayland_thread->push_message(msg); } - if (pd.discrete_scroll_vector - old_pd.discrete_scroll_vector != Vector2i()) { + if (pd.discrete_scroll_vector_120 - old_pd.discrete_scroll_vector_120 != Vector2i()) { // This is a discrete scroll (eg. from a scroll wheel), so we'll just emit // scroll wheel buttons. if (pd.scroll_vector.y != 0) { @@ -1596,13 +1594,13 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point if (test_button == MouseButton::WHEEL_UP || test_button == MouseButton::WHEEL_DOWN) { // If this is a discrete scroll, specify how many "clicks" it did for this // pointer frame. - mb->set_factor(abs(pd.discrete_scroll_vector.y)); + mb->set_factor(Math::abs(pd.discrete_scroll_vector_120.y / (float)120)); } if (test_button == MouseButton::WHEEL_RIGHT || test_button == MouseButton::WHEEL_LEFT) { // If this is a discrete scroll, specify how many "clicks" it did for this // pointer frame. - mb->set_factor(abs(pd.discrete_scroll_vector.x)); + mb->set_factor(fabs(pd.discrete_scroll_vector_120.x / (float)120)); } mb->set_button_mask(pd.pressed_button_mask); @@ -1661,7 +1659,7 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point // Reset the scroll vectors as we already handled them. pd.scroll_vector = Vector2(); - pd.discrete_scroll_vector = Vector2(); + pd.discrete_scroll_vector_120 = Vector2i(); // Update the data all getters read. Wayland's specification requires us to do // this, since all pointer actions are sent in individual events. @@ -1683,6 +1681,9 @@ void WaylandThread::_wl_pointer_on_axis_source(void *data, struct wl_pointer *wl void WaylandThread::_wl_pointer_on_axis_stop(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis) { } +// NOTE: This event is deprecated since version 8 and superseded by +// `wl_pointer::axis_value120`. This thus converts the data to its +// fraction-of-120 format. void WaylandThread::_wl_pointer_on_axis_discrete(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -1694,17 +1695,37 @@ void WaylandThread::_wl_pointer_on_axis_discrete(void *data, struct wl_pointer * PointerData &pd = ss->pointer_data_buffer; + // NOTE: We can allow ourselves to not accumulate this data (and thus just + // assign it) as the spec guarantees only one event per axis type. + if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) { - pd.discrete_scroll_vector.y = discrete; + pd.discrete_scroll_vector_120.y = discrete * 120; } if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) { - pd.discrete_scroll_vector.x = discrete; + pd.discrete_scroll_vector_120.x = discrete * 120; } } -// TODO: Add support to this event. +// Supersedes `wl_pointer::axis_discrete` Since version 8. void WaylandThread::_wl_pointer_on_axis_value120(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t value120) { + SeatState *ss = (SeatState *)data; + ERR_FAIL_NULL(ss); + + if (!ss->pointed_surface) { + // We're probably on a decoration or some other third-party thing. + return; + } + + PointerData &pd = ss->pointer_data_buffer; + + if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) { + pd.discrete_scroll_vector_120.y += value120; + } + + if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) { + pd.discrete_scroll_vector_120.x += value120; + } } // TODO: Add support to this event. @@ -1999,7 +2020,7 @@ void WaylandThread::_wp_relative_pointer_on_relative_motion(void *data, struct z pd.relative_motion_time = uptime_lo; } -void WaylandThread::_wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, struct wl_surface *surface, uint32_t fingers) { +void WaylandThread::_wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, struct wl_surface *surface, uint32_t fingers) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -2009,7 +2030,7 @@ void WaylandThread::_wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_po } } -void WaylandThread::_wp_pointer_gesture_pinch_on_update(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t time, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation) { +void WaylandThread::_wp_pointer_gesture_pinch_on_update(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t time, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -2068,7 +2089,7 @@ void WaylandThread::_wp_pointer_gesture_pinch_on_update(void *data, struct zwp_p } } -void WaylandThread::_wp_pointer_gesture_pinch_on_end(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, int32_t cancelled) { +void WaylandThread::_wp_pointer_gesture_pinch_on_end(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, int32_t cancelled) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -2093,7 +2114,7 @@ void WaylandThread::_wp_primary_selection_device_on_selection(void *data, struct ss->wp_primary_selection_offer = id; } -void WaylandThread::_wp_primary_selection_offer_on_offer(void *data, struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, const char *mime_type) { +void WaylandThread::_wp_primary_selection_offer_on_offer(void *data, struct zwp_primary_selection_offer_v1 *wp_primary_selection_offer_v1, const char *mime_type) { OfferState *os = (OfferState *)data; ERR_FAIL_NULL(os); @@ -2147,10 +2168,10 @@ void WaylandThread::_wp_primary_selection_source_on_cancelled(void *data, struct } } -void WaylandThread::_wp_tablet_seat_on_tablet_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_v2 *id) { +void WaylandThread::_wp_tablet_seat_on_tablet_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_v2 *id) { } -void WaylandThread::_wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_tool_v2 *id) { +void WaylandThread::_wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_tool_v2 *id) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -2163,31 +2184,31 @@ void WaylandThread::_wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_ ss->tablet_tools.push_back(id); } -void WaylandThread::_wp_tablet_seat_on_pad_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_pad_v2 *id) { +void WaylandThread::_wp_tablet_seat_on_pad_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_pad_v2 *id) { } -void WaylandThread::_wp_tablet_tool_on_type(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t tool_type) { - TabletToolState *state = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_type(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t tool_type) { + TabletToolState *state = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (state && tool_type == ZWP_TABLET_TOOL_V2_TYPE_ERASER) { state->is_eraser = true; } } -void WaylandThread::_wp_tablet_tool_on_hardware_serial(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t hardware_serial_hi, uint32_t hardware_serial_lo) { +void WaylandThread::_wp_tablet_tool_on_hardware_serial(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t hardware_serial_hi, uint32_t hardware_serial_lo) { } -void WaylandThread::_wp_tablet_tool_on_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t hardware_id_hi, uint32_t hardware_id_lo) { +void WaylandThread::_wp_tablet_tool_on_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t hardware_id_hi, uint32_t hardware_id_lo) { } -void WaylandThread::_wp_tablet_tool_on_capability(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t capability) { +void WaylandThread::_wp_tablet_tool_on_capability(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t capability) { } -void WaylandThread::_wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { +void WaylandThread::_wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { } -void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2199,7 +2220,7 @@ void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_too return; } - List<struct zwp_tablet_tool_v2 *>::Element *E = ss->tablet_tools.find(zwp_tablet_tool_v2); + List<struct zwp_tablet_tool_v2 *>::Element *E = ss->tablet_tools.find(wp_tablet_tool_v2); if (E && E->get()) { struct zwp_tablet_tool_v2 *tool = E->get(); @@ -2213,8 +2234,8 @@ void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_too } } -void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2241,8 +2262,8 @@ void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_table DEBUG_LOG_WAYLAND_THREAD("Tablet tool entered window."); } -void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2268,8 +2289,8 @@ void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tabl DEBUG_LOG_WAYLAND_THREAD("Tablet tool left window."); } -void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2286,8 +2307,8 @@ void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v td.button_time = OS::get_singleton()->get_ticks_msec(); } -void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2302,8 +2323,8 @@ void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 td.button_time = OS::get_singleton()->get_ticks_msec(); } -void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2323,8 +2344,8 @@ void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool td.motion_time = OS::get_singleton()->get_ticks_msec(); } -void WaylandThread::_wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t pressure) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t pressure) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2333,12 +2354,12 @@ void WaylandThread::_wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_to ts->data_pending.pressure = pressure; } -void WaylandThread::_wp_tablet_tool_on_distance(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t distance) { +void WaylandThread::_wp_tablet_tool_on_distance(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t distance) { // Unsupported } -void WaylandThread::_wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2350,20 +2371,20 @@ void WaylandThread::_wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v td.tilt.y = wl_fixed_to_double(tilt_y); } -void WaylandThread::_wp_tablet_tool_on_rotation(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees) { +void WaylandThread::_wp_tablet_tool_on_rotation(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t degrees) { // Unsupported. } -void WaylandThread::_wp_tablet_tool_on_slider(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, int32_t position) { +void WaylandThread::_wp_tablet_tool_on_slider(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, int32_t position) { // Unsupported. } -void WaylandThread::_wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees, int32_t clicks) { +void WaylandThread::_wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t degrees, int32_t clicks) { // TODO } -void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2398,8 +2419,8 @@ void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool } } -void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t time) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t time) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2440,7 +2461,7 @@ void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_ // According to the tablet proto spec, tilt is expressed in degrees relative // to the Z axis of the tablet, so it shouldn't go over 90 degrees either way, // I think. We'll clamp it just in case. - td.tilt = td.tilt.clamp(Vector2(-90, -90), Vector2(90, 90)); + td.tilt = td.tilt.clampf(-90, 90); mm->set_tilt(td.tilt / 90); @@ -3071,8 +3092,8 @@ void WaylandThread::window_create(DisplayServer::WindowID p_window_id, int p_wid // "loop". wl_surface_commit(ws.wl_surface); - if (registry.wl_exporter) { - ws.xdg_exported = zxdg_exporter_v1_export(registry.wl_exporter, ws.wl_surface); + if (registry.xdg_exporter) { + ws.xdg_exported = zxdg_exporter_v1_export(registry.xdg_exporter, ws.wl_surface); zxdg_exported_v1_add_listener(ws.xdg_exported, &xdg_exported_listener, &ws); } @@ -3529,9 +3550,6 @@ Error WaylandThread::init() { ERR_FAIL_NULL_V_MSG(registry.wl_shm, ERR_UNAVAILABLE, "Can't obtain the Wayland shared memory global."); ERR_FAIL_NULL_V_MSG(registry.wl_compositor, ERR_UNAVAILABLE, "Can't obtain the Wayland compositor global."); - ERR_FAIL_NULL_V_MSG(registry.wl_subcompositor, ERR_UNAVAILABLE, "Can't obtain the Wayland subcompositor global."); - ERR_FAIL_NULL_V_MSG(registry.wl_data_device_manager, ERR_UNAVAILABLE, "Can't obtain the Wayland data device manager global."); - ERR_FAIL_NULL_V_MSG(registry.wp_pointer_constraints, ERR_UNAVAILABLE, "Can't obtain the Wayland pointer constraints global."); ERR_FAIL_NULL_V_MSG(registry.xdg_wm_base, ERR_UNAVAILABLE, "Can't obtain the Wayland XDG shell global."); if (!registry.xdg_decoration_manager) { @@ -3660,7 +3678,10 @@ void WaylandThread::cursor_shape_set_custom_image(DisplayServer::CursorShape p_c munmap(cursor.buffer_data, cursor.buffer_data_size); } - cursor.buffer_data = (uint32_t *)mmap(nullptr, data_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + // NOTE: From `wl_keyboard`s of version 7 or later, the spec requires the mmap + // operation to be done with MAP_PRIVATE, as "MAP_SHARED may fail". We'll do it + // regardless of global version. + cursor.buffer_data = (uint32_t *)mmap(nullptr, data_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); if (cursor.wl_buffer) { // Clean up the old Wayland buffer. @@ -4179,18 +4200,14 @@ void WaylandThread::destroy() { xdg_wm_base_destroy(registry.xdg_wm_base); } - if (registry.wl_exporter) { - zxdg_exporter_v1_destroy(registry.wl_exporter); + if (registry.xdg_exporter) { + zxdg_exporter_v1_destroy(registry.xdg_exporter); } if (registry.wl_shm) { wl_shm_destroy(registry.wl_shm); } - if (registry.wl_subcompositor) { - wl_subcompositor_destroy(registry.wl_subcompositor); - } - if (registry.wl_compositor) { wl_compositor_destroy(registry.wl_compositor); } diff --git a/platform/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h index d49f0c9d34..d35a5b7139 100644 --- a/platform/linuxbsd/wayland/wayland_thread.h +++ b/platform/linuxbsd/wayland/wayland_thread.h @@ -133,8 +133,8 @@ public: struct xdg_wm_base *xdg_wm_base = nullptr; uint32_t xdg_wm_base_name = 0; - struct zxdg_exporter_v1 *wl_exporter = nullptr; - uint32_t wl_exporter_name = 0; + struct zxdg_exporter_v1 *xdg_exporter = nullptr; + uint32_t xdg_exporter_name = 0; // wayland-protocols globals. @@ -300,8 +300,8 @@ public: // The amount "scrolled" in pixels, in each direction. Vector2 scroll_vector; - // The amount of scroll "clicks" in each direction. - Vector2i discrete_scroll_vector; + // The amount of scroll "clicks" in each direction, in fractions of 120. + Vector2i discrete_scroll_vector_120; uint32_t pinch_scale = 1; }; @@ -579,41 +579,41 @@ private: static void _wp_relative_pointer_on_relative_motion(void *data, struct zwp_relative_pointer_v1 *wp_relative_pointer_v1, uint32_t uptime_hi, uint32_t uptime_lo, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel); - static void _wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, struct wl_surface *surface, uint32_t fingers); - static void _wp_pointer_gesture_pinch_on_update(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t time, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation); - static void _wp_pointer_gesture_pinch_on_end(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, int32_t cancelled); + static void _wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, struct wl_surface *surface, uint32_t fingers); + static void _wp_pointer_gesture_pinch_on_update(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t time, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation); + static void _wp_pointer_gesture_pinch_on_end(void *data, struct zwp_pointer_gesture_pinch_v1 *zp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, int32_t cancelled); static void _wp_primary_selection_device_on_data_offer(void *data, struct zwp_primary_selection_device_v1 *wp_primary_selection_device_v1, struct zwp_primary_selection_offer_v1 *offer); static void _wp_primary_selection_device_on_selection(void *data, struct zwp_primary_selection_device_v1 *wp_primary_selection_device_v1, struct zwp_primary_selection_offer_v1 *id); - static void _wp_primary_selection_offer_on_offer(void *data, struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, const char *mime_type); + static void _wp_primary_selection_offer_on_offer(void *data, struct zwp_primary_selection_offer_v1 *wp_primary_selection_offer_v1, const char *mime_type); static void _wp_primary_selection_source_on_send(void *data, struct zwp_primary_selection_source_v1 *wp_primary_selection_source_v1, const char *mime_type, int32_t fd); static void _wp_primary_selection_source_on_cancelled(void *data, struct zwp_primary_selection_source_v1 *wp_primary_selection_source_v1); - static void _wp_tablet_seat_on_tablet_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_v2 *id); - static void _wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_tool_v2 *id); - static void _wp_tablet_seat_on_pad_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_pad_v2 *id); - - static void _wp_tablet_tool_on_type(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t tool_type); - static void _wp_tablet_tool_on_hardware_serial(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t hardware_serial_hi, uint32_t hardware_serial_lo); - static void _wp_tablet_tool_on_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t hardware_id_hi, uint32_t hardware_id_lo); - static void _wp_tablet_tool_on_capability(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t capability); - static void _wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2); - static void _wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2); - static void _wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface); - static void _wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2); - static void _wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial); - static void _wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2); - static void _wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y); - static void _wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t pressure); - static void _wp_tablet_tool_on_distance(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t distance); - static void _wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y); - static void _wp_tablet_tool_on_rotation(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees); - static void _wp_tablet_tool_on_slider(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, int32_t position); - static void _wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees, int32_t clicks); - static void _wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state); - static void _wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t time); + static void _wp_tablet_seat_on_tablet_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_v2 *id); + static void _wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_tool_v2 *id); + static void _wp_tablet_seat_on_pad_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_pad_v2 *id); + + static void _wp_tablet_tool_on_type(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t tool_type); + static void _wp_tablet_tool_on_hardware_serial(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t hardware_serial_hi, uint32_t hardware_serial_lo); + static void _wp_tablet_tool_on_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t hardware_id_hi, uint32_t hardware_id_lo); + static void _wp_tablet_tool_on_capability(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t capability); + static void _wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2); + static void _wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2); + static void _wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface); + static void _wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2); + static void _wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial); + static void _wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2); + static void _wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y); + static void _wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t pressure); + static void _wp_tablet_tool_on_distance(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t distance); + static void _wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y); + static void _wp_tablet_tool_on_rotation(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t degrees); + static void _wp_tablet_tool_on_slider(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, int32_t position); + static void _wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t degrees, int32_t clicks); + static void _wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state); + static void _wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t time); static void _xdg_toplevel_decoration_on_configure(void *data, struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration, uint32_t mode); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 0041b4c7f3..b76cbc126f 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -2225,7 +2225,7 @@ void DisplayServerX11::window_set_size(const Size2i p_size, WindowID p_window) { ERR_FAIL_COND(!windows.has(p_window)); Size2i size = p_size; - size = size.max(Size2i(1, 1)); + size = size.maxi(1); WindowData &wd = windows[p_window]; @@ -4992,7 +4992,7 @@ void DisplayServerX11::process_events() { files.write[i] = files[i].replace("file://", "").uri_decode(); } - if (!windows[window_id].drop_files_callback.is_null()) { + if (windows[window_id].drop_files_callback.is_valid()) { windows[window_id].drop_files_callback.call(files); } diff --git a/platform/macos/detect.py b/platform/macos/detect.py index 3c8b1ebee1..a5ef29e34f 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -1,6 +1,6 @@ import os import sys -from methods import detect_darwin_sdk_path, get_compiler_version, is_vanilla_clang +from methods import print_error, detect_darwin_sdk_path, get_compiler_version, is_vanilla_clang from platform_methods import detect_arch, detect_mvk from typing import TYPE_CHECKING @@ -64,11 +64,11 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_64", "arm64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for macOS. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) ## Build type @@ -254,7 +254,7 @@ def configure(env: "SConsEnvironment"): if mvk_path != "": env.Append(LINKFLAGS=["-L" + mvk_path]) else: - print( + print_error( "MoltenVK SDK installation directory not found, use 'vulkan_sdk_path' SCons parameter to specify SDK path." ) sys.exit(255) diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index 5d38bf55ea..083e9731c9 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -192,7 +192,7 @@ private: HashMap<WindowID, WindowData> windows; struct IndicatorData { - id view; + id delegate; id item; }; @@ -431,9 +431,10 @@ public: virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref<Image> &p_icon) override; - virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) override; - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) override; virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) override; + virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) override; virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) override; virtual void delete_status_indicator(IndicatorID p_id) override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index cfe925e79b..cfa4041147 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -938,7 +938,7 @@ Error DisplayServerMacOS::dialog_show(String p_title, String p_description, Vect button_pressed = int64_t(2 + (ret - NSAlertThirdButtonReturn)); } - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { Variant ret; Callable::CallError ce; const Variant *args[1] = { &button_pressed }; @@ -1018,7 +1018,7 @@ Error DisplayServerMacOS::_file_dialog_with_options_show(const String &p_title, String url; url.parse_utf8([[[panel URL] path] UTF8String]); files.push_back(url); - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = true; Variant v_files = files; @@ -1047,7 +1047,7 @@ Error DisplayServerMacOS::_file_dialog_with_options_show(const String &p_title, } } } else { - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = false; Variant v_files = Vector<String>(); @@ -1134,7 +1134,7 @@ Error DisplayServerMacOS::_file_dialog_with_options_show(const String &p_title, url.parse_utf8([[[urls objectAtIndex:i] path] UTF8String]); files.push_back(url); } - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = true; Variant v_files = files; @@ -1163,7 +1163,7 @@ Error DisplayServerMacOS::_file_dialog_with_options_show(const String &p_title, } } } else { - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = false; Variant v_files = Vector<String>(); @@ -1222,7 +1222,7 @@ Error DisplayServerMacOS::dialog_input_text(String p_title, String p_description String ret; ret.parse_utf8([[input stringValue] UTF8String]); - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { Variant v_result = ret; Variant ret; Callable::CallError ce; @@ -2321,7 +2321,7 @@ void DisplayServerMacOS::window_set_window_buttons_offset(const Vector2i &p_offs WindowData &wd = windows[p_window]; float scale = screen_get_max_scale(); wd.wb_offset = p_offset / scale; - wd.wb_offset = wd.wb_offset.max(Vector2i(12, 12)); + wd.wb_offset = wd.wb_offset.maxi(12); if (wd.window_button_view) { [wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)]; } @@ -3151,10 +3151,11 @@ void DisplayServerMacOS::set_icon(const Ref<Image> &p_icon) { } } -DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) { NSImage *nsimg = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon->duplicate(); + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); img->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] @@ -3192,13 +3193,18 @@ DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref IndicatorData idat; - idat.item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; - idat.view = [[GodotStatusItemView alloc] init]; + NSStatusItem *item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; + idat.item = item; + idat.delegate = [[GodotStatusItemDelegate alloc] init]; + [idat.delegate setCallback:p_callback]; - [idat.view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; - [idat.view setImage:nsimg]; - [idat.view setCallback:p_callback]; - [idat.item setView:idat.view]; + item.button.image = nsimg; + item.button.imagePosition = NSImageOnly; + item.button.imageScaling = NSImageScaleProportionallyUpOrDown; + item.button.target = idat.delegate; + item.button.action = @selector(click:); + [item.button sendActionOn:(NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown)]; + item.button.toolTip = [NSString stringWithUTF8String:p_tooltip.utf8().get_data()]; IndicatorID iid = indicator_id_counter++; indicators[iid] = idat; @@ -3206,12 +3212,13 @@ DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref return iid; } -void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) { +void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) { ERR_FAIL_COND(!indicators.has(p_id)); NSImage *nsimg = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon->duplicate(); + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); img->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] @@ -3247,19 +3254,33 @@ void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<I } } - [indicators[p_id].view setImage:nsimg]; + NSStatusItem *item = indicators[p_id].item; + item.button.image = nsimg; } void DisplayServerMacOS::status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) { ERR_FAIL_COND(!indicators.has(p_id)); - [indicators[p_id].view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; + NSStatusItem *item = indicators[p_id].item; + item.button.toolTip = [NSString stringWithUTF8String:p_tooltip.utf8().get_data()]; +} + +void DisplayServerMacOS::status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) { + ERR_FAIL_COND(!indicators.has(p_id)); + + NSStatusItem *item = indicators[p_id].item; + if (p_menu_rid.is_valid() && native_menu->has_menu(p_menu_rid)) { + NSMenu *menu = native_menu->get_native_menu_handle(p_menu_rid); + item.menu = menu; + } else { + item.menu = nullptr; + } } void DisplayServerMacOS::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { ERR_FAIL_COND(!indicators.has(p_id)); - [indicators[p_id].view setCallback:p_callback]; + [indicators[p_id].delegate setCallback:p_callback]; } void DisplayServerMacOS::delete_status_indicator(IndicatorID p_id) { diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index d75def9b50..5f52d33318 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -909,7 +909,7 @@ Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_pres return OK; } -Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn) { +Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn, bool p_set_id) { int codesign_tool = p_preset->get("codesign/codesign"); switch (codesign_tool) { case 1: { // built-in ad-hoc @@ -953,6 +953,12 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre args.push_back("--code-signature-flags"); args.push_back("runtime"); + if (p_set_id) { + String app_id = p_preset->get("application/bundle_identifier"); + args.push_back("--binary-identifier"); + args.push_back(app_id); + } + args.push_back("-v"); /* provide some more feedback */ args.push_back(p_path); @@ -1012,6 +1018,12 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre args.push_back(p_preset->get("codesign/identity")); } + if (p_set_id) { + String app_id = p_preset->get("application/bundle_identifier"); + args.push_back("-i"); + args.push_back(app_id); + } + args.push_back("-v"); /* provide some more feedback */ args.push_back("-f"); @@ -1043,7 +1055,7 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre } Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, - const String &p_ent_path, bool p_should_error_on_non_code) { + const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code) { static Vector<String> extensions_to_sign; if (extensions_to_sign.is_empty()) { @@ -1070,7 +1082,8 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres } if (extensions_to_sign.find(current_file.get_extension()) > -1) { - Error code_sign_error{ _code_sign(p_preset, current_file_path, p_ent_path, false) }; + int ftype = MachO::get_filetype(current_file_path); + Error code_sign_error{ _code_sign(p_preset, current_file_path, (ftype == 2 || ftype == 5) ? p_helper_ent_path : p_ent_path, false, (ftype == 2 || ftype == 5)) }; if (code_sign_error != OK) { return code_sign_error; } @@ -1079,7 +1092,7 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres FileAccess::set_unix_permissions(current_file_path, 0755); } } else if (dir_access->current_is_dir()) { - Error code_sign_error{ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_should_error_on_non_code) }; + Error code_sign_error{ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code) }; if (code_sign_error != OK) { return code_sign_error; } @@ -1097,6 +1110,7 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, + const String &p_helper_ent_path, bool p_should_error_on_non_code_sign) { static Vector<String> extensions_to_sign; @@ -1186,10 +1200,11 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access if (err == OK && p_sign_enabled) { if (dir_access->dir_exists(p_src_path) && p_src_path.get_extension().is_empty()) { // If it is a directory, find and sign all dynamic libraries. - err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_should_error_on_non_code_sign); + err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code_sign); } else { if (extensions_to_sign.find(p_in_app_path.get_extension()) > -1) { - err = _code_sign(p_preset, p_in_app_path, p_ent_path, false); + int ftype = MachO::get_filetype(p_in_app_path); + err = _code_sign(p_preset, p_in_app_path, (ftype == 2 || ftype == 5) ? p_helper_ent_path : p_ent_path, false, (ftype == 2 || ftype == 5)); } if (dir_access->file_exists(p_in_app_path) && is_executable(p_in_app_path)) { // chmod with 0755 if the file is executable. @@ -1203,13 +1218,13 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access Error EditorExportPlatformMacOS::_export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name, Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, - const String &p_ent_path) { + const String &p_ent_path, const String &p_helper_ent_path) { Error error{ OK }; const Vector<String> &macos_plugins{ p_editor_export_plugin->get_macos_plugin_files() }; for (int i = 0; i < macos_plugins.size(); ++i) { String src_path{ ProjectSettings::get_singleton()->globalize_path(macos_plugins[i]) }; String path_in_app{ p_app_path_name + "/Contents/PlugIns/" + src_path.get_file() }; - error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, false); + error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, p_helper_ent_path, false); if (error != OK) { break; } @@ -1786,8 +1801,9 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("'rcodesign' doesn't support signing applications with embedded dynamic libraries.")); } + bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled"); String ent_path = p_preset->get("codesign/entitlements/custom_file"); - String hlp_ent_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + "_helper.entitlements"); + String hlp_ent_path = sandbox ? EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + "_helper.entitlements") : ent_path; if (sign_enabled && (ent_path.is_empty())) { ent_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + ".entitlements"); @@ -1939,7 +1955,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p err = ERR_CANT_CREATE; } - if ((err == OK) && helpers.size() > 0) { + if ((err == OK) && sandbox && (helpers.size() > 0 || shared_objects.size() > 0)) { ent_f = FileAccess::open(hlp_ent_path, FileAccess::WRITE); if (ent_f.is_valid()) { ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); @@ -1965,7 +1981,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p String hlp_path = helpers[i]; err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file()); if (err == OK && sign_enabled) { - err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false); + err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false, true); } FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755); } @@ -1977,11 +1993,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path); if (shared_objects[i].target.is_empty()) { String path_in_app = tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file(); - err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, true); + err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, hlp_ent_path, true); } else { String path_in_app = tmp_app_path_name.path_join(shared_objects[i].target); tmp_app_dir->make_dir_recursive(path_in_app); - err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, false); + err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, hlp_ent_path, false); } if (err != OK) { break; @@ -1990,7 +2006,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p Vector<Ref<EditorExportPlugin>> export_plugins{ EditorExport::get_singleton()->get_export_plugins() }; for (int i = 0; i < export_plugins.size(); ++i) { - err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path); + err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path, hlp_ent_path); if (err != OK) { break; } @@ -2010,7 +2026,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p if (ep.step(TTR("Code signing bundle"), 2)) { return ERR_SKIP; } - err = _code_sign(p_preset, tmp_app_path_name, ent_path); + err = _code_sign(p_preset, tmp_app_path_name, ent_path, true, false); } String noto_path = p_path; @@ -2028,7 +2044,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p if (ep.step(TTR("Code signing DMG"), 3)) { return ERR_SKIP; } - err = _code_sign(p_preset, p_path, ent_path, false); + err = _code_sign(p_preset, p_path, ent_path, false, false); } } else if (export_format == "pkg") { // Create a Installer. diff --git a/platform/macos/export/export_plugin.h b/platform/macos/export/export_plugin.h index 0764b63e8c..2d615abede 100644 --- a/platform/macos/export/export_plugin.h +++ b/platform/macos/export/export_plugin.h @@ -89,14 +89,14 @@ class EditorExportPlatformMacOS : public EditorExportPlatform { void _make_icon(const Ref<EditorExportPreset> &p_preset, const Ref<Image> &p_icon, Vector<uint8_t> &p_data); Error _notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path); - Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true); - Error _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_should_error_on_non_code = true); + Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true, bool p_set_id = false); + Error _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code = true); Error _copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, - bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, + bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code_sign); Error _export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name, Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, - const String &p_ent_path); + const String &p_ent_path, const String &p_helper_ent_path); Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name); Error _create_pkg(const Ref<EditorExportPreset> &p_preset, const String &p_pkg_path, const String &p_app_path_name); Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path); diff --git a/platform/macos/export/macho.cpp b/platform/macos/export/macho.cpp index c7556c1964..a829774a88 100644 --- a/platform/macos/export/macho.cpp +++ b/platform/macos/export/macho.cpp @@ -105,6 +105,26 @@ bool MachO::is_macho(const String &p_path) { return (magic == 0xcefaedfe || magic == 0xfeedface || magic == 0xcffaedfe || magic == 0xfeedfacf); } +uint32_t MachO::get_filetype(const String &p_path) { + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, vformat("MachO: Can't open file: \"%s\".", p_path)); + uint32_t magic = fa->get_32(); + MachHeader mach_header; + + // Read MachO header. + if (magic == 0xcefaedfe || magic == 0xfeedface) { + // Thin 32-bit binary. + fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader)); + } else if (magic == 0xcffaedfe || magic == 0xfeedfacf) { + // Thin 64-bit binary. + fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader)); + fa->get_32(); // Skip extra reserved field. + } else { + ERR_FAIL_V_MSG(0, vformat("MachO: File is not a valid MachO binary: \"%s\".", p_path)); + } + return mach_header.filetype; +} + bool MachO::open_file(const String &p_path) { fa = FileAccess::open(p_path, FileAccess::READ_WRITE); ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("MachO: Can't open file: \"%s\".", p_path)); diff --git a/platform/macos/export/macho.h b/platform/macos/export/macho.h index 37975f0820..a84de7de60 100644 --- a/platform/macos/export/macho.h +++ b/platform/macos/export/macho.h @@ -181,6 +181,7 @@ class MachO : public RefCounted { public: static bool is_macho(const String &p_path); + static uint32_t get_filetype(const String &p_path); bool open_file(const String &p_path); diff --git a/platform/macos/godot_content_view.mm b/platform/macos/godot_content_view.mm index 93bba84783..68a7288ad4 100644 --- a/platform/macos/godot_content_view.mm +++ b/platform/macos/godot_content_view.mm @@ -313,7 +313,7 @@ } DisplayServerMacOS::WindowData &wd = ds->get_window(window_id); - if (!wd.drop_files_callback.is_null()) { + if (wd.drop_files_callback.is_valid()) { Vector<String> files; NSPasteboard *pboard = [sender draggingPasteboard]; diff --git a/platform/macos/godot_status_item.h b/platform/macos/godot_status_item.h index 1827baa9bd..5bc790956e 100644 --- a/platform/macos/godot_status_item.h +++ b/platform/macos/godot_status_item.h @@ -37,13 +37,12 @@ #import <AppKit/AppKit.h> #import <Foundation/Foundation.h> -@interface GodotStatusItemView : NSView { - NSImage *image; +@interface GodotStatusItemDelegate : NSObject { Callable cb; } -- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index; -- (void)setImage:(NSImage *)image; +- (IBAction)click:(id)sender; + - (void)setCallback:(const Callable &)callback; @end diff --git a/platform/macos/godot_status_item.mm b/platform/macos/godot_status_item.mm index 71ed0a0f71..0990a16b2b 100644 --- a/platform/macos/godot_status_item.mm +++ b/platform/macos/godot_status_item.mm @@ -32,30 +32,32 @@ #include "display_server_macos.h" -@implementation GodotStatusItemView +@implementation GodotStatusItemDelegate - (id)init { self = [super init]; - image = nullptr; return self; } -- (void)setImage:(NSImage *)newImage { - image = newImage; - [self setNeedsDisplayInRect:self.frame]; -} - -- (void)setCallback:(const Callable &)callback { - cb = callback; -} - -- (void)drawRect:(NSRect)rect { - if (image) { - [image drawInRect:rect]; +- (IBAction)click:(id)sender { + NSEvent *current_event = [NSApp currentEvent]; + MouseButton index = MouseButton::LEFT; + if (current_event) { + if (current_event.type == NSEventTypeLeftMouseDown) { + index = MouseButton::LEFT; + } else if (current_event.type == NSEventTypeRightMouseDown) { + index = MouseButton::RIGHT; + } else if (current_event.type == NSEventTypeOtherMouseDown) { + if ((int)[current_event buttonNumber] == 2) { + index = MouseButton::MIDDLE; + } else if ((int)[current_event buttonNumber] == 3) { + index = MouseButton::MB_XBUTTON1; + } else if ((int)[current_event buttonNumber] == 4) { + index = MouseButton::MB_XBUTTON2; + } + } } -} -- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index { DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (!ds) { return; @@ -71,31 +73,8 @@ } } -- (void)mouseDown:(NSEvent *)event { - [super mouseDown:event]; - if (([event modifierFlags] & NSEventModifierFlagControl)) { - [self processMouseEvent:event index:MouseButton::RIGHT]; - } else { - [self processMouseEvent:event index:MouseButton::LEFT]; - } -} - -- (void)rightMouseDown:(NSEvent *)event { - [super rightMouseDown:event]; - - [self processMouseEvent:event index:MouseButton::RIGHT]; -} - -- (void)otherMouseDown:(NSEvent *)event { - [super otherMouseDown:event]; - - if ((int)[event buttonNumber] == 2) { - [self processMouseEvent:event index:MouseButton::MIDDLE]; - } else if ((int)[event buttonNumber] == 3) { - [self processMouseEvent:event index:MouseButton::MB_XBUTTON1]; - } else if ((int)[event buttonNumber] == 4) { - [self processMouseEvent:event index:MouseButton::MB_XBUTTON2]; - } +- (void)setCallback:(const Callable &)callback { + cb = callback; } @end diff --git a/platform/macos/godot_window_delegate.mm b/platform/macos/godot_window_delegate.mm index 2d83b46007..7749debfd6 100644 --- a/platform/macos/godot_window_delegate.mm +++ b/platform/macos/godot_window_delegate.mm @@ -268,7 +268,7 @@ ds->window_resize(window_id, wd.size.width, wd.size.height); - if (!wd.rect_changed_callback.is_null()) { + if (wd.rect_changed_callback.is_valid()) { wd.rect_changed_callback.call(Rect2i(ds->window_get_position(window_id), ds->window_get_size(window_id))); } } @@ -291,7 +291,7 @@ DisplayServerMacOS::WindowData &wd = ds->get_window(window_id); ds->release_pressed_events(); - if (!wd.rect_changed_callback.is_null()) { + if (wd.rect_changed_callback.is_valid()) { wd.rect_changed_callback.call(Rect2i(ds->window_get_position(window_id), ds->window_get_size(window_id))); } } diff --git a/platform/macos/native_menu_macos.h b/platform/macos/native_menu_macos.h index 1d9feb64a7..b5dbb8b9b0 100644 --- a/platform/macos/native_menu_macos.h +++ b/platform/macos/native_menu_macos.h @@ -85,6 +85,8 @@ public: virtual bool has_menu(const RID &p_rid) const override; virtual void free_menu(const RID &p_rid) override; + NSMenu *get_native_menu_handle(const RID &p_rid); + virtual Size2 get_size(const RID &p_rid) const override; virtual void popup(const RID &p_rid, const Vector2i &p_position) override; diff --git a/platform/macos/native_menu_macos.mm b/platform/macos/native_menu_macos.mm index 8c2dd98862..1cf13a2d69 100644 --- a/platform/macos/native_menu_macos.mm +++ b/platform/macos/native_menu_macos.mm @@ -248,6 +248,13 @@ void NativeMenuMacOS::free_menu(const RID &p_rid) { } } +NSMenu *NativeMenuMacOS::get_native_menu_handle(const RID &p_rid) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, nullptr); + + return md->menu; +} + Size2 NativeMenuMacOS::get_size(const RID &p_rid) const { const MenuData *md = menus.get_or_null(p_rid); ERR_FAIL_NULL_V(md, Size2()); diff --git a/platform/web/SCsub b/platform/web/SCsub index 3e0cc9ac4a..bc5893ab3a 100644 --- a/platform/web/SCsub +++ b/platform/web/SCsub @@ -1,5 +1,7 @@ #!/usr/bin/env python +from methods import print_error + Import("env") # The HTTP server "targets". Run with "scons p=web serve", or "scons p=web run" @@ -11,7 +13,7 @@ if "serve" in COMMAND_LINE_TARGETS or "run" in COMMAND_LINE_TARGETS: try: port = int(port) except Exception: - print("GODOT_WEB_TEST_PORT must be a valid integer") + print_error("GODOT_WEB_TEST_PORT must be a valid integer") sys.exit(255) serve(env.Dir(env.GetTemplateZipPath()).abspath, port, "run" in COMMAND_LINE_TARGETS) sys.exit(0) diff --git a/platform/web/api/web_tools_editor_plugin.h b/platform/web/api/web_tools_editor_plugin.h index ac0d5e20ec..2902f60f24 100644 --- a/platform/web/api/web_tools_editor_plugin.h +++ b/platform/web/api/web_tools_editor_plugin.h @@ -34,7 +34,7 @@ #if defined(TOOLS_ENABLED) && defined(WEB_ENABLED) #include "core/io/zip_io.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class WebToolsEditorPlugin : public EditorPlugin { GDCLASS(WebToolsEditorPlugin, EditorPlugin); diff --git a/platform/web/detect.py b/platform/web/detect.py index 2d2cc288a1..ccd884b225 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -10,7 +10,7 @@ from emscripten_helpers import ( create_template_zip, get_template_zip_path, ) -from methods import get_compiler_version +from methods import print_warning, print_error, get_compiler_version from SCons.Util import WhereIs from typing import TYPE_CHECKING @@ -85,16 +85,16 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["wasm32"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for Web. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) try: env["initial_memory"] = int(env["initial_memory"]) except Exception: - print("Initial memory must be a valid integer") + print_error("Initial memory must be a valid integer") sys.exit(255) ## Build type @@ -109,7 +109,7 @@ def configure(env: "SConsEnvironment"): env.Append(LINKFLAGS=["-s", "ASSERTIONS=1"]) if env.editor_build and env["initial_memory"] < 64: - print('Note: Forcing "initial_memory=64" as it is required for the web editor.') + print("Note: Forcing `initial_memory=64` as it is required for the web editor.") env["initial_memory"] = 64 env.Append(LINKFLAGS=["-s", "INITIAL_MEMORY=%sMB" % env["initial_memory"]]) @@ -227,7 +227,7 @@ def configure(env: "SConsEnvironment"): env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=8"]) env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"]) elif env["proxy_to_pthread"]: - print('"threads=no" support requires "proxy_to_pthread=no", disabling proxy to pthread.') + print_warning('"threads=no" support requires "proxy_to_pthread=no", disabling proxy to pthread.') env["proxy_to_pthread"] = False if env["lto"] != "none": @@ -240,11 +240,11 @@ def configure(env: "SConsEnvironment"): if env["dlink_enabled"]: if env["proxy_to_pthread"]: - print("GDExtension support requires proxy_to_pthread=no, disabling proxy to pthread.") + print_warning("GDExtension support requires proxy_to_pthread=no, disabling proxy to pthread.") env["proxy_to_pthread"] = False if cc_semver < (3, 1, 14): - print("GDExtension support requires emscripten >= 3.1.14, detected: %s.%s.%s" % cc_semver) + print_error("GDExtension support requires emscripten >= 3.1.14, detected: %s.%s.%s" % cc_semver) sys.exit(255) env.Append(CCFLAGS=["-s", "SIDE_MODULE=2"]) diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp index 06f5eb82f7..a51c161b9c 100644 --- a/platform/web/display_server_web.cpp +++ b/platform/web/display_server_web.cpp @@ -58,7 +58,7 @@ DisplayServerWeb *DisplayServerWeb::get_singleton() { // Window (canvas) bool DisplayServerWeb::check_size_force_redraw() { bool size_changed = godot_js_display_size_update() != 0; - if (size_changed && !rect_changed_callback.is_null()) { + if (size_changed && rect_changed_callback.is_valid()) { Size2i window_size = window_get_size(); Variant size = Rect2i(Point2i(), window_size); // TODO use window_get_position if implemented. rect_changed_callback.call(size); @@ -109,7 +109,7 @@ void DisplayServerWeb::_drop_files_js_callback(const Vector<String> &p_files) { 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_valid()) { return; } ds->drop_files_callback.call(p_files); @@ -129,7 +129,7 @@ void DisplayServerWeb::request_quit_callback() { void DisplayServerWeb::_request_quit_callback() { DisplayServerWeb *ds = get_singleton(); - if (ds && !ds->window_event_callback.is_null()) { + if (ds && ds->window_event_callback.is_valid()) { Variant event = int(DisplayServer::WINDOW_EVENT_CLOSE_REQUEST); ds->window_event_callback.call(event); } @@ -722,7 +722,7 @@ void DisplayServerWeb::vk_input_text_callback(const char *p_text, int p_cursor) void DisplayServerWeb::_vk_input_text_callback(const String &p_text, int p_cursor) { DisplayServerWeb *ds = DisplayServerWeb::get_singleton(); - if (!ds || ds->input_text_callback.is_null()) { + if (!ds || !ds->input_text_callback.is_valid()) { return; } // Call input_text @@ -972,7 +972,7 @@ void DisplayServerWeb::_send_window_event_callback(int p_notification) { if (godot_js_is_ime_focused() && (p_notification == DisplayServer::WINDOW_EVENT_FOCUS_IN || p_notification == DisplayServer::WINDOW_EVENT_FOCUS_OUT)) { return; } - if (!ds->window_event_callback.is_null()) { + if (ds->window_event_callback.is_valid()) { Variant event = int(p_notification); ds->window_event_callback.call(event); } diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index 41c969b5f4..d42303ad25 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -170,6 +170,7 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito replaces["$GODOT_PROJECT_NAME"] = GLOBAL_GET("application/config/name"); replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include; replaces["$GODOT_CONFIG"] = str_config; + replaces["$GODOT_SPLASH"] = p_name + ".png"; if (p_preset->get("variant/thread_support")) { replaces["$GODOT_THREADS_ENABLED"] = "true"; @@ -584,32 +585,176 @@ bool EditorExportPlatformWeb::poll_export() { } } - int prev = menu_options; - menu_options = preset.is_valid(); + HTTPServerState prev_server_state = server_state; + server_state = HTTP_SERVER_STATE_OFF; if (server->is_listening()) { - if (menu_options == 0) { + if (preset.is_null()) { server->stop(); } else { - menu_options += 1; + server_state = HTTP_SERVER_STATE_ON; } } - return menu_options != prev; + + return server_state != prev_server_state; } Ref<ImageTexture> EditorExportPlatformWeb::get_option_icon(int p_index) const { - return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index); + Ref<ImageTexture> play_icon = EditorExportPlatform::get_option_icon(p_index); + + switch (server_state) { + case HTTP_SERVER_STATE_OFF: { + switch (p_index) { + case 0: + case 1: + return play_icon; + } + } break; + + case HTTP_SERVER_STATE_ON: { + switch (p_index) { + case 0: + return play_icon; + case 1: + return restart_icon; + case 2: + return stop_icon; + } + } break; + } + + ERR_FAIL_V_MSG(nullptr, vformat(R"(EditorExportPlatformWeb option icon index "%s" is invalid.)", p_index)); } int EditorExportPlatformWeb::get_options_count() const { - return menu_options; + if (server_state == HTTP_SERVER_STATE_ON) { + return 3; + } + return 2; +} + +String EditorExportPlatformWeb::get_option_label(int p_index) const { + String run_in_browser = TTR("Run in Browser"); + String start_http_server = TTR("Start HTTP Server"); + String reexport_project = TTR("Re-export Project"); + String stop_http_server = TTR("Stop HTTP Server"); + + switch (server_state) { + case HTTP_SERVER_STATE_OFF: { + switch (p_index) { + case 0: + return run_in_browser; + case 1: + return start_http_server; + } + } break; + + case HTTP_SERVER_STATE_ON: { + switch (p_index) { + case 0: + return run_in_browser; + case 1: + return reexport_project; + case 2: + return stop_http_server; + } + } break; + } + + ERR_FAIL_V_MSG("", vformat(R"(EditorExportPlatformWeb option label index "%s" is invalid.)", p_index)); +} + +String EditorExportPlatformWeb::get_option_tooltip(int p_index) const { + String run_in_browser = TTR("Run exported HTML in the system's default browser."); + String start_http_server = TTR("Start the HTTP server."); + String reexport_project = TTR("Export project again to account for updates."); + String stop_http_server = TTR("Stop the HTTP server."); + + switch (server_state) { + case HTTP_SERVER_STATE_OFF: { + switch (p_index) { + case 0: + return run_in_browser; + case 1: + return start_http_server; + } + } break; + + case HTTP_SERVER_STATE_ON: { + switch (p_index) { + case 0: + return run_in_browser; + case 1: + return reexport_project; + case 2: + return stop_http_server; + } + } break; + } + + ERR_FAIL_V_MSG("", vformat(R"(EditorExportPlatformWeb option tooltip index "%s" is invalid.)", p_index)); } Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) { - if (p_option == 1) { - server->stop(); - return OK; + const uint16_t bind_port = EDITOR_GET("export/web/http_port"); + // Resolve host if needed. + const String bind_host = EDITOR_GET("export/web/http_host"); + const bool use_tls = EDITOR_GET("export/web/use_tls"); + + switch (server_state) { + case HTTP_SERVER_STATE_OFF: { + switch (p_option) { + // Run in Browser. + case 0: { + Error err = _export_project(p_preset, p_debug_flags); + if (err != OK) { + return err; + } + err = _start_server(bind_host, bind_port, use_tls); + if (err != OK) { + return err; + } + return _launch_browser(bind_host, bind_port, use_tls); + } break; + + // Start HTTP Server. + case 1: { + Error err = _export_project(p_preset, p_debug_flags); + if (err != OK) { + return err; + } + return _start_server(bind_host, bind_port, use_tls); + } break; + } + } break; + + case HTTP_SERVER_STATE_ON: { + switch (p_option) { + // Run in Browser. + case 0: { + Error err = _export_project(p_preset, p_debug_flags); + if (err != OK) { + return err; + } + return _launch_browser(bind_host, bind_port, use_tls); + } break; + + // Re-export Project. + case 1: { + return _export_project(p_preset, p_debug_flags); + } break; + + // Stop HTTP Server. + case 2: { + return _stop_server(); + } break; + } + } break; } + ERR_FAIL_V_MSG(ERR_INVALID_PARAMETER, vformat(R"(Trying to run EditorExportPlatformWeb, but option "%s" isn't known.)", p_option)); +} + +Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags) { const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("web"); Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (!da->dir_exists(dest)) { @@ -636,35 +781,40 @@ Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int DirAccess::remove_file_or_error(basepath + ".wasm"); DirAccess::remove_file_or_error(basepath + ".icon.png"); DirAccess::remove_file_or_error(basepath + ".apple-touch-icon.png"); - return err; } + return err; +} - const uint16_t bind_port = EDITOR_GET("export/web/http_port"); - // Resolve host if needed. - const String bind_host = EDITOR_GET("export/web/http_host"); +Error EditorExportPlatformWeb::_launch_browser(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) { + OS::get_singleton()->shell_open(String((p_use_tls ? "https://" : "http://") + p_bind_host + ":" + itos(p_bind_port) + "/tmp_js_export.html")); + // FIXME: Find out how to clean up export files after running the successfully + // exported game. Might not be trivial. + return OK; +} + +Error EditorExportPlatformWeb::_start_server(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) { IPAddress bind_ip; - if (bind_host.is_valid_ip_address()) { - bind_ip = bind_host; + if (p_bind_host.is_valid_ip_address()) { + bind_ip = p_bind_host; } else { - bind_ip = IP::get_singleton()->resolve_hostname(bind_host); + bind_ip = IP::get_singleton()->resolve_hostname(p_bind_host); } - ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + bind_host + "'. Try using '127.0.0.1'."); + ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + p_bind_host + "'. Try using '127.0.0.1'."); - const bool use_tls = EDITOR_GET("export/web/use_tls"); const String tls_key = EDITOR_GET("export/web/tls_key"); const String tls_cert = EDITOR_GET("export/web/tls_certificate"); // Restart server. server->stop(); - err = server->listen(bind_port, bind_ip, use_tls, tls_key, tls_cert); + Error err = server->listen(p_bind_port, bind_ip, p_use_tls, tls_key, tls_cert); if (err != OK) { add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err)); - return err; } + return err; +} - OS::get_singleton()->shell_open(String((use_tls ? "https://" : "http://") + bind_host + ":" + itos(bind_port) + "/tmp_js_export.html")); - // FIXME: Find out how to clean up export files after running the successfully - // exported game. Might not be trivial. +Error EditorExportPlatformWeb::_stop_server() { + server->stop(); return OK; } @@ -690,8 +840,10 @@ EditorExportPlatformWeb::EditorExportPlatformWeb() { Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); if (theme.is_valid()) { stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons)); + restart_icon = theme->get_icon(SNAME("Reload"), EditorStringName(EditorIcons)); } else { stop_icon.instantiate(); + restart_icon.instantiate(); } } } diff --git a/platform/web/export/export_plugin.h b/platform/web/export/export_plugin.h index 952d03cdb4..9d3a1a7861 100644 --- a/platform/web/export/export_plugin.h +++ b/platform/web/export/export_plugin.h @@ -46,10 +46,16 @@ class EditorExportPlatformWeb : public EditorExportPlatform { GDCLASS(EditorExportPlatformWeb, EditorExportPlatform); + enum HTTPServerState { + HTTP_SERVER_STATE_OFF, + HTTP_SERVER_STATE_ON, + }; + Ref<ImageTexture> logo; Ref<ImageTexture> run_icon; Ref<ImageTexture> stop_icon; - int menu_options = 0; + Ref<ImageTexture> restart_icon; + HTTPServerState server_state = HTTP_SERVER_STATE_OFF; Ref<EditorHTTPServer> server; @@ -96,6 +102,11 @@ class EditorExportPlatformWeb : public EditorExportPlatform { Error _build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects); Error _write_or_error(const uint8_t *p_content, int p_len, String p_path); + Error _export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags); + Error _launch_browser(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls); + Error _start_server(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls); + Error _stop_server(); + public: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; @@ -112,8 +123,8 @@ public: virtual bool poll_export() override; virtual int get_options_count() const override; - virtual String get_option_label(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); } - virtual String get_option_tooltip(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); } + virtual String get_option_label(int p_index) const override; + virtual String get_option_tooltip(int p_index) const override; virtual Ref<ImageTexture> get_option_icon(int p_index) const override; virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) override; virtual Ref<Texture2D> get_run_icon() const override; diff --git a/platform/web/javascript_bridge_singleton.cpp b/platform/web/javascript_bridge_singleton.cpp index d72ad8331b..a2c83d2f2b 100644 --- a/platform/web/javascript_bridge_singleton.cpp +++ b/platform/web/javascript_bridge_singleton.cpp @@ -248,7 +248,7 @@ Variant JavaScriptObjectImpl::callp(const StringName &p_method, const Variant ** void JavaScriptObjectImpl::callback(void *p_ref, int p_args_id, int p_argc) { const JavaScriptObjectImpl *obj = (JavaScriptObjectImpl *)p_ref; - ERR_FAIL_COND_MSG(obj->_callable.is_null(), "JavaScript callback failed."); + ERR_FAIL_COND_MSG(!obj->_callable.is_valid(), "JavaScript callback failed."); Vector<const Variant *> argp; Array arg_arr; diff --git a/platform/web/js/engine/features.js b/platform/web/js/engine/features.js index 81bc82f3c6..263ea6ac88 100644 --- a/platform/web/js/engine/features.js +++ b/platform/web/js/engine/features.js @@ -72,8 +72,7 @@ const Features = { // eslint-disable-line no-unused-vars * * @returns {Array<string>} A list of human-readable missing features. * @function Engine.getMissingFeatures - * @typedef {{ threads: boolean }} SupportedFeatures - * @param {SupportedFeatures} supportedFeatures + * @param {{threads: (boolean|undefined)}} supportedFeatures */ getMissingFeatures: function (supportedFeatures = {}) { const { diff --git a/platform/windows/SCsub b/platform/windows/SCsub index 159a273e70..435c501956 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -10,7 +10,6 @@ sources = [] common_win = [ "godot_windows.cpp", - "crash_handler_windows.cpp", "os_windows.cpp", "display_server_windows.cpp", "key_mapping_windows.cpp", @@ -25,6 +24,11 @@ common_win = [ "rendering_context_driver_vulkan_windows.cpp", ] +if env.msvc: + common_win += ["crash_handler_windows_seh.cpp"] +else: + common_win += ["crash_handler_windows_signal.cpp"] + common_win_wrap = [ "console_wrapper_windows.cpp", ] diff --git a/platform/windows/console_wrapper_windows.cpp b/platform/windows/console_wrapper_windows.cpp index de751580b7..133711a9ea 100644 --- a/platform/windows/console_wrapper_windows.cpp +++ b/platform/windows/console_wrapper_windows.cpp @@ -136,6 +136,10 @@ int main(int argc, char *argv[]) { STARTUPINFOW si; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); WCHAR new_command_line[32767]; _snwprintf_s(new_command_line, 32767, _TRUNCATE, L"%ls %ls", exe_name, PathGetArgsW(GetCommandLineW())); diff --git a/platform/windows/crash_handler_windows.h b/platform/windows/crash_handler_windows.h index 3871210977..a0a0b610d0 100644 --- a/platform/windows/crash_handler_windows.h +++ b/platform/windows/crash_handler_windows.h @@ -35,12 +35,15 @@ #include <windows.h> // Crash handler exception only enabled with MSVC -#if defined(DEBUG_ENABLED) && defined(_MSC_VER) +#if defined(DEBUG_ENABLED) #define CRASH_HANDLER_EXCEPTION 1 +#ifdef _MSC_VER extern DWORD CrashHandlerException(EXCEPTION_POINTERS *ep); #endif +#endif + class CrashHandler { bool disabled; diff --git a/platform/windows/crash_handler_windows.cpp b/platform/windows/crash_handler_windows_seh.cpp index 133d36aa0d..2abe285d31 100644 --- a/platform/windows/crash_handler_windows.cpp +++ b/platform/windows/crash_handler_windows_seh.cpp @@ -1,5 +1,5 @@ /**************************************************************************/ -/* crash_handler_windows.cpp */ +/* crash_handler_windows_seh.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ diff --git a/platform/windows/crash_handler_windows_signal.cpp b/platform/windows/crash_handler_windows_signal.cpp new file mode 100644 index 0000000000..e11a60bdc7 --- /dev/null +++ b/platform/windows/crash_handler_windows_signal.cpp @@ -0,0 +1,205 @@ +/**************************************************************************/ +/* crash_handler_windows_signal.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "crash_handler_windows.h" + +#include "core/config/project_settings.h" +#include "core/os/os.h" +#include "core/string/print_string.h" +#include "core/version.h" +#include "main/main.h" + +#ifdef CRASH_HANDLER_EXCEPTION + +#include <cxxabi.h> +#include <signal.h> +#include <algorithm> +#include <iterator> +#include <string> +#include <vector> + +#include <psapi.h> + +#include "thirdparty/libbacktrace/backtrace.h" + +struct CrashHandlerData { + int64_t index = 0; + backtrace_state *state = nullptr; + int64_t offset = 0; +}; + +int symbol_callback(void *data, uintptr_t pc, const char *filename, int lineno, const char *function) { + CrashHandlerData *ch_data = reinterpret_cast<CrashHandlerData *>(data); + if (!function) { + return 0; + } + + char fname[1024]; + snprintf(fname, 1024, "%s", function); + + if (function[0] == '_') { + int status; + char *demangled = abi::__cxa_demangle(function, nullptr, nullptr, &status); + + if (status == 0 && demangled) { + snprintf(fname, 1024, "%s", demangled); + } + + if (demangled) { + free(demangled); + } + } + + print_error(vformat("[%d] %s (%s:%d)", ch_data->index++, String::utf8(fname), String::utf8(filename), lineno)); + return 0; +} + +void error_callback(void *data, const char *msg, int errnum) { + CrashHandlerData *ch_data = reinterpret_cast<CrashHandlerData *>(data); + if (ch_data->index == 0) { + print_error(vformat("Error(%d): %s", errnum, String::utf8(msg))); + } else { + print_error(vformat("[%d] error(%d): %s", ch_data->index++, errnum, String::utf8(msg))); + } +} + +int trace_callback(void *data, uintptr_t pc) { + CrashHandlerData *ch_data = reinterpret_cast<CrashHandlerData *>(data); + backtrace_pcinfo(ch_data->state, pc - ch_data->offset, &symbol_callback, &error_callback, data); + return 0; +} + +int64_t get_image_base(const String &p_path) { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + if (f.is_null()) { + return 0; + } + { + f->seek(0x3c); + uint32_t pe_pos = f->get_32(); + + f->seek(pe_pos); + uint32_t magic = f->get_32(); + if (magic != 0x00004550) { + return 0; + } + } + int64_t opt_header_pos = f->get_position() + 0x14; + f->seek(opt_header_pos); + + uint16_t opt_header_magic = f->get_16(); + if (opt_header_magic == 0x10B) { + f->seek(opt_header_pos + 0x1C); + return f->get_32(); + } else if (opt_header_magic == 0x20B) { + f->seek(opt_header_pos + 0x18); + return f->get_64(); + } else { + return 0; + } +} + +extern void CrashHandlerException(int signal) { + CrashHandlerData data; + + if (OS::get_singleton() == nullptr || OS::get_singleton()->is_disable_crash_handler() || IsDebuggerPresent()) { + return; + } + + String msg; + const ProjectSettings *proj_settings = ProjectSettings::get_singleton(); + if (proj_settings) { + msg = proj_settings->get("debug/settings/crash_handler/message"); + } + + // Tell MainLoop about the crash. This can be handled by users too in Node. + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); + } + + print_error("\n================================================================"); + print_error(vformat("%s: Program crashed with signal %d", __FUNCTION__, signal)); + + // Print the engine version just before, so that people are reminded to include the version in backtrace reports. + if (String(VERSION_HASH).is_empty()) { + print_error(vformat("Engine version: %s", VERSION_FULL_NAME)); + } else { + print_error(vformat("Engine version: %s (%s)", VERSION_FULL_NAME, VERSION_HASH)); + } + print_error(vformat("Dumping the backtrace. %s", msg)); + + String _execpath = OS::get_singleton()->get_executable_path(); + + // Load process and image info to determine ASLR addresses offset. + MODULEINFO mi; + GetModuleInformation(GetCurrentProcess(), GetModuleHandle(NULL), &mi, sizeof(mi)); + int64_t image_mem_base = reinterpret_cast<int64_t>(mi.lpBaseOfDll); + int64_t image_file_base = get_image_base(_execpath); + data.offset = image_mem_base - image_file_base; + + data.state = backtrace_create_state(_execpath.utf8().get_data(), 0, &error_callback, reinterpret_cast<void *>(&data)); + if (data.state != nullptr) { + data.index = 1; + backtrace_simple(data.state, 1, &trace_callback, &error_callback, reinterpret_cast<void *>(&data)); + } + + print_error("-- END OF BACKTRACE --"); + print_error("================================================================"); +} +#endif + +CrashHandler::CrashHandler() { + disabled = false; +} + +CrashHandler::~CrashHandler() { +} + +void CrashHandler::disable() { + if (disabled) { + return; + } + +#if defined(CRASH_HANDLER_EXCEPTION) + signal(SIGSEGV, nullptr); + signal(SIGFPE, nullptr); + signal(SIGILL, nullptr); +#endif + + disabled = true; +} + +void CrashHandler::initialize() { +#if defined(CRASH_HANDLER_EXCEPTION) + signal(SIGSEGV, CrashHandlerException); + signal(SIGFPE, CrashHandlerException); + signal(SIGILL, CrashHandlerException); +#endif +} diff --git a/platform/windows/detect.py b/platform/windows/detect.py index f34d479345..93eb34001e 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -2,6 +2,7 @@ import methods import os import subprocess import sys +from methods import print_warning, print_error from platform_methods import detect_arch from typing import TYPE_CHECKING @@ -293,16 +294,14 @@ def setup_msvc_manual(env: "SConsEnvironment"): env_arch = detect_build_env_arch() if env["arch"] != env_arch: - print( - """ - Arch argument (%s) is not matching Native/Cross Compile Tools Prompt/Developer Console (or Visual Studio settings) that is being used to run SCons (%s). - Run SCons again without arch argument (example: scons p=windows) and SCons will attempt to detect what MSVC compiler will be executed and inform you. - """ + print_error( + "Arch argument (%s) is not matching Native/Cross Compile Tools Prompt/Developer Console (or Visual Studio settings) that is being used to run SCons (%s).\n" + "Run SCons again without arch argument (example: scons p=windows) and SCons will attempt to detect what MSVC compiler will be executed and inform you." % (env["arch"], env_arch) ) - sys.exit(200) + sys.exit(255) - print("Found MSVC, arch %s" % (env_arch)) + print("Using VCVARS-determined MSVC, arch %s" % (env_arch)) def setup_msvc_auto(env: "SConsEnvironment"): @@ -338,7 +337,7 @@ def setup_msvc_auto(env: "SConsEnvironment"): env.Tool("mssdk") # we want the MS SDK # Note: actual compiler version can be found in env['MSVC_VERSION'], e.g. "14.1" for VS2015 - print("Found MSVC version %s, arch %s" % (env["MSVC_VERSION"], env["arch"])) + print("Using SCons-detected MSVC version %s, arch %s" % (env["MSVC_VERSION"], env["arch"])) def setup_mingw(env: "SConsEnvironment"): @@ -346,32 +345,24 @@ def setup_mingw(env: "SConsEnvironment"): env_arch = detect_build_env_arch() if os.getenv("MSYSTEM") == "MSYS": - print( - """ - Running from base MSYS2 console/environment, use target specific environment instead (e.g., mingw32, mingw64, clang32, clang64). - """ + print_error( + "Running from base MSYS2 console/environment, use target specific environment instead (e.g., mingw32, mingw64, clang32, clang64)." ) - sys.exit(201) + sys.exit(255) if env_arch != "" and env["arch"] != env_arch: - print( - """ - Arch argument (%s) is not matching MSYS2 console/environment that is being used to run SCons (%s). - Run SCons again without arch argument (example: scons p=windows) and SCons will attempt to detect what MSYS2 compiler will be executed and inform you. - """ + print_error( + "Arch argument (%s) is not matching MSYS2 console/environment that is being used to run SCons (%s).\n" + "Run SCons again without arch argument (example: scons p=windows) and SCons will attempt to detect what MSYS2 compiler will be executed and inform you." % (env["arch"], env_arch) ) - sys.exit(202) + sys.exit(255) if not try_cmd("gcc --version", env["mingw_prefix"], env["arch"]) and not try_cmd( "clang --version", env["mingw_prefix"], env["arch"] ): - print( - """ - No valid compilers found, use MINGW_PREFIX environment variable to set MinGW path. - """ - ) - sys.exit(202) + print_error("No valid compilers found, use MINGW_PREFIX environment variable to set MinGW path.") + sys.exit(255) print("Using MinGW, arch %s" % (env["arch"])) @@ -454,10 +445,10 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if os.getenv("WindowsSdkDir") is not None: env.Prepend(CPPPATH=[str(os.getenv("WindowsSdkDir")) + "/Include"]) else: - print("Missing environment variable: WindowsSdkDir") + print_warning("Missing environment variable: WindowsSdkDir") if int(env["target_win_version"], 16) < 0x0601: - print("`target_win_version` should be 0x0601 or higher (Windows 7).") + print_error("`target_win_version` should be 0x0601 or higher (Windows 7).") sys.exit(255) env.AppendUnique( @@ -515,10 +506,10 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if env["d3d12"]: # Check whether we have d3d12 dependencies installed. if not os.path.exists(env["mesa_libs"]): - print("The Direct3D 12 rendering driver requires dependencies to be installed.") - print(r"You can install them by running `python misc\scripts\install_d3d12_sdk_windows.py`.") - print("See the documentation for more information:") - print( + print_error( + "The Direct3D 12 rendering driver requires dependencies to be installed.\n" + "You can install them by running `python misc\\scripts\\install_d3d12_sdk_windows.py`.\n" + "See the documentation for more information:\n\t" "https://docs.godotengine.org/en/latest/contributing/development/compiling/compiling_for_windows.html" ) sys.exit(255) @@ -557,13 +548,16 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): LIBS += ["dxgi", "d3d9", "d3d11"] env.Prepend(CPPPATH=["#thirdparty/angle/include"]) + if env["target"] in ["editor", "template_debug"]: + LIBS += ["psapi", "dbghelp"] + env.Append(LINKFLAGS=[p + env["LIBSUFFIX"] for p in LIBS]) if vcvars_msvc_config: if os.getenv("WindowsSdkDir") is not None: env.Append(LIBPATH=[str(os.getenv("WindowsSdkDir")) + "/Lib"]) else: - print("Missing environment variable: WindowsSdkDir") + print_warning("Missing environment variable: WindowsSdkDir") ## LTO @@ -572,7 +566,7 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if env["lto"] != "none": if env["lto"] == "thin": - print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") + print_error("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") sys.exit(255) env.AppendUnique(CCFLAGS=["/GL"]) env.AppendUnique(ARFLAGS=["/LTCG"]) @@ -690,7 +684,7 @@ def configure_mingw(env: "SConsEnvironment"): ## Compile flags if int(env["target_win_version"], 16) < 0x0601: - print("`target_win_version` should be 0x0601 or higher (Windows 7).") + print_error("`target_win_version` should be 0x0601 or higher (Windows 7).") sys.exit(255) if not env["use_llvm"]: @@ -744,10 +738,10 @@ def configure_mingw(env: "SConsEnvironment"): if env["d3d12"]: # Check whether we have d3d12 dependencies installed. if not os.path.exists(env["mesa_libs"]): - print("The Direct3D 12 rendering driver requires dependencies to be installed.") - print(r"You can install them by running `python misc\scripts\install_d3d12_sdk_windows.py`.") - print("See the documentation for more information:") - print( + print_error( + "The Direct3D 12 rendering driver requires dependencies to be installed.\n" + "You can install them by running `python misc\\scripts\\install_d3d12_sdk_windows.py`.\n" + "See the documentation for more information:\n\t" "https://docs.godotengine.org/en/latest/contributing/development/compiling/compiling_for_windows.html" ) sys.exit(255) @@ -794,11 +788,11 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for Windows. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) # At this point the env has been set up with basic tools/compilers. env.Prepend(CPPPATH=["#platform/windows"]) diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index b6b713687f..dbba9b4308 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -545,7 +545,7 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title result->Release(); } } - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { if (p_options_in_cb) { Variant v_result = true; Variant v_files = file_names; @@ -574,7 +574,7 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title } } } else { - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { if (p_options_in_cb) { Variant v_result = false; Variant v_files = Vector<String>(); @@ -2556,7 +2556,7 @@ Error DisplayServerWindows::dialog_show(String p_title, String p_description, Ve int button_pressed; if (task_dialog_indirect && SUCCEEDED(task_dialog_indirect(&config, &button_pressed, nullptr, nullptr))) { - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { Variant button = button_pressed; const Variant *args[1] = { &button }; Variant ret; @@ -3171,14 +3171,12 @@ void DisplayServerWindows::set_icon(const Ref<Image> &p_icon) { } } -DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) { HICON hicon = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon; - if (img != icon) { - img = img->duplicate(); - img->convert(Image::FORMAT_RGBA8); - } + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); + img->convert(Image::FORMAT_RGBA8); int w = img->get_width(); int h = img->get_height(); @@ -3241,16 +3239,14 @@ DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const R return iid; } -void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) { +void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) { ERR_FAIL_COND(!indicators.has(p_id)); HICON hicon = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon; - if (img != icon) { - img = img->duplicate(); - img->convert(Image::FORMAT_RGBA8); - } + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); + img->convert(Image::FORMAT_RGBA8); int w = img->get_width(); int h = img->get_height(); @@ -3317,6 +3313,12 @@ void DisplayServerWindows::status_indicator_set_tooltip(IndicatorID p_id, const Shell_NotifyIconW(NIM_MODIFY, &ndat); } +void DisplayServerWindows::status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) { + ERR_FAIL_COND(!indicators.has(p_id)); + + indicators[p_id].menu_rid = p_menu_rid; +} + void DisplayServerWindows::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { ERR_FAIL_COND(!indicators.has(p_id)); @@ -3838,7 +3840,19 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mb = MouseButton::MB_XBUTTON1; } if (indicators.has(iid)) { - if (indicators[iid].callback.is_valid()) { + if (lParam == WM_RBUTTONDOWN && indicators[iid].menu_rid.is_valid() && native_menu->has_menu(indicators[iid].menu_rid)) { + NOTIFYICONIDENTIFIER nid; + ZeroMemory(&nid, sizeof(NOTIFYICONIDENTIFIER)); + nid.cbSize = sizeof(NOTIFYICONIDENTIFIER); + nid.hWnd = windows[MAIN_WINDOW_ID].hWnd; + nid.uID = iid; + nid.guidItem = GUID_NULL; + + RECT rect; + if (Shell_NotifyIconGetRect(&nid, &rect) == S_OK) { + native_menu->popup(indicators[iid].menu_rid, Vector2i((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2)); + } + } else if (indicators[iid].callback.is_valid()) { Variant v_button = mb; Variant v_pos = mouse_get_position(); Variant *v_args[2] = { &v_button, &v_pos }; @@ -4591,7 +4605,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } if (rect_changed) { - if (!window.rect_changed_callback.is_null()) { + if (window.rect_changed_callback.is_valid()) { window.rect_changed_callback.call(Rect2i(window.last_pos.x, window.last_pos.y, window.width, window.height)); } @@ -4803,7 +4817,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA files.push_back(file); } - if (files.size() && !windows[window_id].drop_files_callback.is_null()) { + if (files.size() && windows[window_id].drop_files_callback.is_valid()) { windows[window_id].drop_files_callback.call(files); } } break; diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 2fe1b0733d..12350d6b34 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -464,6 +464,7 @@ class DisplayServerWindows : public DisplayServer { WNDPROC user_proc = nullptr; struct IndicatorData { + RID menu_rid; Callable callback; }; @@ -684,9 +685,10 @@ public: virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref<Image> &p_icon) override; - virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) override; - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) override; virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) override; + virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_rid) override; virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) override; virtual void delete_status_indicator(IndicatorID p_id) override; diff --git a/platform/windows/godot_windows.cpp b/platform/windows/godot_windows.cpp index 5f41b4e568..486c3120fc 100644 --- a/platform/windows/godot_windows.cpp +++ b/platform/windows/godot_windows.cpp @@ -215,7 +215,7 @@ int main(int argc, char **argv) { // _argc and _argv are ignored // we are going to use the WideChar version of them instead -#ifdef CRASH_HANDLER_EXCEPTION +#if defined(CRASH_HANDLER_EXCEPTION) && defined(_MSC_VER) __try { return _main(); } __except (CrashHandlerException(GetExceptionInformation())) { diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index abed93d414..157702655e 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -115,7 +115,24 @@ void RedirectStream(const char *p_file_name, const char *p_mode, FILE *p_cpp_str } void RedirectIOToConsole() { + // Save current handles. + HANDLE h_stdin = GetStdHandle(STD_INPUT_HANDLE); + HANDLE h_stdout = GetStdHandle(STD_OUTPUT_HANDLE); + HANDLE h_stderr = GetStdHandle(STD_ERROR_HANDLE); + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + // Restore redirection (Note: if not redirected it's NULL handles not INVALID_HANDLE_VALUE). + if (h_stdin != 0) { + SetStdHandle(STD_INPUT_HANDLE, h_stdin); + } + if (h_stdout != 0) { + SetStdHandle(STD_OUTPUT_HANDLE, h_stdout); + } + if (h_stderr != 0) { + SetStdHandle(STD_ERROR_HANDLE, h_stderr); + } + + // Update file handles. RedirectStream("CONIN$", "r", stdin, STD_INPUT_HANDLE); RedirectStream("CONOUT$", "w", stdout, STD_OUTPUT_HANDLE); RedirectStream("CONOUT$", "w", stderr, STD_ERROR_HANDLE); @@ -173,10 +190,6 @@ void OS_Windows::initialize() { add_error_handler(&error_handlers); #endif -#ifndef WINDOWS_SUBSYSTEM_CONSOLE - RedirectIOToConsole(); -#endif - FileAccess::make_default<FileAccessWindows>(FileAccess::ACCESS_RESOURCES); FileAccess::make_default<FileAccessWindows>(FileAccess::ACCESS_USERDATA); FileAccess::make_default<FileAccessWindows>(FileAccess::ACCESS_FILESYSTEM); @@ -1521,10 +1534,10 @@ void OS_Windows::unset_environment(const String &p_var) const { } String OS_Windows::get_stdin_string() { - WCHAR buff[1024]; + char buff[1024]; DWORD count = 0; - if (ReadConsoleW(GetStdHandle(STD_INPUT_HANDLE), buff, 1024, &count, nullptr)) { - return String::utf16((const char16_t *)buff, count); + if (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buff, 1024, &count, nullptr)) { + return String::utf8((const char *)buff, count); } return String(); @@ -1908,6 +1921,13 @@ String OS_Windows::get_system_ca_certificates() { OS_Windows::OS_Windows(HINSTANCE _hInstance) { hInstance = _hInstance; +#ifndef WINDOWS_SUBSYSTEM_CONSOLE + RedirectIOToConsole(); +#endif + + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); #ifdef WASAPI_ENABLED diff --git a/platform/windows/windows_terminal_logger.cpp b/platform/windows/windows_terminal_logger.cpp index 6881a75596..6fc33afe99 100644 --- a/platform/windows/windows_terminal_logger.cpp +++ b/platform/windows/windows_terminal_logger.cpp @@ -53,26 +53,12 @@ void WindowsTerminalLogger::logv(const char *p_format, va_list p_list, bool p_er } buf[len] = 0; - int wlen = MultiByteToWideChar(CP_UTF8, 0, buf, len, nullptr, 0); - if (wlen < 0) { - return; - } - - wchar_t *wbuf = (wchar_t *)memalloc((len + 1) * sizeof(wchar_t)); - ERR_FAIL_NULL_MSG(wbuf, "Out of memory."); - MultiByteToWideChar(CP_UTF8, 0, buf, len, wbuf, wlen); - wbuf[wlen] = 0; - - if (p_err) { - fwprintf(stderr, L"%ls", wbuf); - } else { - wprintf(L"%ls", wbuf); - } - - memfree(wbuf); + DWORD written = 0; + HANDLE h = p_err ? GetStdHandle(STD_ERROR_HANDLE) : GetStdHandle(STD_OUTPUT_HANDLE); + WriteFile(h, &buf[0], len, &written, nullptr); #ifdef DEBUG_ENABLED - fflush(stdout); + FlushFileBuffers(h); #endif } diff --git a/platform_methods.py b/platform_methods.py index 56115db4a4..57b11d1a47 100644 --- a/platform_methods.py +++ b/platform_methods.py @@ -39,8 +39,7 @@ def detect_arch(): # Catches x86, i386, i486, i586, i686, etc. return "x86_32" else: - print("Unsupported CPU architecture: " + host_machine) - print("Falling back to x86_64.") + methods.print_warning(f'Unsupported CPU architecture: "{host_machine}". Falling back to x86_64.') return "x86_64" diff --git a/scene/2d/audio_listener_2d.cpp b/scene/2d/audio_listener_2d.cpp index b4484694a5..cff0654ecc 100644 --- a/scene/2d/audio_listener_2d.cpp +++ b/scene/2d/audio_listener_2d.cpp @@ -45,7 +45,7 @@ bool AudioListener2D::_set(const StringName &p_name, const Variant &p_value) { bool AudioListener2D::_get(const StringName &p_name, Variant &r_ret) const { if (p_name == "current") { - if (is_inside_tree() && get_tree()->is_node_being_edited(this)) { + if (is_part_of_edited_scene()) { r_ret = current; } else { r_ret = is_current(); @@ -63,13 +63,13 @@ void AudioListener2D::_get_property_list(List<PropertyInfo> *p_list) const { void AudioListener2D::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { - if (!get_tree()->is_node_being_edited(this) && current) { + if (!is_part_of_edited_scene() && current) { make_current(); } } break; case NOTIFICATION_EXIT_TREE: { - if (!get_tree()->is_node_being_edited(this)) { + if (!is_part_of_edited_scene()) { if (is_current()) { clear_current(); current = true; // Keep it true. @@ -98,7 +98,7 @@ void AudioListener2D::clear_current() { } bool AudioListener2D::is_current() const { - if (is_inside_tree() && !get_tree()->is_node_being_edited(this)) { + if (is_inside_tree() && !is_part_of_edited_scene()) { return get_viewport()->get_audio_listener_2d() == this; } else { return current; diff --git a/scene/2d/parallax_2d.cpp b/scene/2d/parallax_2d.cpp index 555f3b031c..aacab3213d 100644 --- a/scene/2d/parallax_2d.cpp +++ b/scene/2d/parallax_2d.cpp @@ -144,7 +144,7 @@ void Parallax2D::set_repeat_size(const Size2 &p_repeat_size) { return; } - repeat_size = p_repeat_size.max(Vector2(0, 0)); + repeat_size = p_repeat_size.maxf(0); _update_process(); _update_repeat(); diff --git a/scene/3d/audio_listener_3d.cpp b/scene/3d/audio_listener_3d.cpp index 437d840d5f..4a18447b3b 100644 --- a/scene/3d/audio_listener_3d.cpp +++ b/scene/3d/audio_listener_3d.cpp @@ -55,7 +55,7 @@ bool AudioListener3D::_set(const StringName &p_name, const Variant &p_value) { bool AudioListener3D::_get(const StringName &p_name, Variant &r_ret) const { if (p_name == "current") { - if (is_inside_tree() && get_tree()->is_node_being_edited(this)) { + if (is_part_of_edited_scene()) { r_ret = current; } else { r_ret = is_current(); @@ -81,7 +81,7 @@ void AudioListener3D::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_WORLD: { bool first_listener = get_viewport()->_audio_listener_3d_add(this); - if (!get_tree()->is_node_being_edited(this) && (current || first_listener)) { + if (!is_part_of_edited_scene() && (current || first_listener)) { make_current(); } } break; @@ -91,7 +91,7 @@ void AudioListener3D::_notification(int p_what) { } break; case NOTIFICATION_EXIT_WORLD: { - if (!get_tree()->is_node_being_edited(this)) { + if (!is_part_of_edited_scene()) { if (is_current()) { clear_current(); current = true; //keep it true @@ -133,7 +133,7 @@ void AudioListener3D::clear_current() { } bool AudioListener3D::is_current() const { - if (is_inside_tree() && !get_tree()->is_node_being_edited(this)) { + if (is_inside_tree() && !is_part_of_edited_scene()) { return get_viewport()->get_audio_listener_3d() == this; } else { return current; diff --git a/scene/3d/camera_3d.cpp b/scene/3d/camera_3d.cpp index e8bd498e1f..8515aacba7 100644 --- a/scene/3d/camera_3d.cpp +++ b/scene/3d/camera_3d.cpp @@ -90,7 +90,7 @@ void Camera3D::_update_camera() { RenderingServer::get_singleton()->camera_set_transform(camera, get_camera_transform()); - if (get_tree()->is_node_being_edited(this) || !is_current()) { + if (is_part_of_edited_scene() || !is_current()) { return; } @@ -126,7 +126,7 @@ void Camera3D::_notification(int p_what) { } break; case NOTIFICATION_EXIT_WORLD: { - if (!get_tree()->is_node_being_edited(this)) { + if (!is_part_of_edited_scene()) { if (is_current()) { clear_current(); current = true; //keep it true @@ -286,7 +286,7 @@ void Camera3D::set_current(bool p_enabled) { } bool Camera3D::is_current() const { - if (is_inside_tree() && !get_tree()->is_node_being_edited(this)) { + if (is_inside_tree() && !is_part_of_edited_scene()) { return get_viewport()->get_camera_3d() == this; } else { return current; diff --git a/scene/3d/cpu_particles_3d.cpp b/scene/3d/cpu_particles_3d.cpp index 0dc9834539..db7b80683c 100644 --- a/scene/3d/cpu_particles_3d.cpp +++ b/scene/3d/cpu_particles_3d.cpp @@ -880,7 +880,7 @@ void CPUParticles3D::_particles_process(double p_delta) { } break; case EMISSION_SHAPE_RING: { real_t ring_random_angle = Math::randf() * Math_TAU; - real_t ring_random_radius = Math::randf() * (emission_ring_radius - emission_ring_inner_radius) + emission_ring_inner_radius; + real_t ring_random_radius = Math::sqrt(Math::randf() * (emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius); Vector3 axis = emission_ring_axis == Vector3(0.0, 0.0, 0.0) ? Vector3(0.0, 0.0, 1.0) : emission_ring_axis.normalized(); Vector3 ortho_axis; if (axis.abs() == Vector3(1.0, 0.0, 0.0)) { diff --git a/scene/3d/decal.cpp b/scene/3d/decal.cpp index 8415fb38cb..485599d0fb 100644 --- a/scene/3d/decal.cpp +++ b/scene/3d/decal.cpp @@ -31,7 +31,7 @@ #include "decal.h" void Decal::set_size(const Vector3 &p_size) { - size = p_size.max(Vector3(0.001, 0.001, 0.001)); + size = p_size.maxf(0.001); RS::get_singleton()->decal_set_size(decal, size); update_gizmos(); } diff --git a/scene/3d/fog_volume.cpp b/scene/3d/fog_volume.cpp index 8af386f282..54631a8dff 100644 --- a/scene/3d/fog_volume.cpp +++ b/scene/3d/fog_volume.cpp @@ -73,7 +73,7 @@ bool FogVolume::_get(const StringName &p_name, Variant &r_property) const { void FogVolume::set_size(const Vector3 &p_size) { size = p_size; - size = size.max(Vector3()); + size = size.maxf(0); RS::get_singleton()->fog_volume_set_size(_get_volume(), size); update_gizmos(); } diff --git a/scene/3d/gpu_particles_collision_3d.cpp b/scene/3d/gpu_particles_collision_3d.cpp index 8fd5f25749..3a05ec9c9e 100644 --- a/scene/3d/gpu_particles_collision_3d.cpp +++ b/scene/3d/gpu_particles_collision_3d.cpp @@ -382,7 +382,7 @@ Vector3i GPUParticlesCollisionSDF3D::get_estimated_cell_size() const { float cell_size = aabb.get_longest_axis_size() / float(subdiv); Vector3i sdf_size = Vector3i(aabb.size / cell_size); - sdf_size = sdf_size.max(Vector3i(1, 1, 1)); + sdf_size = sdf_size.maxi(1); return sdf_size; } @@ -395,7 +395,7 @@ Ref<Image> GPUParticlesCollisionSDF3D::bake() { float cell_size = aabb.get_longest_axis_size() / float(subdiv); Vector3i sdf_size = Vector3i(aabb.size / cell_size); - sdf_size = sdf_size.max(Vector3i(1, 1, 1)); + sdf_size = sdf_size.maxi(1); if (bake_begin_function) { bake_begin_function(100); diff --git a/scene/3d/label_3d.cpp b/scene/3d/label_3d.cpp index 9f71c881a9..54370f42da 100644 --- a/scene/3d/label_3d.cpp +++ b/scene/3d/label_3d.cpp @@ -636,6 +636,10 @@ void Label3D::_shape() { } void Label3D::set_text(const String &p_string) { + if (text == p_string) { + return; + } + text = p_string; xl_text = atr(p_string); dirty_text = true; diff --git a/scene/3d/node_3d.cpp b/scene/3d/node_3d.cpp index 3b788b2fd0..98a5134283 100644 --- a/scene/3d/node_3d.cpp +++ b/scene/3d/node_3d.cpp @@ -197,7 +197,7 @@ void Node3D::_notification(int p_what) { } #ifdef TOOLS_ENABLED - if (Engine::get_singleton()->is_editor_hint() && get_tree()->is_node_being_edited(this)) { + if (is_part_of_edited_scene()) { get_tree()->call_group_flags(SceneTree::GROUP_CALL_DEFERRED, SceneStringNames::get_singleton()->_spatial_editor_group, SNAME("_request_gizmo_for_id"), get_instance_id()); } #endif @@ -582,7 +582,7 @@ void Node3D::set_subgizmo_selection(Ref<Node3DGizmo> p_gizmo, int p_id, Transfor return; } - if (Engine::get_singleton()->is_editor_hint() && get_tree()->is_node_being_edited(this)) { + if (is_part_of_edited_scene()) { get_tree()->call_group_flags(SceneTree::GROUP_CALL_DEFERRED, SceneStringNames::get_singleton()->_spatial_editor_group, SceneStringNames::get_singleton()->_set_subgizmo_selection, this, p_gizmo, p_id, p_transform); } #endif @@ -599,7 +599,7 @@ void Node3D::clear_subgizmo_selection() { return; } - if (Engine::get_singleton()->is_editor_hint() && get_tree()->is_node_being_edited(this)) { + if (is_part_of_edited_scene()) { get_tree()->call_group_flags(SceneTree::GROUP_CALL_DEFERRED, SceneStringNames::get_singleton()->_spatial_editor_group, SceneStringNames::get_singleton()->_clear_subgizmo_selection, this); } #endif diff --git a/scene/3d/occluder_instance_3d.cpp b/scene/3d/occluder_instance_3d.cpp index 2f77185d0d..150771545b 100644 --- a/scene/3d/occluder_instance_3d.cpp +++ b/scene/3d/occluder_instance_3d.cpp @@ -192,7 +192,7 @@ void QuadOccluder3D::set_size(const Size2 &p_size) { return; } - size = p_size.max(Size2()); + size = p_size.maxf(0); _update(); } @@ -236,7 +236,7 @@ void BoxOccluder3D::set_size(const Vector3 &p_size) { return; } - size = p_size.max(Vector3()); + size = p_size.maxf(0); _update(); } diff --git a/scene/3d/voxel_gi.cpp b/scene/3d/voxel_gi.cpp index 938d6e5699..fbdda67526 100644 --- a/scene/3d/voxel_gi.cpp +++ b/scene/3d/voxel_gi.cpp @@ -294,7 +294,7 @@ VoxelGI::Subdiv VoxelGI::get_subdiv() const { void VoxelGI::set_size(const Vector3 &p_size) { // Prevent very small size dimensions as these breaks baking if other size dimensions are set very high. - size = p_size.max(Vector3(1.0, 1.0, 1.0)); + size = p_size.maxf(1.0); update_gizmos(); } diff --git a/scene/3d/xr_body_modifier_3d.cpp b/scene/3d/xr_body_modifier_3d.cpp index 8aec3e856e..cf73882a7b 100644 --- a/scene/3d/xr_body_modifier_3d.cpp +++ b/scene/3d/xr_body_modifier_3d.cpp @@ -312,7 +312,7 @@ void XRBodyModifier3D::_process_modification() { } } -void XRBodyModifier3D::_tracker_changed(const StringName &p_tracker_name, const Ref<XRBodyTracker> &p_tracker) { +void XRBodyModifier3D::_tracker_changed(const StringName &p_tracker_name, XRServer::TrackerType p_tracker_type) { if (tracker_name == p_tracker_name) { _get_joint_data(); } @@ -327,18 +327,18 @@ void XRBodyModifier3D::_notification(int p_what) { case NOTIFICATION_ENTER_TREE: { XRServer *xr_server = XRServer::get_singleton(); if (xr_server) { - xr_server->connect("body_tracker_added", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); - xr_server->connect("body_tracker_updated", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); - xr_server->connect("body_tracker_removed", callable_mp(this, &XRBodyModifier3D::_tracker_changed).bind(Ref<XRBodyTracker>())); + xr_server->connect("tracker_added", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); + xr_server->connect("tracker_updated", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); + xr_server->connect("tracker_removed", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); } _get_joint_data(); } break; case NOTIFICATION_EXIT_TREE: { XRServer *xr_server = XRServer::get_singleton(); if (xr_server) { - xr_server->disconnect("body_tracker_added", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); - xr_server->disconnect("body_tracker_updated", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); - xr_server->disconnect("body_tracker_removed", callable_mp(this, &XRBodyModifier3D::_tracker_changed).bind(Ref<XRBodyTracker>())); + xr_server->disconnect("tracker_added", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); + xr_server->disconnect("tracker_updated", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); + xr_server->disconnect("tracker_removed", callable_mp(this, &XRBodyModifier3D::_tracker_changed)); } for (int i = 0; i < XRBodyTracker::JOINT_MAX; i++) { joints[i].bone = -1; diff --git a/scene/3d/xr_body_modifier_3d.h b/scene/3d/xr_body_modifier_3d.h index 9ff0cd7207..78d70146ee 100644 --- a/scene/3d/xr_body_modifier_3d.h +++ b/scene/3d/xr_body_modifier_3d.h @@ -86,7 +86,7 @@ private: JointData joints[XRBodyTracker::JOINT_MAX]; void _get_joint_data(); - void _tracker_changed(const StringName &p_tracker_name, const Ref<XRBodyTracker> &p_tracker); + void _tracker_changed(const StringName &p_tracker_name, XRServer::TrackerType p_tracker_type); }; VARIANT_BITFIELD_CAST(XRBodyModifier3D::BodyUpdate) diff --git a/scene/3d/xr_hand_modifier_3d.cpp b/scene/3d/xr_hand_modifier_3d.cpp index 1e78a4630f..baaa9eee48 100644 --- a/scene/3d/xr_hand_modifier_3d.cpp +++ b/scene/3d/xr_hand_modifier_3d.cpp @@ -70,6 +70,11 @@ void XRHandModifier3D::_get_joint_data() { return; } + if (has_stored_previous_transforms) { + previous_relative_transforms.clear(); + has_stored_previous_transforms = false; + } + // Table of bone names for different rig types. static const String bone_names[XRHandTracker::HAND_JOINT_MAX] = { "Palm", @@ -196,6 +201,18 @@ void XRHandModifier3D::_process_modification() { // Skip if no tracking data if (!tracker->get_has_tracking_data()) { + if (!has_stored_previous_transforms) { + return; + } + + // Apply previous relative transforms if they are stored. + for (int joint = 0; joint < XRHandTracker::HAND_JOINT_MAX; joint++) { + if (bone_update == BONE_UPDATE_FULL) { + skeleton->set_bone_pose_position(joints[joint].bone, previous_relative_transforms[joint].origin); + } + + skeleton->set_bone_pose_rotation(joints[joint].bone, Quaternion(previous_relative_transforms[joint].basis)); + } return; } @@ -223,6 +240,12 @@ void XRHandModifier3D::_process_modification() { return; } + if (!has_stored_previous_transforms) { + previous_relative_transforms.resize(XRHandTracker::HAND_JOINT_MAX); + has_stored_previous_transforms = true; + } + Transform3D *previous_relative_transforms_ptr = previous_relative_transforms.ptrw(); + for (int joint = 0; joint < XRHandTracker::HAND_JOINT_MAX; joint++) { // Get the skeleton bone (skip if none). const int bone = joints[joint].bone; @@ -233,6 +256,7 @@ void XRHandModifier3D::_process_modification() { // Calculate the relative relationship to the parent bone joint. const int parent_joint = joints[joint].parent_joint; const Transform3D relative_transform = inv_transforms[parent_joint] * transforms[joint]; + previous_relative_transforms_ptr[joint] = relative_transform; // Update the bone position if enabled by update mode. if (bone_update == BONE_UPDATE_FULL) { diff --git a/scene/3d/xr_hand_modifier_3d.h b/scene/3d/xr_hand_modifier_3d.h index 67d1694d41..3d78f32b64 100644 --- a/scene/3d/xr_hand_modifier_3d.h +++ b/scene/3d/xr_hand_modifier_3d.h @@ -73,6 +73,9 @@ private: BoneUpdate bone_update = BONE_UPDATE_FULL; JointData joints[XRHandTracker::HAND_JOINT_MAX]; + bool has_stored_previous_transforms = false; + Vector<Transform3D> previous_relative_transforms; + void _get_joint_data(); void _tracker_changed(StringName p_tracker_name, XRServer::TrackerType p_tracker_type); }; diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index 5a3a5f9bc0..bc5c99dbfe 100644 --- a/scene/animation/animation_mixer.cpp +++ b/scene/animation/animation_mixer.cpp @@ -1617,7 +1617,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { } if (seeked) { // Seek. - int idx = a->track_find_key(i, time, is_external_seeking ? Animation::FIND_MODE_NEAREST : Animation::FIND_MODE_EXACT, true); + int idx = a->track_find_key(i, time, Animation::FIND_MODE_NEAREST, true); if (idx < 0) { continue; } @@ -1630,6 +1630,9 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { double at_anim_pos = 0.0; switch (anim->get_loop_mode()) { case Animation::LOOP_NONE: { + if (!is_external_seeking && ((!backward && time >= pos + (double)anim->get_length()) || (backward && time <= pos))) { + continue; // Do nothing if current time is outside of length when started. + } at_anim_pos = MIN((double)anim->get_length(), time - pos); // Seek to end. } break; case Animation::LOOP_LINEAR: { @@ -1641,7 +1644,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { default: break; } - if (player2->is_playing()) { + if (player2->is_playing() || !is_external_seeking) { player2->seek(at_anim_pos, false, p_update_only); player2->play(anim_name); t->playing = true; diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp index d46470282f..7140161eca 100644 --- a/scene/animation/animation_player.cpp +++ b/scene/animation/animation_player.cpp @@ -149,7 +149,7 @@ void AnimationPlayer::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: { if (!Engine::get_singleton()->is_editor_hint() && animation_set.has(autoplay)) { - set_active(true); + set_active(active); play(autoplay); _check_immediately_after_start(); } diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 4f90504e35..8131fe7aaa 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -624,16 +624,31 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const { return TextEdit::get_cursor_shape(p_pos); } +void CodeEdit::_unhide_carets() { + // Unfold caret and selection origin. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_hidden(get_caret_line(i))) { + unfold_line(get_caret_line(i)); + } + if (has_selection(i) && _is_line_hidden(get_selection_origin_line(i))) { + unfold_line(get_selection_origin_line(i)); + } + } +} + /* Text manipulation */ // Overridable actions void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) { start_action(EditAction::ACTION_TYPING); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } bool had_selection = has_selection(i); String selection_text = (had_selection ? get_selected_text(i) : ""); @@ -691,6 +706,7 @@ void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca insert_text_at_caret(chr, i); } } + end_multicaret_edit(); end_action(); } @@ -705,66 +721,80 @@ void CodeEdit::_backspace_internal(int p_caret) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } - int cc = get_caret_column(i); - int cl = get_caret_line(i); + int to_line = get_caret_line(i); + int to_column = get_caret_column(i); - if (cc == 0 && cl == 0) { + if (to_column == 0 && to_line == 0) { continue; } - if (cl > 0 && _is_line_hidden(cl - 1)) { - unfold_line(get_caret_line(i) - 1); + if (to_line > 0 && _is_line_hidden(to_line - 1)) { + unfold_line(to_line - 1); } - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length()); + int from_line = to_column > 0 ? to_line : to_line - 1; + int from_column = to_column > 0 ? (to_column - 1) : (get_line(to_line - 1).length()); - merge_gutters(prev_line, cl); + merge_gutters(from_line, to_line); - if (auto_brace_completion_enabled && cc > 0) { - int idx = _get_auto_brace_pair_open_at_pos(cl, cc); + if (auto_brace_completion_enabled && to_column > 0) { + int idx = _get_auto_brace_pair_open_at_pos(to_line, to_column); if (idx != -1) { - prev_column = cc - auto_brace_completion_pairs[idx].open_key.length(); + from_column = to_column - auto_brace_completion_pairs[idx].open_key.length(); - if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) { - cc += auto_brace_completion_pairs[idx].close_key.length(); + if (_get_auto_brace_pair_close_at_pos(to_line, to_column) == idx) { + to_column += auto_brace_completion_pairs[idx].close_key.length(); } - - remove_text(prev_line, prev_column, cl, cc); - - set_caret_line(prev_line, false, true, 0, i); - set_caret_column(prev_column, i == 0, i); - - adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); - continue; } } // For space indentation we need to do a basic unindent if there are no chars to the left, acting the same way as tabs. - if (indent_using_spaces && cc != 0) { - if (get_first_non_whitespace_column(cl) >= cc) { - prev_column = cc - _calculate_spaces_till_next_left_indent(cc); - prev_line = cl; + if (indent_using_spaces && to_column != 0) { + if (get_first_non_whitespace_column(to_line) >= to_column) { + from_column = to_column - _calculate_spaces_till_next_left_indent(to_column); + from_line = to_line; } } - remove_text(prev_line, prev_column, cl, cc); - - set_caret_line(prev_line, false, true, 0, i); - set_caret_column(prev_column, i == 0, i); + remove_text(from_line, from_column, to_line, to_column); - adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); + set_caret_line(from_line, false, true, -1, i); + set_caret_column(from_column, i == 0, i); } - merge_overlapping_carets(); + + end_multicaret_edit(); end_complex_operation(); } +void CodeEdit::_cut_internal(int p_caret) { + // Overridden to unfold lines. + _copy_internal(p_caret); + + if (!is_editable()) { + return; + } + + if (has_selection(p_caret)) { + delete_selection(p_caret); + return; + } + if (p_caret == -1) { + delete_lines(); + } else { + unfold_line(get_caret_line(p_caret)); + remove_line_at(get_caret_line(p_caret)); + } +} + /* Indent management */ void CodeEdit::set_indent_size(const int p_size) { ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0."); @@ -838,13 +868,17 @@ void CodeEdit::do_indent() { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } int spaces_to_add = _calculate_spaces_till_next_right_indent(get_caret_column(i)); if (spaces_to_add > 0) { insert_text_at_caret(String(" ").repeat(spaces_to_add), i); } } + end_multicaret_edit(); end_complex_operation(); } @@ -854,51 +888,28 @@ void CodeEdit::indent_lines() { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { - // This value informs us by how much we changed selection position by indenting right. - // Default is 1 for tab indentation. - int selection_offset = 1; - - int start_line = get_caret_line(c); - int end_line = start_line; - if (has_selection(c)) { - start_line = get_selection_from_line(c); - end_line = get_selection_to_line(c); + begin_multicaret_edit(); - // Ignore the last line if the selection is not past the first column. - if (get_selection_to_column(c) == 0) { - selection_offset = 0; - end_line--; - } - } - - for (int i = start_line; i <= end_line; i++) { + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { const String line_text = get_line(i); - if (line_text.size() == 0 && has_selection(c)) { + if (line_text.size() == 0) { + // Ignore empty lines. continue; } - if (!indent_using_spaces) { - set_line(i, '\t' + line_text); - continue; + if (indent_using_spaces) { + int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i)); + insert_text(String(" ").repeat(spaces_to_add), i, 0, false); + } else { + insert_text("\t", i, 0, false); } - - // We don't really care where selection is - we just need to know indentation level at the beginning of the line. - // Since we will add this many spaces, we want to move the whole selection and caret by this much. - int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i)); - set_line(i, String(" ").repeat(spaces_to_add) + line_text); - selection_offset = spaces_to_add; } - - // Fix selection and caret being off after shifting selection right. - if (has_selection(c)) { - select(start_line, get_selection_from_column(c) + selection_offset, get_selection_to_line(c), get_selection_to_column(c) + selection_offset, c); - } - set_caret_column(get_caret_column(c) + selection_offset, false, c); } + + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } void CodeEdit::unindent_lines() { @@ -907,76 +918,25 @@ void CodeEdit::unindent_lines() { } begin_complex_operation(); + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { - // Moving caret and selection after unindenting can get tricky because - // changing content of line can move caret and selection on its own (if new line ends before previous position of either) - // therefore we just remember initial values and at the end of the operation offset them by number of removed characters. - int removed_characters = 0; - int initial_selection_end_column = 0; - int initial_cursor_column = get_caret_column(c); - - int start_line = get_caret_line(c); - int end_line = start_line; - if (has_selection(c)) { - start_line = get_selection_from_line(c); - end_line = get_selection_to_line(c); - - // Ignore the last line if the selection is not past the first column. - initial_selection_end_column = get_selection_to_column(c); - if (initial_selection_end_column == 0) { - end_line--; - } - } - - bool first_line_edited = false; - bool last_line_edited = false; - - for (int i = start_line; i <= end_line; i++) { - String line_text = get_line(i); + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { + const String line_text = get_line(i); if (line_text.begins_with("\t")) { - line_text = line_text.substr(1, line_text.length()); - - set_line(i, line_text); - removed_characters = 1; - - first_line_edited = (i == start_line) ? true : first_line_edited; - last_line_edited = (i == end_line) ? true : last_line_edited; - continue; - } - - if (line_text.begins_with(" ")) { - // When unindenting we aim to remove spaces before line that has selection no matter what is selected. - // Here we remove only enough spaces to align text to nearest full multiple of indentation_size. - // In case where selection begins at the start of indentation_size multiple we remove whole indentation level. + remove_text(i, 0, i, 1); + } else if (line_text.begins_with(" ")) { + // Remove only enough spaces to align text to nearest full multiple of indentation_size. int spaces_to_remove = _calculate_spaces_till_next_left_indent(get_first_non_whitespace_column(i)); - line_text = line_text.substr(spaces_to_remove, line_text.length()); - - set_line(i, line_text); - removed_characters = spaces_to_remove; - - first_line_edited = (i == start_line) ? true : first_line_edited; - last_line_edited = (i == end_line) ? true : last_line_edited; + remove_text(i, 0, i, spaces_to_remove); } } - - if (has_selection(c)) { - // Fix selection being off by one on the first line. - if (first_line_edited) { - select(get_selection_from_line(c), get_selection_from_column(c) - removed_characters, get_selection_to_line(c), initial_selection_end_column, c); - } - - // Fix selection being off by one on the last line. - if (last_line_edited) { - select(get_selection_from_line(c), get_selection_from_column(c), get_selection_to_line(c), initial_selection_end_column - removed_characters, c); - } - } - set_caret_column(initial_cursor_column - removed_characters, false, c); } + + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } void CodeEdit::convert_indent(int p_from_line, int p_to_line) { @@ -992,27 +952,6 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { ERR_FAIL_COND(p_to_line >= get_line_count()); ERR_FAIL_COND(p_to_line < p_from_line); - // Store caret states. - Vector<int> caret_columns; - Vector<Pair<int, int>> from_selections; - Vector<Pair<int, int>> to_selections; - caret_columns.resize(get_caret_count()); - from_selections.resize(get_caret_count()); - to_selections.resize(get_caret_count()); - for (int c = 0; c < get_caret_count(); c++) { - caret_columns.write[c] = get_caret_column(c); - - // Set "selection_from_line" to -1 to allow checking if there was a selection later. - if (!has_selection(c)) { - from_selections.write[c].first = -1; - continue; - } - from_selections.write[c].first = get_selection_from_line(c); - from_selections.write[c].second = get_selection_from_column(c); - to_selections.write[c].first = get_selection_to_line(c); - to_selections.write[c].second = get_selection_to_column(c); - } - // Check lines within range. const char32_t from_indent_char = indent_using_spaces ? '\t' : ' '; int size_diff = indent_using_spaces ? indent_size - 1 : -(indent_size - 1); @@ -1044,23 +983,10 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { line_changed = true; if (!changed_indentation) { begin_complex_operation(); + begin_multicaret_edit(); changed_indentation = true; } - // Calculate new caret state. - for (int c = 0; c < get_caret_count(); c++) { - if (get_caret_line(c) != i || caret_columns[c] <= j) { - continue; - } - caret_columns.write[c] += size_diff; - - if (from_selections.write[c].first == -1) { - continue; - } - from_selections.write[c].second = from_selections[c].first == i ? from_selections[c].second + size_diff : from_selections[c].second; - to_selections.write[c].second = to_selections[c].first == i ? to_selections[c].second + size_diff : to_selections[c].second; - } - // Calculate new line. line = line.left(j + ((size_diff < 0) ? size_diff : 0)) + indent_text + line.substr(j + 1); @@ -1069,6 +995,7 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { } if (line_changed) { + // Use set line to preserve carets visual position. set_line(i, line); } } @@ -1077,16 +1004,9 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { return; } - // Restore caret states. - for (int c = 0; c < get_caret_count(); c++) { - set_caret_column(caret_columns[c], c == 0, c); - if (from_selections.write[c].first != -1) { - select(from_selections.write[c].first, from_selections.write[c].second, to_selections.write[c].first, to_selections.write[c].second, c); - } - } merge_overlapping_carets(); + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } int CodeEdit::_calculate_spaces_till_next_left_indent(int p_column) const { @@ -1107,15 +1027,22 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } // When not splitting the line, we need to factor in indentation from the end of the current line. const int cc = p_split_current_line ? get_caret_column(i) : get_line(get_caret_line(i)).length(); const int cl = get_caret_line(i); const String line = get_line(cl); - String ins = "\n"; + String ins = ""; + if (!p_above) { + ins = "\n"; + } // Append current indentation. int space_count = 0; @@ -1138,6 +1065,9 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } break; } + if (p_above) { + ins += "\n"; + } if (is_line_folded(cl)) { unfold_line(cl); @@ -1183,33 +1113,22 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } } - bool first_line = false; - if (!p_split_current_line) { + if (p_split_current_line) { + insert_text_at_caret(ins, i); + } else { + insert_text(ins, cl, p_above ? 0 : get_line(cl).length(), p_above, p_above); deselect(i); - - if (p_above) { - if (cl > 0) { - set_caret_line(cl - 1, false, true, 0, i); - set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i); - } else { - set_caret_column(0, i == 0, i); - first_line = true; - } - } else { - set_caret_column(line.length(), i == 0, i); - } + set_caret_line(p_above ? cl : cl + 1, false, true, -1, i); + set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i); } - - insert_text_at_caret(ins, i); - - if (first_line) { - set_caret_line(0, i == 0, true, 0, i); - } else if (brace_indent) { + if (brace_indent) { + // Move to inner indented line. set_caret_line(get_caret_line(i) - 1, false, true, 0, i); set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i); } } + end_multicaret_edit(); end_complex_operation(); } @@ -1700,27 +1619,8 @@ void CodeEdit::fold_line(int p_line) { _set_line_as_hidden(i, true); } - for (int i = 0; i < get_caret_count(); i++) { - // Fix selection. - if (has_selection(i)) { - if (_is_line_hidden(get_selection_from_line(i)) && _is_line_hidden(get_selection_to_line(i))) { - deselect(i); - } else if (_is_line_hidden(get_selection_from_line(i))) { - select(p_line, 9999, get_selection_to_line(i), get_selection_to_column(i), i); - } else if (_is_line_hidden(get_selection_to_line(i))) { - select(get_selection_from_line(i), get_selection_from_column(i), p_line, 9999, i); - } - } - - // Reset caret. - if (_is_line_hidden(get_caret_line(i))) { - set_caret_line(p_line, false, false, 0, i); - set_caret_column(get_line(p_line).length(), false, i); - } - } - - merge_overlapping_carets(); - queue_redraw(); + // Collapse any carets in the hidden area. + collapse_carets(p_line, get_line(p_line).length(), end_line, get_line(end_line).length(), true); } void CodeEdit::unfold_line(int p_line) { @@ -1769,6 +1669,23 @@ void CodeEdit::toggle_foldable_line(int p_line) { fold_line(p_line); } +void CodeEdit::toggle_foldable_lines_at_carets() { + begin_multicaret_edit(); + int previous_line = -1; + Vector<int> sorted = get_sorted_carets(); + for (int caret_idx : sorted) { + if (multicaret_edit_ignore_caret(caret_idx)) { + continue; + } + int line_idx = get_caret_line(caret_idx); + if (line_idx != previous_line) { + toggle_foldable_line(line_idx); + previous_line = line_idx; + } + } + end_multicaret_edit(); +} + bool CodeEdit::is_line_folded(int p_line) const { ERR_FAIL_INDEX_V(p_line, get_line_count(), false); return p_line + 1 < get_line_count() && !_is_line_hidden(p_line) && _is_line_hidden(p_line + 1); @@ -1795,49 +1712,29 @@ void CodeEdit::create_code_region() { WARN_PRINT_ONCE("Cannot create code region without any one line comment delimiters"); return; } + String region_name = atr(ETR("New Code Region")); + begin_complex_operation(); - // Merge selections if selection starts on the same line the previous one ends. - Vector<int> caret_edit_order = get_caret_index_edit_order(); - Vector<int> carets_to_remove; - for (int i = 1; i < caret_edit_order.size(); i++) { - int current_caret = caret_edit_order[i - 1]; - int next_caret = caret_edit_order[i]; - if (get_selection_from_line(current_caret) == get_selection_to_line(next_caret)) { - select(get_selection_from_line(next_caret), get_selection_from_column(next_caret), get_selection_to_line(current_caret), get_selection_to_column(current_caret), next_caret); - carets_to_remove.append(current_caret); - } - } - // Sort and remove backwards to preserve indices. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); - } - - // Adding start and end region tags. - int first_region_start = -1; - for (int caret_idx : get_caret_index_edit_order()) { - if (!has_selection(caret_idx)) { - continue; - } - int from_line = get_selection_from_line(caret_idx); - if (first_region_start == -1 || from_line < first_region_start) { - first_region_start = from_line; - } - int to_line = get_selection_to_line(caret_idx); - set_line(to_line, get_line(to_line) + "\n" + code_region_end_string); - insert_line_at(from_line, code_region_start_string + " " + atr(ETR("New Code Region"))); - fold_line(from_line); + begin_multicaret_edit(); + Vector<Point2i> line_ranges = get_line_ranges_from_carets(true, false); + + // Add start and end region tags. + int line_offset = 0; + for (Point2i line_range : line_ranges) { + insert_text("\n" + code_region_end_string, line_range.y + line_offset, get_line(line_range.y + line_offset).length()); + insert_line_at(line_range.x + line_offset, code_region_start_string + " " + region_name); + fold_line(line_range.x + line_offset); + line_offset += 2; } + int first_region_start = line_ranges[0].x; // Select name of the first region to allow quick edit. remove_secondary_carets(); - set_caret_line(first_region_start); - int tag_length = code_region_start_string.length() + atr(ETR("New Code Region")).length() + 1; - set_caret_column(tag_length); + int tag_length = code_region_start_string.length() + region_name.length() + 1; select(first_region_start, code_region_start_string.length() + 1, first_region_start, tag_length); + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } String CodeEdit::get_code_region_start_tag() const { @@ -2236,8 +2133,12 @@ void CodeEdit::confirm_code_completion(bool p_replace) { char32_t caret_last_completion_char = 0; begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } int caret_line = get_caret_line(i); const String &insert_text = code_completion_options[code_completion_current_selected].insert_text; @@ -2270,8 +2171,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) { // Replace. remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_remove_line, caret_col); - adjust_carets_after_edit(i, caret_line, caret_col - code_completion_base.length(), caret_remove_line, caret_col); - set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i); insert_text_at_caret(insert_text, i); } else { // Get first non-matching char. @@ -2287,8 +2186,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) { // Remove base completion text. remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i)); - adjust_carets_after_edit(i, caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i)); - set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i); // Merge with text. insert_text_at_caret(insert_text.substr(0, code_completion_base.length()), i); @@ -2313,12 +2210,10 @@ void CodeEdit::confirm_code_completion(bool p_replace) { if (has_string_delimiter(String::chr(last_completion_char))) { if (post_brace_pair != -1 && last_char_matches) { remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); - adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); } } else { if (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && last_char_matches) { remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); - adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); } else if (auto_brace_completion_enabled && pre_brace_pair != -1) { insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key, i); set_caret_column(get_caret_column(i) - auto_brace_completion_pairs[pre_brace_pair].close_key.length(), i == 0, i); @@ -2329,13 +2224,16 @@ void CodeEdit::confirm_code_completion(bool p_replace) { pre_brace_pair = _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column(i) + 1); if (pre_brace_pair != -1 && pre_brace_pair == _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1)) { remove_text(caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i)); - adjust_carets_after_edit(i, caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i)); - if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1) != pre_brace_pair) { - set_caret_column(get_caret_column(i) - 1, i == 0, i); + if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) + 1) != pre_brace_pair) { + set_caret_column(get_caret_column(i) + 1, i == 0, i); + } else { + set_caret_column(get_caret_column(i) + 2, i == 0, i); } } } } + + end_multicaret_edit(); end_complex_operation(); cancel_code_completion(); @@ -2418,65 +2316,154 @@ void CodeEdit::set_symbol_lookup_word_as_valid(bool p_valid) { } /* Text manipulation */ -void CodeEdit::duplicate_lines() { +void CodeEdit::move_lines_up() { begin_complex_operation(); + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &caret_index : caret_edit_order) { - // The text that will be inserted. All lines in one string. - String insert_text; - - // The new line position of the caret after the operation. - int new_caret_line = get_caret_line(caret_index); - // The new column position of the caret after the operation. - int new_caret_column = get_caret_column(caret_index); - // The caret positions of the selection. Stays -1 if there is no selection. - int select_from_line = -1; - int select_to_line = -1; - int select_from_column = -1; - int select_to_column = -1; - // Number of lines of the selection. - int select_num_lines = -1; - - if (has_selection(caret_index)) { - select_from_line = get_selection_from_line(caret_index); - select_to_line = get_selection_to_line(caret_index); - select_from_column = get_selection_from_column(caret_index); - select_to_column = get_selection_to_column(caret_index); - select_num_lines = select_to_line - select_from_line + 1; - - for (int i = select_from_line; i <= select_to_line; i++) { - insert_text += "\n" + get_line(i); - unfold_line(i); - } - new_caret_line = select_to_line + select_num_lines; - } else { - insert_text = "\n" + get_line(new_caret_line); - new_caret_line++; + // Move lines up by swapping each line with the one above it. + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + if (line_range.x == 0) { + continue; + } + unfold_line(line_range.x - 1); + for (int line = line_range.x; line <= line_range.y; line++) { + unfold_line(line); + swap_lines(line - 1, line); + } + } - unfold_line(get_caret_line(caret_index)); + // Fix selection if it ended at column 0, since it wasn't moved. + for (int i = 0; i < get_caret_count(); i++) { + if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != 0) { + if (is_caret_after_selection_origin(i)) { + set_caret_line(get_caret_line(i) - 1, false, true, -1, i); + } else { + set_selection_origin_line(get_selection_origin_line(i) - 1, true, -1, i); + } } + } - // The text will be inserted at the end of the current line. - set_caret_column(get_line(get_caret_line(caret_index)).length(), false, caret_index); + end_multicaret_edit(); + end_complex_operation(); +} - deselect(caret_index); +void CodeEdit::move_lines_down() { + begin_complex_operation(); + begin_multicaret_edit(); - insert_text_at_caret(insert_text, caret_index); - set_caret_line(new_caret_line, false, true, 0, caret_index); - set_caret_column(new_caret_column, true, caret_index); + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); - if (select_from_line != -1) { - // Advance the selection by the number of duplicated lines. - select_from_line += select_num_lines; - select_to_line += select_num_lines; + // Fix selection if it ended at column 0, since it won't be moved. + for (int i = 0; i < get_caret_count(); i++) { + if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != get_line_count() - 1) { + if (is_caret_after_selection_origin(i)) { + set_caret_line(get_caret_line(i) + 1, false, true, -1, i); + } else { + set_selection_origin_line(get_selection_origin_line(i) + 1, true, -1, i); + } + } + } - select(select_from_line, select_from_column, select_to_line, select_to_column, caret_index); + // Move lines down by swapping each line with the one below it. + for (Point2i line_range : line_ranges) { + if (line_range.y == get_line_count() - 1) { + continue; + } + unfold_line(line_range.y + 1); + for (int line = line_range.y; line >= line_range.x; line--) { + unfold_line(line); + swap_lines(line + 1, line); } } + end_multicaret_edit(); + end_complex_operation(); +} + +void CodeEdit::delete_lines() { + begin_complex_operation(); + begin_multicaret_edit(); + + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // Remove last line of range separately to preserve carets. + unfold_line(line_range.y + line_offset); + remove_line_at(line_range.y + line_offset); + if (line_range.x != line_range.y) { + remove_text(line_range.x + line_offset, 0, line_range.y + line_offset, 0); + } + line_offset += line_range.x - line_range.y - 1; + } + + // Deselect all. + deselect(); + + end_multicaret_edit(); + end_complex_operation(); +} + +void CodeEdit::duplicate_selection() { + begin_complex_operation(); + begin_multicaret_edit(); + + // Duplicate lines from carets without selections first. + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + for (int l = get_selection_from_line(i); l <= get_selection_to_line(i); l++) { + unfold_line(l); + } + if (has_selection(i)) { + continue; + } + + String text_to_insert = get_line(get_caret_line(i)) + "\n"; + // Insert new text before the line, so the caret is on the second one. + insert_text(text_to_insert, get_caret_line(i), 0); + } + + // Duplicate selections. + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + if (!has_selection(i)) { + continue; + } + + // Insert new text before the selection, so the caret is on the second one. + insert_text(get_selected_text(i), get_selection_from_line(i), get_selection_from_column(i)); + } + + end_multicaret_edit(); + end_complex_operation(); +} + +void CodeEdit::duplicate_lines() { + begin_complex_operation(); + begin_multicaret_edit(); + + Vector<Point2i> line_ranges = get_line_ranges_from_carets(false, false); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // The text that will be inserted. All lines in one string. + String text_to_insert; + + for (int i = line_range.x + line_offset; i <= line_range.y + line_offset; i++) { + text_to_insert += get_line(i) + "\n"; + unfold_line(i); + } + + // Insert new text before the line. + insert_text(text_to_insert, line_range.x + line_offset, 0); + line_offset += line_range.y - line_range.x + 1; + } + + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } /* Visual */ @@ -2578,6 +2565,7 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("fold_all_lines"), &CodeEdit::fold_all_lines); ClassDB::bind_method(D_METHOD("unfold_all_lines"), &CodeEdit::unfold_all_lines); ClassDB::bind_method(D_METHOD("toggle_foldable_line", "line"), &CodeEdit::toggle_foldable_line); + ClassDB::bind_method(D_METHOD("toggle_foldable_lines_at_carets"), &CodeEdit::toggle_foldable_lines_at_carets); ClassDB::bind_method(D_METHOD("is_line_folded", "line"), &CodeEdit::is_line_folded); ClassDB::bind_method(D_METHOD("get_folded_lines"), &CodeEdit::get_folded_lines); @@ -2679,6 +2667,10 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_symbol_lookup_word_as_valid", "valid"), &CodeEdit::set_symbol_lookup_word_as_valid); /* Text manipulation */ + ClassDB::bind_method(D_METHOD("move_lines_up"), &CodeEdit::move_lines_up); + ClassDB::bind_method(D_METHOD("move_lines_down"), &CodeEdit::move_lines_down); + ClassDB::bind_method(D_METHOD("delete_lines"), &CodeEdit::delete_lines); + ClassDB::bind_method(D_METHOD("duplicate_selection"), &CodeEdit::duplicate_selection); ClassDB::bind_method(D_METHOD("duplicate_lines"), &CodeEdit::duplicate_lines); /* Inspector */ @@ -2846,10 +2838,12 @@ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) { if (p_gutter == line_number_gutter) { remove_secondary_carets(); - set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE, p_line, 0); - select(p_line, 0, p_line + 1, 0); - set_caret_line(p_line + 1); - set_caret_column(0); + set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE); + if (p_line == get_line_count() - 1) { + select(p_line, 0, p_line, INT_MAX); + } else { + select(p_line, 0, p_line + 1, 0); + } return; } diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index 1770d4f4d8..56f8cce548 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -309,11 +309,14 @@ protected: static void _bind_compatibility_methods(); #endif + virtual void _unhide_carets() override; + /* Text manipulation */ // Overridable actions virtual void _handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) override; virtual void _backspace_internal(int p_caret) override; + virtual void _cut_internal(int p_caret) override; GDVIRTUAL1(_confirm_code_completion, bool) GDVIRTUAL1(_request_code_completion, bool) @@ -409,6 +412,7 @@ public: void fold_all_lines(); void unfold_all_lines(); void toggle_foldable_line(int p_line); + void toggle_foldable_lines_at_carets(); bool is_line_folded(int p_line) const; TypedArray<int> get_folded_lines() const; @@ -489,6 +493,10 @@ public: void set_symbol_lookup_word_as_valid(bool p_valid); /* Text manipulation */ + void move_lines_up(); + void move_lines_down(); + void delete_lines(); + void duplicate_selection(); void duplicate_lines(); CodeEdit(); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index d430fe9bfc..7ac7ceb6bc 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -142,8 +142,8 @@ Size2 Control::_edit_get_scale() const { void Control::_edit_set_rect(const Rect2 &p_edit_rect) { ERR_FAIL_COND_MSG(!Engine::get_singleton()->is_editor_hint(), "This function can only be used from editor plugins."); - set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snapped(Vector2(1, 1)), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); - set_size(p_edit_rect.size.snapped(Vector2(1, 1)), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); + set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snappedf(1), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); + set_size(p_edit_rect.size.snappedf(1), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); } Rect2 Control::_edit_get_rect() const { diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index c3a586a1ee..97a2917dc1 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -53,7 +53,7 @@ void FileDialog::_focus_file_text() { int lp = file->get_text().rfind("."); if (lp != -1) { file->select(0, lp); - if (file->is_inside_tree() && !get_tree()->is_node_being_edited(file)) { + if (file->is_inside_tree() && !is_part_of_edited_scene()) { file->grab_focus(); } } diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index ef9c7e35ed..646e45b27a 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -501,7 +501,7 @@ void GraphEdit::_graph_element_resize_request(const Vector2 &p_new_minsize, Node // Snap the new size to the grid if snapping is enabled. Vector2 new_size = p_new_minsize; if (snapping_enabled ^ Input::get_singleton()->is_key_pressed(Key::CTRL)) { - new_size = new_size.snapped(Vector2(snapping_distance, snapping_distance)); + new_size = new_size.snappedf(snapping_distance); } // Disallow resizing the frame to a size smaller than the minimum size of the attached nodes. @@ -851,7 +851,7 @@ void GraphEdit::_set_position_of_frame_attached_nodes(GraphFrame *p_frame, const Vector2 pos = (attached_node->get_drag_from() * zoom + drag_accum) / zoom; if (snapping_enabled ^ Input::get_singleton()->is_key_pressed(Key::CTRL)) { - pos = pos.snapped(Vector2(snapping_distance, snapping_distance)); + pos = pos.snappedf(snapping_distance); } // Recursively move graph frames. @@ -1678,7 +1678,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { // Snapping can be toggled temporarily by holding down Ctrl. // This is done here as to not toggle the grid when holding down Ctrl. if (snapping_enabled ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { - pos = pos.snapped(Vector2(snapping_distance, snapping_distance)); + pos = pos.snappedf(snapping_distance); } graph_element->set_position_offset(pos); diff --git a/scene/gui/graph_edit_arranger.cpp b/scene/gui/graph_edit_arranger.cpp index 49998beb42..fa1059c667 100644 --- a/scene/gui/graph_edit_arranger.cpp +++ b/scene/gui/graph_edit_arranger.cpp @@ -180,7 +180,7 @@ void GraphEditArranger::arrange_nodes() { if (graph_edit->is_snapping_enabled()) { float snapping_distance = graph_edit->get_snapping_distance(); - pos = pos.snapped(Vector2(snapping_distance, snapping_distance)); + pos = pos.snappedf(snapping_distance); } graph_node->set_position_offset(pos); graph_node->set_drag(false); diff --git a/scene/gui/graph_node.cpp b/scene/gui/graph_node.cpp index fea9c377a7..d035515b51 100644 --- a/scene/gui/graph_node.cpp +++ b/scene/gui/graph_node.cpp @@ -336,7 +336,8 @@ void GraphNode::_notification(int p_what) { int width = get_size().width - sb_panel->get_minimum_size().x; - if (get_child_count() > 0) { + // Take the HboxContainer child into account. + if (get_child_count(false) > 0) { int slot_index = 0; for (const KeyValue<int, Slot> &E : slot_table) { if (E.key < 0 || E.key >= slot_y_cache.size()) { diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 1a94d92855..ddfe202c13 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -771,7 +771,7 @@ void LineEdit::_notification(int p_what) { switch (p_what) { #ifdef TOOLS_ENABLED case NOTIFICATION_ENTER_TREE: { - if (Engine::get_singleton()->is_editor_hint() && !get_tree()->is_node_being_edited(this)) { + if (Engine::get_singleton()->is_editor_hint() && !is_part_of_edited_scene()) { set_caret_blink_enabled(EDITOR_GET("text_editor/appearance/caret/caret_blink")); set_caret_blink_interval(EDITOR_GET("text_editor/appearance/caret/caret_blink_interval")); diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index b2617e6fc7..90ce01e383 100644 --- a/scene/gui/progress_bar.cpp +++ b/scene/gui/progress_bar.cpp @@ -41,7 +41,7 @@ Size2 ProgressBar::get_minimum_size() const { TextLine tl = TextLine(txt, theme_cache.font, theme_cache.font_size); minimum_size.height = MAX(minimum_size.height, theme_cache.background_style->get_minimum_size().height + tl.get_size().y); } else { // this is needed, else the progressbar will collapse - minimum_size = minimum_size.max(Size2(1, 1)); + minimum_size = minimum_size.maxf(1); } return minimum_size; } diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index e177bed20a..0773181239 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -931,9 +931,12 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } RID rid = l.text_buf->get_line_rid(line); - //draw_rect(Rect2(p_ofs + off, TS->shaped_text_get_size(rid)), Color(1,0,0), false, 2); //DEBUG_RECTS + double l_ascent = TS->shaped_text_get_ascent(rid); + Size2 l_size = TS->shaped_text_get_size(rid); + double upos = TS->shaped_text_get_underline_position(rid); + double uth = TS->shaped_text_get_underline_thickness(rid); - off.y += TS->shaped_text_get_ascent(rid); + off.y += l_ascent; // Draw inlined objects. Array objects = TS->shaped_text_get_objects(rid); for (int i = 0; i < objects.size(); i++) { @@ -950,7 +953,6 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } } Rect2 rect = TS->shaped_text_get_object_rect(rid, objects[i]); - //draw_rect(rect, Color(1,0,0), false, 2); //DEBUG_RECTS switch (it->type) { case ITEM_IMAGE: { ItemImage *img = static_cast<ItemImage *>(it); @@ -1015,514 +1017,416 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o const Glyph *glyphs = TS->shaped_text_get_glyphs(rid); int gl_size = TS->shaped_text_get_glyph_count(rid); + Vector2i chr_range = TS->shaped_text_get_range(rid); - Vector2 gloff = off; - // Draw outlines and shadow. - int processed_glyphs_ol = r_processed_glyphs; - for (int i = 0; i < gl_size; i++) { - Item *it = _get_item_at_pos(it_from, it_to, glyphs[i].start); - int size = _find_outline_size(it, p_outline_size); - Color font_color = _find_color(it, p_base_color); - Color font_outline_color = _find_outline_color(it, p_outline_color); - Color font_shadow_color = p_font_shadow_color; - if ((size <= 0 || font_outline_color.a == 0) && (font_shadow_color.a == 0)) { - gloff.x += glyphs[i].advance; - continue; - } - - // Get FX. - ItemFade *fade = nullptr; - Item *fade_item = it; - while (fade_item) { - if (fade_item->type == ITEM_FADE) { - fade = static_cast<ItemFade *>(fade_item); - break; - } - fade_item = fade_item->parent; - } - - Vector<ItemFX *> fx_stack; - _fetch_item_fx_stack(it, fx_stack); - bool custom_fx_ok = true; - - Point2 fx_offset = Vector2(glyphs[i].x_off, glyphs[i].y_off); - RID frid = glyphs[i].font_rid; - uint32_t gl = glyphs[i].index; - uint16_t gl_fl = glyphs[i].flags; - uint8_t gl_cn = glyphs[i].count; - bool cprev_cluster = false; - bool cprev_conn = false; - if (gl_cn == 0) { // Parts of the same cluster, always connected. - cprev_cluster = true; - } - if (gl_fl & TextServer::GRAPHEME_IS_RTL) { // Check if previous grapheme cluster is connected. - if (i > 0 && (glyphs[i - 1].flags & TextServer::GRAPHEME_IS_CONNECTED)) { - cprev_conn = true; - } - } else { - if (glyphs[i].flags & TextServer::GRAPHEME_IS_CONNECTED) { - cprev_conn = true; - } - } + int sel_start = -1; + int sel_end = -1; - //Apply fx. - if (fade) { - float faded_visibility = 1.0f; - if (glyphs[i].start >= fade->starting_index) { - faded_visibility -= (float)(glyphs[i].start - fade->starting_index) / (float)fade->length; - faded_visibility = faded_visibility < 0.0f ? 0.0f : faded_visibility; - } - font_outline_color.a = faded_visibility; - font_shadow_color.a = faded_visibility; - } - - bool txt_visible = (font_outline_color.a != 0) || (font_shadow_color.a != 0); - Transform2D char_xform; - char_xform.set_origin(gloff + p_ofs); - - for (int j = 0; j < fx_stack.size(); j++) { - ItemFX *item_fx = fx_stack[j]; - bool cn = cprev_cluster || (cprev_conn && item_fx->connected); - - if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { - ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); - - Ref<CharFXTransform> charfx = item_custom->char_fx_transform; - Ref<RichTextEffect> custom_effect = item_custom->custom_effect; - - if (!custom_effect.is_null()) { - charfx->elapsed_time = item_custom->elapsed_time; - charfx->range = Vector2i(l.char_offset + glyphs[i].start, l.char_offset + glyphs[i].end); - charfx->relative_index = l.char_offset + glyphs[i].start - item_fx->char_ofs; - charfx->visibility = txt_visible; - charfx->outline = true; - charfx->font = frid; - charfx->glyph_index = gl; - charfx->glyph_flags = gl_fl; - charfx->glyph_count = gl_cn; - charfx->offset = fx_offset; - charfx->color = font_color; - charfx->transform = char_xform; - - bool effect_status = custom_effect->_process_effect_impl(charfx); - custom_fx_ok = effect_status; - - char_xform = charfx->transform; - fx_offset += charfx->offset; - font_color = charfx->color; - frid = charfx->font; - gl = charfx->glyph_index; - txt_visible &= charfx->visibility; + if (selection.active && (selection.from_frame->lines[selection.from_line].char_offset + selection.from_char) <= (l.char_offset + chr_range.y) && (selection.to_frame->lines[selection.to_line].char_offset + selection.to_char) >= (l.char_offset + chr_range.x)) { + sel_start = MAX(chr_range.x, (selection.from_frame->lines[selection.from_line].char_offset + selection.from_char) - l.char_offset); + sel_end = MIN(chr_range.y, (selection.to_frame->lines[selection.to_line].char_offset + selection.to_char) - l.char_offset); + } + + int processed_glyphs_step = 0; + for (int step = DRAW_STEP_BACKGROUND; step < DRAW_STEP_MAX; step++) { + Vector2 off_step = off; + processed_glyphs_step = r_processed_glyphs; + + Vector2 ul_start; + bool ul_started = false; + Color ul_color_prev; + Color ul_color; + + Vector2 dot_ul_start; + bool dot_ul_started = false; + Color dot_ul_color_prev; + Color dot_ul_color; + + Vector2 st_start; + bool st_started = false; + Color st_color_prev; + Color st_color; + + float box_start = 0.0; + Color last_color = Color(0, 0, 0, 0); + + for (int i = 0; i < gl_size; i++) { + bool selected = selection.active && (sel_start != -1) && (glyphs[i].start >= sel_start) && (glyphs[i].end <= sel_end); + Item *it = _get_item_at_pos(it_from, it_to, glyphs[i].start); + + Color font_color = (step == DRAW_STEP_SHADOW || step == DRAW_STEP_OUTLINE || step == DRAW_STEP_TEXT) ? _find_color(it, p_base_color) : Color(); + int outline_size = (step == DRAW_STEP_OUTLINE) ? _find_outline_size(it, p_outline_size) : 0; + Color font_outline_color = (step == DRAW_STEP_OUTLINE) ? _find_outline_color(it, p_outline_color) : Color(); + Color font_shadow_color = p_font_shadow_color; + bool txt_visible = false; + if (step == DRAW_STEP_OUTLINE) { + txt_visible = (font_outline_color.a != 0 && outline_size > 0); + } else if (step == DRAW_STEP_SHADOW) { + txt_visible = (font_shadow_color.a != 0); + } else if (step == DRAW_STEP_TEXT) { + txt_visible = (font_color.a != 0); + bool has_ul = _find_underline(it); + if (!has_ul && underline_meta) { + ItemMeta *meta = nullptr; + if (_find_meta(it, nullptr, &meta) && meta) { + switch (meta->underline) { + case META_UNDERLINE_ALWAYS: { + has_ul = true; + } break; + case META_UNDERLINE_NEVER: { + has_ul = false; + } break; + case META_UNDERLINE_ON_HOVER: { + has_ul = (meta == meta_hovering); + } break; + } + } } - } else if (item_fx->type == ITEM_SHAKE) { - ItemShake *item_shake = static_cast<ItemShake *>(item_fx); - - if (!cn) { - uint64_t char_current_rand = item_shake->offset_random(glyphs[i].start); - uint64_t char_previous_rand = item_shake->offset_previous_random(glyphs[i].start); - uint64_t max_rand = 2147483647; - double current_offset = Math::remap(char_current_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); - double previous_offset = Math::remap(char_previous_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); - double n_time = (double)(item_shake->elapsed_time / (0.5f / item_shake->rate)); - n_time = (n_time > 1.0) ? 1.0 : n_time; - item_shake->prev_off = Point2(Math::lerp(Math::sin(previous_offset), Math::sin(current_offset), n_time), Math::lerp(Math::cos(previous_offset), Math::cos(current_offset), n_time)) * (float)item_shake->strength / 10.0f; + if (has_ul) { + if (ul_started && font_color != ul_color_prev) { + float y_off = upos; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), ul_color, underline_width); + ul_start = p_ofs + Vector2(off_step.x, off_step.y); + ul_color_prev = font_color; + ul_color = font_color; + ul_color.a *= 0.5; + } else if (!ul_started) { + ul_started = true; + ul_start = p_ofs + Vector2(off_step.x, off_step.y); + ul_color_prev = font_color; + ul_color = font_color; + ul_color.a *= 0.5; + } + } else if (ul_started) { + ul_started = false; + float y_off = upos; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), ul_color, underline_width); } - fx_offset += item_shake->prev_off; - } else if (item_fx->type == ITEM_WAVE) { - ItemWave *item_wave = static_cast<ItemWave *>(item_fx); - - if (!cn) { - double value = Math::sin(item_wave->frequency * item_wave->elapsed_time + ((p_ofs.x + gloff.x) / 50)) * (item_wave->amplitude / 10.0f); - item_wave->prev_off = Point2(0, 1) * value; + if (_find_hint(it, nullptr) && underline_hint) { + if (dot_ul_started && font_color != dot_ul_color_prev) { + float y_off = upos; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), dot_ul_color, underline_width, MAX(2.0, underline_width * 2)); + dot_ul_start = p_ofs + Vector2(off_step.x, off_step.y); + dot_ul_color_prev = font_color; + dot_ul_color = font_color; + dot_ul_color.a *= 0.5; + } else if (!dot_ul_started) { + dot_ul_started = true; + dot_ul_start = p_ofs + Vector2(off_step.x, off_step.y); + dot_ul_color_prev = font_color; + dot_ul_color = font_color; + dot_ul_color.a *= 0.5; + } + } else if (dot_ul_started) { + dot_ul_started = false; + float y_off = upos; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), dot_ul_color, underline_width, MAX(2.0, underline_width * 2)); } - fx_offset += item_wave->prev_off; - } else if (item_fx->type == ITEM_TORNADO) { - ItemTornado *item_tornado = static_cast<ItemTornado *>(item_fx); - - if (!cn) { - double torn_x = Math::sin(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + gloff.x) / 50)) * (item_tornado->radius); - double torn_y = Math::cos(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + gloff.x) / 50)) * (item_tornado->radius); - item_tornado->prev_off = Point2(torn_x, torn_y); + if (_find_strikethrough(it)) { + if (st_started && font_color != st_color_prev) { + float y_off = -l_ascent + l_size.y / 2; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), st_color, underline_width); + st_start = p_ofs + Vector2(off_step.x, off_step.y); + st_color_prev = font_color; + st_color = font_color; + st_color.a *= 0.5; + } else if (!st_started) { + st_started = true; + st_start = p_ofs + Vector2(off_step.x, off_step.y); + st_color_prev = font_color; + st_color = font_color; + st_color.a *= 0.5; + } + } else if (st_started) { + st_started = false; + float y_off = -l_ascent + l_size.y / 2; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), st_color, underline_width); } - fx_offset += item_tornado->prev_off; - } else if (item_fx->type == ITEM_RAINBOW) { - ItemRainbow *item_rainbow = static_cast<ItemRainbow *>(item_fx); - - font_color = font_color.from_hsv(item_rainbow->frequency * (item_rainbow->elapsed_time + ((p_ofs.x + gloff.x) / 50)), item_rainbow->saturation, item_rainbow->value, font_color.a); - } else if (item_fx->type == ITEM_PULSE) { - ItemPulse *item_pulse = static_cast<ItemPulse *>(item_fx); - - const float sined_time = (Math::ease(Math::pingpong(item_pulse->elapsed_time, 1.0 / item_pulse->frequency) * item_pulse->frequency, item_pulse->ease)); - font_color = font_color.lerp(font_color * item_pulse->color, sined_time); } - } + if (step == DRAW_STEP_SHADOW || step == DRAW_STEP_OUTLINE || step == DRAW_STEP_TEXT) { + ItemFade *fade = nullptr; + Item *fade_item = it; + while (fade_item) { + if (fade_item->type == ITEM_FADE) { + fade = static_cast<ItemFade *>(fade_item); + break; + } + fade_item = fade_item->parent; + } - if (is_inside_tree() && get_viewport()->is_snap_2d_transforms_to_pixel_enabled()) { - fx_offset = fx_offset.round(); - } - Vector2 char_off = char_xform.get_origin(); - - // Draw glyph outlines. - const Color modulated_outline_color = font_outline_color * Color(1, 1, 1, font_color.a); - const Color modulated_shadow_color = font_shadow_color * Color(1, 1, 1, font_color.a); - for (int j = 0; j < glyphs[i].repeat; j++) { - if (txt_visible) { - bool skip = (trim_chars && l.char_offset + glyphs[i].end > visible_characters) || (trim_glyphs_ltr && (processed_glyphs_ol >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs_ol < total_glyphs - visible_glyphs)); - if (!skip && frid != RID()) { - if (modulated_shadow_color.a > 0) { - Transform2D char_reverse_xform; - char_reverse_xform.set_origin(-char_off - p_shadow_ofs); - Transform2D char_final_xform = char_xform * char_reverse_xform; - char_final_xform.columns[2] += p_shadow_ofs; - draw_set_transform_matrix(char_final_xform); - - TS->font_draw_glyph(frid, ci, glyphs[i].font_size, fx_offset + char_off + p_shadow_ofs, gl, modulated_shadow_color); - if (p_shadow_outline_size > 0) { - TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, p_shadow_outline_size, fx_offset + char_off + p_shadow_ofs, gl, modulated_shadow_color); - } + Vector<ItemFX *> fx_stack; + _fetch_item_fx_stack(it, fx_stack); + bool custom_fx_ok = true; + + Point2 fx_offset = Vector2(glyphs[i].x_off, glyphs[i].y_off); + RID frid = glyphs[i].font_rid; + uint32_t gl = glyphs[i].index; + uint16_t gl_fl = glyphs[i].flags; + uint8_t gl_cn = glyphs[i].count; + bool cprev_cluster = false; + bool cprev_conn = false; + if (gl_cn == 0) { // Parts of the same grapheme cluster, always connected. + cprev_cluster = true; + } + if (gl_fl & TextServer::GRAPHEME_IS_RTL) { // Check if previous grapheme cluster is connected. + if (i > 0 && (glyphs[i - 1].flags & TextServer::GRAPHEME_IS_CONNECTED)) { + cprev_conn = true; } - if (modulated_outline_color.a != 0.0 && size > 0) { - Transform2D char_reverse_xform; - char_reverse_xform.set_origin(-char_off); - Transform2D char_final_xform = char_xform * char_reverse_xform; - draw_set_transform_matrix(char_final_xform); + } else { + if (glyphs[i].flags & TextServer::GRAPHEME_IS_CONNECTED) { + cprev_conn = true; + } + } - TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, fx_offset + char_off, gl, modulated_outline_color); + //Apply fx. + if (fade) { + float faded_visibility = 1.0f; + if (glyphs[i].start >= fade->starting_index) { + faded_visibility -= (float)(glyphs[i].start - fade->starting_index) / (float)fade->length; + faded_visibility = faded_visibility < 0.0f ? 0.0f : faded_visibility; } + font_color.a = faded_visibility; } - processed_glyphs_ol++; - } - gloff.x += glyphs[i].advance; - } - } - draw_set_transform_matrix(Transform2D()); - Vector2 fbg_line_off = off + p_ofs; - // Draw background color box - Vector2i chr_range = TS->shaped_text_get_range(rid); - _draw_fbg_boxes(ci, rid, fbg_line_off, it_from, it_to, chr_range.x, chr_range.y, 0); + Transform2D char_xform; + char_xform.set_origin(p_ofs + off_step); + + for (int j = 0; j < fx_stack.size(); j++) { + ItemFX *item_fx = fx_stack[j]; + bool cn = cprev_cluster || (cprev_conn && item_fx->connected); + + if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { + ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); + + Ref<CharFXTransform> charfx = item_custom->char_fx_transform; + Ref<RichTextEffect> custom_effect = item_custom->custom_effect; + + if (!custom_effect.is_null()) { + charfx->elapsed_time = item_custom->elapsed_time; + charfx->range = Vector2i(l.char_offset + glyphs[i].start, l.char_offset + glyphs[i].end); + charfx->relative_index = l.char_offset + glyphs[i].start - item_fx->char_ofs; + charfx->visibility = txt_visible; + charfx->outline = (step == DRAW_STEP_SHADOW) || (step == DRAW_STEP_OUTLINE); + charfx->font = frid; + charfx->glyph_index = gl; + charfx->glyph_flags = gl_fl; + charfx->glyph_count = gl_cn; + charfx->offset = fx_offset; + charfx->color = font_color; + charfx->transform = char_xform; + + bool effect_status = custom_effect->_process_effect_impl(charfx); + custom_fx_ok = effect_status; + + char_xform = charfx->transform; + fx_offset += charfx->offset; + font_color = charfx->color; + frid = charfx->font; + gl = charfx->glyph_index; + txt_visible &= charfx->visibility; + } + } else if (item_fx->type == ITEM_SHAKE) { + ItemShake *item_shake = static_cast<ItemShake *>(item_fx); + + if (!cn) { + uint64_t char_current_rand = item_shake->offset_random(glyphs[i].start); + uint64_t char_previous_rand = item_shake->offset_previous_random(glyphs[i].start); + uint64_t max_rand = 2147483647; + double current_offset = Math::remap(char_current_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double previous_offset = Math::remap(char_previous_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double n_time = (double)(item_shake->elapsed_time / (0.5f / item_shake->rate)); + n_time = (n_time > 1.0) ? 1.0 : n_time; + item_shake->prev_off = Point2(Math::lerp(Math::sin(previous_offset), Math::sin(current_offset), n_time), Math::lerp(Math::cos(previous_offset), Math::cos(current_offset), n_time)) * (float)item_shake->strength / 10.0f; + } + fx_offset += item_shake->prev_off; + } else if (item_fx->type == ITEM_WAVE) { + ItemWave *item_wave = static_cast<ItemWave *>(item_fx); - // Draw main text. - Color selection_bg = theme_cache.selection_color; + if (!cn) { + double value = Math::sin(item_wave->frequency * item_wave->elapsed_time + ((p_ofs.x + off_step.x) / 50)) * (item_wave->amplitude / 10.0f); + item_wave->prev_off = Point2(0, 1) * value; + } + fx_offset += item_wave->prev_off; + } else if (item_fx->type == ITEM_TORNADO) { + ItemTornado *item_tornado = static_cast<ItemTornado *>(item_fx); + + if (!cn) { + double torn_x = Math::sin(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + off_step.x) / 50)) * (item_tornado->radius); + double torn_y = Math::cos(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + off_step.x) / 50)) * (item_tornado->radius); + item_tornado->prev_off = Point2(torn_x, torn_y); + } + fx_offset += item_tornado->prev_off; + } else if (item_fx->type == ITEM_RAINBOW) { + ItemRainbow *item_rainbow = static_cast<ItemRainbow *>(item_fx); - int sel_start = -1; - int sel_end = -1; + font_color = font_color.from_hsv(item_rainbow->frequency * (item_rainbow->elapsed_time + ((p_ofs.x + off_step.x) / 50)), item_rainbow->saturation, item_rainbow->value, font_color.a); + } else if (item_fx->type == ITEM_PULSE) { + ItemPulse *item_pulse = static_cast<ItemPulse *>(item_fx); - if (selection.active && (selection.from_frame->lines[selection.from_line].char_offset + selection.from_char) <= (l.char_offset + TS->shaped_text_get_range(rid).y) && (selection.to_frame->lines[selection.to_line].char_offset + selection.to_char) >= (l.char_offset + TS->shaped_text_get_range(rid).x)) { - sel_start = MAX(TS->shaped_text_get_range(rid).x, (selection.from_frame->lines[selection.from_line].char_offset + selection.from_char) - l.char_offset); - sel_end = MIN(TS->shaped_text_get_range(rid).y, (selection.to_frame->lines[selection.to_line].char_offset + selection.to_char) - l.char_offset); - - Vector<Vector2> sel = TS->shaped_text_get_selection(rid, sel_start, sel_end); - for (int i = 0; i < sel.size(); i++) { - Rect2 rect = Rect2(sel[i].x + p_ofs.x + off.x, p_ofs.y + off.y - TS->shaped_text_get_ascent(rid), sel[i].y - sel[i].x, TS->shaped_text_get_size(rid).y); - RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, selection_bg); - } - } - - Vector2 ul_start; - bool ul_started = false; - Color ul_color_prev; - Color ul_color; - - Vector2 dot_ul_start; - bool dot_ul_started = false; - Color dot_ul_color_prev; - Color dot_ul_color; - - Vector2 st_start; - bool st_started = false; - Color st_color_prev; - Color st_color; - - for (int i = 0; i < gl_size; i++) { - bool selected = selection.active && (sel_start != -1) && (glyphs[i].start >= sel_start) && (glyphs[i].end <= sel_end); - Item *it = _get_item_at_pos(it_from, it_to, glyphs[i].start); - bool has_ul = _find_underline(it); - if (!has_ul && underline_meta) { - ItemMeta *meta = nullptr; - if (_find_meta(it, nullptr, &meta) && meta) { - switch (meta->underline) { - case META_UNDERLINE_ALWAYS: { - has_ul = true; - } break; - case META_UNDERLINE_NEVER: { - has_ul = false; - } break; - case META_UNDERLINE_ON_HOVER: { - has_ul = (meta == meta_hovering); - } break; + const float sined_time = (Math::ease(Math::pingpong(item_pulse->elapsed_time, 1.0 / item_pulse->frequency) * item_pulse->frequency, item_pulse->ease)); + font_color = font_color.lerp(font_color * item_pulse->color, sined_time); + } } - } - } - Color font_color = _find_color(it, p_base_color); - if (has_ul) { - if (ul_started && font_color != ul_color_prev) { - float y_off = TS->shaped_text_get_underline_position(rid); - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), ul_color, underline_width); - ul_start = p_ofs + Vector2(off.x, off.y); - ul_color_prev = font_color; - ul_color = font_color; - ul_color.a *= 0.5; - } else if (!ul_started) { - ul_started = true; - ul_start = p_ofs + Vector2(off.x, off.y); - ul_color_prev = font_color; - ul_color = font_color; - ul_color.a *= 0.5; - } - } else if (ul_started) { - ul_started = false; - float y_off = TS->shaped_text_get_underline_position(rid); - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), ul_color, underline_width); - } - if (_find_hint(it, nullptr) && underline_hint) { - if (dot_ul_started && font_color != dot_ul_color_prev) { - float y_off = TS->shaped_text_get_underline_position(rid); - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), dot_ul_color, underline_width, MAX(2.0, underline_width * 2)); - dot_ul_start = p_ofs + Vector2(off.x, off.y); - dot_ul_color_prev = font_color; - dot_ul_color = font_color; - dot_ul_color.a *= 0.5; - } else if (!dot_ul_started) { - dot_ul_started = true; - dot_ul_start = p_ofs + Vector2(off.x, off.y); - dot_ul_color_prev = font_color; - dot_ul_color = font_color; - dot_ul_color.a *= 0.5; - } - } else if (dot_ul_started) { - dot_ul_started = false; - float y_off = TS->shaped_text_get_underline_position(rid); - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), dot_ul_color, underline_width, MAX(2.0, underline_width * 2)); - } - if (_find_strikethrough(it)) { - if (st_started && font_color != st_color_prev) { - float y_off = -TS->shaped_text_get_ascent(rid) + TS->shaped_text_get_size(rid).y / 2; - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), st_color, underline_width); - st_start = p_ofs + Vector2(off.x, off.y); - st_color_prev = font_color; - st_color = font_color; - st_color.a *= 0.5; - } else if (!st_started) { - st_started = true; - st_start = p_ofs + Vector2(off.x, off.y); - st_color_prev = font_color; - st_color = font_color; - st_color.a *= 0.5; - } - } else if (st_started) { - st_started = false; - float y_off = -TS->shaped_text_get_ascent(rid) + TS->shaped_text_get_size(rid).y / 2; - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), st_color, underline_width); - } - - // Get FX. - ItemFade *fade = nullptr; - Item *fade_item = it; - while (fade_item) { - if (fade_item->type == ITEM_FADE) { - fade = static_cast<ItemFade *>(fade_item); - break; - } - fade_item = fade_item->parent; - } - - Vector<ItemFX *> fx_stack; - _fetch_item_fx_stack(it, fx_stack); - bool custom_fx_ok = true; - - Point2 fx_offset = Vector2(glyphs[i].x_off, glyphs[i].y_off); - RID frid = glyphs[i].font_rid; - uint32_t gl = glyphs[i].index; - uint16_t gl_fl = glyphs[i].flags; - uint8_t gl_cn = glyphs[i].count; - bool cprev_cluster = false; - bool cprev_conn = false; - if (gl_cn == 0) { // Parts of the same grapheme cluster, always connected. - cprev_cluster = true; - } - if (gl_fl & TextServer::GRAPHEME_IS_RTL) { // Check if previous grapheme cluster is connected. - if (i > 0 && (glyphs[i - 1].flags & TextServer::GRAPHEME_IS_CONNECTED)) { - cprev_conn = true; - } - } else { - if (glyphs[i].flags & TextServer::GRAPHEME_IS_CONNECTED) { - cprev_conn = true; - } - } - //Apply fx. - if (fade) { - float faded_visibility = 1.0f; - if (glyphs[i].start >= fade->starting_index) { - faded_visibility -= (float)(glyphs[i].start - fade->starting_index) / (float)fade->length; - faded_visibility = faded_visibility < 0.0f ? 0.0f : faded_visibility; - } - font_color.a = faded_visibility; - } - - bool txt_visible = (font_color.a != 0); - - Transform2D char_xform; - char_xform.set_origin(p_ofs + off); - - for (int j = 0; j < fx_stack.size(); j++) { - ItemFX *item_fx = fx_stack[j]; - bool cn = cprev_cluster || (cprev_conn && item_fx->connected); - - if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { - ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); - - Ref<CharFXTransform> charfx = item_custom->char_fx_transform; - Ref<RichTextEffect> custom_effect = item_custom->custom_effect; - - if (!custom_effect.is_null()) { - charfx->elapsed_time = item_custom->elapsed_time; - charfx->range = Vector2i(l.char_offset + glyphs[i].start, l.char_offset + glyphs[i].end); - charfx->relative_index = l.char_offset + glyphs[i].start - item_fx->char_ofs; - charfx->visibility = txt_visible; - charfx->outline = false; - charfx->font = frid; - charfx->glyph_index = gl; - charfx->glyph_flags = gl_fl; - charfx->glyph_count = gl_cn; - charfx->offset = fx_offset; - charfx->color = font_color; - charfx->transform = char_xform; - - bool effect_status = custom_effect->_process_effect_impl(charfx); - custom_fx_ok = effect_status; - - char_xform = charfx->transform; - fx_offset += charfx->offset; - font_color = charfx->color; - frid = charfx->font; - gl = charfx->glyph_index; - txt_visible &= charfx->visibility; + if (is_inside_tree() && get_viewport()->is_snap_2d_transforms_to_pixel_enabled()) { + fx_offset = fx_offset.round(); } - } else if (item_fx->type == ITEM_SHAKE) { - ItemShake *item_shake = static_cast<ItemShake *>(item_fx); - - if (!cn) { - uint64_t char_current_rand = item_shake->offset_random(glyphs[i].start); - uint64_t char_previous_rand = item_shake->offset_previous_random(glyphs[i].start); - uint64_t max_rand = 2147483647; - double current_offset = Math::remap(char_current_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); - double previous_offset = Math::remap(char_previous_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); - double n_time = (double)(item_shake->elapsed_time / (0.5f / item_shake->rate)); - n_time = (n_time > 1.0) ? 1.0 : n_time; - item_shake->prev_off = Point2(Math::lerp(Math::sin(previous_offset), Math::sin(current_offset), n_time), Math::lerp(Math::cos(previous_offset), Math::cos(current_offset), n_time)) * (float)item_shake->strength / 10.0f; + + Vector2 char_off = char_xform.get_origin(); + Transform2D char_reverse_xform; + if (step == DRAW_STEP_TEXT) { + if (selected && use_selected_font_color) { + font_color = theme_cache.font_selected_color; + } + + char_reverse_xform.set_origin(-char_off); + Transform2D char_final_xform = char_xform * char_reverse_xform; + draw_set_transform_matrix(char_final_xform); + } else if (step == DRAW_STEP_SHADOW) { + font_color = font_shadow_color * Color(1, 1, 1, font_color.a); + + char_reverse_xform.set_origin(-char_off - p_shadow_ofs); + Transform2D char_final_xform = char_xform * char_reverse_xform; + char_final_xform.columns[2] += p_shadow_ofs; + draw_set_transform_matrix(char_final_xform); + } else if (step == DRAW_STEP_OUTLINE) { + font_color = font_outline_color * Color(1, 1, 1, font_color.a); + + char_reverse_xform.set_origin(-char_off); + Transform2D char_final_xform = char_xform * char_reverse_xform; + draw_set_transform_matrix(char_final_xform); } - fx_offset += item_shake->prev_off; - } else if (item_fx->type == ITEM_WAVE) { - ItemWave *item_wave = static_cast<ItemWave *>(item_fx); - if (!cn) { - double value = Math::sin(item_wave->frequency * item_wave->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_wave->amplitude / 10.0f); - item_wave->prev_off = Point2(0, 1) * value; + // Draw glyphs. + for (int j = 0; j < glyphs[i].repeat; j++) { + bool skip = (trim_chars && l.char_offset + glyphs[i].end > visible_characters) || (trim_glyphs_ltr && (processed_glyphs_step >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs_step < total_glyphs - visible_glyphs)); + if (!skip) { + if (txt_visible) { + if (step == DRAW_STEP_TEXT) { + if (frid != RID()) { + TS->font_draw_glyph(frid, ci, glyphs[i].font_size, fx_offset + char_off, gl, font_color); + } else if (((glyphs[i].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) && ((glyphs[i].flags & TextServer::GRAPHEME_IS_EMBEDDED_OBJECT) != TextServer::GRAPHEME_IS_EMBEDDED_OBJECT)) { + TS->draw_hex_code_box(ci, glyphs[i].font_size, fx_offset + char_off, gl, font_color); + } + } else if (step == DRAW_STEP_SHADOW && frid != RID()) { + TS->font_draw_glyph(frid, ci, glyphs[i].font_size, fx_offset + char_off + p_shadow_ofs, gl, font_color); + if (p_shadow_outline_size > 0) { + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, p_shadow_outline_size, fx_offset + char_off + p_shadow_ofs, gl, font_color); + } + } else if (step == DRAW_STEP_OUTLINE && frid != RID() && outline_size > 0) { + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, outline_size, fx_offset + char_off, gl, font_color); + } + } + processed_glyphs_step++; + } + if (step == DRAW_STEP_TEXT && skip) { + // Finish underline/overline/strikethrough is previous glyph is skipped. + if (ul_started) { + ul_started = false; + float y_off = upos; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), ul_color, underline_width); + } + if (dot_ul_started) { + dot_ul_started = false; + float y_off = upos; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), dot_ul_color, underline_width, MAX(2.0, underline_width * 2)); + } + if (st_started) { + st_started = false; + float y_off = -l_ascent + l_size.y / 2; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), st_color, underline_width); + } + } + off_step.x += glyphs[i].advance; } - fx_offset += item_wave->prev_off; - } else if (item_fx->type == ITEM_TORNADO) { - ItemTornado *item_tornado = static_cast<ItemTornado *>(item_fx); - - if (!cn) { - double torn_x = Math::sin(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_tornado->radius); - double torn_y = Math::cos(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_tornado->radius); - item_tornado->prev_off = Point2(torn_x, torn_y); + draw_set_transform_matrix(Transform2D()); + } + // Draw boxes. + if (step == DRAW_STEP_BACKGROUND || step == DRAW_STEP_FOREGROUND) { + for (int j = 0; j < glyphs[i].repeat; j++) { + bool skip = (trim_chars && l.char_offset + glyphs[i].end > visible_characters) || (trim_glyphs_ltr && (processed_glyphs_step >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs_step < total_glyphs - visible_glyphs)); + if (!skip) { + Color color; + if (step == DRAW_STEP_BACKGROUND) { + color = _find_bgcolor(it); + } else if (step == DRAW_STEP_FOREGROUND) { + color = _find_fgcolor(it); + } + if (color != last_color) { + if (last_color.a > 0.0) { + Vector2 rect_off = p_ofs + Vector2(box_start - theme_cache.text_highlight_h_padding, off_step.y - l_ascent - theme_cache.text_highlight_v_padding); + Vector2 rect_size = Vector2(off_step.x - box_start + 2 * theme_cache.text_highlight_h_padding, l_size.y + 2 * theme_cache.text_highlight_v_padding); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(rect_off, rect_size), last_color); + } + if (color.a > 0.0) { + box_start = off_step.x; + } + } + last_color = color; + processed_glyphs_step++; + } else { + // Finish box is previous glyph is skipped. + if (last_color.a > 0.0) { + Vector2 rect_off = p_ofs + Vector2(box_start - theme_cache.text_highlight_h_padding, off_step.y - l_ascent - theme_cache.text_highlight_v_padding); + Vector2 rect_size = Vector2(off_step.x - box_start + 2 * theme_cache.text_highlight_h_padding, l_size.y + 2 * theme_cache.text_highlight_v_padding); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(rect_off, rect_size), last_color); + } + last_color = Color(0, 0, 0, 0); + } + off_step.x += glyphs[i].advance; } - fx_offset += item_tornado->prev_off; - } else if (item_fx->type == ITEM_RAINBOW) { - ItemRainbow *item_rainbow = static_cast<ItemRainbow *>(item_fx); - - font_color = font_color.from_hsv(item_rainbow->frequency * (item_rainbow->elapsed_time + ((p_ofs.x + off.x) / 50)), item_rainbow->saturation, item_rainbow->value, font_color.a); - } else if (item_fx->type == ITEM_PULSE) { - ItemPulse *item_pulse = static_cast<ItemPulse *>(item_fx); - - const float sined_time = (Math::ease(Math::pingpong(item_pulse->elapsed_time, 1.0 / item_pulse->frequency) * item_pulse->frequency, item_pulse->ease)); - font_color = font_color.lerp(font_color * item_pulse->color, sined_time); } } - - if (is_inside_tree() && get_viewport()->is_snap_2d_transforms_to_pixel_enabled()) { - fx_offset = fx_offset.round(); + // Finish lines and boxes. + if (step == DRAW_STEP_BACKGROUND) { + if (sel_start != -1) { + Color selection_bg = theme_cache.selection_color; + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, sel_start, sel_end); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + p_ofs.x + off.x, p_ofs.y + off.y - l_ascent, sel[i].y - sel[i].x, l_size.y); // Note: use "off" not "off_step", selection is relative to the line start. + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, selection_bg); + } + } } - Vector2 char_off = char_xform.get_origin(); - - Transform2D char_reverse_xform; - char_reverse_xform.set_origin(-char_off); - char_xform = char_xform * char_reverse_xform; - draw_set_transform_matrix(char_xform); - - if (selected && use_selected_font_color) { - font_color = theme_cache.font_selected_color; + if (step == DRAW_STEP_BACKGROUND || step == DRAW_STEP_FOREGROUND) { + if (last_color.a > 0.0) { + Vector2 rect_off = p_ofs + Vector2(box_start - theme_cache.text_highlight_h_padding, off_step.y - l_ascent - theme_cache.text_highlight_v_padding); + Vector2 rect_size = Vector2(off_step.x - box_start + 2 * theme_cache.text_highlight_h_padding, l_size.y + 2 * theme_cache.text_highlight_v_padding); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(rect_off, rect_size), last_color); + } } - - // Draw glyphs. - for (int j = 0; j < glyphs[i].repeat; j++) { - bool skip = (trim_chars && l.char_offset + glyphs[i].end > visible_characters) || (trim_glyphs_ltr && (r_processed_glyphs >= visible_glyphs)) || (trim_glyphs_rtl && (r_processed_glyphs < total_glyphs - visible_glyphs)); - if (txt_visible) { - if (!skip) { - if (frid != RID()) { - TS->font_draw_glyph(frid, ci, glyphs[i].font_size, fx_offset + char_off, gl, font_color); - } else if (((glyphs[i].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) && ((glyphs[i].flags & TextServer::GRAPHEME_IS_EMBEDDED_OBJECT) != TextServer::GRAPHEME_IS_EMBEDDED_OBJECT)) { - TS->draw_hex_code_box(ci, glyphs[i].font_size, fx_offset + char_off, gl, font_color); - } - } - r_processed_glyphs++; + if (step == DRAW_STEP_TEXT) { + if (ul_started) { + ul_started = false; + float y_off = upos; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), ul_color, underline_width); } - if (skip) { - // End underline/overline/strikethrough is previous glyph is skipped. - if (ul_started) { - ul_started = false; - float y_off = TS->shaped_text_get_underline_position(rid); - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), ul_color, underline_width); - } - if (dot_ul_started) { - dot_ul_started = false; - float y_off = TS->shaped_text_get_underline_position(rid); - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), dot_ul_color, underline_width, MAX(2.0, underline_width * 2)); - } - if (st_started) { - st_started = false; - float y_off = -TS->shaped_text_get_ascent(rid) + TS->shaped_text_get_size(rid).y / 2; - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), st_color, underline_width); - } + if (dot_ul_started) { + dot_ul_started = false; + float y_off = upos; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), dot_ul_color, underline_width, MAX(2.0, underline_width * 2)); + } + if (st_started) { + st_started = false; + float y_off = -l_ascent + l_size.y / 2; + float underline_width = MAX(1.0, uth * theme_cache.base_scale); + draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off_step.x, off_step.y + y_off), st_color, underline_width); } - off.x += glyphs[i].advance; } - - draw_set_transform_matrix(Transform2D()); - } - if (ul_started) { - ul_started = false; - float y_off = TS->shaped_text_get_underline_position(rid); - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), ul_color, underline_width); } - if (dot_ul_started) { - dot_ul_started = false; - float y_off = TS->shaped_text_get_underline_position(rid); - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), dot_ul_color, underline_width, MAX(2.0, underline_width * 2)); - } - if (st_started) { - st_started = false; - float y_off = -TS->shaped_text_get_ascent(rid) + TS->shaped_text_get_size(rid).y / 2; - float underline_width = MAX(1.0, TS->shaped_text_get_underline_thickness(rid) * theme_cache.base_scale); - draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), st_color, underline_width); - } - // Draw foreground color box - _draw_fbg_boxes(ci, rid, fbg_line_off, it_from, it_to, chr_range.x, chr_range.y, 1); + r_processed_glyphs = processed_glyphs_step; off.y += TS->shaped_text_get_descent(rid); } @@ -1803,7 +1707,7 @@ void RichTextLabel::_scroll_changed(double) { return; } - if (scroll_follow && vscroll->get_value() >= (vscroll->get_max() - Math::round(vscroll->get_page()))) { + if (scroll_follow && vscroll->get_value() > (vscroll->get_max() - vscroll->get_page() - 1)) { scroll_following = true; } else { scroll_following = false; @@ -4121,7 +4025,7 @@ bool RichTextLabel::is_scroll_active() const { void RichTextLabel::set_scroll_follow(bool p_follow) { scroll_follow = p_follow; - if (!vscroll->is_visible_in_tree() || vscroll->get_value() >= (vscroll->get_max() - vscroll->get_page())) { + if (!vscroll->is_visible_in_tree() || vscroll->get_value() > (vscroll->get_max() - vscroll->get_page() - 1)) { scroll_following = true; } } @@ -6404,65 +6308,6 @@ void RichTextLabel::menu_option(int p_option) { } } -void RichTextLabel::_draw_fbg_boxes(RID p_ci, RID p_rid, Vector2 line_off, Item *it_from, Item *it_to, int start, int end, int fbg_flag) { - Vector2i fbg_index = Vector2i(end, start); - Color last_color = Color(0, 0, 0, 0); - bool draw_box = false; - // Draw a box based on color tags associated with glyphs - for (int i = start; i < end; i++) { - Item *it = _get_item_at_pos(it_from, it_to, i); - Color color; - - if (fbg_flag == 0) { - color = _find_bgcolor(it); - } else { - color = _find_fgcolor(it); - } - - bool change_to_color = ((color.a > 0) && ((last_color.a - 0.0) < 0.01)); - bool change_from_color = (((color.a - 0.0) < 0.01) && (last_color.a > 0.0)); - bool change_color = (((color.a > 0) == (last_color.a > 0)) && (color != last_color)); - - if (change_to_color) { - fbg_index.x = MIN(i, fbg_index.x); - fbg_index.y = MAX(i, fbg_index.y); - } - - if (change_from_color || change_color) { - fbg_index.x = MIN(i, fbg_index.x); - fbg_index.y = MAX(i, fbg_index.y); - draw_box = true; - } - - if (draw_box) { - Vector<Vector2> sel = TS->shaped_text_get_selection(p_rid, fbg_index.x, fbg_index.y); - for (int j = 0; j < sel.size(); j++) { - Vector2 rect_off = line_off + Vector2(sel[j].x - theme_cache.text_highlight_h_padding, -TS->shaped_text_get_ascent(p_rid) - theme_cache.text_highlight_v_padding); - Vector2 rect_size = Vector2(sel[j].y - sel[j].x + 2 * theme_cache.text_highlight_h_padding, TS->shaped_text_get_size(p_rid).y + 2 * theme_cache.text_highlight_v_padding); - RenderingServer::get_singleton()->canvas_item_add_rect(p_ci, Rect2(rect_off, rect_size), last_color); - } - fbg_index = Vector2i(end, start); - draw_box = false; - } - - if (change_color) { - fbg_index.x = MIN(i, fbg_index.x); - fbg_index.y = MAX(i, fbg_index.y); - } - - last_color = color; - } - - if (last_color.a > 0) { - Vector<Vector2> sel = TS->shaped_text_get_selection(p_rid, fbg_index.x, end); - for (int i = 0; i < sel.size(); i++) { - Vector2 rect_off = line_off + Vector2(sel[i].x - theme_cache.text_highlight_h_padding, -TS->shaped_text_get_ascent(p_rid) - theme_cache.text_highlight_v_padding); - Vector2 rect_size = Vector2(sel[i].y - sel[i].x + 2 * theme_cache.text_highlight_h_padding, TS->shaped_text_get_size(p_rid).y + 2 * theme_cache.text_highlight_v_padding); - RenderingServer::get_singleton()->canvas_item_add_rect(p_ci, Rect2(rect_off, rect_size), last_color); - } - } -} - Ref<RichTextEffect> RichTextLabel::_get_custom_effect_by_code(String p_bbcode_identifier) { for (int i = 0; i < custom_effects.size(); i++) { Ref<RichTextEffect> effect = custom_effects[i]; diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index a993d922d2..371f6724d7 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -43,6 +43,15 @@ class RichTextEffect; class RichTextLabel : public Control { GDCLASS(RichTextLabel, Control); + enum RTLDrawStep { + DRAW_STEP_BACKGROUND, + DRAW_STEP_SHADOW, + DRAW_STEP_OUTLINE, + DRAW_STEP_TEXT, + DRAW_STEP_FOREGROUND, + DRAW_STEP_MAX, + }; + public: enum ListType { LIST_NUMBERS, @@ -596,7 +605,6 @@ private: Size2 _get_image_size(const Ref<Texture2D> &p_image, int p_width = 0, int p_height = 0, const Rect2 &p_region = Rect2()); - void _draw_fbg_boxes(RID p_ci, RID p_rid, Vector2 line_off, Item *it_from, Item *it_to, int start, int end, int fbg_flag); #ifndef DISABLE_DEPRECATED // Kept for compatibility from 3.x to 4.0. bool _set(const StringName &p_name, const Variant &p_value); diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index e15dca61cc..dc53cf82e6 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -812,6 +812,22 @@ Ref<Texture2D> TabContainer::get_tab_icon(int p_tab) const { return tab_bar->get_tab_icon(p_tab); } +void TabContainer::set_tab_icon_max_width(int p_tab, int p_width) { + if (tab_bar->get_tab_icon_max_width(p_tab) == p_width) { + return; + } + + tab_bar->set_tab_icon_max_width(p_tab, p_width); + + _update_margins(); + _repaint(); + queue_redraw(); +} + +int TabContainer::get_tab_icon_max_width(int p_tab) const { + return tab_bar->get_tab_icon_max_width(p_tab); +} + void TabContainer::set_tab_disabled(int p_tab, bool p_disabled) { if (tab_bar->is_tab_disabled(p_tab) == p_disabled) { return; @@ -1008,6 +1024,8 @@ void TabContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tab_tooltip", "tab_idx"), &TabContainer::get_tab_tooltip); ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &TabContainer::set_tab_icon); ClassDB::bind_method(D_METHOD("get_tab_icon", "tab_idx"), &TabContainer::get_tab_icon); + ClassDB::bind_method(D_METHOD("set_tab_icon_max_width", "tab_idx", "width"), &TabContainer::set_tab_icon_max_width); + ClassDB::bind_method(D_METHOD("get_tab_icon_max_width", "tab_idx"), &TabContainer::get_tab_icon_max_width); ClassDB::bind_method(D_METHOD("set_tab_disabled", "tab_idx", "disabled"), &TabContainer::set_tab_disabled); ClassDB::bind_method(D_METHOD("is_tab_disabled", "tab_idx"), &TabContainer::is_tab_disabled); ClassDB::bind_method(D_METHOD("set_tab_hidden", "tab_idx", "hidden"), &TabContainer::set_tab_hidden); diff --git a/scene/gui/tab_container.h b/scene/gui/tab_container.h index c11d9824e7..e00bc780d4 100644 --- a/scene/gui/tab_container.h +++ b/scene/gui/tab_container.h @@ -161,6 +161,9 @@ public: void set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon); Ref<Texture2D> get_tab_icon(int p_tab) const; + void set_tab_icon_max_width(int p_tab, int p_width); + int get_tab_icon_max_width(int p_tab) const; + void set_tab_disabled(int p_tab, bool p_disabled); bool is_tab_disabled(int p_tab) const; diff --git a/scene/gui/text_edit.compat.inc b/scene/gui/text_edit.compat.inc new file mode 100644 index 0000000000..bf73229868 --- /dev/null +++ b/scene/gui/text_edit.compat.inc @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* text_edit.compat.inc */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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 DISABLE_DEPRECATED + +void TextEdit::_set_selection_mode_compat_86978(SelectionMode p_mode, int p_line, int p_column, int p_caret) { + set_selection_mode(p_mode); +} + +void TextEdit::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::_set_selection_mode_compat_86978, DEFVAL(-1), DEFVAL(-1), DEFVAL(0)); +} + +#endif diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 39fba72e09..4fda49a877 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "text_edit.h" +#include "text_edit.compat.inc" #include "core/config/project_settings.h" #include "core/input/input.h" @@ -451,7 +452,7 @@ void TextEdit::_notification(int p_what) { callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); } if (text_changed_dirty) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); + callable_mp(this, &TextEdit::_emit_text_changed).call_deferred(); } _update_wrap_at_column(true); } break; @@ -565,9 +566,9 @@ void TextEdit::_notification(int p_what) { Vector<BraceMatchingData> brace_matching; if (highlight_matching_braces_enabled) { - brace_matching.resize(carets.size()); + brace_matching.resize(get_caret_count()); - for (int caret = 0; caret < carets.size(); caret++) { + for (int caret = 0; caret < get_caret_count(); caret++) { if (get_caret_line(caret) < 0 || get_caret_line(caret) >= text.size() || get_caret_column(caret) < 0) { continue; } @@ -1104,7 +1105,7 @@ void TextEdit::_notification(int p_what) { // Draw selections. float char_w = theme_cache.font->get_char_size(' ', theme_cache.font_size).width; - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if (!clipped && has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c); int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c); @@ -1257,7 +1258,7 @@ void TextEdit::_notification(int p_what) { } Color gl_color = current_color; - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if (has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { // Selection int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c); int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c); @@ -1271,7 +1272,7 @@ void TextEdit::_notification(int p_what) { float char_pos = char_ofs + char_margin + ofs_x; if (char_pos >= xmargin_beg) { if (highlight_matching_braces_enabled) { - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if ((brace_matching[c].open_match_line == line && brace_matching[c].open_match_column == glyphs[j].start) || (get_caret_column(c) == glyphs[j].start && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_matching[c].open_matching || brace_matching[c].open_mismatch))) { if (brace_matching[c].open_mismatch) { @@ -1562,10 +1563,15 @@ void TextEdit::_notification(int p_what) { case MainLoop::NOTIFICATION_OS_IME_UPDATE: { if (has_focus()) { + bool had_ime_text = has_ime_text(); ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); - if (!ime_text.is_empty() && has_selection()) { + if (!had_ime_text && has_ime_text()) { + _cancel_drag_and_drop_text(); + } + + if (has_ime_text() && has_selection()) { delete_selection(); } @@ -1576,7 +1582,7 @@ void TextEdit::_notification(int p_what) { } break; case NOTIFICATION_DRAG_BEGIN: { - selecting_mode = SelectionMode::SELECTION_MODE_NONE; + set_selection_mode(SelectionMode::SELECTION_MODE_NONE); drag_action = true; dragging_minimap = false; dragging_selection = false; @@ -1587,19 +1593,31 @@ void TextEdit::_notification(int p_what) { case NOTIFICATION_DRAG_END: { if (is_drag_successful()) { if (selection_drag_attempt) { - selection_drag_attempt = false; + // Dropped elsewhere. if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { delete_selection(); } else if (deselect_on_focus_loss_enabled) { deselect(); } } - } else { - selection_drag_attempt = false; } + if (drag_caret_index >= 0) { + if (drag_caret_index < carets.size()) { + remove_caret(drag_caret_index); + } + drag_caret_index = -1; + } + selection_drag_attempt = false; drag_action = false; drag_caret_force_displayed = false; } break; + + case NOTIFICATION_MOUSE_EXIT_SELF: { + if (drag_caret_force_displayed) { + drag_caret_force_displayed = false; + queue_redraw(); + } + } break; } } @@ -1702,15 +1720,17 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { if (mb->get_button_index() == MouseButton::WHEEL_RIGHT) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); } + if (mb->get_button_index() == MouseButton::LEFT) { _reset_caret_blink_timer(); apply_ime(); Point2i pos = get_line_column_at_pos(mpos); - int row = pos.y; + int line = pos.y; int col = pos.x; + // Gutters. int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT); for (int i = 0; i < gutters.size(); i++) { if (!gutters[i].draw || gutters[i].width <= 0) { @@ -1718,14 +1738,14 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } if (mpos.x >= left_margin && mpos.x <= left_margin + gutters[i].width) { - emit_signal(SNAME("gutter_clicked"), row, i); + emit_signal(SNAME("gutter_clicked"), line, i); return; } left_margin += gutters[i].width; } - // Minimap + // Minimap. if (draw_minimap) { _update_minimap_click(); if (dragging_minimap) { @@ -1733,121 +1753,86 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } } + // Update caret. + int caret = carets.size() - 1; int prev_col = get_caret_column(caret); int prev_line = get_caret_line(caret); + int mouse_over_selection_caret = get_selection_at_line_column(line, col, true); + const int triple_click_timeout = 600; const int triple_click_tolerance = 5; bool is_triple_click = (!mb->is_double_click() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < triple_click_timeout && mb->get_position().distance_to(last_dblclk_pos) < triple_click_tolerance); if (!mb->is_double_click() && !is_triple_click) { if (mb->is_alt_pressed()) { - prev_line = row; + prev_line = line; prev_col = col; // Remove caret at clicked location. - if (carets.size() > 1) { - for (int i = 0; i < carets.size(); i++) { - // Deselect if clicked on caret or its selection. - if ((get_caret_column(i) == col && get_caret_line(i) == row) || is_mouse_over_selection(true, i)) { - remove_caret(i); - last_dblclk = 0; - return; - } + if (get_caret_count() > 1) { + // Deselect if clicked on caret or its selection. + int clicked_caret = get_selection_at_line_column(line, col, true, false); + if (clicked_caret != -1) { + remove_caret(clicked_caret); + last_dblclk = 0; + return; } } - if (is_mouse_over_selection()) { + if (mouse_over_selection_caret >= 0) { + // Did not remove selection under mouse, don't add a new caret. return; } - caret = add_caret(row, col); + // Create new caret at clicked location. + caret = add_caret(line, col); if (caret == -1) { return; } - carets.write[caret].selection.selecting_line = row; - carets.write[caret].selection.selecting_column = col; - last_dblclk = 0; - } else if (!mb->is_shift_pressed() && !is_mouse_over_selection()) { - caret = 0; - remove_secondary_carets(); - } - } - - _push_current_op(); - set_caret_line(row, false, true, 0, caret); - set_caret_column(col, false, caret); - selection_drag_attempt = false; - - if (selecting_enabled && mb->is_shift_pressed() && (get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line)) { - if (!has_selection(caret)) { - carets.write[caret].selection.active = true; - selecting_mode = SelectionMode::SELECTION_MODE_POINTER; - carets.write[caret].selection.from_column = prev_col; - carets.write[caret].selection.from_line = prev_line; - carets.write[caret].selection.to_column = carets[caret].column; - carets.write[caret].selection.to_line = carets[caret].line; - - if (get_selection_from_line(caret) > get_selection_to_line(caret) || (get_selection_from_line(caret) == get_selection_to_line(caret) && get_selection_from_column(caret) > get_selection_to_column(caret))) { - SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column); - SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line); - carets.write[caret].selection.shiftclick_left = false; - } else { - carets.write[caret].selection.shiftclick_left = true; - } - carets.write[caret].selection.selecting_line = prev_line; - carets.write[caret].selection.selecting_column = prev_col; - caret_index_edit_dirty = true; - merge_overlapping_carets(); - queue_redraw(); - } else { - if (carets[caret].line < get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column < get_selection_column(caret))) { - if (carets[caret].selection.shiftclick_left) { - carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left; - } - carets.write[caret].selection.from_column = carets[caret].column; - carets.write[caret].selection.from_line = carets[caret].line; - - } else if (carets[caret].line > get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column > get_selection_column(caret))) { - if (!carets[caret].selection.shiftclick_left) { - SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column); - SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line); - carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left; - } - carets.write[caret].selection.to_column = carets[caret].column; - carets.write[caret].selection.to_line = carets[caret].line; - + } else if (!mb->is_shift_pressed()) { + if (drag_and_drop_selection_enabled && mouse_over_selection_caret >= 0) { + // Try to drag and drop. + set_selection_mode(SelectionMode::SELECTION_MODE_NONE); + selection_drag_attempt = true; + drag_and_drop_origin_caret_index = mouse_over_selection_caret; + last_dblclk = 0; + // Don't update caret until we know if it is not drag and drop. + return; } else { - deselect(caret); + // A regular click clears all other carets. + caret = 0; + remove_secondary_carets(); + deselect(); } - caret_index_edit_dirty = true; - merge_overlapping_carets(); - queue_redraw(); } - } else if (drag_and_drop_selection_enabled && is_mouse_over_selection()) { - set_selection_mode(SelectionMode::SELECTION_MODE_NONE, get_selection_line(caret), get_selection_column(caret), caret); - // We use the main caret for dragging, so reset this one. - set_caret_line(prev_line, false, true, 0, caret); - set_caret_column(prev_col, false, caret); - selection_drag_attempt = true; - } else if (caret == 0) { - deselect(); - set_selection_mode(SelectionMode::SELECTION_MODE_POINTER, row, col); - } - if (is_triple_click) { - // Triple-click select line. - selecting_mode = SelectionMode::SELECTION_MODE_LINE; + _push_current_op(); + set_caret_line(line, false, true, -1, caret); + set_caret_column(col, false, caret); selection_drag_attempt = false; - _update_selection_mode_line(); + bool caret_moved = get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line; + + if (selecting_enabled && mb->is_shift_pressed() && !has_selection(caret) && caret_moved) { + // Select from the previous caret position. + select(prev_line, prev_col, line, col, caret); + } + + // Start regular select mode. + set_selection_mode(SelectionMode::SELECTION_MODE_POINTER); + _update_selection_mode_pointer(true); + } else if (is_triple_click) { + // Start triple-click select line mode. + set_selection_mode(SelectionMode::SELECTION_MODE_LINE); + _update_selection_mode_line(true); last_dblclk = 0; - } else if (mb->is_double_click() && text[get_caret_line(caret)].length()) { - // Double-click select word. - selecting_mode = SelectionMode::SELECTION_MODE_WORD; - _update_selection_mode_word(); + } else if (mb->is_double_click()) { + // Start double-click select word mode. + set_selection_mode(SelectionMode::SELECTION_MODE_WORD); + _update_selection_mode_word(true); last_dblclk = OS::get_singleton()->get_ticks_msec(); last_dblclk_pos = mb->get_position(); } @@ -1863,34 +1848,20 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { _push_current_op(); _reset_caret_blink_timer(); apply_ime(); + _cancel_drag_and_drop_text(); Point2i pos = get_line_column_at_pos(mpos); - int row = pos.y; - int col = pos.x; + int mouse_line = pos.y; + int mouse_column = pos.x; - bool selection_clicked = false; if (is_move_caret_on_right_click_enabled()) { - if (has_selection()) { - for (int i = 0; i < get_caret_count(); i++) { - int from_line = get_selection_from_line(i); - int to_line = get_selection_to_line(i); - int from_column = get_selection_from_column(i); - int to_column = get_selection_to_column(i); - - if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) { - // Right click in one of the selected text - selection_clicked = true; - break; - } - } - } + bool selection_clicked = get_selection_at_line_column(mouse_line, mouse_column, true) >= 0; if (!selection_clicked) { deselect(); remove_secondary_carets(); - set_caret_line(row, false, false); - set_caret_column(col); + set_caret_line(mouse_line, false, false, -1); + set_caret_column(mouse_column); } - merge_overlapping_carets(); } if (context_menu_enabled) { @@ -1908,22 +1879,20 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } if (mb->get_button_index() == MouseButton::LEFT) { - if (selection_drag_attempt && is_mouse_over_selection()) { + if (!drag_action && selection_drag_attempt && is_mouse_over_selection()) { + // This is not a drag and drop attempt, update the caret. + selection_drag_attempt = false; remove_secondary_carets(); + deselect(); Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - set_caret_line(pos.y, false, true, 0, 0); + set_caret_line(pos.y, false, true, -1, 0); set_caret_column(pos.x, true, 0); - - deselect(); } dragging_minimap = false; dragging_selection = false; can_drag_minimap = false; click_select_held->stop(); - if (!drag_action) { - selection_drag_attempt = false; - } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } @@ -1958,7 +1927,8 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { mpos.x = get_size().x - mpos.x; } - if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging. + if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) { + // Update if not in drag and drop. _reset_caret_blink_timer(); if (draw_minimap && !dragging_selection) { @@ -2011,10 +1981,19 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { if (drag_action && can_drop_data(mpos, get_viewport()->gui_get_drag_data())) { apply_ime(); + // Update drag and drop caret. drag_caret_force_displayed = true; Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - set_caret_line(pos.y, false, true, 0, 0); - set_caret_column(pos.x, true, 0); + + if (drag_caret_index == -1) { + // Force create a new caret for drag and drop. + carets.push_back(Caret()); + drag_caret_index = carets.size() - 1; + } + + drag_caret_force_displayed = true; + set_caret_line(pos.y, false, true, -1, drag_caret_index); + set_caret_column(pos.x, true, drag_caret_index); dragging_selection = true; } } @@ -2043,6 +2022,8 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { return; } + _cancel_drag_and_drop_text(); + _reset_caret_blink_timer(); // Allow unicode handling if: @@ -2321,42 +2302,36 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { - bool first_line = false; - if (!p_split_current_line) { - deselect(i); - if (p_above) { - if (get_caret_line(i) > 0) { - set_caret_line(get_caret_line(i) - 1, false, true, 0, i); - set_caret_column(text[get_caret_line(i)].length(), i == 0, i); - } else { - set_caret_column(0, i == 0, i); - first_line = true; - } - } else { - set_caret_column(text[get_caret_line(i)].length(), i == 0, i); - } - } - - insert_text_at_caret("\n", i); + begin_multicaret_edit(); - if (first_line) { - set_caret_line(0, i == 0, true, 0, i); + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + if (p_split_current_line) { + insert_text_at_caret("\n", i); + } else { + int line = get_caret_line(i); + insert_text("\n", line, p_above ? 0 : text[line].length(), p_above, p_above); + deselect(i); + set_caret_line(p_above ? line : line + 1, false, true, -1, i); + set_caret_column(0, i == 0, i); } } + + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { // Handle selection. if (p_select) { _pre_shift_selection(i); } else if (has_selection(i) && !p_move_by_word) { // If a selection is active, move caret to start of selection. - set_caret_line(get_selection_from_line(i), false, true, 0, i); + set_caret_line(get_selection_from_line(i), false, true, -1, i); set_caret_column(get_selection_from_column(i), i == 0, i); deselect(i); continue; @@ -2368,7 +2343,7 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { int cc = get_caret_column(i); // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. if (cc == 0 && get_caret_line(i) > 0) { - set_caret_line(get_caret_line(i) - 1, false, true, 0, i); + set_caret_line(get_caret_line(i) - 1, false, true, -1, i); set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); @@ -2389,7 +2364,8 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. if (get_caret_column(i) == 0) { if (get_caret_line(i) > 0) { - set_caret_line(get_caret_line(i) - get_next_visible_line_offset_from(CLAMP(get_caret_line(i) - 1, 0, text.size() - 1), -1), false, true, 0, i); + int new_caret_line = get_caret_line(i) - get_next_visible_line_offset_from(CLAMP(get_caret_line(i) - 1, 0, text.size() - 1), -1); + set_caret_line(new_caret_line, false, true, -1, i); set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } } else { @@ -2400,23 +2376,19 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { } } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { // Handle selection. if (p_select) { _pre_shift_selection(i); } else if (has_selection(i) && !p_move_by_word) { // If a selection is active, move caret to end of selection. - set_caret_line(get_selection_to_line(i), false, true, 0, i); + set_caret_line(get_selection_to_line(i), false, true, -1, i); set_caret_column(get_selection_to_column(i), i == 0, i); deselect(i); continue; @@ -2428,7 +2400,7 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { int cc = get_caret_column(i); // If the caret is at the end of the line, and not on the last line, move it down to the beginning of the next line. if (cc == text[get_caret_line(i)].length() && get_caret_line(i) < text.size() - 1) { - set_caret_line(get_caret_line(i) + 1, false, true, 0, i); + set_caret_line(get_caret_line(i) + 1, false, true, -1, i); set_caret_column(0, i == 0, i); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); @@ -2449,7 +2421,8 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { // If we are at the end of the line, move the caret to the next line down. if (get_caret_column(i) == text[get_caret_line(i)].length()) { if (get_caret_line(i) < text.size() - 1) { - set_caret_line(get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1), false, false, 0, i); + int new_caret_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1); + set_caret_line(new_caret_line, false, false, -1, i); set_caret_column(0, i == 0, i); } } else { @@ -2460,17 +2433,13 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { } } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_up(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2490,17 +2459,13 @@ void TextEdit::_move_caret_up(bool p_select) { set_caret_line(new_line, i == 0, false, 0, i); } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_down(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2516,17 +2481,13 @@ void TextEdit::_move_caret_down(bool p_select) { int new_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1); set_caret_line(new_line, i == 0, false, 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_start(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2551,17 +2512,13 @@ void TextEdit::_move_caret_to_line_start(bool p_select) { } else { set_caret_column(row_start_col, i == 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_end(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2580,17 +2537,13 @@ void TextEdit::_move_caret_to_line_end(bool p_select) { } else { set_caret_column(row_end_col, i == 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_page_up(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2600,17 +2553,13 @@ void TextEdit::_move_caret_page_up(bool p_select) { Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), -get_visible_line_count()); int n_line = get_caret_line(i) - next_line.x + 1; set_caret_line(n_line, i == 0, false, next_line.y, i); - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_page_down(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2620,10 +2569,6 @@ void TextEdit::_move_caret_page_down(bool p_select) { Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), get_visible_line_count()); int n_line = get_caret_line(i) + next_line.x - 1; set_caret_line(n_line, i == 0, false, next_line.y, i); - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } @@ -2634,58 +2579,47 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { } start_action(EditAction::ACTION_BACKSPACE); - Vector<int> carets_to_remove; + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = 0; i < caret_edit_order.size(); i++) { - int caret_idx = caret_edit_order[i]; - if (get_caret_column(caret_idx) == 0 && get_caret_line(caret_idx) == 0 && !has_selection(caret_idx)) { + Vector<int> sorted_carets = get_sorted_carets(); + sorted_carets.reverse(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (multicaret_edit_ignore_caret(caret_index)) { continue; } - if (has_selection(caret_idx) || (!p_all_to_left && !p_word) || get_caret_column(caret_idx) == 0) { - backspace(caret_idx); + if (get_caret_column(caret_index) == 0 && get_caret_line(caret_index) == 0 && !has_selection(caret_index)) { continue; } - if (p_all_to_left) { - int caret_current_column = get_caret_column(caret_idx); - set_caret_column(0, caret_idx == 0, caret_idx); - _remove_text(get_caret_line(caret_idx), 0, get_caret_line(caret_idx), caret_current_column); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), caret_current_column, get_caret_line(caret_idx), get_caret_column(caret_idx)); - - // Check for any overlapping carets since we removed the entire line. - for (int j = i + 1; j < caret_edit_order.size(); j++) { - // Selection only end on this line, only the one as carets cannot overlap. - if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx) && get_selection_to_line(caret_edit_order[j]) == get_caret_line(caret_idx)) { - carets.write[caret_edit_order[j]].selection.to_column = 0; - break; - } - - // Check for caret. - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) || (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx))) { - break; - } + if (has_selection(caret_index) || (!p_all_to_left && !p_word) || get_caret_column(caret_index) == 0) { + backspace(caret_index); + continue; + } - deselect(caret_edit_order[j]); - carets_to_remove.push_back(caret_edit_order[j]); - set_caret_column(0, caret_idx == 0, caret_idx); - i = j; - } + if (p_all_to_left) { + // Remove everything to left of caret to the start of the line. + int caret_current_column = get_caret_column(caret_index); + _remove_text(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column); + collapse_carets(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column); + set_caret_column(0, caret_index == 0, caret_index); + _offset_carets_after(get_caret_line(caret_index), caret_current_column, get_caret_line(caret_index), 0); continue; } if (p_word) { - // Save here as the caret may change when resolving overlaps. - int from_column = get_caret_column(caret_idx); - int column = get_caret_column(caret_idx); + // Remove text to the start of the word left of the caret. + int from_column = get_caret_column(caret_index); + int column = get_caret_column(caret_index); // Check for the case "<word><space><caret>" and ignore the space. // No need to check for column being 0 since it is checked above. - if (is_whitespace(text[get_caret_line(caret_idx)][get_caret_column(caret_idx) - 1])) { + if (is_whitespace(text[get_caret_line(caret_index)][get_caret_column(caret_index) - 1])) { column -= 1; } + // Get a list with the indices of the word bounds of the given text line. - const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_idx))->get_rid()); + const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_index))->get_rid()); if (words.is_empty() || column <= words[0]) { // If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line. column = 0; @@ -2699,57 +2633,14 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { } } - // Check for any other carets in this range. - int overlapping_caret_index = -1; - for (int j = i + 1; j < caret_edit_order.size(); j++) { - // Check caret and selection in on the right line. - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) && (!has_selection(caret_edit_order[j]) || get_selection_to_line(caret_edit_order[j]) != get_caret_line(caret_idx))) { - break; - } - - // If it has a selection, check it ends with in the range. - if ((has_selection(caret_edit_order[j]) && get_selection_to_column(caret_edit_order[j]) < column)) { - break; - } - - // If it has a selection and it starts outside our word, we need to adjust the selection, and handle it later to prevent overlap. - if ((has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) < column)) { - carets.write[caret_edit_order[j]].selection.to_column = column; - overlapping_caret_index = caret_edit_order[j]; - break; - } - - // Otherwise we can remove it. - if (get_caret_column(caret_edit_order[j]) > column || (has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) > column)) { - deselect(caret_edit_order[j]); - carets_to_remove.push_back(caret_edit_order[j]); - set_caret_column(0, caret_idx == 0, caret_idx); - i = j; - } - } - - _remove_text(get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column); - - set_caret_line(get_caret_line(caret_idx), false, true, 0, caret_idx); - set_caret_column(column, caret_idx == 0, caret_idx); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column); - - // Now we can clean up the overlapping caret. - if (overlapping_caret_index != -1) { - backspace(overlapping_caret_index); - i++; - carets_to_remove.push_back(overlapping_caret_index); - set_caret_column(get_caret_column(overlapping_caret_index), caret_idx == 0, caret_idx); - } - continue; + _remove_text(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column); + collapse_carets(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column); + set_caret_column(column, caret_index == 0, caret_index); + _offset_carets_after(get_caret_line(caret_index), from_column, get_caret_line(caret_index), column); } } - // Sort and remove backwards to preserve indexes. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); - } + end_multicaret_edit(); end_action(); } @@ -2759,61 +2650,40 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { } start_action(EditAction::ACTION_DELETE); - Vector<int> carets_to_remove; + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = 0; i < caret_edit_order.size(); i++) { - int caret_idx = caret_edit_order[i]; - if (has_selection(caret_idx)) { - delete_selection(caret_idx); + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (multicaret_edit_ignore_caret(caret_index)) { continue; } - int curline_len = text[get_caret_line(caret_idx)].length(); - if (get_caret_line(caret_idx) == text.size() - 1 && get_caret_column(caret_idx) == curline_len) { + if (has_selection(caret_index)) { + delete_selection(caret_index); + continue; + } + + int curline_len = text[get_caret_line(caret_index)].length(); + if (get_caret_line(caret_index) == text.size() - 1 && get_caret_column(caret_index) == curline_len) { continue; // Last line, last column: Nothing to do. } - int next_line = get_caret_column(caret_idx) < curline_len ? get_caret_line(caret_idx) : get_caret_line(caret_idx) + 1; + int next_line = get_caret_column(caret_index) < curline_len ? get_caret_line(caret_index) : get_caret_line(caret_index) + 1; int next_column; if (p_all_to_right) { - // Get caret furthest to the left. - for (int j = i + 1; j < caret_edit_order.size(); j++) { - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - - if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - - if (!has_selection(caret_edit_order[j])) { - i = j; - caret_idx = caret_edit_order[i]; - } - } - - if (get_caret_column(caret_idx) == curline_len) { + if (get_caret_column(caret_index) == curline_len) { continue; } // Delete everything to right of caret. next_column = curline_len; - next_line = get_caret_line(caret_idx); - - // Remove overlapping carets. - for (int j = i - 1; j >= 0; j--) { - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - carets_to_remove.push_back(caret_edit_order[j]); - } - - } else if (p_word && get_caret_column(caret_idx) < curline_len - 1) { + next_line = get_caret_line(caret_index); + } else if (p_word && get_caret_column(caret_index) < curline_len - 1) { // Delete next word to right of caret. - int line = get_caret_line(caret_idx); - int column = get_caret_column(caret_idx); + int line = get_caret_line(caret_index); + int column = get_caret_column(caret_index); PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); for (int j = 1; j < words.size(); j = j + 2) { @@ -2825,49 +2695,22 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { next_line = line; next_column = column; - - // Remove overlapping carets. - for (int j = i - 1; j >= 0; j--) { - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - - if (get_caret_column(caret_edit_order[j]) > column) { - break; - } - carets_to_remove.push_back(caret_edit_order[j]); - } } else { // Delete one character. if (caret_mid_grapheme_enabled) { - next_column = get_caret_column(caret_idx) < curline_len ? (get_caret_column(caret_idx) + 1) : 0; + next_column = get_caret_column(caret_index) < curline_len ? (get_caret_column(caret_index) + 1) : 0; } else { - next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_idx))->get_rid(), (get_caret_column(caret_idx))) : 0; - } - - // Remove overlapping carets. - if (i > 0) { - int prev_caret_idx = caret_edit_order[i - 1]; - if (get_caret_line(prev_caret_idx) == next_line && get_caret_column(prev_caret_idx) == next_column) { - carets_to_remove.push_back(prev_caret_idx); - } + next_column = get_caret_column(caret_index) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_index))->get_rid(), (get_caret_column(caret_index))) : 0; } } - _remove_text(get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column); - } - - // Sort and remove backwards to preserve indexes. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); + _remove_text(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column); + collapse_carets(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column); + _offset_carets_after(next_line, next_column, get_caret_line(caret_index), get_caret_column(caret_index)); } - // If we are deleting from the end of a line, due to column preservation we could still overlap with another caret. - merge_overlapping_carets(); + end_multicaret_edit(); end_action(); - queue_redraw(); } void TextEdit::_move_caret_document_start(bool p_select) { @@ -2878,12 +2721,8 @@ void TextEdit::_move_caret_document_start(bool p_select) { deselect(); } - set_caret_line(0, false); + set_caret_line(0, false, true, -1); set_caret_column(0); - - if (p_select) { - _post_shift_selection(0); - } } void TextEdit::_move_caret_document_end(bool p_select) { @@ -2894,12 +2733,8 @@ void TextEdit::_move_caret_document_end(bool p_select) { deselect(); } - set_caret_line(get_last_unhidden_line(), true, false, 9999); + set_caret_line(get_last_unhidden_line(), true, false, -1); set_caret_column(text[get_caret_line()].length()); - - if (p_select) { - _post_shift_selection(0); - } } bool TextEdit::_clear_carets_and_selection() { @@ -2917,51 +2752,6 @@ bool TextEdit::_clear_carets_and_selection() { return false; } -void TextEdit::_get_above_below_caret_line_column(int p_old_line, int p_old_wrap_index, int p_old_column, bool p_below, int &p_new_line, int &p_new_column, int p_last_fit_x) const { - if (p_last_fit_x == -1) { - p_last_fit_x = _get_column_x_offset_for_line(p_old_column, p_old_line, p_old_column); - } - - // Calculate the new line and wrap index. - p_new_line = p_old_line; - int caret_wrap_index = p_old_wrap_index; - if (p_below) { - if (caret_wrap_index < get_line_wrap_count(p_new_line)) { - caret_wrap_index++; - } else { - p_new_line++; - caret_wrap_index = 0; - } - } else { - if (caret_wrap_index == 0) { - p_new_line--; - caret_wrap_index = get_line_wrap_count(p_new_line); - } else { - caret_wrap_index--; - } - } - - // Boundary checks. - if (p_new_line < 0) { - p_new_line = 0; - } - if (p_new_line >= text.size()) { - p_new_line = text.size() - 1; - } - - p_new_column = _get_char_pos_for_line(p_last_fit_x, p_new_line, caret_wrap_index); - if (p_new_column != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && caret_wrap_index < get_line_wrap_count(p_new_line)) { - Vector<String> rows = get_line_wrapped_text(p_new_line); - int row_end_col = 0; - for (int i = 0; i < caret_wrap_index + 1; i++) { - row_end_col += rows[i].length(); - } - if (p_new_column >= row_end_col) { - p_new_column -= 1; - } - } -} - void TextEdit::_update_placeholder() { if (theme_cache.font.is_null() || theme_cache.font_size <= 0) { return; // Not in tree? @@ -3127,53 +2917,48 @@ void TextEdit::drop_data(const Point2 &p_point, const Variant &p_data) { if (p_data.get_type() == Variant::STRING && is_editable()) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - int caret_row_tmp = pos.y; - int caret_column_tmp = pos.x; + int drop_at_line = pos.y; + int drop_at_column = pos.x; + int selection_index = get_selection_at_line_column(drop_at_line, drop_at_column, !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)); + + // Remove drag caret before the complex operation starts so it won't appear in undo. + remove_caret(drag_caret_index); + + if (selection_drag_attempt && selection_index >= 0 && selection_index == drag_and_drop_origin_caret_index) { + // Dropped onto original selection, do nothing. + selection_drag_attempt = false; + return; + } + + begin_complex_operation(); + begin_multicaret_edit(); if (selection_drag_attempt) { + // Drop from self. selection_drag_attempt = false; - if (!is_mouse_over_selection(!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL))) { - // Set caret back at selection for undo / redo. - set_caret_line(get_selection_to_line(), false, false); - set_caret_column(get_selection_to_column()); - - begin_complex_operation(); - if (!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { - if (caret_row_tmp > get_selection_to_line()) { - caret_row_tmp = caret_row_tmp - (get_selection_to_line() - get_selection_from_line()); - } else if (caret_row_tmp == get_selection_to_line() && caret_column_tmp >= get_selection_to_column()) { - caret_column_tmp = caret_column_tmp - (get_selection_to_column() - get_selection_from_column()); - } - delete_selection(); - } else { - deselect(); - } + if (!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { + // Delete all selections. + int temp_caret = add_caret(drop_at_line, drop_at_column); - remove_secondary_carets(); - set_caret_line(caret_row_tmp, true, false); - set_caret_column(caret_column_tmp); - insert_text_at_caret(p_data); - end_complex_operation(); - } - } else if (is_mouse_over_selection()) { - remove_secondary_carets(); - caret_row_tmp = get_selection_from_line(); - caret_column_tmp = get_selection_from_column(); - set_caret_line(caret_row_tmp, true, false); - set_caret_column(caret_column_tmp); - insert_text_at_caret(p_data); - grab_focus(); - } else { - remove_secondary_carets(); - deselect(); - set_caret_line(caret_row_tmp, true, false); - set_caret_column(caret_column_tmp); - insert_text_at_caret(p_data); - grab_focus(); - } + delete_selection(); - if (caret_row_tmp != get_caret_line() || caret_column_tmp != get_caret_column()) { - select(caret_row_tmp, caret_column_tmp, get_caret_line(), get_caret_column()); + // Use a temporary caret to update the drop at position. + drop_at_line = get_caret_line(temp_caret); + drop_at_column = get_caret_column(temp_caret); + } } + remove_secondary_carets(); + deselect(); + + // Insert the dragged text. + set_caret_line(drop_at_line, true, false, -1); + set_caret_column(drop_at_column); + insert_text_at_caret(p_data); + + select(drop_at_line, drop_at_column, get_caret_line(), get_caret_column()); + grab_focus(); + adjust_viewport_to_caret(); + end_multicaret_edit(); + end_complex_operation(); } } @@ -3459,7 +3244,7 @@ void TextEdit::_clear() { clear_undo_history(); text.clear(); remove_secondary_carets(); - set_caret_line(0, false); + set_caret_line(0, false, true, -1); set_caret_column(0); first_visible_col = 0; first_visible_line = 0; @@ -3532,17 +3317,36 @@ void TextEdit::set_line(int p_line, const String &p_new_text) { return; } begin_complex_operation(); - _remove_text(p_line, 0, p_line, text[p_line].length()); - _insert_text(p_line, 0, p_new_text); - for (int i = 0; i < carets.size(); i++) { - if (get_caret_line(i) == p_line && get_caret_column(i) > p_new_text.length()) { - set_caret_column(p_new_text.length(), false, i); + + int old_column = text[p_line].length(); + + // Set the affected carets column to update their last offset x. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) { + set_caret_column(get_caret_column(i), false, i); } + if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) { + set_selection_origin_column(get_selection_origin_column(i), i); + } + } + + _remove_text(p_line, 0, p_line, old_column); + int new_line, new_column; + _insert_text(p_line, 0, p_new_text, &new_line, &new_column); - if (has_selection(i) && p_line == get_selection_to_line(i) && get_selection_to_column(i) > text[p_line].length()) { - carets.write[i].selection.to_column = text[p_line].length(); + // Don't offset carets that were on the old line. + _offset_carets_after(p_line, old_column, new_line, new_column, false, false); + + // Set the caret lines to update the column to match visually. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) { + set_caret_line(get_caret_line(i), false, true, 0, i); + } + if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) { + set_selection_origin_line(get_selection_origin_line(i), true, 0, i); } } + merge_overlapping_carets(); end_complex_operation(); } @@ -3596,71 +3400,163 @@ void TextEdit::swap_lines(int p_from_line, int p_to_line) { ERR_FAIL_INDEX(p_from_line, text.size()); ERR_FAIL_INDEX(p_to_line, text.size()); - String tmp = get_line(p_from_line); - String tmp2 = get_line(p_to_line); + if (p_from_line == p_to_line) { + return; + } + + String from_line_text = get_line(p_from_line); + String to_line_text = get_line(p_to_line); begin_complex_operation(); - set_line(p_to_line, tmp); - set_line(p_from_line, tmp2); + begin_multicaret_edit(); + // Don't use set_line to avoid clamping and updating carets. + _remove_text(p_to_line, 0, p_to_line, text[p_to_line].length()); + _insert_text(p_to_line, 0, from_line_text); + _remove_text(p_from_line, 0, p_from_line, text[p_from_line].length()); + _insert_text(p_from_line, 0, to_line_text); + + // Swap carets. + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + if (get_caret_line(i) == p_from_line || get_caret_line(i) == p_to_line) { + int caret_new_line = get_caret_line(i) == p_from_line ? p_to_line : p_from_line; + int caret_column = get_caret_column(i); + set_caret_line(caret_new_line, false, true, -1, i); + set_caret_column(caret_column, false, i); + } + if (selected && (get_selection_origin_line(i) == p_from_line || get_selection_origin_line(i) == p_to_line)) { + int origin_new_line = get_selection_origin_line(i) == p_from_line ? p_to_line : p_from_line; + int origin_column = get_selection_origin_column(i); + select(origin_new_line, origin_column, get_caret_line(i), get_caret_column(i), i); + } + } + // If only part of a selection was changed, it may now overlap. + merge_overlapping_carets(); + + end_multicaret_edit(); end_complex_operation(); } -void TextEdit::insert_line_at(int p_at, const String &p_text) { - ERR_FAIL_INDEX(p_at, text.size()); +void TextEdit::insert_line_at(int p_line, const String &p_text) { + ERR_FAIL_INDEX(p_line, text.size()); - _insert_text(p_at, 0, p_text + "\n"); + // Use a complex operation so subsequent calls aren't merged together. + begin_complex_operation(); - for (int i = 0; i < carets.size(); i++) { - if (get_caret_line(i) >= p_at) { - // Offset caret when located after inserted line. - set_caret_line(get_caret_line(i) + 1, false, true, 0, i); - } - if (has_selection(i)) { - if (get_selection_from_line(i) >= p_at) { - // Offset selection when located after inserted line. - select(get_selection_from_line(i) + 1, get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i); - } else if (get_selection_to_line(i) >= p_at) { - // Extend selection that includes inserted line. - select(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i); + int new_line, new_column; + _insert_text(p_line, 0, p_text + "\n", &new_line, &new_column); + _offset_carets_after(p_line, 0, new_line, new_column); + + end_complex_operation(); +} + +void TextEdit::remove_line_at(int p_line, bool p_move_carets_down) { + ERR_FAIL_INDEX(p_line, text.size()); + + if (get_line_count() == 1) { + // Only one line, just remove contents. + begin_complex_operation(); + int line_length = get_line(p_line).length(); + _remove_text(p_line, 0, p_line, line_length); + collapse_carets(p_line, 0, p_line, line_length, true); + end_complex_operation(); + return; + } + + begin_complex_operation(); + + bool is_last_line = p_line == get_line_count() - 1; + int from_line = is_last_line ? p_line - 1 : p_line; + int next_line = is_last_line ? p_line : p_line + 1; + int from_column = is_last_line ? get_line(from_line).length() : 0; + int next_column = is_last_line ? get_line(next_line).length() : 0; + + if ((!is_last_line && p_move_carets_down) || (p_line != 0 && !p_move_carets_down)) { + // Set the carets column to update their last offset x. + for (int i = 0; i < get_caret_count(); i++) { + if (get_caret_line(i) == p_line) { + set_caret_column(get_caret_column(i), false, i); + } + if (has_selection(i) && get_selection_origin_line(i) == p_line) { + set_selection_origin_column(get_selection_origin_column(i), i); } } } - // Need to apply the above adjustments to the undo / redo carets. - current_op.end_carets = carets; - queue_redraw(); + // Remove line. + _remove_text(from_line, from_column, next_line, next_column); + + begin_multicaret_edit(); + if ((is_last_line && p_move_carets_down) || (p_line == 0 && !p_move_carets_down)) { + // Collapse carets. + collapse_carets(from_line, from_column, next_line, next_column, true); + } else { + // Move carets to visually line up. + int target_line = p_move_carets_down ? p_line : p_line - 1; + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + if (get_caret_line(i) == p_line) { + set_caret_line(target_line, i == 0, true, 0, i); + } + if (selected && get_selection_origin_line(i) == p_line) { + set_selection_origin_line(target_line, true, 0, i); + select(get_selection_origin_line(i), get_selection_origin_column(i), get_caret_line(i), get_caret_column(i), i); + } + } + + merge_overlapping_carets(); + } + _offset_carets_after(next_line, next_column, from_line, from_column); + end_multicaret_edit(); + end_complex_operation(); } void TextEdit::insert_text_at_caret(const String &p_text, int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } delete_selection(i); int from_line = get_caret_line(i); int from_col = get_caret_column(i); - int new_column, new_line; + int new_line, new_column; _insert_text(from_line, from_col, p_text, &new_line, &new_column); _update_scrollbars(); + _offset_carets_after(from_line, from_col, new_line, new_column); - set_caret_line(new_line, false, true, 0, i); + set_caret_line(new_line, false, true, -1, i); set_caret_column(new_column, i == 0, i); - - adjust_carets_after_edit(i, new_line, new_column, from_line, from_col); } if (has_ime_text()) { _update_ime_text(); } + end_multicaret_edit(); + end_complex_operation(); +} + +void TextEdit::insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin, bool p_before_selection_end) { + ERR_FAIL_INDEX(p_line, text.size()); + ERR_FAIL_INDEX(p_column, text[p_line].length() + 1); + + begin_complex_operation(); + + int new_line, new_column; + _insert_text(p_line, p_column, p_text, &new_line, &new_column); + + _offset_carets_after(p_line, p_column, new_line, new_column, p_before_selection_begin, p_before_selection_end); + end_complex_operation(); - queue_redraw(); } void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { @@ -3671,7 +3567,13 @@ void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, in ERR_FAIL_COND(p_to_line < p_from_line); ERR_FAIL_COND(p_to_line == p_from_line && p_to_column < p_from_column); + begin_complex_operation(); + _remove_text(p_from_line, p_from_column, p_to_line, p_to_column); + collapse_carets(p_from_line, p_from_column, p_to_line, p_to_column); + _offset_carets_after(p_to_line, p_to_column, p_from_line, p_from_column); + + end_complex_operation(); } int TextEdit::get_last_unhidden_line() const { @@ -4040,7 +3942,7 @@ void TextEdit::undo() { _push_current_op(); if (undo_stack_pos == nullptr) { - if (!undo_stack.size()) { + if (undo_stack.is_empty()) { return; // Nothing to undo. } @@ -4059,6 +3961,7 @@ void TextEdit::undo() { current_op.version = op.prev_version; if (undo_stack_pos->get().chain_backward) { + // This was part of a complex operation, undo until the chain forward at the start of the complex operation. while (true) { ERR_BREAK(!undo_stack_pos->prev()); undo_stack_pos = undo_stack_pos->prev(); @@ -4072,9 +3975,9 @@ void TextEdit::undo() { } _update_scrollbars(); - bool dirty_carets = carets.size() != undo_stack_pos->get().start_carets.size(); + bool dirty_carets = get_caret_count() != undo_stack_pos->get().start_carets.size(); if (!dirty_carets) { - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (carets[i].line != undo_stack_pos->get().start_carets[i].line || carets[i].column != undo_stack_pos->get().start_carets[i].column) { dirty_carets = true; break; @@ -4084,11 +3987,11 @@ void TextEdit::undo() { carets = undo_stack_pos->get().start_carets; - if (dirty_carets && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + _unhide_carets(); + + if (dirty_carets) { + _caret_changed(); + _selection_changed(); } adjust_viewport_to_caret(); } @@ -4113,6 +4016,7 @@ void TextEdit::redo() { _do_text_op(op, false); current_op.version = op.version; if (undo_stack_pos->get().chain_forward) { + // This was part of a complex operation, redo until the chain backward at the end of the complex operation. while (true) { ERR_BREAK(!undo_stack_pos->next()); undo_stack_pos = undo_stack_pos->next(); @@ -4126,9 +4030,9 @@ void TextEdit::redo() { } _update_scrollbars(); - bool dirty_carets = carets.size() != undo_stack_pos->get().end_carets.size(); + bool dirty_carets = get_caret_count() != undo_stack_pos->get().end_carets.size(); if (!dirty_carets) { - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (carets[i].line != undo_stack_pos->get().end_carets[i].line || carets[i].column != undo_stack_pos->get().end_carets[i].column) { dirty_carets = true; break; @@ -4139,11 +4043,11 @@ void TextEdit::redo() { carets = undo_stack_pos->get().end_carets; undo_stack_pos = undo_stack_pos->next(); - if (dirty_carets && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + _unhide_carets(); + + if (dirty_carets) { + _caret_changed(); + _selection_changed(); } adjust_viewport_to_caret(); } @@ -4358,13 +4262,7 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ } } - if (row < 0) { - row = 0; - } - - if (row >= text.size()) { - row = text.size() - 1; - } + row = CLAMP(row, 0, text.size() - 1); int visible_lines = get_visible_line_count_in_range(first_vis_line, row); if (rows > visible_lines) { @@ -4510,29 +4408,13 @@ bool TextEdit::is_dragging_cursor() const { } bool TextEdit::is_mouse_over_selection(bool p_edges, int p_caret) const { - for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; - } - - if (!has_selection(i)) { - continue; - } - - Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - int row = pos.y; - int col = pos.x; - if (p_edges) { - if ((row == get_selection_from_line(i) && col == get_selection_from_column(i)) || (row == get_selection_to_line(i) && col == get_selection_to_column(i))) { - return true; - } - } + Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); + int line = pos.y; + int column = pos.x; - if (row >= get_selection_from_line(i) && row <= get_selection_to_line(i) && (row > get_selection_from_line(i) || col > get_selection_from_column(i)) && (row < get_selection_to_line(i) || col < get_selection_to_column(i))) { - return true; - } + if ((p_caret == -1 && get_selection_at_line_column(line, column, p_edges) != -1) || (p_caret != -1 && _selection_contains(p_caret, line, column, p_edges))) { + return true; } - return false; } @@ -4619,270 +4501,401 @@ bool TextEdit::is_multiple_carets_enabled() const { return multi_carets_enabled; } -int TextEdit::add_caret(int p_line, int p_col) { +int TextEdit::add_caret(int p_line, int p_column) { if (!multi_carets_enabled) { return -1; } + _cancel_drag_and_drop_text(); p_line = CLAMP(p_line, 0, text.size() - 1); - p_col = CLAMP(p_col, 0, get_line(p_line).length()); + p_column = CLAMP(p_column, 0, get_line(p_line).length()); - for (int i = 0; i < carets.size(); i++) { - if (get_caret_line(i) == p_line && get_caret_column(i) == p_col) { + if (!is_in_mulitcaret_edit()) { + // Carets cannot overlap. + if (get_selection_at_line_column(p_line, p_column, true, false) != -1) { return -1; } - - if (has_selection(i)) { - if (p_line >= get_selection_from_line(i) && p_line <= get_selection_to_line(i) && (p_line > get_selection_from_line(i) || p_col >= get_selection_from_column(i)) && (p_line < get_selection_to_line(i) || p_col <= get_selection_to_column(i))) { - return -1; - } - } } carets.push_back(Caret()); - set_caret_line(p_line, false, false, 0, carets.size() - 1); - set_caret_column(p_col, false, carets.size() - 1); - caret_index_edit_dirty = true; - return carets.size() - 1; + int new_index = carets.size() - 1; + set_caret_line(p_line, false, false, -1, new_index); + set_caret_column(p_column, false, new_index); + _caret_changed(new_index); + + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(new_index); + merge_overlapping_carets(); + } + return new_index; } void TextEdit::remove_caret(int p_caret) { ERR_FAIL_COND_MSG(carets.size() <= 1, "The main caret should not be removed."); ERR_FAIL_INDEX(p_caret, carets.size()); + + _caret_changed(p_caret); carets.remove_at(p_caret); - caret_index_edit_dirty = true; + + if (drag_caret_index >= 0) { + if (p_caret == drag_caret_index) { + drag_caret_index = -1; + } else if (p_caret < drag_caret_index) { + drag_caret_index -= 1; + } + } } void TextEdit::remove_secondary_carets() { + if (carets.size() == 1) { + return; + } + + _caret_changed(); carets.resize(1); - caret_index_edit_dirty = true; - queue_redraw(); + + if (drag_caret_index >= 0) { + drag_caret_index = -1; + } } -void TextEdit::merge_overlapping_carets() { - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = 0; i < caret_edit_order.size() - 1; i++) { - int first_caret = caret_edit_order[i]; - int second_caret = caret_edit_order[i + 1]; +int TextEdit::get_caret_count() const { + // Don't include drag caret. + if (drag_caret_index >= 0) { + return carets.size() - 1; + } + return carets.size(); +} - // Both have selection. - if (has_selection(first_caret) && has_selection(second_caret)) { - bool should_merge = false; - if (get_selection_from_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_from_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_from_line(first_caret) > get_selection_from_line(second_caret) || get_selection_from_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_from_line(first_caret) < get_selection_to_line(second_caret) || get_selection_from_column(first_caret) <= get_selection_to_column(second_caret))) { - should_merge = true; - } +void TextEdit::add_caret_at_carets(bool p_below) { + const int last_line_max_wrap = get_line_wrap_count(text.size() - 1); + + begin_multicaret_edit(); + int view_target_caret = -1; + int view_line = p_below ? -1 : INT_MAX; + int num_carets = get_caret_count(); + for (int i = 0; i < num_carets; i++) { + const int caret_line = get_caret_line(i); + const int caret_column = get_caret_column(i); + const bool is_selected = has_selection(i) || carets[i].last_fit_x != carets[i].selection.origin_last_fit_x; + const int selection_origin_line = get_selection_origin_line(i); + const int selection_origin_column = get_selection_origin_column(i); + const int caret_wrap_index = get_caret_wrap_index(i); + const int selection_origin_wrap_index = !is_selected ? -1 : get_line_wrap_index_at_column(selection_origin_line, selection_origin_column); + + if (caret_line == 0 && !p_below && (caret_wrap_index == 0 || selection_origin_wrap_index == 0)) { + // Can't add above the first line. + continue; + } + if (caret_line == text.size() - 1 && p_below && (caret_wrap_index == last_line_max_wrap || selection_origin_wrap_index == last_line_max_wrap)) { + // Can't add below the last line. + continue; + } - if (get_selection_to_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_to_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_to_line(first_caret) > get_selection_from_line(second_caret) || get_selection_to_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_to_line(first_caret) < get_selection_to_line(second_caret) || get_selection_to_column(first_caret) <= get_selection_to_column(second_caret))) { - should_merge = true; - } + // Add a new caret. + int new_caret_index = add_caret(caret_line, caret_column); - if (!should_merge) { - continue; - } + // Copy the selection origin and last fit. + set_selection_origin_line(selection_origin_line, true, -1, new_caret_index); + set_selection_origin_column(selection_origin_column, new_caret_index); + carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x; + carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x; - // Save the newest one for Click + Drag. - int caret_to_save = first_caret; - int caret_to_remove = second_caret; - if (first_caret < second_caret) { - caret_to_save = second_caret; - caret_to_remove = first_caret; + // Move the caret up or down one visible line. + if (!p_below) { + // Move caret up. + if (caret_wrap_index > 0) { + set_caret_line(caret_line, false, false, caret_wrap_index - 1, new_caret_index); + } else { + int new_line = caret_line - get_next_visible_line_offset_from(caret_line - 1, -1); + if (is_line_wrapped(new_line)) { + set_caret_line(new_line, false, false, get_line_wrap_count(new_line), new_caret_index); + } else { + set_caret_line(new_line, false, false, 0, new_caret_index); + } } - - int from_line = MIN(get_selection_from_line(caret_to_save), get_selection_from_line(caret_to_remove)); - int to_line = MAX(get_selection_to_line(caret_to_save), get_selection_to_line(caret_to_remove)); - int from_col = get_selection_from_column(caret_to_save); - int to_col = get_selection_to_column(caret_to_save); - int selection_line = get_selection_line(caret_to_save); - int selection_col = get_selection_column(caret_to_save); - - bool at_from = (get_caret_line(caret_to_save) == get_selection_from_line(caret_to_save) && get_caret_column(caret_to_save) == get_selection_from_column(caret_to_save)); - - if (at_from) { - if (get_selection_line(caret_to_remove) > get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) >= get_selection_column(caret_to_save))) { - selection_line = get_selection_line(caret_to_remove); - selection_col = get_selection_column(caret_to_remove); + // Move selection origin up. + if (is_selected) { + if (selection_origin_wrap_index > 0) { + set_selection_origin_line(caret_line, false, selection_origin_wrap_index - 1, new_caret_index); + } else { + int new_line = selection_origin_line - get_next_visible_line_offset_from(selection_origin_line - 1, -1); + if (is_line_wrapped(new_line)) { + set_selection_origin_line(new_line, false, get_line_wrap_count(new_line), new_caret_index); + } else { + set_selection_origin_line(new_line, false, 0, new_caret_index); + } } - } else if (get_selection_line(caret_to_remove) < get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) <= get_selection_column(caret_to_save))) { - selection_line = get_selection_line(caret_to_remove); - selection_col = get_selection_column(caret_to_remove); } - - if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save) || (get_selection_from_line(caret_to_remove) == get_selection_from_line(caret_to_save) && get_selection_from_column(caret_to_remove) <= get_selection_from_column(caret_to_save))) { - from_col = get_selection_from_column(caret_to_remove); + if (get_caret_line(new_caret_index) < view_line) { + view_line = get_caret_line(new_caret_index); + view_target_caret = new_caret_index; + } + } else { + // Move caret down. + if (caret_wrap_index < get_line_wrap_count(caret_line)) { + set_caret_line(caret_line, false, false, caret_wrap_index + 1, new_caret_index); } else { - to_col = get_selection_to_column(caret_to_remove); + int new_line = caret_line + get_next_visible_line_offset_from(CLAMP(caret_line + 1, 0, text.size() - 1), 1); + set_caret_line(new_line, false, false, 0, new_caret_index); + } + // Move selection origin down. + if (is_selected) { + if (selection_origin_wrap_index < get_line_wrap_count(selection_origin_line)) { + set_selection_origin_line(selection_origin_line, false, selection_origin_wrap_index + 1, new_caret_index); + } else { + int new_line = selection_origin_line + get_next_visible_line_offset_from(CLAMP(selection_origin_line + 1, 0, text.size() - 1), 1); + set_selection_origin_line(new_line, false, 0, new_caret_index); + } + } + if (get_caret_line(new_caret_index) > view_line) { + view_line = get_caret_line(new_caret_index); + view_target_caret = new_caret_index; } + } + if (is_selected) { + // Make sure selection is active. + select(get_selection_origin_line(new_caret_index), get_selection_origin_column(new_caret_index), get_caret_line(new_caret_index), get_caret_column(new_caret_index), new_caret_index); + carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x; + carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x; + } - select(from_line, from_col, to_line, to_col, caret_to_save); - set_selection_mode(selecting_mode, selection_line, selection_col, caret_to_save); - set_caret_line((at_from ? from_line : to_line), caret_to_save == 0, true, 0, caret_to_save); - set_caret_column((at_from ? from_col : to_col), caret_to_save == 0, caret_to_save); - remove_caret(caret_to_remove); - i--; - caret_edit_order = get_caret_index_edit_order(); - continue; + bool check_edges = !has_selection(0) || !has_selection(new_caret_index); + bool will_merge_with_main_caret = _selection_contains(0, get_caret_line(new_caret_index), get_caret_column(new_caret_index), check_edges, false) || _selection_contains(new_caret_index, get_caret_line(0), get_caret_column(0), check_edges, false); + if (will_merge_with_main_caret) { + // Move next to the main caret so it stays the main caret after merging. + Caret new_caret = carets[new_caret_index]; + carets.remove_at(new_caret_index); + carets.insert(0, new_caret); + i++; } + } - // Only first has selection. - if (has_selection(first_caret)) { - if (get_caret_line(second_caret) >= get_selection_from_line(first_caret) && get_caret_line(second_caret) <= get_selection_to_line(first_caret) && (get_caret_line(second_caret) > get_selection_from_line(first_caret) || get_caret_column(second_caret) >= get_selection_from_column(first_caret)) && (get_caret_line(second_caret) < get_selection_to_line(first_caret) || get_caret_column(second_caret) <= get_selection_to_column(first_caret))) { - remove_caret(second_caret); - caret_edit_order = get_caret_index_edit_order(); - i--; - } - continue; + // Show the topmost caret if added above or bottommost caret if added below. + if (view_target_caret >= 0 && view_target_caret < get_caret_count()) { + adjust_viewport_to_caret(view_target_caret); + } + + merge_overlapping_carets(); + end_multicaret_edit(); +} + +struct _CaretSortComparator { + _FORCE_INLINE_ bool operator()(const Vector3i &a, const Vector3i &b) const { + // x is column, y is line, z is caret index. + if (a.y == b.y) { + return a.x < b.x; } + return a.y < b.y; + } +}; - // Only second has selection. - if (has_selection(second_caret)) { - if (get_caret_line(first_caret) >= get_selection_from_line(second_caret) && get_caret_line(first_caret) <= get_selection_to_line(second_caret) && (get_caret_line(first_caret) > get_selection_from_line(second_caret) || get_caret_column(first_caret) >= get_selection_from_column(second_caret)) && (get_caret_line(first_caret) < get_selection_to_line(second_caret) || get_caret_column(first_caret) <= get_selection_to_column(second_caret))) { - remove_caret(first_caret); - caret_edit_order = get_caret_index_edit_order(); - i--; - } +Vector<int> TextEdit::get_sorted_carets(bool p_include_ignored_carets) const { + // Returns caret indexes sorted by selection start or caret position from top to bottom of text. + Vector<Vector3i> caret_line_col_indexes; + for (int i = 0; i < get_caret_count(); i++) { + if (!p_include_ignored_carets && multicaret_edit_ignore_caret(i)) { continue; } + caret_line_col_indexes.push_back(Vector3i(get_selection_from_column(i), get_selection_from_line(i), i)); + } + caret_line_col_indexes.sort_custom<_CaretSortComparator>(); + Vector<int> sorted; + sorted.resize(caret_line_col_indexes.size()); + for (int i = 0; i < caret_line_col_indexes.size(); i++) { + sorted.set(i, caret_line_col_indexes[i].z); + } + return sorted; +} - // Both have no selection. - if (get_caret_line(first_caret) == get_caret_line(second_caret) && get_caret_column(first_caret) == get_caret_column(second_caret)) { - // Save the newest one for Click + Drag. - if (first_caret < second_caret) { - remove_caret(first_caret); - } else { - remove_caret(second_caret); +void TextEdit::collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive) { + // Collapse carets in the selected range to the from position. + + // Clamp the collapse target position. + int collapse_line = CLAMP(p_from_line, 0, text.size() - 1); + int collapse_column = CLAMP(p_from_column, 0, text[collapse_line].length()); + + // Swap the lines if they are in the wrong order. + if (p_from_line > p_to_line) { + SWAP(p_from_line, p_to_line); + SWAP(p_from_column, p_to_column); + } + if (p_from_line == p_to_line && p_from_column > p_to_column) { + SWAP(p_from_column, p_to_column); + } + bool any_collapsed = false; + + // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together. + for (int i = 0; i < get_caret_count(); i++) { + bool is_caret_in = _is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive); + if (!has_selection(i)) { + if (is_caret_in) { + // Caret was in the collapsed area. + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(i); + } + any_collapsed = true; + } + } else { + bool is_origin_in = _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive); + + if (is_caret_in && is_origin_in) { + // Selection was completely encapsulated. + deselect(i); + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(i); + } + any_collapsed = true; + } else if (is_caret_in) { + // Only caret was inside. + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + any_collapsed = true; + } else if (is_origin_in) { + // Only selection origin was inside. + set_selection_origin_line(collapse_line, true, -1, i); + set_selection_origin_column(collapse_column, i); + any_collapsed = true; } - i--; - caret_edit_order = get_caret_index_edit_order(); - continue; } + if (!p_inclusive && !any_collapsed) { + if ((get_caret_line(i) == collapse_line && get_caret_column(i) == collapse_column) || (get_selection_origin_line(i) == collapse_line && get_selection_origin_column(i) == collapse_column)) { + // Make sure to queue a merge, even if we didn't include it. + any_collapsed = true; + } + } + } + if (any_collapsed) { + merge_overlapping_carets(); } } -int TextEdit::get_caret_count() const { - return carets.size(); -} +void TextEdit::merge_overlapping_carets() { + if (is_in_mulitcaret_edit()) { + // Queue merge to be performed the end of the multicaret edit. + multicaret_edit_merge_queued = true; + return; + } -void TextEdit::add_caret_at_carets(bool p_below) { - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &caret_index : caret_edit_order) { - const int caret_line = get_caret_line(caret_index); - const int caret_column = get_caret_column(caret_index); - - // The last fit x will be cleared if the caret has a selection, - // but if it does not have a selection the last fit x will be - // transferred to the new caret. - int caret_from_column = 0, caret_to_column = 0, caret_last_fit_x = carets[caret_index].last_fit_x; - if (has_selection(caret_index)) { - // If the selection goes over multiple lines, deselect it. - if (get_selection_from_line(caret_index) != get_selection_to_line(caret_index)) { - deselect(caret_index); + multicaret_edit_merge_queued = false; + multicaret_edit_ignore_carets.clear(); + + if (get_caret_count() == 1) { + return; + } + + Vector<int> sorted_carets = get_sorted_carets(true); + for (int i = 0; i < sorted_carets.size() - 1; i++) { + int first_caret = sorted_carets[i]; + int second_caret = sorted_carets[i + 1]; + + bool merge_carets; + if (!has_selection(first_caret) || !has_selection(second_caret)) { + // Merge if touching. + merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) <= get_selection_to_column(first_caret)); + } else { + // Merge two selections if overlapping. + merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) < get_selection_to_column(first_caret)); + } + + if (!merge_carets) { + continue; + } + + // Save the newest one for Click + Drag. + int caret_to_save = first_caret; + int caret_to_remove = second_caret; + if (first_caret < second_caret) { + caret_to_save = second_caret; + caret_to_remove = first_caret; + } + + if (get_selection_from_line(caret_to_save) != get_selection_from_line(caret_to_remove) || get_selection_to_line(caret_to_save) != get_selection_to_line(caret_to_remove) || get_selection_from_column(caret_to_save) != get_selection_from_column(caret_to_remove) || get_selection_to_column(caret_to_save) != get_selection_to_column(caret_to_remove)) { + // Selections are not the same, merge them into one bigger selection. + int new_from_line = MIN(get_selection_from_line(caret_to_remove), get_selection_from_line(caret_to_save)); + int new_to_line = MAX(get_selection_to_line(caret_to_remove), get_selection_to_line(caret_to_save)); + int new_from_col; + int new_to_col; + if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save)) { + new_from_col = get_selection_from_column(caret_to_remove); + } else if (get_selection_from_line(caret_to_remove) > get_selection_from_line(caret_to_save)) { + new_from_col = get_selection_from_column(caret_to_save); } else { - caret_from_column = get_selection_from_column(caret_index); - caret_to_column = get_selection_to_column(caret_index); - caret_last_fit_x = -1; - carets.write[caret_index].last_fit_x = _get_column_x_offset_for_line(caret_column, caret_line, caret_column); + new_from_col = MIN(get_selection_from_column(caret_to_remove), get_selection_from_column(caret_to_save)); + } + if (get_selection_to_line(caret_to_remove) < get_selection_to_line(caret_to_save)) { + new_to_col = get_selection_to_column(caret_to_save); + } else if (get_selection_to_line(caret_to_remove) > get_selection_to_line(caret_to_save)) { + new_to_col = get_selection_to_column(caret_to_remove); + } else { + new_to_col = MAX(get_selection_to_column(caret_to_remove), get_selection_to_column(caret_to_save)); } - } - // Get the line and column of the new caret as if you would move the caret by pressing the arrow keys. - int new_caret_line, new_caret_column, new_caret_from_column = 0, new_caret_to_column = 0; - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_column, p_below, new_caret_line, new_caret_column, caret_last_fit_x); + // Use the direction from the last caret or the saved one. + int caret_dir_to_copy; + if (has_selection(caret_to_remove) && has_selection(caret_to_save)) { + caret_dir_to_copy = caret_to_remove == get_caret_count() - 1 ? caret_to_remove : caret_to_save; + } else { + caret_dir_to_copy = !has_selection(caret_to_remove) ? caret_to_save : caret_to_remove; + } - // If the caret does have a selection calculate the new from and to columns. - if (caret_from_column != caret_to_column) { - // We only need to calculate the selection columns if the column of the caret changed. - if (caret_column != new_caret_column) { - int _; // Unused placeholder for p_new_line. - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_from_column, p_below, _, new_caret_from_column); - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_to_column, p_below, _, new_caret_to_column); + if (is_caret_after_selection_origin(caret_dir_to_copy)) { + select(new_from_line, new_from_col, new_to_line, new_to_col, caret_to_save); } else { - new_caret_from_column = caret_from_column; - new_caret_to_column = caret_to_column; + select(new_to_line, new_to_col, new_from_line, new_from_col, caret_to_save); } } - // Add the new caret. - const int new_caret_index = add_caret(new_caret_line, new_caret_column); - - if (new_caret_index == -1) { - continue; + if (caret_to_save == 0) { + adjust_viewport_to_caret(caret_to_save); } - // Also add the selection if there should be one. - if (new_caret_from_column != new_caret_to_column) { - select(new_caret_line, new_caret_from_column, new_caret_line, new_caret_to_column, new_caret_index); - // Necessary to properly modify the selection after adding the new caret. - carets.write[new_caret_index].selection.selecting_line = new_caret_line; - carets.write[new_caret_index].selection.selecting_column = new_caret_column == new_caret_from_column ? new_caret_to_column : new_caret_from_column; - continue; + remove_caret(caret_to_remove); + + // Update the rest of the sorted list. + for (int j = i; j < sorted_carets.size(); j++) { + if (sorted_carets[j] > caret_to_remove) { + // Shift the index since a caret before it was removed. + sorted_carets.write[j] -= 1; + } } + // Remove the caret from the sorted array. + sorted_carets.remove_at(caret_to_remove == first_caret ? i : i + 1); - // Copy the last fit x over. - carets.write[new_caret_index].last_fit_x = carets[caret_index].last_fit_x; + // Process the caret again, since it and the next caret might also overlap. + i--; } +} - merge_overlapping_carets(); - queue_redraw(); +// Starts a multicaret edit operation. Call this before iterating over the carets and call [end_multicaret_edit] afterwards. +void TextEdit::begin_multicaret_edit() { + multicaret_edit_count++; } -Vector<int> TextEdit::get_caret_index_edit_order() { - if (!caret_index_edit_dirty) { - return caret_index_edit_order; +void TextEdit::end_multicaret_edit() { + if (multicaret_edit_count > 0) { + multicaret_edit_count--; + } + if (multicaret_edit_count != 0) { + return; } - caret_index_edit_order.clear(); - caret_index_edit_order.push_back(0); - for (int i = 1; i < carets.size(); i++) { - int j = 0; - - int line = has_selection(i) ? get_selection_to_line(i) : carets[i].line; - int col = has_selection(i) ? get_selection_to_column(i) : carets[i].column; - - for (; j < caret_index_edit_order.size(); j++) { - int idx = caret_index_edit_order[j]; - int other_line = has_selection(idx) ? get_selection_to_line(idx) : carets[idx].line; - int other_col = has_selection(idx) ? get_selection_to_column(idx) : carets[idx].column; - if (line > other_line || (line == other_line && col > other_col)) { - break; - } - } - caret_index_edit_order.insert(j, i); + // This was the last multicaret edit operation. + if (multicaret_edit_merge_queued) { + merge_overlapping_carets(); } - caret_index_edit_dirty = false; - return caret_index_edit_order; + multicaret_edit_ignore_carets.clear(); } -void TextEdit::adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col) { - int edit_height = p_from_line - p_to_line; - int edit_size = ((edit_height == 0) ? p_from_col : 0) - p_to_col; - - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int j = 0; j < caret_edit_order.size(); j++) { - if (caret_edit_order[j] == p_caret) { - return; - } - - // Adjust caret. - // set_caret_line could adjust the column, so save here. - int cc = get_caret_column(caret_edit_order[j]); - if (edit_height != 0) { - set_caret_line(get_caret_line(caret_edit_order[j]) + edit_height, false, true, 0, caret_edit_order[j]); - } - if (get_caret_line(p_caret) == get_caret_line(caret_edit_order[j])) { - set_caret_column(cc + edit_size, false, caret_edit_order[j]); - } +bool TextEdit::is_in_mulitcaret_edit() const { + return multicaret_edit_count > 0; +} - // Adjust selection. - if (!has_selection(caret_edit_order[j])) { - continue; - } - if (edit_height != 0) { - carets.write[caret_edit_order[j]].selection.from_line += edit_height; - carets.write[caret_edit_order[j]].selection.to_line += edit_height; - } - if (get_caret_line(p_caret) == get_selection_from_line(caret_edit_order[j])) { - carets.write[caret_edit_order[j]].selection.from_column += edit_size; - } - } +bool TextEdit::multicaret_edit_ignore_caret(int p_caret) const { + return multicaret_edit_ignore_carets.has(p_caret); } bool TextEdit::is_caret_visible(int p_caret) const { @@ -4902,16 +4915,10 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ } setting_caret_line = true; - if (p_line < 0) { - p_line = 0; - } - - if (p_line >= text.size()) { - p_line = text.size() - 1; - } + p_line = CLAMP(p_line, 0, text.size() - 1); if (!p_can_be_hidden) { - if (_is_line_hidden(CLAMP(p_line, 0, text.size() - 1))) { + if (_is_line_hidden(p_line)) { int move_down = get_next_visible_line_offset_from(p_line, 1) - 1; if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) { p_line += move_down; @@ -4920,7 +4927,7 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) { p_line -= move_up; } else { - WARN_PRINT(("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines.")); + WARN_PRINT("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines."); } } } @@ -4928,31 +4935,36 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ bool caret_moved = get_caret_line(p_caret) != p_line; carets.write[p_caret].line = p_line; - int n_col = _get_char_pos_for_line(carets[p_caret].last_fit_x, p_line, p_wrap_index); - if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { - Vector<String> rows = get_line_wrapped_text(p_line); - int row_end_col = 0; - for (int i = 0; i < p_wrap_index + 1; i++) { - row_end_col += rows[i].length(); - } - if (n_col >= row_end_col) { - n_col -= 1; + int n_col; + if (p_wrap_index >= 0) { + // Keep caret in same visual x position it was at previously. + n_col = _get_char_pos_for_line(carets[p_caret].last_fit_x, p_line, p_wrap_index); + if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { + // Offset by one to not go past the end of the wrapped line. + if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) { + n_col -= 1; + } } + } else { + // Clamp the column. + n_col = MIN(get_caret_column(p_caret), get_line(p_line).length()); } caret_moved = (caret_moved || get_caret_column(p_caret) != n_col); carets.write[p_caret].column = n_col; + // Unselect if the caret moved to the selection origin. + if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(p_caret); } setting_caret_line = false; - if (caret_moved && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + if (caret_moved) { + _caret_changed(p_caret); } } @@ -4961,29 +4973,32 @@ int TextEdit::get_caret_line(int p_caret) const { return carets[p_caret].line; } -void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport, int p_caret) { +void TextEdit::set_caret_column(int p_column, bool p_adjust_viewport, int p_caret) { ERR_FAIL_INDEX(p_caret, carets.size()); - if (p_col < 0) { - p_col = 0; - } - if (p_col > get_line(get_caret_line(p_caret)).length()) { - p_col = get_line(get_caret_line(p_caret)).length(); - } - bool caret_moved = get_caret_column(p_caret) != p_col; - carets.write[p_caret].column = p_col; + p_column = CLAMP(p_column, 0, get_line(get_caret_line(p_caret)).length()); + + bool caret_moved = get_caret_column(p_caret) != p_column; + carets.write[p_caret].column = p_column; carets.write[p_caret].last_fit_x = _get_column_x_offset_for_line(get_caret_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret)); + if (!has_selection(p_caret)) { + // Set the selection origin last fit x to be the same, so we can tell if there was a selection. + carets.write[p_caret].selection.origin_last_fit_x = carets[p_caret].last_fit_x; + } + + // Unselect if the caret moved to the selection origin. + if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(p_caret); } - if (caret_moved && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + if (caret_moved) { + _caret_changed(p_caret); } } @@ -4998,7 +5013,7 @@ int TextEdit::get_caret_wrap_index(int p_caret) const { } String TextEdit::get_word_under_caret(int p_caret) const { - ERR_FAIL_COND_V(p_caret > carets.size(), ""); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); StringBuilder selected_text; for (int c = 0; c < carets.size(); c++) { @@ -5059,20 +5074,8 @@ bool TextEdit::is_drag_and_drop_selection_enabled() const { return drag_and_drop_selection_enabled; } -void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column, int p_caret) { - ERR_FAIL_INDEX(p_caret, carets.size()); - +void TextEdit::set_selection_mode(SelectionMode p_mode) { selecting_mode = p_mode; - if (p_line >= 0) { - ERR_FAIL_INDEX(p_line, text.size()); - carets.write[p_caret].selection.selecting_line = p_line; - carets.write[p_caret].selection.selecting_column = CLAMP(carets[p_caret].selection.selecting_column, 0, text[carets[p_caret].selection.selecting_line].length()); - } - if (p_column >= 0) { - ERR_FAIL_INDEX(carets[p_caret].selection.selecting_line, text.size()); - ERR_FAIL_INDEX(p_column, text[carets[p_caret].selection.selecting_line].length() + 1); - carets.write[p_caret].selection.selecting_column = p_column; - } } TextEdit::SelectionMode TextEdit::get_selection_mode() const { @@ -5090,16 +5093,12 @@ void TextEdit::select_all() { } remove_secondary_carets(); + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT); select(0, 0, text.size() - 1, text[text.size() - 1].length()); - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, 0, 0); - carets.write[0].selection.shiftclick_left = true; - set_caret_line(get_selection_to_line(), false); - set_caret_column(get_selection_to_column(), false); - queue_redraw(); } void TextEdit::select_word_under_caret(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1); _push_current_op(); if (!selecting_enabled) { @@ -5140,8 +5139,6 @@ void TextEdit::select_word_under_caret(int p_caret) { } select(get_caret_line(c), begin, get_caret_line(c), end, c); - // Move the caret to the end of the word for easier editing. - set_caret_column(end, false, c); } merge_overlapping_carets(); } @@ -5234,53 +5231,37 @@ void TextEdit::skip_selection_for_next_occurrence() { } } -void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret) { - ERR_FAIL_INDEX(p_caret, carets.size()); +void TextEdit::select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret) { + ERR_FAIL_INDEX(p_caret, get_caret_count()); + + p_caret_line = CLAMP(p_caret_line, 0, text.size() - 1); + p_caret_column = CLAMP(p_caret_column, 0, text[p_caret_line].length()); + set_caret_line(p_caret_line, false, true, -1, p_caret); + set_caret_column(p_caret_column, false, p_caret); + if (!selecting_enabled) { return; } - p_from_line = CLAMP(p_from_line, 0, text.size() - 1); - p_from_column = CLAMP(p_from_column, 0, text[p_from_line].length()); - p_to_line = CLAMP(p_to_line, 0, text.size() - 1); - p_to_column = CLAMP(p_to_column, 0, text[p_to_line].length()); - - carets.write[p_caret].selection.from_line = p_from_line; - carets.write[p_caret].selection.from_column = p_from_column; - carets.write[p_caret].selection.to_line = p_to_line; - carets.write[p_caret].selection.to_column = p_to_column; + p_origin_line = CLAMP(p_origin_line, 0, text.size() - 1); + p_origin_column = CLAMP(p_origin_column, 0, text[p_origin_line].length()); + set_selection_origin_line(p_origin_line, true, -1, p_caret); + set_selection_origin_column(p_origin_column, p_caret); - carets.write[p_caret].selection.active = true; - - if (get_selection_from_line(p_caret) == get_selection_to_line(p_caret)) { - if (get_selection_from_column(p_caret) == get_selection_to_column(p_caret)) { - carets.write[p_caret].selection.active = false; - - } else if (get_selection_from_column(p_caret) > get_selection_to_column(p_caret)) { - carets.write[p_caret].selection.shiftclick_left = false; - SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column); - } else { - carets.write[p_caret].selection.shiftclick_left = true; - } - } else if (get_selection_from_line(p_caret) > get_selection_to_line(p_caret)) { - carets.write[p_caret].selection.shiftclick_left = false; - SWAP(carets.write[p_caret].selection.from_line, carets.write[p_caret].selection.to_line); - SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column); - } else { - carets.write[p_caret].selection.shiftclick_left = true; + bool had_selection = has_selection(p_caret); + bool activate = p_origin_line != p_caret_line || p_origin_column != p_caret_column; + carets.write[p_caret].selection.active = activate; + if (had_selection != activate) { + _selection_changed(p_caret); } - - caret_index_edit_dirty = true; - queue_redraw(); } bool TextEdit::has_selection(int p_caret) const { - ERR_FAIL_COND_V(p_caret > carets.size(), false); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, false); + if (p_caret >= 0) { + return carets[p_caret].selection.active; + } for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; - } - if (carets[i].selection.active) { return true; } @@ -5289,100 +5270,268 @@ bool TextEdit::has_selection(int p_caret) const { } String TextEdit::get_selected_text(int p_caret) { - ERR_FAIL_COND_V(p_caret > carets.size(), ""); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); - StringBuilder selected_text; - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = caret_edit_order.size() - 1; i >= 0; i--) { - int caret_idx = caret_edit_order[i]; - if (p_caret != -1 && p_caret != caret_idx) { - continue; + if (p_caret >= 0) { + if (!has_selection(p_caret)) { + return ""; } + return _base_get_text(get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret)); + } + + StringBuilder selected_text; + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; - if (!has_selection(caret_idx)) { + if (!has_selection(caret_index)) { continue; } - selected_text += _base_get_text(get_selection_from_line(caret_idx), get_selection_from_column(caret_idx), get_selection_to_line(caret_idx), get_selection_to_column(caret_idx)); - if (p_caret == -1 && i != 0) { + if (selected_text.get_string_length() != 0) { selected_text += "\n"; } + selected_text += _base_get_text(get_selection_from_line(caret_index), get_selection_from_column(caret_index), get_selection_to_line(caret_index), get_selection_to_column(caret_index)); } return selected_text.as_string(); } -int TextEdit::get_selection_line(int p_caret) const { +int TextEdit::get_selection_at_line_column(int p_line, int p_column, bool p_include_edges, bool p_only_selections) const { + // Return the caret index of the found selection, or -1. + for (int i = 0; i < get_caret_count(); i++) { + if (_selection_contains(i, p_line, p_column, p_include_edges, p_only_selections)) { + return i; + } + } + return -1; +} + +Vector<Point2i> TextEdit::get_line_ranges_from_carets(bool p_only_selections, bool p_merge_adjacent) const { + // Get a series of line ranges that cover all lines that have a caret or selection. + // For each Point2i range, x is the first line and y is the last line. + Vector<Point2i> ret; + int last_to_line = INT_MIN; + + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (p_only_selections && !has_selection(caret_index)) { + continue; + } + Point2i range = Point2i(get_selection_from_line(caret_index), get_selection_to_line(caret_index)); + if (has_selection(caret_index) && get_selection_to_column(caret_index) == 0) { + // Dont include selection end line if it ends at column 0. + range.y--; + } + if (range.x == last_to_line || (p_merge_adjacent && range.x - 1 == last_to_line)) { + // Merge if starts on the same line or adjacent line. + ret.write[ret.size() - 1].y = range.y; + } else { + ret.append(range); + } + last_to_line = range.y; + } + return ret; +} + +TypedArray<Vector2i> TextEdit::get_line_ranges_from_carets_typed_array(bool p_only_selections, bool p_merge_adjacent) const { + // Wrapper for `get_line_ranges_from_carets` to return a datatype that can be exposed. + TypedArray<Vector2i> ret; + Vector<Point2i> ranges = get_line_ranges_from_carets(p_only_selections, p_merge_adjacent); + for (const Point2i &range : ranges) { + ret.push_back(range); + } + return ret; +} + +void TextEdit::set_selection_origin_line(int p_line, bool p_can_be_hidden, int p_wrap_index, int p_caret) { + if (!selecting_enabled) { + return; + } + ERR_FAIL_INDEX(p_caret, carets.size()); + p_line = CLAMP(p_line, 0, text.size() - 1); + + if (!p_can_be_hidden) { + if (_is_line_hidden(p_line)) { + int move_down = get_next_visible_line_offset_from(p_line, 1) - 1; + if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) { + p_line += move_down; + } else { + int move_up = get_next_visible_line_offset_from(p_line, -1) - 1; + if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) { + p_line -= move_up; + } else { + WARN_PRINT("Selection origin set to hidden line " + itos(p_line) + " and there are no nonhidden lines."); + } + } + } + } + + bool selection_moved = get_selection_origin_line(p_caret) != p_line; + carets.write[p_caret].selection.origin_line = p_line; + + int n_col; + if (p_wrap_index >= 0) { + // Keep selection origin in same visual x position it was at previously. + n_col = _get_char_pos_for_line(carets[p_caret].selection.origin_last_fit_x, p_line, p_wrap_index); + if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { + // Offset by one to not go past the end of the wrapped line. + if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) { + n_col -= 1; + } + } + } else { + // Clamp the column. + n_col = MIN(get_selection_origin_column(p_caret), get_line(p_line).length()); + } + selection_moved = (selection_moved || get_selection_origin_column(p_caret) != n_col); + carets.write[p_caret].selection.origin_column = n_col; + + // Unselect if the selection origin moved to the caret. + if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); + } +} + +void TextEdit::set_selection_origin_column(int p_column, int p_caret) { + if (!selecting_enabled) { + return; + } + ERR_FAIL_INDEX(p_caret, carets.size()); + + p_column = CLAMP(p_column, 0, get_line(get_selection_origin_line(p_caret)).length()); + + bool selection_moved = get_selection_origin_column(p_caret) != p_column; + + carets.write[p_caret].selection.origin_column = p_column; + + carets.write[p_caret].selection.origin_last_fit_x = _get_column_x_offset_for_line(get_selection_origin_column(p_caret), get_selection_origin_line(p_caret), get_selection_origin_column(p_caret)); + + // Unselect if the selection origin moved to the caret. + if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); + } +} + +int TextEdit::get_selection_origin_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.selecting_line; + return carets[p_caret].selection.origin_line; } -int TextEdit::get_selection_column(int p_caret) const { +int TextEdit::get_selection_origin_column(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.selecting_column; + return carets[p_caret].selection.origin_column; } int TextEdit::get_selection_from_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.from_line; + if (!has_selection(p_caret)) { + return carets[p_caret].line; + } + return MIN(carets[p_caret].selection.origin_line, carets[p_caret].line); } int TextEdit::get_selection_from_column(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.from_column; + if (!has_selection(p_caret)) { + return carets[p_caret].column; + } + if (carets[p_caret].selection.origin_line < carets[p_caret].line) { + return carets[p_caret].selection.origin_column; + } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) { + return carets[p_caret].column; + } else { + return MIN(carets[p_caret].selection.origin_column, carets[p_caret].column); + } } int TextEdit::get_selection_to_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.to_line; + if (!has_selection(p_caret)) { + return carets[p_caret].line; + } + return MAX(carets[p_caret].selection.origin_line, carets[p_caret].line); } int TextEdit::get_selection_to_column(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.to_column; + if (!has_selection(p_caret)) { + return carets[p_caret].column; + } + if (carets[p_caret].selection.origin_line < carets[p_caret].line) { + return carets[p_caret].column; + } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) { + return carets[p_caret].selection.origin_column; + } else { + return MAX(carets[p_caret].selection.origin_column, carets[p_caret].column); + } +} + +bool TextEdit::is_caret_after_selection_origin(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), false); + if (!has_selection(p_caret)) { + return true; + } + return carets[p_caret].line > carets[p_caret].selection.origin_line || (carets[p_caret].line == carets[p_caret].selection.origin_line && carets[p_caret].column >= carets[p_caret].selection.origin_column); } void TextEdit::deselect(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); - for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; + ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1); + bool selection_changed = false; + if (p_caret >= 0) { + selection_changed = carets.write[p_caret].selection.active; + carets.write[p_caret].selection.active = false; + } else { + for (int i = 0; i < carets.size(); i++) { + selection_changed |= carets.write[i].selection.active; + carets.write[i].selection.active = false; } - carets.write[i].selection.active = false; } - caret_index_edit_dirty = true; - queue_redraw(); + if (selection_changed) { + _selection_changed(p_caret); + } } void TextEdit::delete_selection(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (!has_selection(i)) { continue; } - selecting_mode = SelectionMode::SELECTION_MODE_NONE; - _remove_text(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i), get_selection_to_column(i)); - set_caret_line(get_selection_from_line(i), false, false, 0, i); - set_caret_column(get_selection_from_column(i), i == 0, i); - carets.write[i].selection.active = false; + int selection_from_line = get_selection_from_line(i); + int selection_from_column = get_selection_from_column(i); + int selection_to_line = get_selection_to_line(i); + int selection_to_column = get_selection_to_column(i); + + _remove_text(selection_from_line, selection_from_column, selection_to_line, selection_to_column); + _offset_carets_after(selection_to_line, selection_to_column, selection_from_line, selection_from_column); + merge_overlapping_carets(); - adjust_carets_after_edit(i, carets[i].selection.from_line, carets[i].selection.from_column, carets[i].selection.to_line, carets[i].selection.to_column); + deselect(i); + set_caret_line(selection_from_line, false, false, -1, i); + set_caret_column(selection_from_column, i == 0, i); } + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } /* Line wrapping. */ @@ -6224,8 +6373,10 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("swap_lines", "from_line", "to_line"), &TextEdit::swap_lines); ClassDB::bind_method(D_METHOD("insert_line_at", "line", "text"), &TextEdit::insert_line_at); + ClassDB::bind_method(D_METHOD("remove_line_at", "line", "move_carets_down"), &TextEdit::remove_line_at, DEFVAL(true)); ClassDB::bind_method(D_METHOD("insert_text_at_caret", "text", "caret_index"), &TextEdit::insert_text_at_caret, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("insert_text", "text", "line", "column", "before_selection_begin", "before_selection_end"), &TextEdit::insert_text, DEFVAL(true), DEFVAL(false)); ClassDB::bind_method(D_METHOD("remove_text", "from_line", "from_column", "to_line", "to_column"), &TextEdit::remove_text); ClassDB::bind_method(D_METHOD("get_last_unhidden_line"), &TextEdit::get_last_unhidden_line); @@ -6311,7 +6462,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_search_text", "search_text"), &TextEdit::set_search_text); ClassDB::bind_method(D_METHOD("set_search_flags", "flags"), &TextEdit::set_search_flags); - ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_colum"), &TextEdit::search); + ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_column"), &TextEdit::search); /* Tooltip */ ClassDB::bind_method(D_METHOD("set_tooltip_request_func", "callback"), &TextEdit::set_tooltip_request_func); @@ -6355,15 +6506,20 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_multiple_carets_enabled", "enabled"), &TextEdit::set_multiple_carets_enabled); ClassDB::bind_method(D_METHOD("is_multiple_carets_enabled"), &TextEdit::is_multiple_carets_enabled); - ClassDB::bind_method(D_METHOD("add_caret", "line", "col"), &TextEdit::add_caret); + ClassDB::bind_method(D_METHOD("add_caret", "line", "column"), &TextEdit::add_caret); ClassDB::bind_method(D_METHOD("remove_caret", "caret"), &TextEdit::remove_caret); ClassDB::bind_method(D_METHOD("remove_secondary_carets"), &TextEdit::remove_secondary_carets); - ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets); ClassDB::bind_method(D_METHOD("get_caret_count"), &TextEdit::get_caret_count); ClassDB::bind_method(D_METHOD("add_caret_at_carets", "below"), &TextEdit::add_caret_at_carets); - ClassDB::bind_method(D_METHOD("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order); - ClassDB::bind_method(D_METHOD("adjust_carets_after_edit", "caret", "from_line", "from_col", "to_line", "to_col"), &TextEdit::adjust_carets_after_edit); + ClassDB::bind_method(D_METHOD("get_sorted_carets", "include_ignored_carets"), &TextEdit::get_sorted_carets, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("collapse_carets", "from_line", "from_column", "to_line", "to_column", "inclusive"), &TextEdit::collapse_carets, DEFVAL(false)); + + ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets); + ClassDB::bind_method(D_METHOD("begin_multicaret_edit"), &TextEdit::begin_multicaret_edit); + ClassDB::bind_method(D_METHOD("end_multicaret_edit"), &TextEdit::end_multicaret_edit); + ClassDB::bind_method(D_METHOD("is_in_mulitcaret_edit"), &TextEdit::is_in_mulitcaret_edit); + ClassDB::bind_method(D_METHOD("multicaret_edit_ignore_caret", "caret_index"), &TextEdit::multicaret_edit_ignore_caret); ClassDB::bind_method(D_METHOD("is_caret_visible", "caret_index"), &TextEdit::is_caret_visible, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_caret_draw_pos", "caret_index"), &TextEdit::get_caret_draw_pos, DEFVAL(0)); @@ -6394,27 +6550,33 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_drag_and_drop_selection_enabled", "enable"), &TextEdit::set_drag_and_drop_selection_enabled); ClassDB::bind_method(D_METHOD("is_drag_and_drop_selection_enabled"), &TextEdit::is_drag_and_drop_selection_enabled); - ClassDB::bind_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::set_selection_mode, DEFVAL(-1), DEFVAL(-1), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_mode", "mode"), &TextEdit::set_selection_mode); ClassDB::bind_method(D_METHOD("get_selection_mode"), &TextEdit::get_selection_mode); ClassDB::bind_method(D_METHOD("select_all"), &TextEdit::select_all); ClassDB::bind_method(D_METHOD("select_word_under_caret", "caret_index"), &TextEdit::select_word_under_caret, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("add_selection_for_next_occurrence"), &TextEdit::add_selection_for_next_occurrence); ClassDB::bind_method(D_METHOD("skip_selection_for_next_occurrence"), &TextEdit::skip_selection_for_next_occurrence); - ClassDB::bind_method(D_METHOD("select", "from_line", "from_column", "to_line", "to_column", "caret_index"), &TextEdit::select, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("select", "origin_line", "origin_column", "caret_line", "caret_column", "caret_index"), &TextEdit::select, DEFVAL(0)); ClassDB::bind_method(D_METHOD("has_selection", "caret_index"), &TextEdit::has_selection, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_selected_text", "caret_index"), &TextEdit::get_selected_text, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("get_selection_at_line_column", "line", "column", "include_edges", "only_selections"), &TextEdit::get_selection_at_line_column, DEFVAL(true), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_line_ranges_from_carets", "only_selections", "merge_adjacent"), &TextEdit::get_line_ranges_from_carets_typed_array, DEFVAL(false), DEFVAL(true)); - ClassDB::bind_method(D_METHOD("get_selection_line", "caret_index"), &TextEdit::get_selection_line, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("get_selection_column", "caret_index"), &TextEdit::get_selection_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_origin_line", "caret_index"), &TextEdit::get_selection_origin_line, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_origin_column", "caret_index"), &TextEdit::get_selection_origin_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_origin_line", "line", "can_be_hidden", "wrap_index", "caret_index"), &TextEdit::set_selection_origin_line, DEFVAL(true), DEFVAL(-1), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_origin_column", "column", "caret_index"), &TextEdit::set_selection_origin_column, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_from_line", "caret_index"), &TextEdit::get_selection_from_line, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_from_column", "caret_index"), &TextEdit::get_selection_from_column, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_to_line", "caret_index"), &TextEdit::get_selection_to_line, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_to_column", "caret_index"), &TextEdit::get_selection_to_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("is_caret_after_selection_origin", "caret_index"), &TextEdit::is_caret_after_selection_origin, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("deselect", "caret_index"), &TextEdit::deselect, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("delete_selection", "caret_index"), &TextEdit::delete_selection, DEFVAL(-1)); @@ -6550,6 +6712,14 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("is_menu_visible"), &TextEdit::is_menu_visible); ClassDB::bind_method(D_METHOD("menu_option", "option"), &TextEdit::menu_option); + /* Deprecated */ +#ifndef DISABLE_DEPRECATED + ClassDB::bind_method(D_METHOD("adjust_carets_after_edit", "caret", "from_line", "from_col", "to_line", "to_col"), &TextEdit::adjust_carets_after_edit); + ClassDB::bind_method(D_METHOD("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order); + ClassDB::bind_method(D_METHOD("get_selection_line", "caret_index"), &TextEdit::get_selection_line, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_column", "caret_index"), &TextEdit::get_selection_column, DEFVAL(0)); +#endif + /* Inspector */ ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text", PROPERTY_HINT_MULTILINE_TEXT), "set_placeholder", "get_placeholder"); @@ -6617,7 +6787,7 @@ void TextEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("gutter_added")); ADD_SIGNAL(MethodInfo("gutter_removed")); - /* Theme items */ + // Theme items /* Search */ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_color); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_border_color); @@ -6690,6 +6860,10 @@ void TextEdit::_unhide_all_lines() { queue_redraw(); } +void TextEdit::_unhide_carets() { + // Override for functionality. +} + void TextEdit::_set_line_as_hidden(int p_line, bool p_hidden) { ERR_FAIL_INDEX(p_line, text.size()); @@ -6717,14 +6891,17 @@ void TextEdit::_set_symbol_lookup_word(const String &p_symbol) { // Overridable actions void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } start_action(EditAction::ACTION_TYPING); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (p_caret != -1 && p_caret != i) { continue; } @@ -6742,11 +6919,12 @@ void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca const char32_t chr[2] = { (char32_t)p_unicode, 0 }; insert_text_at_caret(chr, i); } + end_multicaret_edit(); end_action(); } void TextEdit::_backspace_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } @@ -6757,194 +6935,163 @@ void TextEdit::_backspace_internal(int p_caret) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (p_caret != -1 && p_caret != i) { continue; } - int cc = get_caret_column(i); - int cl = get_caret_line(i); + int to_line = get_caret_line(i); + int to_column = get_caret_column(i); - if (cc == 0 && cl == 0) { + if (to_column == 0 && to_line == 0) { continue; } - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (text[cl - 1].length()); + int from_line = to_column > 0 ? to_line : to_line - 1; + int from_column = to_column > 0 ? (to_column - 1) : (text[to_line - 1].length()); - merge_gutters(prev_line, cl); + merge_gutters(from_line, to_line); - if (_is_line_hidden(cl)) { - _set_line_as_hidden(prev_line, true); - } - _remove_text(prev_line, prev_column, cl, cc); - - set_caret_line(prev_line, false, true, 0, i); - set_caret_column(prev_column, i == 0, i); + _remove_text(from_line, from_column, to_line, to_column); + collapse_carets(from_line, from_column, to_line, to_column); + _offset_carets_after(to_line, to_column, from_line, from_column); - adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); + set_caret_line(from_line, false, true, -1, i); + set_caret_column(from_column, i == 0, i); } - merge_overlapping_carets(); + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_cut_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); + + _copy_internal(p_caret); + if (!editable) { return; } if (has_selection(p_caret)) { - DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); delete_selection(p_caret); - cut_copy_line = ""; return; } + // Remove full lines. begin_complex_operation(); - Vector<int> carets_to_remove; - - StringBuilder clipboard; - // This is the exception and has to edit in reverse order else the string copied to the clipboard will be backwards. - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = caret_edit_order.size() - 1; i >= 0; i--) { - int caret_idx = caret_edit_order[i]; - if (p_caret != -1 && p_caret != caret_idx) { - continue; - } - - int cl = get_caret_line(caret_idx); - int cc = get_caret_column(caret_idx); - int indent_level = get_indent_level(cl); - double hscroll = get_h_scroll(); - - // Check for overlapping carets. - // We don't need to worry about selections as that is caught before this entire section. - for (int j = i - 1; j >= 0; j--) { - if (get_caret_line(caret_edit_order[j]) == cl) { - carets_to_remove.push_back(caret_edit_order[j]); - i = j; - } - } - - clipboard += text[cl]; - if (p_caret == -1 && caret_idx != 0) { - clipboard += "\n"; - } - - if (cl == 0 && get_line_count() > 1) { - _remove_text(cl, 0, cl + 1, 0); - adjust_carets_after_edit(caret_idx, cl, 0, cl + 1, text[cl].length()); - } else { - _remove_text(cl, 0, cl, text[cl].length()); - set_caret_column(0, false, caret_idx); - backspace(caret_idx); - set_caret_line(get_caret_line(caret_idx) + 1, caret_idx == 0, 0, 0, caret_idx); - } - - // Correct the visually perceived caret column taking care of indentation level of the lines. - int diff_indent = indent_level - get_indent_level(get_caret_line(caret_idx)); - cc += diff_indent; - if (diff_indent != 0) { - cc += diff_indent > 0 ? -1 : 1; - } - - // Restore horizontal scroll and caret column modified by the backspace() call. - set_h_scroll(hscroll); - set_caret_column(cc, caret_idx == 0, caret_idx); + begin_multicaret_edit(); + Vector<Point2i> line_ranges; + if (p_caret == -1) { + line_ranges = get_line_ranges_from_carets(); + } else { + line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret))); } - - // Sort and remove backwards to preserve indexes. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // Preserve carets on the last line. + remove_line_at(line_range.y + line_offset); + if (line_range.x != line_range.y) { + remove_text(line_range.x + line_offset, 0, line_range.y + line_offset, 0); + } + line_offset += line_range.x - line_range.y - 1; } + end_multicaret_edit(); end_complex_operation(); - - String clipboard_string = clipboard.as_string(); - DisplayServer::get_singleton()->clipboard_set(clipboard_string); - cut_copy_line = clipboard_string; } void TextEdit::_copy_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (has_selection(p_caret)) { DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); cut_copy_line = ""; return; } + // Copy full lines. StringBuilder clipboard; - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = caret_edit_order.size() - 1; i >= 0; i--) { - int caret_idx = caret_edit_order[i]; - if (p_caret != -1 && p_caret != caret_idx) { - continue; - } - - int cl = get_caret_line(caret_idx); - if (text[cl].length() != 0) { - clipboard += _base_get_text(cl, 0, cl, text[cl].length()); - if (p_caret == -1 && i != 0) { - clipboard += "\n"; + Vector<Point2i> line_ranges; + if (p_caret == -1) { + // When there are multiple carets on a line, only copy it once. + line_ranges = get_line_ranges_from_carets(false, true); + } else { + line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret))); + } + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { + if (text[i].length() != 0) { + clipboard += _base_get_text(i, 0, i, text[i].length()); } + clipboard += "\n"; } } String clipboard_string = clipboard.as_string(); DisplayServer::get_singleton()->clipboard_set(clipboard_string); - cut_copy_line = clipboard_string; + // Set the cut copy line so we know to paste as a line. + if (get_caret_count() == 1) { + cut_copy_line = clipboard_string; + } else { + cut_copy_line = ""; + } } void TextEdit::_paste_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } String clipboard = DisplayServer::get_singleton()->clipboard_get(); + + // Paste a full line. Ignore '\r' characters that may have been added to the clipboard by the OS. + if (get_caret_count() == 1 && !has_selection(0) && !cut_copy_line.is_empty() && cut_copy_line == clipboard.replace("\r", "")) { + insert_text(clipboard, get_caret_line(), 0); + return; + } + + // Paste text at each caret or one line per caret. Vector<String> clipboad_lines = clipboard.split("\n"); - bool insert_line_per_caret = p_caret == -1 && carets.size() > 1 && clipboad_lines.size() == carets.size(); + bool insert_line_per_caret = p_caret == -1 && get_caret_count() > 1 && clipboad_lines.size() == get_caret_count(); begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - int clipboad_line = clipboad_lines.size() - 1; - for (const int &i : caret_edit_order) { - if (p_caret != -1 && p_caret != i) { + begin_multicaret_edit(); + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < get_caret_count(); i++) { + int caret_index = sorted_carets[i]; + if (p_caret != -1 && p_caret != caret_index) { continue; } - if (has_selection(i)) { - delete_selection(i); - } else if (!cut_copy_line.is_empty() && cut_copy_line == clipboard) { - set_caret_column(0, i == 0, i); - String ins = "\n"; - clipboard += ins; + if (has_selection(caret_index)) { + delete_selection(caret_index); } if (insert_line_per_caret) { - clipboard = clipboad_lines[clipboad_line]; + clipboard = clipboad_lines[i]; } - insert_text_at_caret(clipboard, i); - clipboad_line--; + insert_text_at_caret(clipboard, caret_index); } + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_paste_primary_clipboard_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!is_editable() || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { return; } String paste_buffer = DisplayServer::get_singleton()->clipboard_get_primary(); - if (carets.size() == 1) { + if (get_caret_count() == 1) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); deselect(); - set_caret_line(pos.y, true, false); + set_caret_line(pos.y, true, false, -1); set_caret_column(pos.x); } @@ -7203,10 +7350,26 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con } /* Caret */ +void TextEdit::_caret_changed(int p_caret) { + queue_redraw(); + + if (has_selection(p_caret)) { + _selection_changed(p_caret); + } + + if (caret_pos_dirty) { + return; + } + + if (is_inside_tree()) { + callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); + } + caret_pos_dirty = true; +} + void TextEdit::_emit_caret_changed() { emit_signal(SNAME("caret_changed")); caret_pos_dirty = false; - caret_index_edit_dirty = true; } void TextEdit::_reset_caret_blink_timer() { @@ -7251,60 +7414,152 @@ int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line, int p_column } } -/* Selection */ -void TextEdit::_click_selection_held() { - // Warning: is_mouse_button_pressed(MouseButton::LEFT) returns false for double+ clicks, so this doesn't work for MODE_WORD - // and MODE_LINE. However, moving the mouse triggers _gui_input, which calls these functions too, so that's not a huge problem. - // I'm unsure if there's an actual fix that doesn't have a ton of side effects. - if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) && get_selection_mode() != SelectionMode::SELECTION_MODE_NONE) { - switch (get_selection_mode()) { - case SelectionMode::SELECTION_MODE_POINTER: { - _update_selection_mode_pointer(); - } break; - case SelectionMode::SELECTION_MODE_WORD: { - _update_selection_mode_word(); - } break; - case SelectionMode::SELECTION_MODE_LINE: { - _update_selection_mode_line(); - } break; - default: { - break; +bool TextEdit::_is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges) const { + if (p_line >= p_from_line && p_line <= p_to_line && (p_line > p_from_line || p_column > p_from_column) && (p_line < p_to_line || p_column < p_to_column)) { + return true; + } + if (p_include_edges) { + if ((p_line == p_from_line && p_column == p_from_column) || (p_line == p_to_line && p_column == p_to_column)) { + return true; + } + } + return false; +} + +void TextEdit::_offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin, bool p_include_selection_end) { + // Moves all carets at or after old_line and old_column. + // Called after deleting or inserting text so that the carets stay with the text they are at. + + int edit_height = p_new_line - p_old_line; + int edit_size = p_new_column - p_old_column; + if (edit_height == 0 && edit_size == 0) { + return; + } + + // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together. + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + bool caret_at_end = selected && is_caret_after_selection_origin(i); + bool include_caret_at = caret_at_end ? p_include_selection_end : p_include_selection_begin; + + // Move caret. + int caret_line = get_caret_line(i); + int caret_column = get_caret_column(i); + bool caret_after = caret_line > p_old_line || (caret_line == p_old_line && caret_column > p_old_column); + bool caret_at = caret_line == p_old_line && caret_column == p_old_column; + if (caret_after || (caret_at && include_caret_at)) { + caret_line += edit_height; + if (caret_line == p_new_line) { + caret_column += edit_size; } + + if (edit_height != 0) { + set_caret_line(caret_line, false, true, -1, i); + } + set_caret_column(caret_column, false, i); } - } else { + + // Move selection origin. + if (!selected) { + continue; + } + bool include_selection_origin_at = !caret_at_end ? p_include_selection_end : p_include_selection_begin; + + int selection_origin_line = get_selection_origin_line(i); + int selection_origin_column = get_selection_origin_column(i); + bool selection_origin_after = selection_origin_line > p_old_line || (selection_origin_line == p_old_line && selection_origin_column > p_old_column); + bool selection_origin_at = selection_origin_line == p_old_line && selection_origin_column == p_old_column; + if (selection_origin_after || (selection_origin_at && include_selection_origin_at)) { + selection_origin_line += edit_height; + if (selection_origin_line == p_new_line) { + selection_origin_column += edit_size; + } + select(selection_origin_line, selection_origin_column, caret_line, caret_column, i); + } + } + if (!p_include_selection_begin && p_include_selection_end && has_selection()) { + // It is possible that two adjacent selections now overlap. + merge_overlapping_carets(); + } +} + +void TextEdit::_cancel_drag_and_drop_text() { + // Cancel the drag operation if drag originated from here. + if (selection_drag_attempt && get_viewport()) { + get_viewport()->gui_cancel_drag(); + } +} + +/* Selection */ +void TextEdit::_selection_changed(int p_caret) { + if (!selecting_enabled) { + return; + } + + _cancel_drag_and_drop_text(); + queue_redraw(); +} + +void TextEdit::_click_selection_held() { + // Update the selection mode on a timer so it is updated when the view scrolls even if the mouse isn't moving. + if (!Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) { click_select_held->stop(); + return; + } + switch (get_selection_mode()) { + case SelectionMode::SELECTION_MODE_POINTER: { + _update_selection_mode_pointer(); + } break; + case SelectionMode::SELECTION_MODE_WORD: { + _update_selection_mode_word(); + } break; + case SelectionMode::SELECTION_MODE_LINE: { + _update_selection_mode_line(); + } break; + default: { + break; + } } } -void TextEdit::_update_selection_mode_pointer() { - dragging_selection = true; +void TextEdit::_update_selection_mode_pointer(bool p_initial) { Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; - - select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); + int column = pos.x; + int caret_index = get_caret_count() - 1; + + if (p_initial && !has_selection(caret_index)) { + set_selection_origin_line(line, true, -1, caret_index); + set_selection_origin_column(column, caret_index); + // Set the word begin and end to the column in case the mode changes later. + carets.write[caret_index].selection.word_begin_column = column; + carets.write[caret_index].selection.word_end_column = column; + } else { + select(get_selection_origin_line(caret_index), get_selection_origin_column(caret_index), line, column, caret_index); + } + adjust_viewport_to_caret(caret_index); - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(col, true, caret_idx); - queue_redraw(); + if (has_selection(caret_index)) { + // Only set to true if any selection has been made. + dragging_selection = true; + } click_select_held->start(); merge_overlapping_carets(); } -void TextEdit::_update_selection_mode_word() { +void TextEdit::_update_selection_mode_word(bool p_initial) { dragging_selection = true; Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; + int column = pos.x; + int caret_index = get_caret_count() - 1; - int caret_pos = CLAMP(col, 0, text[line].length()); + int caret_pos = CLAMP(column, 0, text[line].length()); int beg = caret_pos; int end = beg; PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); @@ -7316,70 +7571,57 @@ void TextEdit::_update_selection_mode_word() { } } - /* Initial selection. */ - if (!has_selection(caret_idx)) { - select(line, beg, line, end, caret_idx); - carets.write[caret_idx].selection.selecting_column = beg; - carets.write[caret_idx].selection.selected_word_beg = beg; - carets.write[caret_idx].selection.selected_word_end = end; - carets.write[caret_idx].selection.selected_word_origin = beg; - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(end, true, caret_idx); + if (p_initial && !has_selection(caret_index)) { + // Set the selection origin if there is no existing selection. + select(line, beg, line, end, caret_index); + carets.write[caret_index].selection.word_begin_column = beg; + carets.write[caret_index].selection.word_end_column = end; } else { - if ((col <= carets[caret_idx].selection.selected_word_origin && line == get_selection_line(caret_idx)) || line < get_selection_line(caret_idx)) { - carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_end; - select(line, beg, get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_end, caret_idx); - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(beg, true, caret_idx); - } else { - carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_beg; - select(get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_beg, line, end, caret_idx); - set_caret_line(get_selection_to_line(caret_idx), false, true, 0, caret_idx); - set_caret_column(get_selection_to_column(caret_idx), true, caret_idx); - } + // Expand the word selection to the mouse. + int origin_line = get_selection_origin_line(caret_index); + bool is_new_selection_dir_right = line > origin_line || (line == origin_line && column >= carets[caret_index].selection.word_begin_column); + int origin_col = is_new_selection_dir_right ? carets[caret_index].selection.word_begin_column : carets[caret_index].selection.word_end_column; + int caret_col = is_new_selection_dir_right ? end : beg; + + select(origin_line, origin_col, line, caret_col, caret_index); } + adjust_viewport_to_caret(caret_index); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } - queue_redraw(); - click_select_held->start(); merge_overlapping_carets(); } -void TextEdit::_update_selection_mode_line() { +void TextEdit::_update_selection_mode_line(bool p_initial) { dragging_selection = true; Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; - - col = 0; - if (line < carets[caret_idx].selection.selecting_line) { - // Caret is above us. - set_caret_line(line - 1, false, true, 0, caret_idx); - carets.write[caret_idx].selection.selecting_column = has_selection(caret_idx) - ? text[get_selection_line(caret_idx)].length() - : 0; - } else { - // Caret is below us. - set_caret_line(line + 1, false, true, 0, caret_idx); - carets.write[caret_idx].selection.selecting_column = 0; - col = text[line].length(); + int caret_index = get_caret_count() - 1; + + int origin_line = p_initial && !has_selection(caret_index) ? line : get_selection_origin_line(); + bool line_below = line >= origin_line; + int origin_col = line_below ? 0 : get_line(origin_line).length(); + int caret_line = line_below ? line + 1 : line; + int caret_col = caret_line < text.size() ? 0 : get_line(text.size() - 1).length(); + + select(origin_line, origin_col, caret_line, caret_col, caret_index); + adjust_viewport_to_caret(caret_index); + + if (p_initial) { + // Set the word begin and end to the start and end of the origin line in case the mode changes later. + carets.write[caret_index].selection.word_begin_column = 0; + carets.write[caret_index].selection.word_end_column = get_line(origin_line).length(); } - set_caret_column(0, false, caret_idx); - select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } - queue_redraw(); - click_select_held->start(); merge_overlapping_carets(); } @@ -7389,23 +7631,23 @@ void TextEdit::_pre_shift_selection(int p_caret) { return; } - if (!has_selection(p_caret) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) { - carets.write[p_caret].selection.active = true; - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_caret_line(p_caret), get_caret_column(p_caret), p_caret); + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT); + if (has_selection(p_caret)) { return; } - - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_selection_line(p_caret), get_selection_column(p_caret), p_caret); + // Prepare selection to start at current caret position. + set_selection_origin_line(get_caret_line(p_caret), true, -1, p_caret); + set_selection_origin_column(get_caret_column(p_caret), p_caret); + carets.write[p_caret].selection.active = true; + carets.write[p_caret].selection.word_begin_column = get_caret_column(p_caret); + carets.write[p_caret].selection.word_end_column = get_caret_column(p_caret); } -void TextEdit::_post_shift_selection(int p_caret) { - if (!selecting_enabled) { - return; - } - - if (has_selection(p_caret) && get_selection_mode() == SelectionMode::SELECTION_MODE_SHIFT) { - select(get_selection_line(p_caret), get_selection_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret), p_caret); +bool TextEdit::_selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges, bool p_only_selections) const { + if (!has_selection(p_caret)) { + return !p_only_selections && p_line == get_caret_line(p_caret) && p_column == get_caret_column(p_caret); } + return _is_line_col_in_range(p_line, p_column, get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret), p_include_edges); } /* Line Wrapping */ @@ -7491,7 +7733,7 @@ void TextEdit::_update_scrollbars() { updating_scrolls = true; - if (total_rows > visible_rows) { + if (!fit_content_height && total_rows > visible_rows) { v_scroll->show(); v_scroll->set_max(total_rows + _get_visible_lines_offset()); v_scroll->set_page(visible_rows + _get_visible_lines_offset()); @@ -7780,9 +8022,43 @@ Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) { return syntax_highlighter.is_null() && !setting_text ? Dictionary() : syntax_highlighter->get_line_syntax_highlighting(p_line); } +/* Deprecated. */ +#ifndef DISABLE_DEPRECATED +Vector<int> TextEdit::get_caret_index_edit_order() { + Vector<int> carets_order = get_sorted_carets(); + carets_order.reverse(); + return carets_order; +} + +void TextEdit::adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col) { +} + +int TextEdit::get_selection_line(int p_caret) const { + return get_selection_origin_line(p_caret); +} + +int TextEdit::get_selection_column(int p_caret) const { + return get_selection_origin_column(p_caret); +} +#endif + /*** Super internal Core API. Everything builds on it. ***/ -void TextEdit::_text_changed_emit() { +void TextEdit::_text_changed() { + _cancel_drag_and_drop_text(); + queue_redraw(); + + if (text_changed_dirty || setting_text) { + return; + } + + if (is_inside_tree()) { + callable_mp(this, &TextEdit::_emit_text_changed).call_deferred(); + } + text_changed_dirty = true; +} + +void TextEdit::_emit_text_changed() { emit_signal(SNAME("text_changed")); text_changed_dirty = false; } @@ -7918,12 +8194,7 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i input_direction = (TextDirection)dir; } - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); - } - text_changed_dirty = true; - } + _text_changed(); emit_signal(SNAME("lines_edited_from"), p_line, r_end_line); } @@ -7964,12 +8235,7 @@ void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_li text.remove_range(p_from_line, p_to_line); text.set(p_from_line, pre_text + post_text, structured_text_parser(st_parser, st_args, pre_text + post_text)); - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); - } - text_changed_dirty = true; - } + _text_changed(); emit_signal(SNAME("lines_edited_from"), p_to_line, p_from_line); } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 1099295d3b..efade39876 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -389,18 +389,12 @@ private: /* Caret. */ struct Selection { bool active = false; - bool shiftclick_left = false; - int selecting_line = 0; - int selecting_column = 0; - int selected_word_beg = 0; - int selected_word_end = 0; - int selected_word_origin = 0; - - int from_line = 0; - int from_column = 0; - int to_line = 0; - int to_column = 0; + int origin_line = 0; + int origin_column = 0; + int origin_last_fit_x = 0; + int word_begin_column = 0; + int word_end_column = 0; }; struct Caret { @@ -415,11 +409,13 @@ private: // Vector containing all the carets, index '0' is the "main caret" and should never be removed. Vector<Caret> carets; - Vector<int> caret_index_edit_order; bool setting_caret_line = false; bool caret_pos_dirty = false; - bool caret_index_edit_dirty = true; + + int multicaret_edit_count = 0; + bool multicaret_edit_merge_queued = false; + HashSet<int> multicaret_edit_ignore_carets; CaretType caret_type = CaretType::CARET_TYPE_LINE; @@ -438,12 +434,18 @@ private: bool drag_action = false; bool drag_caret_force_displayed = false; + void _caret_changed(int p_caret = -1); void _emit_caret_changed(); void _reset_caret_blink_timer(); void _toggle_draw_caret(); int _get_column_x_offset_for_line(int p_char, int p_line, int p_column) const; + bool _is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges = true) const; + + void _offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin = true, bool p_include_selection_end = true); + + void _cancel_drag_and_drop_text(); /* Selection. */ SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE; @@ -456,18 +458,23 @@ private: bool selection_drag_attempt = false; bool dragging_selection = false; + int drag_and_drop_origin_caret_index = -1; + int drag_caret_index = -1; Timer *click_select_held = nullptr; uint64_t last_dblclk = 0; Vector2 last_dblclk_pos; + + void _selection_changed(int p_caret = -1); void _click_selection_held(); - void _update_selection_mode_pointer(); - void _update_selection_mode_word(); - void _update_selection_mode_line(); + void _update_selection_mode_pointer(bool p_initial = false); + void _update_selection_mode_word(bool p_initial = false); + void _update_selection_mode_line(bool p_initial = false); void _pre_shift_selection(int p_caret); - void _post_shift_selection(int p_caret); + + bool _selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const; /* Line wrapping. */ LineWrappingMode line_wrapping_mode = LineWrappingMode::LINE_WRAPPING_NONE; @@ -599,7 +606,8 @@ private: /*** Super internal Core API. Everything builds on it. ***/ bool text_changed_dirty = false; - void _text_changed_emit(); + void _text_changed(); + void _emit_text_changed(); void _insert_text(int p_line, int p_char, const String &p_text, int *r_end_line = nullptr, int *r_end_char = nullptr); void _remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); @@ -625,13 +633,15 @@ private: void _move_caret_document_end(bool p_select); bool _clear_carets_and_selection(); - // Used in add_caret_at_carets - void _get_above_below_caret_line_column(int p_old_line, int p_old_wrap_index, int p_old_column, bool p_below, int &p_new_line, int &p_new_column, int p_last_fit_x = -1) const; - protected: void _notification(int p_what); static void _bind_methods(); +#ifndef DISABLE_DEPRECATED + void _set_selection_mode_compat_86978(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0); + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + virtual void _update_theme_item_cache() override; /* Internal API for CodeEdit, pending public API. */ @@ -659,6 +669,7 @@ protected: bool _is_line_hidden(int p_line) const; void _unhide_all_lines(); + virtual void _unhide_carets(); // Symbol lookup. String lookup_symbol_word; @@ -765,9 +776,11 @@ public: void swap_lines(int p_from_line, int p_to_line); - void insert_line_at(int p_at, const String &p_text); - void insert_text_at_caret(const String &p_text, int p_caret = -1); + void insert_line_at(int p_line, const String &p_text); + void remove_line_at(int p_line, bool p_move_carets_down = true); + void insert_text_at_caret(const String &p_text, int p_caret = -1); + void insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin = true, bool p_before_selection_end = false); void remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); int get_last_unhidden_line() const; @@ -851,15 +864,20 @@ public: void set_multiple_carets_enabled(bool p_enabled); bool is_multiple_carets_enabled() const; - int add_caret(int p_line, int p_col); + int add_caret(int p_line, int p_column); void remove_caret(int p_caret); void remove_secondary_carets(); - void merge_overlapping_carets(); int get_caret_count() const; void add_caret_at_carets(bool p_below); - Vector<int> get_caret_index_edit_order(); - void adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col); + Vector<int> get_sorted_carets(bool p_include_ignored_carets = false) const; + void collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive = false); + + void merge_overlapping_carets(); + void begin_multicaret_edit(); + void end_multicaret_edit(); + bool is_in_mulitcaret_edit() const; + bool multicaret_edit_ignore_caret(int p_caret) const; bool is_caret_visible(int p_caret = 0) const; Point2 get_caret_draw_pos(int p_caret = 0) const; @@ -867,7 +885,7 @@ public: void set_caret_line(int p_line, bool p_adjust_viewport = true, bool p_can_be_hidden = true, int p_wrap_index = 0, int p_caret = 0); int get_caret_line(int p_caret = 0) const; - void set_caret_column(int p_col, bool p_adjust_viewport = true, int p_caret = 0); + void set_caret_column(int p_column, bool p_adjust_viewport = true, int p_caret = 0); int get_caret_column(int p_caret = 0) const; int get_caret_wrap_index(int p_caret = 0) const; @@ -884,27 +902,34 @@ public: void set_drag_and_drop_selection_enabled(const bool p_enabled); bool is_drag_and_drop_selection_enabled() const; - void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0); + void set_selection_mode(SelectionMode p_mode); SelectionMode get_selection_mode() const; void select_all(); void select_word_under_caret(int p_caret = -1); void add_selection_for_next_occurrence(); void skip_selection_for_next_occurrence(); - void select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret = 0); + void select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret = 0); bool has_selection(int p_caret = -1) const; String get_selected_text(int p_caret = -1); + int get_selection_at_line_column(int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const; + Vector<Point2i> get_line_ranges_from_carets(bool p_only_selections = false, bool p_merge_adjacent = true) const; + TypedArray<Vector2i> get_line_ranges_from_carets_typed_array(bool p_only_selections = false, bool p_merge_adjacent = true) const; - int get_selection_line(int p_caret = 0) const; - int get_selection_column(int p_caret = 0) const; + void set_selection_origin_line(int p_line, bool p_can_be_hidden = true, int p_wrap_index = -1, int p_caret = 0); + void set_selection_origin_column(int p_column, int p_caret = 0); + int get_selection_origin_line(int p_caret = 0) const; + int get_selection_origin_column(int p_caret = 0) const; int get_selection_from_line(int p_caret = 0) const; int get_selection_from_column(int p_caret = 0) const; int get_selection_to_line(int p_caret = 0) const; int get_selection_to_column(int p_caret = 0) const; + bool is_caret_after_selection_origin(int p_caret = 0) const; + void deselect(int p_caret = -1); void delete_selection(int p_caret = -1); @@ -1043,6 +1068,15 @@ public: Color get_font_color() const; + /* Deprecated. */ +#ifndef DISABLE_DEPRECATED + Vector<int> get_caret_index_edit_order(); + void adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col); + + int get_selection_line(int p_caret = 0) const; + int get_selection_column(int p_caret = 0) const; +#endif + TextEdit(const String &p_placeholder = String()); }; diff --git a/scene/gui/texture_button.cpp b/scene/gui/texture_button.cpp index 0b197c8c02..df90257e03 100644 --- a/scene/gui/texture_button.cpp +++ b/scene/gui/texture_button.cpp @@ -103,7 +103,7 @@ bool TextureButton::has_point(const Point2 &p_point) const { point *= scale; // finally, we need to check if the point is inside a rectangle with a position >= 0,0 and a size <= mask_size - rect.position = Point2().max(_texture_region.position); + rect.position = _texture_region.position.maxf(0); rect.size = mask_size.min(_texture_region.size); } diff --git a/scene/gui/texture_progress_bar.cpp b/scene/gui/texture_progress_bar.cpp index 5261cbe3eb..bbe5ddf1c3 100644 --- a/scene/gui/texture_progress_bar.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -249,7 +249,7 @@ Point2 TextureProgressBar::get_relative_center() { p += rad_center_off; p.x /= progress->get_width(); p.y /= progress->get_height(); - p = p.clamp(Point2(), Point2(1, 1)); + p = p.clampf(0, 1); return p; } @@ -494,7 +494,7 @@ void TextureProgressBar::_notification(int p_what) { Rect2 source = Rect2(Point2(), progress->get_size()); draw_texture_rect_region(progress, region, source, tint_progress); } else if (val != 0) { - Array pts; + LocalVector<float> pts; float direction = mode == FILL_COUNTER_CLOCKWISE ? -1 : 1; float start; @@ -507,11 +507,11 @@ void TextureProgressBar::_notification(int p_what) { float end = start + direction * val; float from = MIN(start, end); float to = MAX(start, end); - pts.append(from); + pts.push_back(from); for (float corner = Math::floor(from * 4 + 0.5) * 0.25 + 0.125; corner < to; corner += 0.25) { - pts.append(corner); + pts.push_back(corner); } - pts.append(to); + pts.push_back(to); Ref<AtlasTexture> atlas_progress = progress; bool valid_atlas_progress = atlas_progress.is_valid() && atlas_progress->get_atlas().is_valid(); @@ -524,8 +524,8 @@ void TextureProgressBar::_notification(int p_what) { Vector<Point2> uvs; Vector<Point2> points; - for (int i = 0; i < pts.size(); i++) { - Point2 uv = unit_val_to_uv(pts[i]); + for (const float &f : pts) { + Point2 uv = unit_val_to_uv(f); if (uvs.find(uv) >= 0) { continue; } diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index b17d345f1f..376ace2fe2 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -3429,7 +3429,7 @@ Rect2 Tree::_get_content_rect() const { const real_t v_size = v_scroll->is_visible() ? (v_scroll->get_combined_minimum_size().x + theme_cache.scrollbar_h_separation) : 0; const real_t h_size = h_scroll->is_visible() ? (h_scroll->get_combined_minimum_size().y + theme_cache.scrollbar_v_separation) : 0; const Point2 scroll_begin = _get_scrollbar_layout_rect().get_end() - Vector2(v_size, h_size); - const Size2 offset = (content_rect.get_end() - scroll_begin).max(Vector2(0, 0)); + const Size2 offset = (content_rect.get_end() - scroll_begin).maxf(0); return content_rect.grow_individual(0, 0, -offset.x, -offset.y); } diff --git a/scene/main/canvas_item.compat.inc b/scene/main/canvas_item.compat.inc new file mode 100644 index 0000000000..7136fded15 --- /dev/null +++ b/scene/main/canvas_item.compat.inc @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* canvas_item.compat.inc */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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 DISABLE_DEPRECATED + +void CanvasItem::_draw_circle_compat_84472(const Point2 &p_pos, real_t p_radius, const Color &p_color) { + draw_circle(p_pos, p_radius, p_color, true, -1.0, false); +} + +void CanvasItem::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("draw_circle", "position", "radius", "color"), &CanvasItem::_draw_circle_compat_84472); +} + +#endif diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index 56aa453407..cabba0f2ed 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "canvas_item.h" +#include "canvas_item.compat.inc" #include "scene/2d/canvas_group.h" #include "scene/main/canvas_layer.h" @@ -726,11 +727,40 @@ void CanvasItem::draw_rect(const Rect2 &p_rect, const Color &p_color, bool p_fil } } -void CanvasItem::draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color) { +void CanvasItem::draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color, bool p_filled, real_t p_width, bool p_antialiased) { ERR_THREAD_GUARD; ERR_DRAW_GUARD; - RenderingServer::get_singleton()->canvas_item_add_circle(canvas_item, p_pos, p_radius, p_color); + if (p_filled) { + if (p_width != -1.0) { + WARN_PRINT("The draw_circle() \"width\" argument has no effect when \"filled\" is \"true\"."); + } + + RenderingServer::get_singleton()->canvas_item_add_circle(canvas_item, p_pos, p_radius, p_color); + } else if (p_width >= 2.0 * p_radius) { + RenderingServer::get_singleton()->canvas_item_add_circle(canvas_item, p_pos, p_radius + 0.5 * p_width, p_color); + } else { + // Tessellation count is hardcoded. Keep in sync with the same variable in `RendererCanvasCull::canvas_item_add_circle()`. + const int circle_segments = 64; + + Vector<Vector2> points; + points.resize(circle_segments + 1); + + Vector2 *points_ptr = points.ptrw(); + const real_t circle_point_step = Math_TAU / circle_segments; + + for (int i = 0; i < circle_segments; i++) { + float angle = i * circle_point_step; + points_ptr[i].x = Math::cos(angle) * p_radius; + points_ptr[i].y = Math::sin(angle) * p_radius; + points_ptr[i] += p_pos; + } + points_ptr[circle_segments] = points_ptr[0]; + + Vector<Color> colors = { p_color }; + + RenderingServer::get_singleton()->canvas_item_add_polyline(canvas_item, points, colors, p_width, p_antialiased); + } } void CanvasItem::draw_texture(const Ref<Texture2D> &p_texture, const Point2 &p_pos, const Color &p_modulate) { @@ -1163,7 +1193,7 @@ void CanvasItem::_bind_methods() { ClassDB::bind_method(D_METHOD("draw_multiline", "points", "color", "width"), &CanvasItem::draw_multiline, DEFVAL(-1.0)); ClassDB::bind_method(D_METHOD("draw_multiline_colors", "points", "colors", "width"), &CanvasItem::draw_multiline_colors, DEFVAL(-1.0)); ClassDB::bind_method(D_METHOD("draw_rect", "rect", "color", "filled", "width"), &CanvasItem::draw_rect, DEFVAL(true), DEFVAL(-1.0)); - ClassDB::bind_method(D_METHOD("draw_circle", "position", "radius", "color"), &CanvasItem::draw_circle); + ClassDB::bind_method(D_METHOD("draw_circle", "position", "radius", "color", "filled", "width", "antialiased"), &CanvasItem::draw_circle, DEFVAL(true), DEFVAL(-1.0), DEFVAL(false)); ClassDB::bind_method(D_METHOD("draw_texture", "texture", "position", "modulate"), &CanvasItem::draw_texture, DEFVAL(Color(1, 1, 1, 1))); ClassDB::bind_method(D_METHOD("draw_texture_rect", "texture", "rect", "tile", "modulate", "transpose"), &CanvasItem::draw_texture_rect, DEFVAL(Color(1, 1, 1, 1)), DEFVAL(false)); ClassDB::bind_method(D_METHOD("draw_texture_rect_region", "texture", "rect", "src_rect", "modulate", "transpose", "clip_uv"), &CanvasItem::draw_texture_rect_region, DEFVAL(Color(1, 1, 1, 1)), DEFVAL(false), DEFVAL(true)); diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h index 8cec086ca6..ae7b195ead 100644 --- a/scene/main/canvas_item.h +++ b/scene/main/canvas_item.h @@ -166,6 +166,12 @@ protected: void _notification(int p_what); static void _bind_methods(); + +#ifndef DISABLE_DEPRECATED + void _draw_circle_compat_84472(const Point2 &p_pos, real_t p_radius, const Color &p_color); + static void _bind_compatibility_methods(); +#endif + void _validate_property(PropertyInfo &p_property) const; _FORCE_INLINE_ void set_hide_clip_children(bool p_value) { hide_clip_children = p_value; } @@ -273,7 +279,7 @@ public: void draw_multiline(const Vector<Point2> &p_points, const Color &p_color, real_t p_width = -1.0); void draw_multiline_colors(const Vector<Point2> &p_points, const Vector<Color> &p_colors, real_t p_width = -1.0); void draw_rect(const Rect2 &p_rect, const Color &p_color, bool p_filled = true, real_t p_width = -1.0); - void draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color); + void draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color, bool p_filled = true, real_t p_width = -1.0, bool p_antialiased = false); void draw_texture(const Ref<Texture2D> &p_texture, const Point2 &p_pos, const Color &p_modulate = Color(1, 1, 1, 1)); void draw_texture_rect(const Ref<Texture2D> &p_texture, const Rect2 &p_rect, bool p_tile = false, const Color &p_modulate = Color(1, 1, 1), bool p_transpose = false); void draw_texture_rect_region(const Ref<Texture2D> &p_texture, const Rect2 &p_rect, const Rect2 &p_src_rect, const Color &p_modulate = Color(1, 1, 1), bool p_transpose = false, bool p_clip_uv = false); diff --git a/scene/main/node.cpp b/scene/main/node.cpp index 5c5049759f..305b80ffe5 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -3520,6 +3520,7 @@ void Node::_bind_methods() { ClassDB::bind_method(D_METHOD("get_node_and_resource", "path"), &Node::_get_node_and_resource); ClassDB::bind_method(D_METHOD("is_inside_tree"), &Node::is_inside_tree); + ClassDB::bind_method(D_METHOD("is_part_of_edited_scene"), &Node::is_part_of_edited_scene); ClassDB::bind_method(D_METHOD("is_ancestor_of", "node"), &Node::is_ancestor_of); ClassDB::bind_method(D_METHOD("is_greater_than", "node"), &Node::is_greater_than); ClassDB::bind_method(D_METHOD("get_path"), &Node::get_path); diff --git a/scene/main/node.h b/scene/main/node.h index f49eeec9cd..fe212ae0f7 100644 --- a/scene/main/node.h +++ b/scene/main/node.h @@ -525,6 +525,8 @@ public: bool is_property_pinned(const StringName &p_property) const; virtual StringName get_property_store_alias(const StringName &p_property) const; bool is_part_of_edited_scene() const; +#else + bool is_part_of_edited_scene() const { return false; } #endif void get_storable_properties(HashSet<StringName> &r_storable_properties) const; diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index c465a3385f..870bed7409 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -729,13 +729,6 @@ void SceneTree::set_quit_on_go_back(bool p_enable) { quit_on_go_back = p_enable; } -#ifdef TOOLS_ENABLED - -bool SceneTree::is_node_being_edited(const Node *p_node) const { - return Engine::get_singleton()->is_editor_hint() && edited_scene_root && (edited_scene_root->is_ancestor_of(p_node) || edited_scene_root == p_node); -} -#endif - #ifdef DEBUG_ENABLED void SceneTree::set_debug_collisions_hint(bool p_enabled) { debug_collisions_hint = p_enabled; diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h index c9f3a4de1f..6f0a61ec51 100644 --- a/scene/main/scene_tree.h +++ b/scene/main/scene_tree.h @@ -330,12 +330,6 @@ public: _FORCE_INLINE_ double get_physics_process_time() const { return physics_process_time; } _FORCE_INLINE_ double get_process_time() const { return process_time; } -#ifdef TOOLS_ENABLED - bool is_node_being_edited(const Node *p_node) const; -#else - bool is_node_being_edited(const Node *p_node) const { return false; } -#endif - void set_pause(bool p_enabled); bool is_paused() const; diff --git a/scene/main/status_indicator.cpp b/scene/main/status_indicator.cpp index 54b2ff75ca..22aa051c70 100644 --- a/scene/main/status_indicator.cpp +++ b/scene/main/status_indicator.cpp @@ -30,6 +30,8 @@ #include "status_indicator.h" +#include "scene/gui/popup_menu.h" + void StatusIndicator::_notification(int p_what) { ERR_MAIN_THREAD_GUARD; #ifdef TOOLS_ENABLED @@ -43,12 +45,22 @@ void StatusIndicator::_notification(int p_what) { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { if (visible && iid == DisplayServer::INVALID_INDICATOR_ID) { iid = DisplayServer::get_singleton()->create_status_indicator(icon, tooltip, callable_mp(this, &StatusIndicator::_callback)); + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + RID menu_rid = pm->bind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid); + } } } } break; case NOTIFICATION_EXIT_TREE: { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { if (iid != DisplayServer::INVALID_INDICATOR_ID) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + pm->unbind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID()); + } DisplayServer::get_singleton()->delete_status_indicator(iid); iid = DisplayServer::INVALID_INDICATOR_ID; } @@ -66,11 +78,14 @@ void StatusIndicator::_bind_methods() { ClassDB::bind_method(D_METHOD("get_icon"), &StatusIndicator::get_icon); ClassDB::bind_method(D_METHOD("set_visible", "visible"), &StatusIndicator::set_visible); ClassDB::bind_method(D_METHOD("is_visible"), &StatusIndicator::is_visible); + ClassDB::bind_method(D_METHOD("set_menu", "menu"), &StatusIndicator::set_menu); + ClassDB::bind_method(D_METHOD("get_menu"), &StatusIndicator::get_menu); ADD_SIGNAL(MethodInfo("pressed", PropertyInfo(Variant::INT, "mouse_button"), PropertyInfo(Variant::VECTOR2I, "mouse_position"))); ADD_PROPERTY(PropertyInfo(Variant::STRING, "tooltip", PROPERTY_HINT_MULTILINE_TEXT), "set_tooltip", "get_tooltip"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Image"), "set_icon", "get_icon"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_icon", "get_icon"); + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "menu", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "PopupMenu"), "set_menu", "get_menu"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visible", "is_visible"); } @@ -78,7 +93,7 @@ void StatusIndicator::_callback(MouseButton p_index, const Point2i &p_pos) { emit_signal(SNAME("pressed"), p_index, p_pos); } -void StatusIndicator::set_icon(const Ref<Image> &p_icon) { +void StatusIndicator::set_icon(const Ref<Texture2D> &p_icon) { ERR_MAIN_THREAD_GUARD; icon = p_icon; if (iid != DisplayServer::INVALID_INDICATOR_ID) { @@ -86,7 +101,7 @@ void StatusIndicator::set_icon(const Ref<Image> &p_icon) { } } -Ref<Image> StatusIndicator::get_icon() const { +Ref<Texture2D> StatusIndicator::get_icon() const { return icon; } @@ -102,6 +117,30 @@ String StatusIndicator::get_tooltip() const { return tooltip; } +void StatusIndicator::set_menu(const NodePath &p_menu) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + pm->unbind_global_menu(); + if (iid != DisplayServer::INVALID_INDICATOR_ID) { + DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID()); + } + } + + menu = p_menu; + + pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + if (iid != DisplayServer::INVALID_INDICATOR_ID) { + RID menu_rid = pm->bind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid); + } + } +} + +NodePath StatusIndicator::get_menu() const { + return menu; +} + void StatusIndicator::set_visible(bool p_visible) { ERR_MAIN_THREAD_GUARD; if (visible == p_visible) { @@ -122,8 +161,18 @@ void StatusIndicator::set_visible(bool p_visible) { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { if (visible && iid == DisplayServer::INVALID_INDICATOR_ID) { iid = DisplayServer::get_singleton()->create_status_indicator(icon, tooltip, callable_mp(this, &StatusIndicator::_callback)); + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + RID menu_rid = pm->bind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid); + } } if (!visible && iid != DisplayServer::INVALID_INDICATOR_ID) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + pm->unbind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID()); + } DisplayServer::get_singleton()->delete_status_indicator(iid); iid = DisplayServer::INVALID_INDICATOR_ID; } diff --git a/scene/main/status_indicator.h b/scene/main/status_indicator.h index aa3aa68d78..cc137391a9 100644 --- a/scene/main/status_indicator.h +++ b/scene/main/status_indicator.h @@ -37,10 +37,11 @@ class StatusIndicator : public Node { GDCLASS(StatusIndicator, Node); - Ref<Image> icon; + Ref<Texture2D> icon; String tooltip; bool visible = true; DisplayServer::IndicatorID iid = DisplayServer::INVALID_INDICATOR_ID; + NodePath menu; protected: void _notification(int p_what); @@ -49,12 +50,15 @@ protected: void _callback(MouseButton p_index, const Point2i &p_pos); public: - void set_icon(const Ref<Image> &p_icon); - Ref<Image> get_icon() const; + void set_icon(const Ref<Texture2D> &p_icon); + Ref<Texture2D> get_icon() const; void set_tooltip(const String &p_tooltip); String get_tooltip() const; + void set_menu(const NodePath &p_menu); + NodePath get_menu() const; + void set_visible(bool p_visible); bool is_visible() const; }; diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 2d30ea345d..9acb25b133 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -688,6 +688,18 @@ void Viewport::_process_picking() { physics_picking_events.clear(); return; } +#ifndef _3D_DISABLED + if (use_xr) { + if (XRServer::get_singleton() != nullptr) { + Ref<XRInterface> xr_interface = XRServer::get_singleton()->get_primary_interface(); + if (xr_interface.is_valid() && xr_interface->is_initialized() && xr_interface->get_view_count() > 1) { + WARN_PRINT_ONCE("Object picking can't be used when stereo rendering, this will be turned off!"); + physics_object_picking = false; // don't try again. + return; + } + } + } +#endif _drop_physics_mouseover(true); @@ -856,9 +868,10 @@ void Viewport::_process_picking() { if (send_event) { co->_input_event_call(this, ev, res[i].shape); - if (physics_object_picking_first_only) { - break; - } + } + + if (physics_object_picking_first_only) { + break; } } } @@ -970,7 +983,7 @@ void Viewport::_set_size(const Size2i &p_size, const Size2i &p_size_2d_override, stretch_transform_new.scale(scale); } - Size2i new_size = p_size.max(Size2i(2, 2)); + Size2i new_size = p_size.maxi(2); if (size == new_size && size_allocated == p_allocated && stretch_transform == stretch_transform_new && p_size_2d_override == size_2d_override) { return; } @@ -1721,7 +1734,6 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) { gui.mouse_focus_mask.set_flag(button_mask); } else { gui.mouse_focus = gui_find_control(mpos); - gui.last_mouse_focus = gui.mouse_focus; if (!gui.mouse_focus) { return; @@ -2378,9 +2390,6 @@ void Viewport::_gui_remove_control(Control *p_control) { gui.forced_mouse_focus = false; gui.mouse_focus_mask.clear(); } - if (gui.last_mouse_focus == p_control) { - gui.last_mouse_focus = nullptr; - } if (gui.key_focus == p_control) { gui.key_focus = nullptr; } @@ -2758,7 +2767,7 @@ bool Viewport::_sub_windows_forward_input(const Ref<InputEvent> &p_event) { Size2i min_size = gui.currently_dragged_subwindow->get_min_size(); Size2i min_size_clamped = gui.currently_dragged_subwindow->get_clamped_minimum_size(); - min_size_clamped = min_size_clamped.max(Size2i(1, 1)); + min_size_clamped = min_size_clamped.maxi(1); Rect2i r = gui.subwindow_resize_from_rect; @@ -2819,7 +2828,7 @@ bool Viewport::_sub_windows_forward_input(const Ref<InputEvent> &p_event) { Size2i max_size = gui.currently_dragged_subwindow->get_max_size(); if ((max_size.x > 0 || max_size.y > 0) && (max_size.x >= min_size.x && max_size.y >= min_size.y)) { - max_size = max_size.max(Size2i(1, 1)); + max_size = max_size.maxi(1); if (r.size.x > max_size.x) { r.size.x = max_size.x; @@ -3578,6 +3587,13 @@ bool Viewport::gui_is_drag_successful() const { return gui.drag_successful; } +void Viewport::gui_cancel_drag() { + ERR_MAIN_THREAD_GUARD; + if (gui_is_dragging()) { + _perform_drop(); + } +} + void Viewport::set_input_as_handled() { ERR_MAIN_THREAD_GUARD; if (!handle_input_locally) { @@ -4954,7 +4970,7 @@ Viewport::Viewport() { unhandled_key_input_group = "_vp_unhandled_key_input" + id; // Window tooltip. - gui.tooltip_delay = GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "gui/timers/tooltip_delay_sec", PROPERTY_HINT_RANGE, "0,5,0.01,or_greater"), 0.5); + gui.tooltip_delay = GLOBAL_GET("gui/timers/tooltip_delay_sec"); #ifndef _3D_DISABLED set_scaling_3d_mode((Viewport::Scaling3DMode)(int)GLOBAL_GET("rendering/scaling_3d/mode")); diff --git a/scene/main/viewport.h b/scene/main/viewport.h index 21832a454c..394d48143c 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -345,7 +345,6 @@ private: bool key_event_accepted = false; HashMap<int, ObjectID> touch_focus; Control *mouse_focus = nullptr; - Control *last_mouse_focus = nullptr; Control *mouse_click_grabber = nullptr; BitField<MouseButtonMask> mouse_focus_mask; Control *key_focus = nullptr; @@ -616,6 +615,7 @@ public: bool gui_is_dragging() const; bool gui_is_drag_successful() const; + void gui_cancel_drag(); Control *gui_find_control(const Point2 &p_global); diff --git a/scene/main/window.cpp b/scene/main/window.cpp index 0ccc056a8d..929720fcf4 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -415,7 +415,7 @@ Size2i Window::_clamp_limit_size(const Size2i &p_limit_size) { if (max_window_size != Size2i()) { return p_limit_size.clamp(Vector2i(), max_window_size); } else { - return p_limit_size.max(Vector2i()); + return p_limit_size.maxi(0); } } @@ -1036,7 +1036,7 @@ void Window::_update_window_size() { } if (embedder) { - size = size.max(Size2i(1, 1)); + size = size.maxi(1); embedder->_sub_window_update(this); } else if (window_id != DisplayServer::INVALID_WINDOW_ID) { diff --git a/scene/resources/2d/tile_set.cpp b/scene/resources/2d/tile_set.cpp index 57cc4ad602..6649cb9b82 100644 --- a/scene/resources/2d/tile_set.cpp +++ b/scene/resources/2d/tile_set.cpp @@ -4650,7 +4650,7 @@ Ref<Texture2D> TileSetAtlasSource::get_texture() const { void TileSetAtlasSource::set_margins(Vector2i p_margins) { if (p_margins.x < 0 || p_margins.y < 0) { WARN_PRINT("Atlas source margins should be positive."); - margins = p_margins.max(Vector2i()); + margins = p_margins.maxi(0); } else { margins = p_margins; } @@ -4666,7 +4666,7 @@ Vector2i TileSetAtlasSource::get_margins() const { void TileSetAtlasSource::set_separation(Vector2i p_separation) { if (p_separation.x < 0 || p_separation.y < 0) { WARN_PRINT("Atlas source separation should be positive."); - separation = p_separation.max(Vector2i()); + separation = p_separation.maxi(0); } else { separation = p_separation; } @@ -4682,7 +4682,7 @@ Vector2i TileSetAtlasSource::get_separation() const { void TileSetAtlasSource::set_texture_region_size(Vector2i p_tile_size) { if (p_tile_size.x <= 0 || p_tile_size.y <= 0) { WARN_PRINT("Atlas source tile_size should be strictly positive."); - texture_region_size = p_tile_size.max(Vector2i(1, 1)); + texture_region_size = p_tile_size.maxi(1); } else { texture_region_size = p_tile_size; } diff --git a/scene/resources/audio_stream_wav.cpp b/scene/resources/audio_stream_wav.cpp index 0185c6ef85..ba5dad088f 100644 --- a/scene/resources/audio_stream_wav.cpp +++ b/scene/resources/audio_stream_wav.cpp @@ -86,15 +86,15 @@ void AudioStreamPlaybackWAV::seek(double p_time) { offset = uint64_t(p_time * base->mix_rate) << MIX_FRAC_BITS; } -template <typename Depth, bool is_stereo, bool is_ima_adpcm> -void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm) { +template <typename Depth, bool is_stereo, bool is_ima_adpcm, bool is_qoa> +void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm, QOA_State *p_qoa) { // this function will be compiled branchless by any decent compiler - int32_t final, final_r, next, next_r; + int32_t final = 0, final_r = 0, next = 0, next_r = 0; while (p_amount) { p_amount--; int64_t pos = p_offset >> MIX_FRAC_BITS; - if (is_stereo && !is_ima_adpcm) { + if (is_stereo && !is_ima_adpcm && !is_qoa) { pos <<= 1; } @@ -175,32 +175,77 @@ void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, } } else { - final = p_src[pos]; - if (is_stereo) { - final_r = p_src[pos + 1]; - } + if (is_qoa) { + if (pos != p_qoa->cache_pos) { // Prevents triple decoding on lower mix rates. + for (int i = 0; i < 2; i++) { + // Sign operations prevent triple decoding on backward loops, maxing prevents pop. + uint32_t interp_pos = MIN(pos + (i * sign) + (sign < 0), p_qoa->desc->samples - 1); + uint32_t new_data_ofs = 8 + interp_pos / QOA_FRAME_LEN * p_qoa->frame_len; + + if (p_qoa->data_ofs != new_data_ofs) { + p_qoa->data_ofs = new_data_ofs; + const uint8_t *src_ptr = (const uint8_t *)base->data; + src_ptr += p_qoa->data_ofs + AudioStreamWAV::DATA_PAD; + qoa_decode_frame(src_ptr, p_qoa->frame_len, p_qoa->desc, p_qoa->dec, &p_qoa->dec_len); + } - if constexpr (sizeof(Depth) == 1) { /* conditions will not exist anymore when compiled! */ - final <<= 8; + uint32_t dec_idx = (interp_pos % QOA_FRAME_LEN) * p_qoa->desc->channels; + + if ((sign > 0 && i == 0) || (sign < 0 && i == 1)) { + final = p_qoa->dec[dec_idx]; + p_qoa->cache[0] = final; + if (is_stereo) { + final_r = p_qoa->dec[dec_idx + 1]; + p_qoa->cache_r[0] = final_r; + } + } else { + next = p_qoa->dec[dec_idx]; + p_qoa->cache[1] = next; + if (is_stereo) { + next_r = p_qoa->dec[dec_idx + 1]; + p_qoa->cache_r[1] = next_r; + } + } + } + p_qoa->cache_pos = pos; + } else { + final = p_qoa->cache[0]; + if (is_stereo) { + final_r = p_qoa->cache_r[0]; + } + + next = p_qoa->cache[1]; + if (is_stereo) { + next_r = p_qoa->cache_r[1]; + } + } + } else { + final = p_src[pos]; if (is_stereo) { - final_r <<= 8; + final_r = p_src[pos + 1]; } - } - if (is_stereo) { - next = p_src[pos + 2]; - next_r = p_src[pos + 3]; - } else { - next = p_src[pos + 1]; - } + if constexpr (sizeof(Depth) == 1) { /* conditions will not exist anymore when compiled! */ + final <<= 8; + if (is_stereo) { + final_r <<= 8; + } + } - if constexpr (sizeof(Depth) == 1) { - next <<= 8; if (is_stereo) { - next_r <<= 8; + next = p_src[pos + 2]; + next_r = p_src[pos + 3]; + } else { + next = p_src[pos + 1]; } - } + if constexpr (sizeof(Depth) == 1) { + next <<= 8; + if (is_stereo) { + next_r <<= 8; + } + } + } int32_t frac = int64_t(p_offset & MIX_FRAC_MASK); final = final + ((next - final) * frac >> MIX_FRAC_BITS); @@ -240,6 +285,9 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_ case AudioStreamWAV::FORMAT_IMA_ADPCM: len *= 2; break; + case AudioStreamWAV::FORMAT_QOA: + len = qoa.desc->samples * qoa.desc->channels; + break; } if (base->stereo) { @@ -368,27 +416,34 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_ switch (base->format) { case AudioStreamWAV::FORMAT_8_BITS: { if (is_stereo) { - do_resample<int8_t, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int8_t, true, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } else { - do_resample<int8_t, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int8_t, false, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } } break; case AudioStreamWAV::FORMAT_16_BITS: { if (is_stereo) { - do_resample<int16_t, true, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int16_t, true, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } else { - do_resample<int16_t, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int16_t, false, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } } break; case AudioStreamWAV::FORMAT_IMA_ADPCM: { if (is_stereo) { - do_resample<int8_t, true, true>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int8_t, true, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } else { - do_resample<int8_t, false, true>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int8_t, false, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } } break; + case AudioStreamWAV::FORMAT_QOA: { + if (is_stereo) { + do_resample<uint8_t, true, false, true>((uint8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); + } else { + do_resample<uint8_t, false, false, true>((uint8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); + } + } break; } dst_buff += target; @@ -412,6 +467,16 @@ void AudioStreamPlaybackWAV::tag_used_streams() { AudioStreamPlaybackWAV::AudioStreamPlaybackWAV() {} +AudioStreamPlaybackWAV::~AudioStreamPlaybackWAV() { + if (qoa.desc) { + memfree(qoa.desc); + } + + if (qoa.dec) { + memfree(qoa.dec); + } +} + ///////////////////// void AudioStreamWAV::set_format(Format p_format) { @@ -475,6 +540,10 @@ double AudioStreamWAV::get_length() const { case AudioStreamWAV::FORMAT_IMA_ADPCM: len *= 2; break; + case AudioStreamWAV::FORMAT_QOA: + qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } }; + qoa_decode_header((uint8_t *)data + DATA_PAD, QOA_MIN_FILESIZE, &desc); + len = desc.samples * desc.channels; } if (stereo) { @@ -526,8 +595,8 @@ Vector<uint8_t> AudioStreamWAV::get_data() const { } Error AudioStreamWAV::save_to_wav(const String &p_path) { - if (format == AudioStreamWAV::FORMAT_IMA_ADPCM) { - WARN_PRINT("Saving IMA_ADPC samples are not supported yet"); + if (format == AudioStreamWAV::FORMAT_IMA_ADPCM || format == AudioStreamWAV::FORMAT_QOA) { + WARN_PRINT("Saving IMA_ADPCM and QOA samples is not supported yet"); return ERR_UNAVAILABLE; } @@ -548,6 +617,7 @@ Error AudioStreamWAV::save_to_wav(const String &p_path) { byte_pr_sample = 1; break; case AudioStreamWAV::FORMAT_16_BITS: + case AudioStreamWAV::FORMAT_QOA: byte_pr_sample = 2; break; case AudioStreamWAV::FORMAT_IMA_ADPCM: @@ -590,6 +660,7 @@ Error AudioStreamWAV::save_to_wav(const String &p_path) { } break; case AudioStreamWAV::FORMAT_16_BITS: + case AudioStreamWAV::FORMAT_QOA: for (unsigned int i = 0; i < data_bytes / 2; i++) { uint16_t data_point = decode_uint16(&read_data[i * 2]); file->store_16(data_point); @@ -607,6 +678,16 @@ Ref<AudioStreamPlayback> AudioStreamWAV::instantiate_playback() { Ref<AudioStreamPlaybackWAV> sample; sample.instantiate(); sample->base = Ref<AudioStreamWAV>(this); + + if (format == AudioStreamWAV::FORMAT_QOA) { + sample->qoa.desc = (qoa_desc *)memalloc(sizeof(qoa_desc)); + qoa_decode_header((uint8_t *)data + DATA_PAD, QOA_MIN_FILESIZE, sample->qoa.desc); + sample->qoa.frame_len = qoa_max_frame_size(sample->qoa.desc); + int samples_len = (sample->qoa.desc->samples > QOA_FRAME_LEN ? QOA_FRAME_LEN : sample->qoa.desc->samples); + int alloc_len = sample->qoa.desc->channels * samples_len * sizeof(int16_t); + sample->qoa.dec = (int16_t *)memalloc(alloc_len); + } + return sample; } @@ -639,7 +720,7 @@ void AudioStreamWAV::_bind_methods() { ClassDB::bind_method(D_METHOD("save_to_wav", "path"), &AudioStreamWAV::save_to_wav); ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_data", "get_data"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM"), "set_format", "get_format"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM,QOA"), "set_format", "get_format"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_mode", PROPERTY_HINT_ENUM, "Disabled,Forward,Ping-Pong,Backward"), "set_loop_mode", "get_loop_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_begin"), "set_loop_begin", "get_loop_begin"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_end"), "set_loop_end", "get_loop_end"); @@ -649,6 +730,7 @@ void AudioStreamWAV::_bind_methods() { BIND_ENUM_CONSTANT(FORMAT_8_BITS); BIND_ENUM_CONSTANT(FORMAT_16_BITS); BIND_ENUM_CONSTANT(FORMAT_IMA_ADPCM); + BIND_ENUM_CONSTANT(FORMAT_QOA); BIND_ENUM_CONSTANT(LOOP_DISABLED); BIND_ENUM_CONSTANT(LOOP_FORWARD); diff --git a/scene/resources/audio_stream_wav.h b/scene/resources/audio_stream_wav.h index 959d1ceca0..146142d8a4 100644 --- a/scene/resources/audio_stream_wav.h +++ b/scene/resources/audio_stream_wav.h @@ -31,7 +31,11 @@ #ifndef AUDIO_STREAM_WAV_H #define AUDIO_STREAM_WAV_H +#define QOA_IMPLEMENTATION +#define QOA_NO_STDIO + #include "servers/audio/audio_stream.h" +#include "thirdparty/misc/qoa.h" class AudioStreamWAV; @@ -54,14 +58,25 @@ class AudioStreamPlaybackWAV : public AudioStreamPlayback { int32_t window_ofs = 0; } ima_adpcm[2]; + struct QOA_State { + qoa_desc *desc = nullptr; + uint32_t data_ofs = 0; + uint32_t frame_len = 0; + int16_t *dec = nullptr; + uint32_t dec_len = 0; + int64_t cache_pos = -1; + int16_t cache[2] = { 0, 0 }; + int16_t cache_r[2] = { 0, 0 }; + } qoa; + int64_t offset = 0; int sign = 1; bool active = false; friend class AudioStreamWAV; Ref<AudioStreamWAV> base; - template <typename Depth, bool is_stereo, bool is_ima_adpcm> - void do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm); + template <typename Depth, bool is_stereo, bool is_ima_adpcm, bool is_qoa> + void do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm, QOA_State *p_qoa); public: virtual void start(double p_from_pos = 0.0) override; @@ -78,6 +93,7 @@ public: virtual void tag_used_streams() override; AudioStreamPlaybackWAV(); + ~AudioStreamPlaybackWAV(); }; class AudioStreamWAV : public AudioStream { @@ -88,7 +104,8 @@ public: enum Format { FORMAT_8_BITS, FORMAT_16_BITS, - FORMAT_IMA_ADPCM + FORMAT_IMA_ADPCM, + FORMAT_QOA, }; // Keep the ResourceImporterWAV `edit/loop_mode` enum hint in sync with these options. diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index b381096df8..15b40e776c 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -688,6 +688,9 @@ void BaseMaterial3D::_update_shader() { case BLEND_MODE_MUL: code += "blend_mul"; break; + case BLEND_MODE_PREMULT_ALPHA: + code += "blend_premul_alpha"; + break; case BLEND_MODE_MAX: break; // Internal value, skip. } @@ -1819,6 +1822,11 @@ void fragment() {)"; vec3 detail = mix(ALBEDO.rgb, ALBEDO.rgb * detail_tex.rgb, detail_tex.a); )"; } break; + case BLEND_MODE_PREMULT_ALPHA: { + // This is unlikely to ever be used for detail textures, and in order for it to function in the editor, another bit must be used in MaterialKey, + // but there are only 5 bits left, so I'm going to leave this disabled unless it's actually requested. + //code += "\tvec3 detail = (1.0-detail_tex.a)*ALBEDO.rgb+detail_tex.rgb;\n"; + } break; case BLEND_MODE_MAX: break; // Internal value, skip. } @@ -3040,7 +3048,7 @@ void BaseMaterial3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "alpha_hash_scale", PROPERTY_HINT_RANGE, "0,2,0.01"), "set_alpha_hash_scale", "get_alpha_hash_scale"); ADD_PROPERTY(PropertyInfo(Variant::INT, "alpha_antialiasing_mode", PROPERTY_HINT_ENUM, "Disabled,Alpha Edge Blend,Alpha Edge Clip"), "set_alpha_antialiasing", "get_alpha_antialiasing"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "alpha_antialiasing_edge", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_alpha_antialiasing_edge", "get_alpha_antialiasing_edge"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Mix,Add,Subtract,Multiply"), "set_blend_mode", "get_blend_mode"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Mix,Add,Subtract,Multiply,Premultiplied Alpha"), "set_blend_mode", "get_blend_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "cull_mode", PROPERTY_HINT_ENUM, "Back,Front,Disabled"), "set_cull_mode", "get_cull_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "depth_draw_mode", PROPERTY_HINT_ENUM, "Opaque Only,Always,Never"), "set_depth_draw_mode", "get_depth_draw_mode"); ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "no_depth_test"), "set_flag", "get_flag", FLAG_DISABLE_DEPTH_TEST); @@ -3269,6 +3277,7 @@ void BaseMaterial3D::_bind_methods() { BIND_ENUM_CONSTANT(BLEND_MODE_ADD); BIND_ENUM_CONSTANT(BLEND_MODE_SUB); BIND_ENUM_CONSTANT(BLEND_MODE_MUL); + BIND_ENUM_CONSTANT(BLEND_MODE_PREMULT_ALPHA); BIND_ENUM_CONSTANT(ALPHA_ANTIALIASING_OFF); BIND_ENUM_CONSTANT(ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); diff --git a/scene/resources/material.h b/scene/resources/material.h index 073403f71e..ecf79c581b 100644 --- a/scene/resources/material.h +++ b/scene/resources/material.h @@ -219,6 +219,7 @@ public: BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, + BLEND_MODE_PREMULT_ALPHA, BLEND_MODE_MAX }; diff --git a/scene/resources/particle_process_material.cpp b/scene/resources/particle_process_material.cpp index 685625ab72..0b65b33240 100644 --- a/scene/resources/particle_process_material.cpp +++ b/scene/resources/particle_process_material.cpp @@ -634,7 +634,7 @@ void ParticleProcessMaterial::_update_shader() { if (emission_shape == EMISSION_SHAPE_RING) { code += " \n"; code += " float ring_spawn_angle = rand_from_seed(alt_seed) * 2.0 * pi;\n"; - code += " float ring_random_radius = rand_from_seed(alt_seed) * (emission_ring_radius - emission_ring_inner_radius) + emission_ring_inner_radius;\n"; + code += " float ring_random_radius = sqrt(rand_from_seed(alt_seed) * (emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius);\n"; code += " vec3 axis = emission_ring_axis == vec3(0.0) ? vec3(0.0, 0.0, 1.0) : normalize(emission_ring_axis);\n"; code += " vec3 ortho_axis = vec3(0.0);\n"; code += " if (abs(axis) == vec3(1.0, 0.0, 0.0)) {\n"; @@ -1136,9 +1136,9 @@ void ParticleProcessMaterial::_update_shader() { code += " if (COLLIDED) emit_count = sub_emitter_amount_at_collision;\n"; } break; case SUB_EMITTER_AT_END: { - code += " float unit_delta = DELTA/LIFETIME;\n"; - code += " float end_time = CUSTOM.w * 0.95;\n"; // if we do at the end we might miss it, as it can just get deactivated by emitter - code += " if (CUSTOM.y < end_time && (CUSTOM.y + unit_delta) >= end_time) emit_count = sub_emitter_amount_at_end;\n"; + code += " if ((CUSTOM.y / CUSTOM.w * LIFETIME) > (LIFETIME - DELTA)) {\n"; + code += " emit_count = sub_emitter_amount_at_end;\n"; + code += " }\n"; } break; default: { } diff --git a/scene/resources/shader.cpp b/scene/resources/shader.cpp index 5b375905cc..0087a5e7f2 100644 --- a/scene/resources/shader.cpp +++ b/scene/resources/shader.cpp @@ -37,6 +37,15 @@ #include "servers/rendering_server.h" #include "texture.h" +#ifdef TOOLS_ENABLED +#include "editor/editor_help.h" + +#include "modules/modules_enabled.gen.h" // For regex. +#ifdef MODULE_REGEX_ENABLED +#include "modules/regex/regex.h" +#endif +#endif + Shader::Mode Shader::get_mode() const { return mode; } @@ -121,6 +130,12 @@ void Shader::get_shader_uniform_list(List<PropertyInfo> *p_params, bool p_get_gr List<PropertyInfo> local; RenderingServer::get_singleton()->get_shader_parameter_list(shader, &local); +#ifdef TOOLS_ENABLED + DocData::ClassDoc class_doc; + class_doc.name = get_path(); + class_doc.is_script_doc = true; +#endif + for (PropertyInfo &pi : local) { bool is_group = pi.usage == PROPERTY_USAGE_GROUP || pi.usage == PROPERTY_USAGE_SUBGROUP; if (!p_get_groups && is_group) { @@ -136,9 +151,33 @@ void Shader::get_shader_uniform_list(List<PropertyInfo> *p_params, bool p_get_gr if (pi.type == Variant::RID) { pi.type = Variant::OBJECT; } +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + DocData::PropertyDoc prop_doc; + prop_doc.name = "shader_parameter/" + pi.name; +#ifdef MODULE_REGEX_ENABLED + const RegEx pattern("/\\*\\*([^*]|[\\r\\n]|(\\*+([^*/]|[\\r\\n])))*\\*+/\\s*uniform\\s+\\w+\\s+" + pi.name + "(?=[\\s:;=])"); + Ref<RegExMatch> pattern_ref = pattern.search(code); + if (pattern_ref != nullptr) { + RegExMatch *match = pattern_ref.ptr(); + const RegEx pattern_tip("\\/\\*\\*([\\s\\S]*?)\\*/"); + Ref<RegExMatch> pattern_tip_ref = pattern_tip.search(match->get_string(0)); + RegExMatch *match_tip = pattern_tip_ref.ptr(); + const RegEx pattern_stripped("\\n\\s*\\*\\s*"); + prop_doc.description = pattern_stripped.sub(match_tip->get_string(1), "\n", true); + } +#endif + class_doc.properties.push_back(prop_doc); + } +#endif p_params->push_back(pi); } } +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint() && !class_doc.name.is_empty() && p_params) { + EditorHelp::get_doc_data()->add_doc(class_doc); + } +#endif } RID Shader::get_rid() const { diff --git a/scene/resources/surface_tool.cpp b/scene/resources/surface_tool.cpp index 9f2fad410c..b83be8b6ef 100644 --- a/scene/resources/surface_tool.cpp +++ b/scene/resources/surface_tool.cpp @@ -803,6 +803,8 @@ const uint32_t SurfaceTool::custom_mask[RS::ARRAY_CUSTOM_COUNT] = { Mesh::ARRAY_ const uint32_t SurfaceTool::custom_shift[RS::ARRAY_CUSTOM_COUNT] = { Mesh::ARRAY_FORMAT_CUSTOM0_SHIFT, Mesh::ARRAY_FORMAT_CUSTOM1_SHIFT, Mesh::ARRAY_FORMAT_CUSTOM2_SHIFT, Mesh::ARRAY_FORMAT_CUSTOM3_SHIFT }; void SurfaceTool::create_vertex_array_from_arrays(const Array &p_arrays, LocalVector<SurfaceTool::Vertex> &ret, uint64_t *r_format) { + ERR_FAIL_INDEX(RS::ARRAY_WEIGHTS, p_arrays.size()); + ret.clear(); Vector<Vector3> varr = p_arrays[RS::ARRAY_VERTEX]; diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp index 6f1aa5c850..4b51f6c471 100644 --- a/scene/resources/visual_shader.cpp +++ b/scene/resources/visual_shader.cpp @@ -4928,6 +4928,10 @@ String VisualShaderNodeExpression::generate_code(Shader::Mode p_mode, VisualShad return code; } +bool VisualShaderNodeExpression::is_output_port_expandable(int p_port) const { + return false; +} + void VisualShaderNodeExpression::_bind_methods() { ClassDB::bind_method(D_METHOD("set_expression", "expression"), &VisualShaderNodeExpression::set_expression); ClassDB::bind_method(D_METHOD("get_expression"), &VisualShaderNodeExpression::get_expression); diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h index d7270f3ac6..d32e2465b9 100644 --- a/scene/resources/visual_shader.h +++ b/scene/resources/visual_shader.h @@ -878,6 +878,7 @@ public: String get_expression() const; virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual bool is_output_port_expandable(int p_port) const override; VisualShaderNodeExpression(); }; diff --git a/scu_builders.py b/scu_builders.py index e6adf6543c..a9ae428222 100644 --- a/scu_builders.py +++ b/scu_builders.py @@ -3,6 +3,7 @@ import glob, os import math +from methods import print_error from pathlib import Path from os.path import normpath, basename @@ -38,7 +39,7 @@ def find_files_in_folder(folder, sub_folder, include_list, extension, sought_exc abs_folder = base_folder_path + folder + "/" + sub_folder if not os.path.isdir(abs_folder): - print("SCU: ERROR: %s not found." % abs_folder) + print_error(f'SCU: "{abs_folder}" not found.') return include_list, found_exceptions os.chdir(abs_folder) @@ -70,7 +71,7 @@ def write_output_file(file_count, include_list, start_line, end_line, output_fol # create os.mkdir(output_folder) if not os.path.isdir(output_folder): - print("SCU: ERROR: %s could not be created." % output_folder) + print_error(f'SCU: "{output_folder}" could not be created.') return if _verbose: print("SCU: Creating folder: %s" % output_folder) @@ -104,7 +105,7 @@ def write_output_file(file_count, include_list, start_line, end_line, output_fol def write_exception_output_file(file_count, exception_string, output_folder, output_filename_prefix, extension): output_folder = os.path.abspath(output_folder) if not os.path.isdir(output_folder): - print("SCU: ERROR: %s does not exist." % output_folder) + print_error(f"SCU: {output_folder} does not exist.") return file_text = exception_string + "\n" diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 9600caa214..f1e3479eae 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -709,12 +709,12 @@ void DisplayServer::set_icon(const Ref<Image> &p_icon) { WARN_PRINT("Icon not supported by this display server."); } -DisplayServer::IndicatorID DisplayServer::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServer::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) { WARN_PRINT("Status indicator not supported by this display server."); return INVALID_INDICATOR_ID; } -void DisplayServer::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) { +void DisplayServer::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) { WARN_PRINT("Status indicator not supported by this display server."); } @@ -722,6 +722,10 @@ void DisplayServer::status_indicator_set_tooltip(IndicatorID p_id, const String WARN_PRINT("Status indicator not supported by this display server."); } +void DisplayServer::status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) { + WARN_PRINT("Status indicator not supported by this display server."); +} + void DisplayServer::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { WARN_PRINT("Status indicator not supported by this display server."); } @@ -977,6 +981,7 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("create_status_indicator", "icon", "tooltip", "callback"), &DisplayServer::create_status_indicator); ClassDB::bind_method(D_METHOD("status_indicator_set_icon", "id", "icon"), &DisplayServer::status_indicator_set_icon); ClassDB::bind_method(D_METHOD("status_indicator_set_tooltip", "id", "tooltip"), &DisplayServer::status_indicator_set_tooltip); + ClassDB::bind_method(D_METHOD("status_indicator_set_menu", "id", "menu_rid"), &DisplayServer::status_indicator_set_menu); ClassDB::bind_method(D_METHOD("status_indicator_set_callback", "id", "callback"), &DisplayServer::status_indicator_set_callback); ClassDB::bind_method(D_METHOD("delete_status_indicator", "id"), &DisplayServer::delete_status_indicator); diff --git a/servers/display_server.h b/servers/display_server.h index aab51644c0..0391edecd4 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -564,9 +564,10 @@ public: virtual void set_native_icon(const String &p_filename); virtual void set_icon(const Ref<Image> &p_icon); - virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback); - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon); + virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback); + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon); virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip); + virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid); virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback); virtual void delete_status_indicator(IndicatorID p_id); diff --git a/servers/physics_2d/godot_area_2d.h b/servers/physics_2d/godot_area_2d.h index d14ddb6a2e..e6c3b45d6c 100644 --- a/servers/physics_2d/godot_area_2d.h +++ b/servers/physics_2d/godot_area_2d.h @@ -101,10 +101,10 @@ class GodotArea2D : public GodotCollisionObject2D { public: void set_monitor_callback(const Callable &p_callback); - _FORCE_INLINE_ bool has_monitor_callback() const { return !monitor_callback.is_null(); } + _FORCE_INLINE_ bool has_monitor_callback() const { return monitor_callback.is_valid(); } void set_area_monitor_callback(const Callable &p_callback); - _FORCE_INLINE_ bool has_area_monitor_callback() const { return !area_monitor_callback.is_null(); } + _FORCE_INLINE_ bool has_area_monitor_callback() const { return area_monitor_callback.is_valid(); } _FORCE_INLINE_ void add_body_to_query(GodotBody2D *p_body, uint32_t p_body_shape, uint32_t p_area_shape); _FORCE_INLINE_ void remove_body_from_query(GodotBody2D *p_body, uint32_t p_body_shape, uint32_t p_area_shape); diff --git a/servers/physics_3d/godot_area_3d.h b/servers/physics_3d/godot_area_3d.h index c3c9e494a4..701dc73917 100644 --- a/servers/physics_3d/godot_area_3d.h +++ b/servers/physics_3d/godot_area_3d.h @@ -107,10 +107,10 @@ class GodotArea3D : public GodotCollisionObject3D { public: void set_monitor_callback(const Callable &p_callback); - _FORCE_INLINE_ bool has_monitor_callback() const { return !monitor_callback.is_null(); } + _FORCE_INLINE_ bool has_monitor_callback() const { return monitor_callback.is_valid(); } void set_area_monitor_callback(const Callable &p_callback); - _FORCE_INLINE_ bool has_area_monitor_callback() const { return !area_monitor_callback.is_null(); } + _FORCE_INLINE_ bool has_area_monitor_callback() const { return area_monitor_callback.is_valid(); } _FORCE_INLINE_ void add_body_to_query(GodotBody3D *p_body, uint32_t p_body_shape, uint32_t p_area_shape); _FORCE_INLINE_ void remove_body_from_query(GodotBody3D *p_body, uint32_t p_body_shape, uint32_t p_area_shape); diff --git a/servers/rendering/renderer_canvas_cull.cpp b/servers/rendering/renderer_canvas_cull.cpp index 34f9069649..e48c72cec7 100644 --- a/servers/rendering/renderer_canvas_cull.cpp +++ b/servers/rendering/renderer_canvas_cull.cpp @@ -2106,7 +2106,7 @@ void RendererCanvasCull::update_visibility_notifiers() { if (visibility_notifier->just_visible) { visibility_notifier->just_visible = false; - if (!visibility_notifier->enter_callable.is_null()) { + if (visibility_notifier->enter_callable.is_valid()) { if (RSG::threaded) { visibility_notifier->enter_callable.call_deferred(); } else { @@ -2117,7 +2117,7 @@ void RendererCanvasCull::update_visibility_notifiers() { if (visibility_notifier->visible_in_frame != RSG::rasterizer->get_frame_number()) { visibility_notifier_list.remove(E); - if (!visibility_notifier->exit_callable.is_null()) { + if (visibility_notifier->exit_callable.is_valid()) { if (RSG::threaded) { visibility_notifier->exit_callable.call_deferred(); } else { diff --git a/servers/rendering/renderer_rd/effects/ss_effects.cpp b/servers/rendering/renderer_rd/effects/ss_effects.cpp index 3db82c8fbd..36a2470c7b 100644 --- a/servers/rendering/renderer_rd/effects/ss_effects.cpp +++ b/servers/rendering/renderer_rd/effects/ss_effects.cpp @@ -521,8 +521,7 @@ void SSEffects::downsample_depth(Ref<RenderSceneBuffersRD> p_render_buffers, uin RD::get_singleton()->compute_list_set_push_constant(compute_list, &ss_effects.downsample_push_constant, sizeof(SSEffectsDownsamplePushConstant)); if (use_half_size) { - size.x = MAX(1, size.x >> 1); - size.y = MAX(1, size.y >> 1); + size = Size2i(size.x >> 1, size.y >> 1).maxi(1); } RD::get_singleton()->compute_list_dispatch_threads(compute_list, size.x, size.y, 1); diff --git a/servers/rendering/renderer_rd/environment/fog.cpp b/servers/rendering/renderer_rd/environment/fog.cpp index 48537a97d9..2dfcd67411 100644 --- a/servers/rendering/renderer_rd/environment/fog.cpp +++ b/servers/rendering/renderer_rd/environment/fog.cpp @@ -541,7 +541,7 @@ void Fog::volumetric_fog_update(const VolumetricFogSettings &p_settings, const P if (p_cam_projection.is_orthogonal()) { fog_near_size = fog_far_size; } else { - fog_near_size = frustum_near_size.max(Vector2(0.001, 0.001)); + fog_near_size = frustum_near_size.maxf(0.001); } params.fog_frustum_size_begin[0] = fog_near_size.x; @@ -1001,7 +1001,7 @@ void Fog::volumetric_fog_update(const VolumetricFogSettings &p_settings, const P if (p_cam_projection.is_orthogonal()) { fog_near_size = fog_far_size; } else { - fog_near_size = frustum_near_size.max(Vector2(0.001, 0.001)); + fog_near_size = frustum_near_size.maxf(0.001); } params.fog_frustum_size_begin[0] = fog_near_size.x; diff --git a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp index c7ab7ea462..2df0331688 100644 --- a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp @@ -1706,7 +1706,7 @@ void RenderForwardClustered::_render_scene(RenderDataRD *p_render_data, const Co } if (p_render_data->environment.is_valid()) { - if (environment_get_sdfgi_enabled(p_render_data->environment)) { + if (environment_get_sdfgi_enabled(p_render_data->environment) && get_debug_draw_mode() != RS::VIEWPORT_DEBUG_DRAW_UNSHADED) { using_sdfgi = true; } if (environment_get_ssr_enabled(p_render_data->environment)) { diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp index 8d865ba440..9e0dacc1f2 100644 --- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp @@ -90,6 +90,7 @@ void SceneShaderForwardClustered::ShaderData::set_code(const String &p_code) { actions.render_mode_values["blend_mix"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MIX); actions.render_mode_values["blend_sub"] = Pair<int *, int>(&blend_mode, BLEND_MODE_SUB); actions.render_mode_values["blend_mul"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MUL); + actions.render_mode_values["blend_premul_alpha"] = Pair<int *, int>(&blend_mode, BLEND_MODE_PREMULT_ALPHA); actions.render_mode_values["alpha_to_coverage"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); actions.render_mode_values["alpha_to_coverage_and_one"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE_AND_TO_ONE); @@ -244,7 +245,17 @@ void SceneShaderForwardClustered::ShaderData::set_code(const String &p_code) { blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE; blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ZERO; - } + } break; + case BLEND_MODE_PREMULT_ALPHA: { + blend_attachment.enable_blend = true; + blend_attachment.alpha_blend_op = RD::BLEND_OP_ADD; + blend_attachment.color_blend_op = RD::BLEND_OP_ADD; + blend_attachment.src_color_blend_factor = RD::BLEND_FACTOR_ONE; + blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE; + blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + uses_blend_alpha = true; // Force alpha used because of blend. + } break; } // Color pass -> attachment 0: Color/Diffuse, attachment 1: Separate Specular, attachment 2: Motion Vectors @@ -593,6 +604,7 @@ void SceneShaderForwardClustered::init(const String p_defines) { actions.renames["NORMAL_MAP_DEPTH"] = "normal_map_depth"; actions.renames["ALBEDO"] = "albedo"; actions.renames["ALPHA"] = "alpha"; + actions.renames["PREMUL_ALPHA_FACTOR"] = "premul_alpha"; actions.renames["METALLIC"] = "metallic"; actions.renames["SPECULAR"] = "specular"; actions.renames["ROUGHNESS"] = "roughness"; @@ -672,6 +684,7 @@ void SceneShaderForwardClustered::init(const String p_defines) { actions.usage_defines["INSTANCE_CUSTOM"] = "#define ENABLE_INSTANCE_CUSTOM\n"; actions.usage_defines["POSITION"] = "#define OVERRIDE_POSITION\n"; actions.usage_defines["LIGHT_VERTEX"] = "#define LIGHT_VERTEX_USED\n"; + actions.usage_defines["PREMUL_ALPHA_FACTOR"] = "#define PREMUL_ALPHA_USED\n"; actions.usage_defines["ALPHA_SCISSOR_THRESHOLD"] = "#define ALPHA_SCISSOR_USED\n"; actions.usage_defines["ALPHA_HASH_SCALE"] = "#define ALPHA_HASH_USED\n"; diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h index 3b83b2b582..d5332032f9 100644 --- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h +++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h @@ -106,7 +106,8 @@ public: BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, - BLEND_MODE_ALPHA_TO_COVERAGE + BLEND_MODE_ALPHA_TO_COVERAGE, + BLEND_MODE_PREMULT_ALPHA, }; enum DepthDraw { diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp index 0810f567cb..733cbd1ff2 100644 --- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp +++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp @@ -91,6 +91,7 @@ void SceneShaderForwardMobile::ShaderData::set_code(const String &p_code) { actions.render_mode_values["blend_mix"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MIX); actions.render_mode_values["blend_sub"] = Pair<int *, int>(&blend_mode, BLEND_MODE_SUB); actions.render_mode_values["blend_mul"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MUL); + actions.render_mode_values["blend_premul_alpha"] = Pair<int *, int>(&blend_mode, BLEND_MODE_PREMULT_ALPHA); actions.render_mode_values["alpha_to_coverage"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); actions.render_mode_values["alpha_to_coverage_and_one"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE_AND_TO_ONE); @@ -255,7 +256,17 @@ void SceneShaderForwardMobile::ShaderData::set_code(const String &p_code) { blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE; blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ZERO; - } + } break; + case BLEND_MODE_PREMULT_ALPHA: { + blend_attachment.enable_blend = true; + blend_attachment.alpha_blend_op = RD::BLEND_OP_ADD; + blend_attachment.color_blend_op = RD::BLEND_OP_ADD; + blend_attachment.src_color_blend_factor = RD::BLEND_FACTOR_ONE; + blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE; + blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + uses_blend_alpha = true; // Force alpha used because of blend. + } break; } RD::PipelineColorBlendState blend_state_blend; @@ -497,6 +508,7 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.renames["NORMAL_MAP_DEPTH"] = "normal_map_depth"; actions.renames["ALBEDO"] = "albedo"; actions.renames["ALPHA"] = "alpha"; + actions.renames["PREMUL_ALPHA_FACTOR"] = "premul_alpha"; actions.renames["METALLIC"] = "metallic"; actions.renames["SPECULAR"] = "specular"; actions.renames["ROUGHNESS"] = "roughness"; @@ -581,6 +593,7 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.usage_defines["ALPHA_HASH_SCALE"] = "#define ALPHA_HASH_USED\n"; actions.usage_defines["ALPHA_ANTIALIASING_EDGE"] = "#define ALPHA_ANTIALIASING_EDGE_USED\n"; actions.usage_defines["ALPHA_TEXTURE_COORDINATE"] = "@ALPHA_ANTIALIASING_EDGE"; + actions.usage_defines["PREMUL_ALPHA_FACTOR"] = "#define PREMUL_ALPHA_USED"; actions.usage_defines["SSS_STRENGTH"] = "#define ENABLE_SSS\n"; actions.usage_defines["SSS_TRANSMITTANCE_DEPTH"] = "#define ENABLE_TRANSMITTANCE\n"; diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h index da189c6f67..833b06c1e3 100644 --- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h +++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h @@ -61,6 +61,7 @@ public: BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, + BLEND_MODE_PREMULT_ALPHA, BLEND_MODE_ALPHA_TO_COVERAGE }; diff --git a/servers/rendering/renderer_rd/shaders/canvas.glsl b/servers/rendering/renderer_rd/shaders/canvas.glsl index 235c772e2d..dbff09c301 100644 --- a/servers/rendering/renderer_rd/shaders/canvas.glsl +++ b/servers/rendering/renderer_rd/shaders/canvas.glsl @@ -665,6 +665,12 @@ void main() { vec2 tex_uv = (vec4(vertex, 0.0, 1.0) * mat4(light_array.data[light_base].texture_matrix[0], light_array.data[light_base].texture_matrix[1], vec4(0.0, 0.0, 1.0, 0.0), vec4(0.0, 0.0, 0.0, 1.0))).xy; //multiply inverse given its transposed. Optimizer removes useless operations. vec2 tex_uv_atlas = tex_uv * light_array.data[light_base].atlas_rect.zw + light_array.data[light_base].atlas_rect.xy; + + if (any(lessThan(tex_uv, vec2(0.0, 0.0))) || any(greaterThanEqual(tex_uv, vec2(1.0, 1.0)))) { + //if outside the light texture, light color is zero + continue; + } + vec4 light_color = textureLod(sampler2D(atlas_texture, texture_sampler), tex_uv_atlas, 0.0); vec4 light_base_color = light_array.data[light_base].color; @@ -689,10 +695,6 @@ void main() { light_color.rgb *= base_color.rgb; } #endif - if (any(lessThan(tex_uv, vec2(0.0, 0.0))) || any(greaterThanEqual(tex_uv, vec2(1.0, 1.0)))) { - //if outside the light texture, light color is zero - light_color.a = 0.0; - } if (bool(light_array.data[light_base].flags & LIGHT_FLAGS_HAS_SHADOW)) { vec2 shadow_pos = (vec4(shadow_vertex, 0.0, 1.0) * mat4(light_array.data[light_base].shadow_matrix[0], light_array.data[light_base].shadow_matrix[1], vec4(0.0, 0.0, 1.0, 0.0), vec4(0.0, 0.0, 0.0, 1.0))).xy; //multiply inverse given its transposed. Optimizer removes useless operations. diff --git a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl index 59f1b1fd94..20b080da4d 100644 --- a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl @@ -914,6 +914,9 @@ vec3 encode24(vec3 v) { void fragment_shader(in SceneData scene_data) { uint instance_index = instance_index_interp; +#ifdef PREMUL_ALPHA_USED + float premul_alpha = 1.0; +#endif // PREMUL_ALPHA_USED //lay out everything, whatever is unused is optimized away anyway vec3 vertex = vertex_interp; #ifdef USE_MULTIVIEW @@ -2057,7 +2060,7 @@ void fragment_shader(in SceneData scene_data) { shadow = 1.0; #endif - float size_A = sc_use_light_soft_shadows ? directional_lights.data[i].size : 0.0; + float size_A = sc_use_directional_soft_shadows ? directional_lights.data[i].size : 0.0; light_compute(normal, directional_lights.data[i].direction, normalize(view), size_A, #ifndef DEBUG_DRAW_PSSM_SPLITS @@ -2235,24 +2238,16 @@ void fragment_shader(in SceneData scene_data) { } #ifdef USE_SHADOW_TO_OPACITY +#ifndef MODE_RENDER_DEPTH alpha = min(alpha, clamp(length(ambient_light), 0.0, 1.0)); #if defined(ALPHA_SCISSOR_USED) if (alpha < alpha_scissor) { discard; } -#else -#ifdef MODE_RENDER_DEPTH -#ifdef USE_OPAQUE_PREPASS - - if (alpha < scene_data.opaque_prepass_threshold) { - discard; - } - -#endif // USE_OPAQUE_PREPASS -#endif // MODE_RENDER_DEPTH #endif // ALPHA_SCISSOR_USED +#endif // !MODE_RENDER_DEPTH #endif // USE_SHADOW_TO_OPACITY #endif //!defined(MODE_RENDER_DEPTH) && !defined(MODE_UNSHADED) @@ -2466,6 +2461,10 @@ void fragment_shader(in SceneData scene_data) { motion_vector = prev_position_uv - position_uv; #endif + +#if defined(PREMUL_ALPHA_USED) && !defined(MODE_RENDER_DEPTH) + frag_color.rgb *= premul_alpha; +#endif //PREMUL_ALPHA_USED } void main() { diff --git a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl index f0a5141856..1637326b48 100644 --- a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl @@ -749,6 +749,9 @@ void main() { float clearcoat_roughness = 0.0; float anisotropy = 0.0; vec2 anisotropy_flow = vec2(1.0, 0.0); +#ifdef PREMUL_ALPHA_USED + float premul_alpha = 1.0; +#endif #ifndef FOG_DISABLED vec4 fog = vec4(0.0); #endif // !FOG_DISABLED @@ -1756,24 +1759,16 @@ void main() { } //spot lights #ifdef USE_SHADOW_TO_OPACITY +#ifndef MODE_RENDER_DEPTH alpha = min(alpha, clamp(length(ambient_light), 0.0, 1.0)); #if defined(ALPHA_SCISSOR_USED) if (alpha < alpha_scissor) { discard; } -#else -#ifdef MODE_RENDER_DEPTH -#ifdef USE_OPAQUE_PREPASS - - if (alpha < scene_data.opaque_prepass_threshold) { - discard; - } - -#endif // USE_OPAQUE_PREPASS -#endif // MODE_RENDER_DEPTH #endif // !ALPHA_SCISSOR_USED +#endif // !MODE_RENDER_DEPTH #endif // USE_SHADOW_TO_OPACITY #endif //!defined(MODE_RENDER_DEPTH) && !defined(MODE_UNSHADED) @@ -1854,6 +1849,9 @@ void main() { // On mobile we use a UNORM buffer with 10bpp which results in a range from 0.0 - 1.0 resulting in HDR breaking // We divide by sc_luminance_multiplier to support a range from 0.0 - 2.0 both increasing precision on bright and darker images frag_color.rgb = frag_color.rgb / sc_luminance_multiplier; +#ifdef PREMUL_ALPHA_USED + frag_color.rgb *= premul_alpha; +#endif #endif //MODE_MULTIPLE_RENDER_TARGETS diff --git a/servers/rendering/renderer_rd/shaders/particles.glsl b/servers/rendering/renderer_rd/shaders/particles.glsl index 5fa4154727..efdf1c2278 100644 --- a/servers/rendering/renderer_rd/shaders/particles.glsl +++ b/servers/rendering/renderer_rd/shaders/particles.glsl @@ -612,14 +612,14 @@ void main() { vec3 uvw_pos = vec3(local_pos_bottom / FRAME.colliders[i].extents) * 0.5 + 0.5; - float y = 1.0 - texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uvw_pos.xz).r; + float y = texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uvw_pos.xz).r; if (y > uvw_pos.y) { //inside heightfield vec3 pos1 = (vec3(uvw_pos.x, y, uvw_pos.z) * 2.0 - 1.0) * FRAME.colliders[i].extents; - vec3 pos2 = (vec3(uvw_pos.x + DELTA, 1.0 - texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uvw_pos.xz + vec2(DELTA, 0)).r, uvw_pos.z) * 2.0 - 1.0) * FRAME.colliders[i].extents; - vec3 pos3 = (vec3(uvw_pos.x, 1.0 - texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uvw_pos.xz + vec2(0, DELTA)).r, uvw_pos.z + DELTA) * 2.0 - 1.0) * FRAME.colliders[i].extents; + vec3 pos2 = (vec3(uvw_pos.x + DELTA, texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uvw_pos.xz + vec2(DELTA, 0)).r, uvw_pos.z) * 2.0 - 1.0) * FRAME.colliders[i].extents; + vec3 pos3 = (vec3(uvw_pos.x, texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uvw_pos.xz + vec2(0, DELTA)).r, uvw_pos.z + DELTA) * 2.0 - 1.0) * FRAME.colliders[i].extents; normal = normalize(cross(pos1 - pos2, pos1 - pos3)); float local_y = (vec3(local_pos / FRAME.colliders[i].extents) * 0.5 + 0.5).y; diff --git a/servers/rendering/renderer_rd/shaders/scene_forward_lights_inc.glsl b/servers/rendering/renderer_rd/shaders/scene_forward_lights_inc.glsl index 47e6fe5873..40ca74ae07 100644 --- a/servers/rendering/renderer_rd/shaders/scene_forward_lights_inc.glsl +++ b/servers/rendering/renderer_rd/shaders/scene_forward_lights_inc.glsl @@ -375,7 +375,7 @@ float sample_directional_soft_shadow(texture2D shadow, vec3 pssm_coord, vec2 tex for (uint i = 0; i < sc_directional_penumbra_shadow_samples; i++) { vec2 suv = pssm_coord.xy + (disk_rotation * scene_data_block.data.directional_penumbra_shadow_kernel[i].xy) * tex_scale; float d = textureLod(sampler2D(shadow, SAMPLER_LINEAR_CLAMP), suv, 0.0).r; - if (d < pssm_coord.z) { + if (d > pssm_coord.z) { blocker_average += d; blocker_count += 1.0; } @@ -384,7 +384,7 @@ float sample_directional_soft_shadow(texture2D shadow, vec3 pssm_coord, vec2 tex if (blocker_count > 0.0) { //blockers found, do soft shadow blocker_average /= blocker_count; - float penumbra = (pssm_coord.z - blocker_average) / blocker_average; + float penumbra = (-pssm_coord.z + blocker_average) / (1.0 - blocker_average); tex_scale *= penumbra; float s = 0.0; @@ -488,7 +488,7 @@ float light_process_omni_shadow(uint idx, vec3 vertex, vec3 normal) { if (blocker_count > 0.0) { //blockers found, do soft shadow blocker_average /= blocker_count; - float penumbra = (z_norm + blocker_average) / blocker_average; + float penumbra = (-z_norm + blocker_average) / (1.0 - blocker_average); tangent *= penumbra; bitangent *= penumbra; @@ -736,7 +736,7 @@ float light_process_spot_shadow(uint idx, vec3 vertex, vec3 normal) { vec2 suv = shadow_uv + (disk_rotation * scene_data_block.data.penumbra_shadow_kernel[i].xy) * uv_size; suv = clamp(suv, spot_lights.data[idx].atlas_rect.xy, clamp_max); float d = textureLod(sampler2D(shadow_atlas, SAMPLER_LINEAR_CLAMP), suv, 0.0).r; - if (d < splane.z) { + if (d > splane.z) { blocker_average += d; blocker_count += 1.0; } @@ -745,7 +745,7 @@ float light_process_spot_shadow(uint idx, vec3 vertex, vec3 normal) { if (blocker_count > 0.0) { //blockers found, do soft shadow blocker_average /= blocker_count; - float penumbra = (z_norm - blocker_average) / blocker_average; + float penumbra = (-z_norm + blocker_average) / (1.0 - blocker_average); uv_size *= penumbra; shadow = 0.0; diff --git a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp index 1c3076b128..a10c672379 100644 --- a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp @@ -1656,13 +1656,9 @@ void MaterialStorage::global_shader_parameters_load_settings(bool p_load_texture Variant value = d["value"]; if (gvtype >= RS::GLOBAL_VAR_TYPE_SAMPLER2D) { - //textire - if (!p_load_textures) { - continue; - } - String path = value; - if (path.is_empty()) { + // Don't load the textures, but still add the parameter so shaders compile correctly while loading. + if (!p_load_textures || path.is_empty()) { value = RID(); } else { Ref<Resource> resource = ResourceLoader::load(path); diff --git a/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp b/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp index b7934cb3de..b5fdf8bebb 100644 --- a/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp +++ b/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp @@ -83,8 +83,7 @@ void RenderSceneBuffersRD::update_sizes(NamedTexture &p_named_texture) { for (uint32_t mipmap = 0; mipmap < p_named_texture.format.mipmaps; mipmap++) { p_named_texture.sizes.ptrw()[mipmap] = mipmap_size; - mipmap_size.width = MAX(1, mipmap_size.width >> 1); - mipmap_size.height = MAX(1, mipmap_size.height >> 1); + mipmap_size = Size2i(mipmap_size.width >> 1, mipmap_size.height >> 1).maxi(1); } } diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp index af30a32866..76ff566b18 100644 --- a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp @@ -2681,8 +2681,7 @@ void TextureStorage::update_decal_atlas() { mm.size = s; decal_atlas.texture_mipmaps.push_back(mm); - s.width = MAX(1, s.width >> 1); - s.height = MAX(1, s.height >> 1); + s = Vector2i(s.width >> 1, s.height >> 1).maxi(1); } { //create the SRGB variant @@ -3637,7 +3636,7 @@ void TextureStorage::_render_target_allocate_sdf(RenderTarget *rt) { } rt->process_size = size * scale / 100; - rt->process_size = rt->process_size.max(Size2i(1, 1)); + rt->process_size = rt->process_size.maxi(1); tformat.format = RD::DATA_FORMAT_R16G16_SINT; tformat.width = rt->process_size.width; @@ -3838,10 +3837,8 @@ void TextureStorage::render_target_copy_to_back_buffer(RID p_render_target, cons for (int i = 0; i < rt->backbuffer_mipmaps.size(); i++) { region.position.x >>= 1; region.position.y >>= 1; - region.size.x = MAX(1, region.size.x >> 1); - region.size.y = MAX(1, region.size.y >> 1); - texture_size.x = MAX(1, texture_size.x >> 1); - texture_size.y = MAX(1, texture_size.y >> 1); + region.size = Size2i(region.size.x >> 1, region.size.y >> 1).maxi(1); + texture_size = Size2i(texture_size.x >> 1, texture_size.y >> 1).maxi(1); RID mipmap = rt->backbuffer_mipmaps[i]; if (RendererSceneRenderRD::get_singleton()->_render_buffers_can_be_storage()) { @@ -3911,10 +3908,8 @@ void TextureStorage::render_target_gen_back_buffer_mipmaps(RID p_render_target, for (int i = 0; i < rt->backbuffer_mipmaps.size(); i++) { region.position.x >>= 1; region.position.y >>= 1; - region.size.x = MAX(1, region.size.x >> 1); - region.size.y = MAX(1, region.size.y >> 1); - texture_size.x = MAX(1, texture_size.x >> 1); - texture_size.y = MAX(1, texture_size.y >> 1); + region.size = Size2i(region.size.x >> 1, region.size.y >> 1).maxi(1); + texture_size = Size2i(texture_size.x >> 1, texture_size.y >> 1).maxi(1); RID mipmap = rt->backbuffer_mipmaps[i]; diff --git a/servers/rendering/renderer_rd/storage_rd/utilities.cpp b/servers/rendering/renderer_rd/storage_rd/utilities.cpp index 8b780a6f7b..8ff1d2bc46 100644 --- a/servers/rendering/renderer_rd/storage_rd/utilities.cpp +++ b/servers/rendering/renderer_rd/storage_rd/utilities.cpp @@ -198,7 +198,7 @@ void Utilities::visibility_notifier_call(RID p_notifier, bool p_enter, bool p_de ERR_FAIL_NULL(vn); if (p_enter) { - if (!vn->enter_callback.is_null()) { + if (vn->enter_callback.is_valid()) { if (p_deferred) { vn->enter_callback.call_deferred(); } else { @@ -206,7 +206,7 @@ void Utilities::visibility_notifier_call(RID p_notifier, bool p_enter, bool p_de } } } else { - if (!vn->exit_callback.is_null()) { + if (vn->exit_callback.is_valid()) { if (p_deferred) { vn->exit_callback.call_deferred(); } else { diff --git a/servers/rendering/renderer_scene_occlusion_cull.h b/servers/rendering/renderer_scene_occlusion_cull.h index 5adba5dc6a..a848c86bd2 100644 --- a/servers/rendering/renderer_scene_occlusion_cull.h +++ b/servers/rendering/renderer_scene_occlusion_cull.h @@ -98,8 +98,8 @@ public: rect_max = rect_max.max(normalized); } - rect_max = rect_max.min(Vector2(1, 1)); - rect_min = rect_min.max(Vector2(0, 0)); + rect_max = rect_max.minf(1); + rect_min = rect_min.maxf(0); int mip_count = mips.size(); diff --git a/servers/rendering/rendering_device_graph.cpp b/servers/rendering/rendering_device_graph.cpp index adac7ee3eb..b04f2ebbaa 100644 --- a/servers/rendering/rendering_device_graph.cpp +++ b/servers/rendering/rendering_device_graph.cpp @@ -495,18 +495,19 @@ void RenderingDeviceGraph::_add_command_to_graph(ResourceTracker **p_resource_tr // We add this command to the adjacency list of all commands that were reading from the entire resource. int32_t read_full_command_list_index = search_tracker->read_full_command_list_index; while (read_full_command_list_index >= 0) { - const RecordedCommandListNode &command_list_node = command_list_nodes[read_full_command_list_index]; - if (command_list_node.command_index == p_command_index) { + int32_t read_full_command_index = command_list_nodes[read_full_command_list_index].command_index; + int32_t read_full_next_index = command_list_nodes[read_full_command_list_index].next_list_index; + if (read_full_command_index == p_command_index) { if (!resource_has_parent) { // Only slices are allowed to be in different usages in the same command as they are guaranteed to have no overlap in the same command. ERR_FAIL_MSG("Command can't have itself as a dependency."); } } else { // Add this command to the adjacency list of each command that was reading this resource. - _add_adjacent_command(command_list_node.command_index, p_command_index, r_command); + _add_adjacent_command(read_full_command_index, p_command_index, r_command); } - read_full_command_list_index = command_list_node.next_list_index; + read_full_command_list_index = read_full_next_index; } if (!resource_has_parent) { diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h index f94323f198..e0049e3fa4 100644 --- a/servers/rendering/rendering_server_default.h +++ b/servers/rendering/rendering_server_default.h @@ -105,10 +105,6 @@ public: _changes_changed(); } -#define DISPLAY_CHANGED \ - changes++; \ - _changes_changed(); - #else _FORCE_INLINE_ static void redraw_request() { changes++; @@ -1052,6 +1048,10 @@ public: virtual void init() override; virtual void finish() override; + virtual bool is_on_render_thread() override { + return Thread::get_caller_id() == server_thread; + } + virtual void call_on_render_thread(const Callable &p_callable) override { if (Thread::get_caller_id() == server_thread) { command_queue.flush_if_pending(); diff --git a/servers/rendering/shader_types.cpp b/servers/rendering/shader_types.cpp index af51083dc3..b2fa0ea9d2 100644 --- a/servers/rendering/shader_types.cpp +++ b/servers/rendering/shader_types.cpp @@ -124,6 +124,7 @@ ShaderTypes::ShaderTypes() { shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["UV2"] = constt(ShaderLanguage::TYPE_VEC2); shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["COLOR"] = constt(ShaderLanguage::TYPE_VEC4); shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["ALBEDO"] = ShaderLanguage::TYPE_VEC3; + shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["PREMUL_ALPHA_FACTOR"] = ShaderLanguage::TYPE_FLOAT; shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["ALPHA"] = ShaderLanguage::TYPE_FLOAT; shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["METALLIC"] = ShaderLanguage::TYPE_FLOAT; shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["SPECULAR"] = ShaderLanguage::TYPE_FLOAT; @@ -208,7 +209,7 @@ ShaderTypes::ShaderTypes() { // spatial render modes { - shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("blend"), "mix", "add", "sub", "mul" }); + shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("blend"), "mix", "add", "sub", "mul", "premul_alpha" }); shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("depth_draw"), "opaque", "always", "never" }); shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("depth_prepass_alpha") }); shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("depth_test_disabled") }); diff --git a/servers/rendering/storage/environment_storage.cpp b/servers/rendering/storage/environment_storage.cpp index 7b75a12a19..1bbb5da6bb 100644 --- a/servers/rendering/storage/environment_storage.cpp +++ b/servers/rendering/storage/environment_storage.cpp @@ -773,11 +773,7 @@ RS::EnvironmentSDFGIYScale RendererEnvironmentStorage::environment_get_sdfgi_y_s void RendererEnvironmentStorage::environment_set_adjustment(RID p_env, bool p_enable, float p_brightness, float p_contrast, float p_saturation, bool p_use_1d_color_correction, RID p_color_correction) { Environment *env = environment_owner.get_or_null(p_env); ERR_FAIL_NULL(env); -#ifdef DEBUG_ENABLED - if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" && p_enable) { - WARN_PRINT_ONCE_ED("Adjustments are not supported when using the GL Compatibility backend yet. Support will be added in a future release."); - } -#endif + env->adjustments_enabled = p_enable; env->adjustments_brightness = p_brightness; env->adjustments_contrast = p_contrast; diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index bbe6b1ad0d..60e8f18c19 100644 --- a/servers/rendering_server.cpp +++ b/servers/rendering_server.cpp @@ -1226,6 +1226,10 @@ Error RenderingServer::mesh_create_surface_data_from_arrays(SurfaceData *r_surfa bsformat |= (1 << j); } } + if (bsformat & RS::ARRAY_FORMAT_NORMAL) { + // We must use tangents if using normals. + bsformat |= RS::ARRAY_FORMAT_TANGENT; + } ERR_FAIL_COND_V_MSG(bsformat != (format & RS::ARRAY_FORMAT_BLEND_SHAPE_MASK), ERR_INVALID_PARAMETER, "Blend shape format must match the main array format for Vertex, Normal and Tangent arrays."); } @@ -3426,6 +3430,7 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rendering_device"), &RenderingServer::get_rendering_device); ClassDB::bind_method(D_METHOD("create_local_rendering_device"), &RenderingServer::create_local_rendering_device); + ClassDB::bind_method(D_METHOD("is_on_render_thread"), &RenderingServer::is_on_render_thread); ClassDB::bind_method(D_METHOD("call_on_render_thread", "callable"), &RenderingServer::call_on_render_thread); #ifndef DISABLE_DEPRECATED diff --git a/servers/rendering_server.h b/servers/rendering_server.h index 8f0150f180..240d82c90b 100644 --- a/servers/rendering_server.h +++ b/servers/rendering_server.h @@ -41,6 +41,32 @@ #include "servers/display_server.h" #include "servers/rendering/rendering_device.h" +// Helper macros for code outside of the rendering server, but that is +// called by the rendering server. +#ifdef DEBUG_ENABLED +#define ERR_ON_RENDER_THREAD \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL(rendering_server); \ + ERR_FAIL_COND(rendering_server->is_on_render_thread()); +#define ERR_ON_RENDER_THREAD_V(m_ret) \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL_V(rendering_server, m_ret); \ + ERR_FAIL_COND_V(rendering_server->is_on_render_thread(), m_ret); +#define ERR_NOT_ON_RENDER_THREAD \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL(rendering_server); \ + ERR_FAIL_COND(!rendering_server->is_on_render_thread()); +#define ERR_NOT_ON_RENDER_THREAD_V(m_ret) \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL_V(rendering_server, m_ret); \ + ERR_FAIL_COND_V(!rendering_server->is_on_render_thread(), m_ret); +#else +#define ERR_ON_RENDER_THREAD +#define ERR_ON_RENDER_THREAD_V(m_ret) +#define ERR_NOT_ON_RENDER_THREAD +#define ERR_NOT_ON_RENDER_THREAD_V(m_ret) +#endif + template <typename T> class TypedArray; @@ -1684,7 +1710,7 @@ public: #ifndef DISABLE_DEPRECATED // Never actually used, should be removed when we can break compatibility. - enum Features { + enum Features{ FEATURE_SHADERS, FEATURE_MULTITHREADED, }; @@ -1708,6 +1734,7 @@ public: bool is_render_loop_enabled() const; void set_render_loop_enabled(bool p_enabled); + virtual bool is_on_render_thread() = 0; virtual void call_on_render_thread(const Callable &p_callable) = 0; #ifdef TOOLS_ENABLED diff --git a/servers/xr/xr_hand_tracker.cpp b/servers/xr/xr_hand_tracker.cpp index cb0fbfb35f..abfe2e9867 100644 --- a/servers/xr/xr_hand_tracker.cpp +++ b/servers/xr/xr_hand_tracker.cpp @@ -33,9 +33,6 @@ #include "xr_body_tracker.h" void XRHandTracker::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_hand", "hand"), &XRHandTracker::set_hand); - ClassDB::bind_method(D_METHOD("get_hand"), &XRHandTracker::get_hand); - ClassDB::bind_method(D_METHOD("set_has_tracking_data", "has_data"), &XRHandTracker::set_has_tracking_data); ClassDB::bind_method(D_METHOD("get_has_tracking_data"), &XRHandTracker::get_has_tracking_data); @@ -60,10 +57,6 @@ void XRHandTracker::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "has_tracking_data", PROPERTY_HINT_NONE), "set_has_tracking_data", "get_has_tracking_data"); ADD_PROPERTY(PropertyInfo(Variant::INT, "hand_tracking_source", PROPERTY_HINT_ENUM, "Unknown,Unobstructed,Controller"), "set_hand_tracking_source", "get_hand_tracking_source"); - BIND_ENUM_CONSTANT(HAND_LEFT); - BIND_ENUM_CONSTANT(HAND_RIGHT); - BIND_ENUM_CONSTANT(HAND_MAX); - BIND_ENUM_CONSTANT(HAND_TRACKING_SOURCE_UNKNOWN); BIND_ENUM_CONSTANT(HAND_TRACKING_SOURCE_UNOBSTRUCTED); BIND_ENUM_CONSTANT(HAND_TRACKING_SOURCE_CONTROLLER); @@ -110,48 +103,8 @@ void XRHandTracker::set_tracker_type(XRServer::TrackerType p_type) { } void XRHandTracker::set_tracker_hand(const XRPositionalTracker::TrackerHand p_hand) { - ERR_FAIL_INDEX(p_hand, TRACKER_HAND_MAX); - - switch (p_hand) { - case TRACKER_HAND_LEFT: - tracker_hand = TRACKER_HAND_LEFT; - hand = HAND_LEFT; - break; - - case TRACKER_HAND_RIGHT: - tracker_hand = TRACKER_HAND_RIGHT; - hand = HAND_RIGHT; - break; - - case TRACKER_HAND_UNKNOWN: - default: - ERR_FAIL_MSG("XRHandTracker must specify hand"); - break; - } -} - -void XRHandTracker::set_hand(XRHandTracker::Hand p_hand) { - ERR_FAIL_INDEX(p_hand, HAND_MAX); - - switch (p_hand) { - case HAND_LEFT: - tracker_hand = TRACKER_HAND_LEFT; - hand = HAND_LEFT; - break; - - case HAND_RIGHT: - tracker_hand = TRACKER_HAND_RIGHT; - hand = HAND_RIGHT; - break; - - default: - ERR_FAIL_MSG("XRHandTracker must specify hand"); - break; - } -} - -XRHandTracker::Hand XRHandTracker::get_hand() const { - return hand; + ERR_FAIL_COND_MSG(p_hand != TRACKER_HAND_LEFT && p_hand != TRACKER_HAND_RIGHT, "XRHandTracker must specify hand."); + tracker_hand = p_hand; } void XRHandTracker::set_has_tracking_data(bool p_has_tracking_data) { @@ -222,4 +175,5 @@ Vector3 XRHandTracker::get_hand_joint_angular_velocity(XRHandTracker::HandJoint XRHandTracker::XRHandTracker() { type = XRServer::TRACKER_HAND; + tracker_hand = TRACKER_HAND_LEFT; } diff --git a/servers/xr/xr_hand_tracker.h b/servers/xr/xr_hand_tracker.h index 8ef3c229c3..c7c18a31f8 100644 --- a/servers/xr/xr_hand_tracker.h +++ b/servers/xr/xr_hand_tracker.h @@ -38,12 +38,6 @@ class XRHandTracker : public XRPositionalTracker { _THREAD_SAFE_CLASS_ public: - enum Hand { - HAND_LEFT, - HAND_RIGHT, - HAND_MAX, - }; - enum HandTrackingSource { HAND_TRACKING_SOURCE_UNKNOWN, HAND_TRACKING_SOURCE_UNOBSTRUCTED, @@ -93,9 +87,6 @@ public: void set_tracker_type(XRServer::TrackerType p_type) override; void set_tracker_hand(const XRPositionalTracker::TrackerHand p_hand) override; - void set_hand(Hand p_hand); - Hand get_hand() const; - void set_has_tracking_data(bool p_has_tracking_data); bool get_has_tracking_data() const; @@ -123,7 +114,6 @@ protected: static void _bind_methods(); private: - Hand hand = HAND_LEFT; bool has_tracking_data = false; HandTrackingSource hand_tracking_source = HAND_TRACKING_SOURCE_UNKNOWN; @@ -134,7 +124,6 @@ private: Vector3 hand_joint_angular_velocities[HAND_JOINT_MAX]; }; -VARIANT_ENUM_CAST(XRHandTracker::Hand) VARIANT_ENUM_CAST(XRHandTracker::HandTrackingSource) VARIANT_ENUM_CAST(XRHandTracker::HandJoint) VARIANT_BITFIELD_CAST(XRHandTracker::HandJointFlags) diff --git a/servers/xr/xr_interface.h b/servers/xr/xr_interface.h index d7bd212449..809800d8b9 100644 --- a/servers/xr/xr_interface.h +++ b/servers/xr/xr_interface.h @@ -122,17 +122,21 @@ public: /** rendering and internal **/ + // These methods are called from the main thread. + virtual Transform3D get_camera_transform() = 0; /* returns the position of our camera, only used for updating reference frame. For monoscopic this is equal to the views transform, for stereoscopic this should be an average */ + virtual void process() = 0; + + // These methods can be called from both main and render thread. virtual Size2 get_render_target_size() = 0; /* returns the recommended render target size per eye for this device */ virtual uint32_t get_view_count() = 0; /* returns the view count we need (1 is monoscopic, 2 is stereoscopic but can be more) */ - virtual Transform3D get_camera_transform() = 0; /* returns the position of our camera for updating our camera node. For monoscopic this is equal to the views transform, for stereoscopic this should be an average */ + + // These methods are called from the rendering thread. virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) = 0; /* get each views transform */ virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) = 0; /* get each view projection matrix */ virtual RID get_vrs_texture(); /* obtain VRS texture */ virtual RID get_color_texture(); /* obtain color output texture (if applicable) */ virtual RID get_depth_texture(); /* obtain depth output texture (if applicable, used for reprojection) */ virtual RID get_velocity_texture(); /* obtain velocity output texture (if applicable, used for spacewarp) */ - - virtual void process() = 0; virtual void pre_render(){}; virtual bool pre_draw_viewport(RID p_render_target) { return true; }; /* inform XR interface we are about to start our viewport draw process */ virtual Vector<BlitToScreen> post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) = 0; /* inform XR interface we finished our viewport draw process */ diff --git a/servers/xr_server.cpp b/servers/xr_server.cpp index f1105a650d..2cfe98ea1e 100644 --- a/servers/xr_server.cpp +++ b/servers/xr_server.cpp @@ -51,7 +51,7 @@ XRServer *XRServer::singleton = nullptr; XRServer *XRServer::get_singleton() { return singleton; -}; +} void XRServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_world_scale"), &XRServer::get_world_scale); @@ -59,7 +59,7 @@ void XRServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_world_origin"), &XRServer::get_world_origin); ClassDB::bind_method(D_METHOD("set_world_origin", "world_origin"), &XRServer::set_world_origin); ClassDB::bind_method(D_METHOD("get_reference_frame"), &XRServer::get_reference_frame); - ClassDB::bind_method(D_METHOD("clear_reference_frame"), &XRServer::get_reference_frame); + ClassDB::bind_method(D_METHOD("clear_reference_frame"), &XRServer::clear_reference_frame); ClassDB::bind_method(D_METHOD("center_on_hmd", "rotation_mode", "keep_height"), &XRServer::center_on_hmd); ClassDB::bind_method(D_METHOD("get_hmd_transform"), &XRServer::get_hmd_transform); @@ -104,11 +104,20 @@ void XRServer::_bind_methods() { ADD_SIGNAL(MethodInfo("tracker_added", PropertyInfo(Variant::STRING_NAME, "tracker_name"), PropertyInfo(Variant::INT, "type"))); ADD_SIGNAL(MethodInfo("tracker_updated", PropertyInfo(Variant::STRING_NAME, "tracker_name"), PropertyInfo(Variant::INT, "type"))); ADD_SIGNAL(MethodInfo("tracker_removed", PropertyInfo(Variant::STRING_NAME, "tracker_name"), PropertyInfo(Variant::INT, "type"))); -}; +} double XRServer::get_world_scale() const { - return world_scale; -}; + RenderingServer *rendering_server = RenderingServer::get_singleton(); + + if (rendering_server && rendering_server->is_on_render_thread()) { + // Return the value with which we're currently rendering, + // if we're on the render thread + return render_state.world_scale; + } else { + // Return our current value + return world_scale; + } +} void XRServer::set_world_scale(double p_world_scale) { if (p_world_scale < 0.01) { @@ -118,19 +127,58 @@ void XRServer::set_world_scale(double p_world_scale) { } world_scale = p_world_scale; -}; + set_render_world_scale(world_scale); +} + +void XRServer::_set_render_world_scale(double p_world_scale) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL(xr_server); + xr_server->render_state.world_scale = p_world_scale; +} Transform3D XRServer::get_world_origin() const { - return world_origin; -}; + RenderingServer *rendering_server = RenderingServer::get_singleton(); + + if (rendering_server && rendering_server->is_on_render_thread()) { + // Return the value with which we're currently rendering, + // if we're on the render thread + return render_state.world_origin; + } else { + // Return our current value + return world_origin; + } +} void XRServer::set_world_origin(const Transform3D &p_world_origin) { world_origin = p_world_origin; -}; + set_render_world_origin(world_origin); +} + +void XRServer::_set_render_world_origin(const Transform3D &p_world_origin) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL(xr_server); + xr_server->render_state.world_origin = p_world_origin; +} Transform3D XRServer::get_reference_frame() const { - return reference_frame; -}; + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL_V(rendering_server, reference_frame); + + if (rendering_server->is_on_render_thread()) { + // Return the value with which we're currently rendering, + // if we're on the render thread + return render_state.reference_frame; + } else { + // Return our current value + return reference_frame; + } +} void XRServer::center_on_hmd(RotationMode p_rotation_mode, bool p_keep_height) { if (primary_interface == nullptr) { @@ -156,27 +204,38 @@ void XRServer::center_on_hmd(RotationMode p_rotation_mode, bool p_keep_height) { } else if (p_rotation_mode == 2) { // remove our rotation, we're only interesting in centering on position new_reference_frame.basis = Basis(); - }; + } // don't negate our height if (p_keep_height) { new_reference_frame.origin.y = 0.0; - }; + } reference_frame = new_reference_frame.inverse(); -}; + set_render_reference_frame(reference_frame); +} void XRServer::clear_reference_frame() { reference_frame = Transform3D(); + set_render_reference_frame(reference_frame); +} + +void XRServer::_set_render_reference_frame(const Transform3D &p_reference_frame) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL(xr_server); + xr_server->render_state.reference_frame = p_reference_frame; } Transform3D XRServer::get_hmd_transform() { Transform3D hmd_transform; if (primary_interface != nullptr) { hmd_transform = primary_interface->get_camera_transform(); - }; + } return hmd_transform; -}; +} void XRServer::add_interface(const Ref<XRInterface> &p_interface) { ERR_FAIL_COND(p_interface.is_null()); @@ -185,12 +244,12 @@ void XRServer::add_interface(const Ref<XRInterface> &p_interface) { if (interfaces[i] == p_interface) { ERR_PRINT("Interface was already added"); return; - }; - }; + } + } interfaces.push_back(p_interface); emit_signal(SNAME("interface_added"), p_interface->get_name()); -}; +} void XRServer::remove_interface(const Ref<XRInterface> &p_interface) { ERR_FAIL_COND(p_interface.is_null()); @@ -200,33 +259,33 @@ void XRServer::remove_interface(const Ref<XRInterface> &p_interface) { if (interfaces[i] == p_interface) { idx = i; break; - }; - }; + } + } ERR_FAIL_COND_MSG(idx == -1, "Interface not found."); print_verbose("XR: Removed interface \"" + p_interface->get_name() + "\""); emit_signal(SNAME("interface_removed"), p_interface->get_name()); interfaces.remove_at(idx); -}; +} int XRServer::get_interface_count() const { return interfaces.size(); -}; +} Ref<XRInterface> XRServer::get_interface(int p_index) const { ERR_FAIL_INDEX_V(p_index, interfaces.size(), nullptr); return interfaces[p_index]; -}; +} Ref<XRInterface> XRServer::find_interface(const String &p_name) const { for (int i = 0; i < interfaces.size(); i++) { if (interfaces[i]->get_name() == p_name) { return interfaces[i]; - }; - }; + } + } return Ref<XRInterface>(); -}; +} TypedArray<Dictionary> XRServer::get_interfaces() const { Array ret; @@ -238,14 +297,14 @@ TypedArray<Dictionary> XRServer::get_interfaces() const { iface_info["name"] = interfaces[i]->get_name(); ret.push_back(iface_info); - }; + } return ret; -}; +} Ref<XRInterface> XRServer::get_primary_interface() const { return primary_interface; -}; +} void XRServer::set_primary_interface(const Ref<XRInterface> &p_primary_interface) { if (p_primary_interface.is_null()) { @@ -256,7 +315,7 @@ void XRServer::set_primary_interface(const Ref<XRInterface> &p_primary_interface print_verbose("XR: Primary interface set to: " + primary_interface->get_name()); } -}; +} void XRServer::add_tracker(const Ref<XRTracker> &p_tracker) { ERR_FAIL_COND(p_tracker.is_null()); @@ -272,7 +331,7 @@ void XRServer::add_tracker(const Ref<XRTracker> &p_tracker) { trackers[tracker_name] = p_tracker; emit_signal(SNAME("tracker_added"), tracker_name, p_tracker->get_tracker_type()); } -}; +} void XRServer::remove_tracker(const Ref<XRTracker> &p_tracker) { ERR_FAIL_COND(p_tracker.is_null()); @@ -285,7 +344,7 @@ void XRServer::remove_tracker(const Ref<XRTracker> &p_tracker) { // and remove it trackers.erase(tracker_name); } -}; +} Dictionary XRServer::get_trackers(int p_tracker_types) { Dictionary res; @@ -307,7 +366,7 @@ Ref<XRTracker> XRServer::get_tracker(const StringName &p_name) const { // tracker hasn't been registered yet, which is fine, no need to spam the error log... return Ref<XRTracker>(); } -}; +} PackedStringArray XRServer::get_suggested_tracker_names() const { PackedStringArray arr; @@ -369,9 +428,9 @@ void XRServer::_process() { // ignore, not a valid reference } else if (interfaces[i]->is_initialized()) { interfaces.write[i]->process(); - }; - }; -}; + } + } +} void XRServer::pre_render() { // called from RendererViewport.draw_viewports right before we start drawing our viewports @@ -383,8 +442,8 @@ void XRServer::pre_render() { // ignore, not a valid reference } else if (interfaces[i]->is_initialized()) { interfaces.write[i]->pre_render(); - }; - }; + } + } } void XRServer::end_frame() { @@ -396,14 +455,13 @@ void XRServer::end_frame() { // ignore, not a valid reference } else if (interfaces[i]->is_initialized()) { interfaces.write[i]->end_frame(); - }; - }; + } + } } XRServer::XRServer() { singleton = this; - world_scale = 1.0; -}; +} XRServer::~XRServer() { primary_interface.unref(); @@ -412,4 +470,4 @@ XRServer::~XRServer() { trackers.clear(); singleton = nullptr; -}; +} diff --git a/servers/xr_server.h b/servers/xr_server.h index 717728171a..cd9c241bb0 100644 --- a/servers/xr_server.h +++ b/servers/xr_server.h @@ -36,6 +36,7 @@ #include "core/os/thread_safe.h" #include "core/templates/rid.h" #include "core/variant/variant.h" +#include "rendering_server.h" class XRInterface; class XRTracker; @@ -92,10 +93,46 @@ private: Ref<XRInterface> primary_interface; /* we'll identify one interface as primary, this will be used by our viewports */ - double world_scale; /* scale by which we multiply our tracker positions */ + double world_scale = 1.0; /* scale by which we multiply our tracker positions */ Transform3D world_origin; /* our world origin point, maps a location in our virtual world to the origin point in our real world tracking volume */ Transform3D reference_frame; /* our reference frame */ + // As we may be updating our main state for our next frame while we're still rendering our previous frame, + // we need to keep copies around. + struct RenderState { + double world_scale = 1.0; /* scale by which we multiply our tracker positions */ + Transform3D world_origin; /* our world origin point, maps a location in our virtual world to the origin point in our real world tracking volume */ + Transform3D reference_frame; /* our reference frame */ + } render_state; + + static void _set_render_world_scale(double p_world_scale); + static void _set_render_world_origin(const Transform3D &p_world_origin); + static void _set_render_reference_frame(const Transform3D &p_reference_frame); + + _FORCE_INLINE_ void set_render_world_scale(double p_world_scale) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&XRServer::_set_render_world_scale).bind(p_world_scale)); + } + + _FORCE_INLINE_ void set_render_world_origin(const Transform3D &p_world_origin) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&XRServer::_set_render_world_origin).bind(p_world_origin)); + } + + _FORCE_INLINE_ void set_render_reference_frame(const Transform3D &p_reference_frame) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&XRServer::_set_render_reference_frame).bind(p_reference_frame)); + } + protected: static XRServer *singleton; diff --git a/tests/display_server_mock.h b/tests/display_server_mock.h index 8d8a678e20..fd79a46c5c 100644 --- a/tests/display_server_mock.h +++ b/tests/display_server_mock.h @@ -47,6 +47,9 @@ private: Callable event_callback; Callable input_event_callback; + String clipboard_text; + String primary_clipboard_text; + static Vector<String> get_rendering_drivers_func() { Vector<String> drivers; drivers.push_back("dummy"); @@ -86,7 +89,7 @@ private: } void _send_window_event(WindowEvent p_event) { - if (!event_callback.is_null()) { + if (event_callback.is_valid()) { Variant event = int(p_event); event_callback.call(event); } @@ -97,6 +100,8 @@ public: switch (p_feature) { case FEATURE_MOUSE: case FEATURE_CURSOR_SHAPE: + case FEATURE_CLIPBOARD: + case FEATURE_CLIPBOARD_PRIMARY: return true; default: { } @@ -131,6 +136,11 @@ public: virtual Point2i mouse_get_position() const override { return mouse_position; } + virtual void clipboard_set(const String &p_text) override { clipboard_text = p_text; } + virtual String clipboard_get() const override { return clipboard_text; } + virtual void clipboard_set_primary(const String &p_text) override { primary_clipboard_text = p_text; } + virtual String clipboard_get_primary() const override { return primary_clipboard_text; } + virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override { return Size2i(1920, 1080); } diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h index b0a46b8107..c02830b6df 100644 --- a/tests/scene/test_code_edit.h +++ b/tests/scene/test_code_edit.h @@ -36,6 +36,15 @@ #include "tests/test_macros.h" namespace TestCodeEdit { +static inline Array build_array() { + return Array(); +} +template <typename... Targs> +static inline Array build_array(Variant item, Targs... Fargs) { + Array a = build_array(Fargs...); + a.push_front(item); + return a; +} TEST_CASE("[SceneTree][CodeEdit] line gutters") { CodeEdit *code_edit = memnew(CodeEdit); @@ -67,10 +76,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { ERR_PRINT_ON; - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); code_edit->set_line_as_breakpoint(0, true); CHECK(code_edit->is_line_breakpointed(0)); @@ -86,10 +92,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->clear_breakpointed_lines(); SIGNAL_CHECK_FALSE("breakpoint_toggled"); - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); code_edit->set_line_as_breakpoint(0, true); CHECK(code_edit->is_line_breakpointed(0)); @@ -101,10 +104,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and set text") { - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); code_edit->set_text("test\nline"); code_edit->set_line_as_breakpoint(0, true); @@ -121,7 +121,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->clear_breakpointed_lines(); SIGNAL_DISCARD("breakpoint_toggled") - ((Array)args[0])[0] = 1; + args = build_array(build_array(1)); code_edit->set_text("test\nline"); code_edit->set_line_as_breakpoint(1, true); CHECK(code_edit->is_line_breakpointed(1)); @@ -137,10 +137,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and clear") { - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); code_edit->set_text("test\nline"); code_edit->set_line_as_breakpoint(0, true); @@ -157,7 +154,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->clear_breakpointed_lines(); SIGNAL_DISCARD("breakpoint_toggled") - ((Array)args[0])[0] = 1; + args = build_array(build_array(1)); code_edit->set_text("test\nline"); code_edit->set_line_as_breakpoint(1, true); CHECK(code_edit->is_line_breakpointed(1)); @@ -173,21 +170,15 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and new lines no text") { - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); /* No text moves breakpoint. */ code_edit->set_line_as_breakpoint(0, true); CHECK(code_edit->is_line_breakpointed(0)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Normal. */ - ((Array)args[0])[0] = 0; - Array arg2; - arg2.push_back(1); - args.push_back(arg2); + // Normal. + args = build_array(build_array(0), build_array(1)); SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line_count() == 2); @@ -195,18 +186,16 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { CHECK(code_edit->is_line_breakpointed(1)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Non-Breaking. */ - ((Array)args[0])[0] = 1; - ((Array)args[1])[0] = 2; + // Non-Breaking. + args = build_array(build_array(1), build_array(2)); SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line_count() == 3); CHECK_FALSE(code_edit->is_line_breakpointed(1)); CHECK(code_edit->is_line_breakpointed(2)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Above. */ - ((Array)args[0])[0] = 2; - ((Array)args[1])[0] = 3; + // Above. + args = build_array(build_array(2), build_array(3)); SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line_count() == 4); CHECK_FALSE(code_edit->is_line_breakpointed(2)); @@ -215,10 +204,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and new lines with text") { - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); /* Having text does not move breakpoint. */ code_edit->insert_text_at_caret("text"); @@ -241,11 +227,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { CHECK_FALSE(code_edit->is_line_breakpointed(1)); SIGNAL_CHECK_FALSE("breakpoint_toggled"); - /* Above does move. */ - ((Array)args[0])[0] = 0; - Array arg2; - arg2.push_back(1); - args.push_back(arg2); + // Above does move. + args = build_array(build_array(0), build_array(1)); code_edit->set_caret_line(0); SEND_GUI_ACTION("ui_text_newline_above"); @@ -256,10 +239,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and backspace") { - Array arg1; - arg1.push_back(1); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(1)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(1, true); @@ -281,8 +261,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { ERR_PRINT_ON; SIGNAL_CHECK("breakpoint_toggled", args); - /* Backspace above breakpointed line moves it. */ - ((Array)args[0])[0] = 2; + // Backspace above breakpointed line moves it. + args = build_array(build_array(2)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(2, true); @@ -291,9 +271,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->set_caret_line(1); - Array arg2; - arg2.push_back(1); - args.push_back(arg2); + args = build_array(build_array(2), build_array(1)); SEND_GUI_ACTION("ui_text_backspace"); ERR_PRINT_OFF; CHECK_FALSE(code_edit->is_line_breakpointed(2)); @@ -303,10 +281,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and delete") { - Array arg1; - arg1.push_back(1); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(1)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(1, true); @@ -329,8 +304,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { ERR_PRINT_ON; SIGNAL_CHECK("breakpoint_toggled", args); - /* Delete above breakpointed line moves it. */ - ((Array)args[0])[0] = 2; + // Delete above breakpointed line moves it. + args = build_array(build_array(2)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(2, true); @@ -339,9 +314,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->set_caret_line(0); - Array arg2; - arg2.push_back(1); - args.push_back(arg2); + args = build_array(build_array(2), build_array(1)); SEND_GUI_ACTION("ui_text_delete"); ERR_PRINT_OFF; CHECK_FALSE(code_edit->is_line_breakpointed(2)); @@ -351,10 +324,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and delete selection") { - Array arg1; - arg1.push_back(1); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(1)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(1, true); @@ -367,8 +337,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { CHECK_FALSE(code_edit->is_line_breakpointed(0)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Should handle breakpoint move when deleting selection by adding less text then removed. */ - ((Array)args[0])[0] = 9; + // Should handle breakpoint move when deleting selection by adding less text then removed. + args = build_array(build_array(9)); code_edit->set_text("\n\n\n\n\n\n\n\n\n"); code_edit->set_line_as_breakpoint(9, true); @@ -377,9 +347,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->select(0, 0, 6, 0); - Array arg2; - arg2.push_back(4); - args.push_back(arg2); + args = build_array(build_array(9), build_array(4)); SEND_GUI_ACTION("ui_text_newline"); ERR_PRINT_OFF; CHECK_FALSE(code_edit->is_line_breakpointed(9)); @@ -387,9 +355,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { CHECK(code_edit->is_line_breakpointed(4)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Should handle breakpoint move when deleting selection by adding more text then removed. */ - ((Array)args[0])[0] = 9; - ((Array)args[1])[0] = 14; + // Should handle breakpoint move when deleting selection by adding more text then removed. + args = build_array(build_array(9), build_array(14)); code_edit->insert_text_at_caret("\n\n\n\n\n"); MessageQueue::get_singleton()->flush(); @@ -404,10 +371,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and undo") { - Array arg1; - arg1.push_back(1); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(1)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(1, true); @@ -1849,17 +1813,47 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "test\t"); - /* Indent lines does entire line and works without selection. */ + // Insert in place with multiple carets. + code_edit->set_text("test text"); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 7); + code_edit->add_caret(0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "te\tst \tte\txt"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->get_caret_column(0) == 7); + CHECK(code_edit->get_caret_column(1) == 10); + CHECK(code_edit->get_caret_column(2) == 3); + code_edit->remove_secondary_carets(); + + // Indent lines does entire line and works without selection. code_edit->set_text(""); code_edit->insert_text_at_caret("test"); code_edit->indent_lines(); CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_column() == 5); /* Selection does entire line. */ code_edit->set_text("test"); code_edit->select_all(); code_edit->do_indent(); CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); + + // Selection does entire line, right to left selection. + code_edit->set_text("test"); + code_edit->select(0, 4, 0, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 5); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Handles multiple lines. */ code_edit->set_text("test\ntext"); @@ -1867,6 +1861,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "\ttest"); CHECK(code_edit->get_line(1) == "\ttext"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 5); /* Do not indent line if last col is zero. */ code_edit->set_text("test\ntext"); @@ -1874,6 +1873,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "\ttest"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Indent even if last column of first line. */ code_edit->set_text("test\ntext"); @@ -1881,15 +1885,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "\ttest"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 5); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Indent even if last column of first line, reversed. + code_edit->set_text("test\ntext"); + code_edit->select(1, 0, 0, 4); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); /* Check selection is adjusted. */ code_edit->set_text("test"); code_edit->select(0, 1, 0, 2); code_edit->do_indent(); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); CHECK(code_edit->get_line(0) == "\ttest"); - code_edit->undo(); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 3); + + // Indent once with multiple selections. + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->add_caret(0, 4); + code_edit->select(0, 4, 0, 3, 1); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 0); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 3); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 0); + CHECK(code_edit->get_selection_origin_column(1) == 5); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 4); } SUBCASE("[CodeEdit] indent spaces") { @@ -1922,23 +1964,58 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "test "); - /* Indent lines does entire line and works without selection. */ + // Insert in place with multiple carets. + code_edit->set_text("test text"); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 7); + code_edit->add_caret(0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "te st te xt"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->get_caret_column(0) == 10); + CHECK(code_edit->get_caret_column(1) == 14); + CHECK(code_edit->get_caret_column(2) == 4); + code_edit->remove_secondary_carets(); + + // Indent lines does entire line and works without selection. code_edit->set_text(""); code_edit->insert_text_at_caret("test"); code_edit->indent_lines(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_column() == 8); /* Selection does entire line. */ code_edit->set_text("test"); code_edit->select_all(); code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 8); + + // Selection does entire line, right to left selection. + code_edit->set_text("test"); + code_edit->select(0, 4, 0, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* single indent only add required spaces. */ code_edit->set_text(" test"); code_edit->select_all(); code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 8); /* Handles multiple lines. */ code_edit->set_text("test\ntext"); @@ -1946,6 +2023,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); CHECK(code_edit->get_line(1) == " text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 8); /* Do not indent line if last col is zero. */ code_edit->set_text("test\ntext"); @@ -1953,6 +2035,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Indent even if last column of first line. */ code_edit->set_text("test\ntext"); @@ -1960,14 +2047,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Indent even if last column of first line, right to left selection. + code_edit->set_text("test\ntext"); + code_edit->select(1, 0, 0, 4); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 8); /* Check selection is adjusted. */ code_edit->set_text("test"); code_edit->select(0, 1, 0, 2); code_edit->do_indent(); - CHECK(code_edit->get_selection_from_column() == 5); - CHECK(code_edit->get_selection_to_column() == 6); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 5); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 6); + + // Indent once with multiple selections. + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->add_caret(0, 4); + code_edit->select(0, 4, 0, 3, 1); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 0); + CHECK(code_edit->get_selection_origin_column(0) == 5); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 6); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 0); + CHECK(code_edit->get_selection_origin_column(1) == 8); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 7); } SUBCASE("[CodeEdit] unindent tabs") { @@ -2003,11 +2129,28 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->insert_text_at_caret("\ttest"); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_caret_column() == 4); + + // Unindent lines once with multiple carets. + code_edit->set_text("\t\ttest"); + code_edit->set_caret_column(1); + code_edit->add_caret(0, 3); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_count() == 2); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 2); + code_edit->remove_secondary_carets(); /* Caret on col zero unindent line. */ code_edit->set_text("\t\ttest"); + code_edit->set_caret_column(0); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_column() == 0); /* Check input action. */ code_edit->set_text("\t\ttest"); @@ -2019,13 +2162,34 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->select_all(); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); - /* Handles multiple lines. */ - code_edit->set_text("\ttest\n\ttext"); + // Selection does entire line, right to left selection. + code_edit->set_text("\t\ttest"); + code_edit->select(0, 6, 0, 0); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 5); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Handles multiple lines. + code_edit->set_text("\t\ttest\n\t\ttext"); code_edit->select_all(); code_edit->unindent_lines(); - CHECK(code_edit->get_line(0) == "test"); - CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "\ttext"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 5); /* Do not unindent line if last col is zero. */ code_edit->set_text("\ttest\n\ttext"); @@ -2033,6 +2197,23 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); CHECK(code_edit->get_line(1) == "\ttext"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Do not unindent line if last col is zero, right to left selection. + code_edit->set_text("\ttest\n\ttext"); + code_edit->select(1, 0, 0, 0); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "\ttext"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Unindent even if last column of first line. */ code_edit->set_text("\ttest\n\ttext"); @@ -2040,14 +2221,50 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 4); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Check selection is adjusted. */ code_edit->set_text("\ttest"); code_edit->select(0, 1, 0, 2); code_edit->unindent_lines(); - CHECK(code_edit->get_selection_from_column() == 0); - CHECK(code_edit->get_selection_to_column() == 1); CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 1); + + // Deselect if only the tab was selected. + code_edit->set_text("\ttest"); + code_edit->select(0, 0, 0, 1); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Unindent once with multiple selections. + code_edit->set_text("\t\ttest"); + code_edit->select(0, 1, 0, 2); + code_edit->add_caret(0, 4); + code_edit->select(0, 4, 0, 3, 1); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 0); + CHECK(code_edit->get_selection_origin_column(0) == 0); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 0); + CHECK(code_edit->get_selection_origin_column(1) == 3); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 2); } SUBCASE("[CodeEdit] unindent spaces") { @@ -2089,11 +2306,28 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->insert_text_at_caret(" test"); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_caret_column() == 4); + + // Unindent lines once with multiple carets. + code_edit->set_text(" test"); + code_edit->set_caret_column(1); + code_edit->add_caret(0, 9); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_count() == 2); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 5); + code_edit->remove_secondary_carets(); /* Caret on col zero unindent line. */ code_edit->set_text(" test"); + code_edit->set_caret_column(0); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_column() == 0); /* Only as far as needed */ code_edit->set_text(" test"); @@ -2110,13 +2344,34 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->select_all(); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 8); - /* Handles multiple lines. */ - code_edit->set_text(" test\n text"); + // Selection does entire line, right to left selection. + code_edit->set_text(" test"); + code_edit->select(0, 12, 0, 0); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Handles multiple lines. + code_edit->set_text(" test\n text"); code_edit->select_all(); code_edit->unindent_lines(); - CHECK(code_edit->get_line(0) == "test"); - CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == " text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 8); /* Do not unindent line if last col is zero. */ code_edit->set_text(" test\n text"); @@ -2124,6 +2379,23 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); CHECK(code_edit->get_line(1) == " text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Do not unindent line if last col is zero, right to left selection. + code_edit->set_text(" test\n text"); + code_edit->select(1, 0, 0, 0); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == " text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Unindent even if last column of first line. */ code_edit->set_text(" test\n text"); @@ -2131,14 +2403,48 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 1); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Check selection is adjusted. */ code_edit->set_text(" test"); code_edit->select(0, 4, 0, 5); code_edit->unindent_lines(); - CHECK(code_edit->get_selection_from_column() == 0); - CHECK(code_edit->get_selection_to_column() == 1); CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 1); + + // Deselect if only the tab was selected. + code_edit->set_text(" test"); + code_edit->select(0, 0, 0, 4); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Unindent once with multiple selections. + code_edit->set_text(" test"); + code_edit->select(0, 1, 0, 2); + code_edit->add_caret(0, 4); + code_edit->select(0, 12, 0, 10, 1); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_count() == 2); + CHECK_FALSE(code_edit->has_selection(0)); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 0); + CHECK(code_edit->get_selection_origin_column(1) == 8); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 6); } SUBCASE("[CodeEdit] auto indent") { @@ -2153,6 +2459,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test:"); CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* new blank line should still indent. */ code_edit->set_text(""); @@ -2160,6 +2468,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line(0) == "test:"); CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* new line above should not indent. */ code_edit->set_text(""); @@ -2167,6 +2477,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line(0) == ""); CHECK(code_edit->get_line(1) == "test:"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Whitespace between symbol and caret is okay. */ code_edit->set_text(""); @@ -2174,6 +2486,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test: "); CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* Comment between symbol and caret is okay. */ code_edit->add_comment_delimiter("#", ""); @@ -2183,6 +2497,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test: # comment"); CHECK(code_edit->get_line(1) == "\t"); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* Strings between symbol and caret are not okay. */ code_edit->add_string_delimiter("#", ""); @@ -2192,6 +2508,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test: # string"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_string_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Non-whitespace prevents auto-indentation. */ code_edit->add_comment_delimiter("#", ""); @@ -2201,6 +2519,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test := 0 # comment"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Even when there's no comments. */ code_edit->set_text(""); @@ -2208,6 +2528,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test := 0"); CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Preserve current indentation. + code_edit->set_text("\ttest"); + code_edit->set_caret_column(3); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "\tte"); + CHECK(code_edit->get_line(1) == "\tst"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Preserve current indentation blank. + code_edit->set_text("\ttest"); + code_edit->set_caret_column(3); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Preserve current indentation above. + code_edit->set_text("\ttest"); + code_edit->set_caret_column(3); + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(code_edit->get_line(0) == "\t"); + CHECK(code_edit->get_line(1) == "\ttest"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 1); + + // Increase existing indentation. + code_edit->set_text("\ttest:"); + code_edit->set_caret_column(6); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "\ttest:"); + CHECK(code_edit->get_line(1) == "\t\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 2); + + // Increase existing indentation blank. + code_edit->set_text("\ttest:"); + code_edit->set_caret_column(3); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "\ttest:"); + CHECK(code_edit->get_line(1) == "\t\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 2); /* If between brace pairs an extra line is added. */ code_edit->set_text(""); @@ -2217,6 +2584,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test{"); CHECK(code_edit->get_line(1) == "\t"); CHECK(code_edit->get_line(2) == "}"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* Except when we are going above. */ code_edit->set_text(""); @@ -2225,6 +2594,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line(0) == ""); CHECK(code_edit->get_line(1) == "test{}"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* or below. */ code_edit->set_text(""); @@ -2233,6 +2604,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line(0) == "test{}"); CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); } SUBCASE("[CodeEdit] auto indent spaces") { @@ -2246,6 +2619,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test:"); CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* new blank line should still indent. */ code_edit->set_text(""); @@ -2253,6 +2628,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line(0) == "test:"); CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* new line above should not indent. */ code_edit->set_text(""); @@ -2260,6 +2637,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line(0) == ""); CHECK(code_edit->get_line(1) == "test:"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Whitespace between symbol and caret is okay. */ code_edit->set_text(""); @@ -2267,6 +2646,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test: "); CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* Comment between symbol and caret is okay. */ code_edit->add_comment_delimiter("#", ""); @@ -2276,6 +2657,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test: # comment"); CHECK(code_edit->get_line(1) == " "); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* Strings between symbol and caret are not okay. */ code_edit->add_string_delimiter("#", ""); @@ -2285,6 +2668,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test: # string"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_string_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Non-whitespace prevents auto-indentation. */ code_edit->add_comment_delimiter("#", ""); @@ -2294,6 +2679,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test := 0 # comment"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Even when there's no comments. */ code_edit->set_text(""); @@ -2301,6 +2688,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test := 0"); CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Preserve current indentation. + code_edit->set_text(" test"); + code_edit->set_caret_column(6); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == " te"); + CHECK(code_edit->get_line(1) == " st"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); + + // Preserve current indentation blank. + code_edit->set_text(" test"); + code_edit->set_caret_column(6); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); + + // Preserve current indentation above. + code_edit->set_text(" test"); + code_edit->set_caret_column(6); + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(code_edit->get_line(0) == " "); + CHECK(code_edit->get_line(1) == " test"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 4); + + // Increase existing indentation. + code_edit->set_text(" test:"); + code_edit->set_caret_column(9); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == " test:"); + CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 8); + + // Increase existing indentation blank. + code_edit->set_text(" test:"); + code_edit->set_caret_column(9); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == " test:"); + CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 8); /* If between brace pairs an extra line is added. */ code_edit->set_text(""); @@ -2310,6 +2744,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test{"); CHECK(code_edit->get_line(1) == " "); CHECK(code_edit->get_line(2) == "}"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* Except when we are going above. */ code_edit->set_text(""); @@ -2318,6 +2754,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line(0) == ""); CHECK(code_edit->get_line(1) == "test{}"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* or below. */ code_edit->set_text(""); @@ -2326,6 +2764,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line(0) == "test{}"); CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* If there is something after a colon and there is a colon in the comment it @@ -2337,6 +2777,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test:test#:"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); } } @@ -2345,64 +2787,50 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->set_indent_using_spaces(false); // Only line. - code_edit->insert_text_at_caret(" test"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(8); - code_edit->select(0, 8, 0, 9); + code_edit->set_text(" test"); + code_edit->select(0, 9, 0, 8); code_edit->convert_indent(); CHECK(code_edit->get_line(0) == "\t\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 3); CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); // First line. - code_edit->set_text(""); - code_edit->insert_text_at_caret(" test\n"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(8); + code_edit->set_text(" test\n"); code_edit->select(0, 8, 0, 9); code_edit->convert_indent(); CHECK(code_edit->get_line(0) == "\t\ttest"); - CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_column() == 3); // Middle line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\n test\n"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(8); + code_edit->set_text("\n test\n"); code_edit->select(1, 8, 1, 9); code_edit->convert_indent(); CHECK(code_edit->get_line(1) == "\t\ttest"); - CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_column() == 3); // End line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\n test"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(8); + code_edit->set_text("\n test"); code_edit->select(1, 8, 1, 9); code_edit->convert_indent(); CHECK(code_edit->get_line(1) == "\t\ttest"); - CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_column() == 3); // Within provided range. - code_edit->set_text(""); - code_edit->insert_text_at_caret(" test\n test\n"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(8); + code_edit->set_text(" test\n test\n"); code_edit->select(1, 8, 1, 9); code_edit->convert_indent(1, 1); CHECK(code_edit->get_line(0) == " test"); CHECK(code_edit->get_line(1) == "\t\ttest"); - CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_column() == 3); } SUBCASE("[CodeEdit] convert indent to spaces") { @@ -2410,64 +2838,50 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->set_indent_using_spaces(true); // Only line. - code_edit->insert_text_at_caret("\t\ttest"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(2); - code_edit->select(0, 2, 0, 3); + code_edit->set_text("\t\ttest"); + code_edit->select(0, 3, 0, 2); code_edit->convert_indent(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 9); CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); // First line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\t\ttest\n"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(2); + code_edit->set_text("\t\ttest\n"); code_edit->select(0, 2, 0, 3); code_edit->convert_indent(); CHECK(code_edit->get_line(0) == " test"); - CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_column() == 9); // Middle line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\n\t\ttest\n"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(2); + code_edit->set_text("\n\t\ttest\n"); code_edit->select(1, 2, 1, 3); code_edit->convert_indent(); CHECK(code_edit->get_line(1) == " test"); - CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_column() == 9); // End line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\n\t\ttest"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(2); + code_edit->set_text("\n\t\ttest"); code_edit->select(1, 2, 1, 3); code_edit->convert_indent(); CHECK(code_edit->get_line(1) == " test"); - CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_column() == 9); // Within provided range. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\ttest\n\t\ttest\n"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(2); + code_edit->set_text("\ttest\n\t\ttest\n"); code_edit->select(1, 2, 1, 3); code_edit->convert_indent(1, 1); CHECK(code_edit->get_line(0) == "\ttest"); CHECK(code_edit->get_line(1) == " test"); - CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_column() == 9); // Outside of range. ERR_PRINT_OFF; @@ -2484,6 +2898,7 @@ TEST_CASE("[SceneTree][CodeEdit] folding") { CodeEdit *code_edit = memnew(CodeEdit); SceneTree::get_singleton()->get_root()->add_child(code_edit); code_edit->grab_focus(); + code_edit->set_line_folding_enabled(true); SUBCASE("[CodeEdit] folding settings") { code_edit->set_line_folding_enabled(true); @@ -2494,8 +2909,6 @@ TEST_CASE("[SceneTree][CodeEdit] folding") { } SUBCASE("[CodeEdit] folding") { - code_edit->set_line_folding_enabled(true); - // No indent. code_edit->set_text("line1\nline2\nline3"); for (int i = 0; i < 2; i++) { @@ -2862,6 +3275,100 @@ TEST_CASE("[SceneTree][CodeEdit] folding") { CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); } + SUBCASE("[CodeEdit] folding carets") { + // Folding a line moves all carets that would be hidden. + code_edit->set_text("test\n\tline1\n\t\tline 2\n"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(0); + code_edit->add_caret(1, 3); + code_edit->add_caret(2, 8); + code_edit->add_caret(2, 1); + code_edit->select(2, 0, 2, 1, 3); + + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_caret_count() == 1); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 4); + + // Undoing an action that puts the caret on a folded line unfolds it. + code_edit->set_text("test\n\tline1"); + code_edit->select(1, 1, 1, 2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\n\tlline1"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 3); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 2); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 4); + + code_edit->undo(); + CHECK(code_edit->get_text() == "test\n\tline1"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 2); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + + // Redoing doesn't refold. + code_edit->redo(); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 3); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + } + + SUBCASE("[CodeEdit] toggle folding carets") { + code_edit->set_text("test\n\tline1\ntest2\n\tline2"); + + // Fold lines with carets on them. + code_edit->set_caret_line(0); + code_edit->set_caret_column(1); + code_edit->toggle_foldable_lines_at_carets(); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(2)); + + // Toggle fold on lines with carets. + code_edit->add_caret(2, 0); + code_edit->toggle_foldable_lines_at_carets(); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK(code_edit->is_line_folded(2)); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK(code_edit->get_caret_line(1) == 2); + CHECK(code_edit->get_caret_column(1) == 0); + + // Multiple carets as part of one fold. + code_edit->unfold_all_lines(); + code_edit->remove_secondary_carets(); + code_edit->set_caret_line(0); + code_edit->set_caret_column(1); + code_edit->add_caret(0, 4); + code_edit->add_caret(1, 2); + code_edit->toggle_foldable_lines_at_carets(); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 4); + } + memdelete(code_edit); } @@ -2870,7 +3377,7 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { SceneTree::get_singleton()->get_root()->add_child(code_edit); code_edit->grab_focus(); - SUBCASE("[CodeEdit] region folding") { + SUBCASE("[CodeEdit] region tags") { code_edit->set_line_folding_enabled(true); // Region tag detection. @@ -2907,16 +3414,51 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { ERR_PRINT_ON; CHECK(code_edit->get_code_region_start_tag() == "region"); CHECK(code_edit->get_code_region_end_tag() == "endregion"); + } - // Region creation with selection adds start / close region lines. + SUBCASE("[CodeEdit] create code region") { + code_edit->set_line_folding_enabled(true); + + // Region creation with selection adds start and close region lines. Region name is selected and the region is folded. code_edit->set_text("line1\nline2\nline3"); code_edit->clear_comment_delimiters(); code_edit->add_comment_delimiter("#", ""); code_edit->select(1, 0, 1, 4); code_edit->create_code_region(); CHECK(code_edit->is_line_code_region_start(1)); - CHECK(code_edit->get_line(2).contains("line2")); CHECK(code_edit->is_line_code_region_end(3)); + CHECK(code_edit->get_text() == "line1\n#region New Code Region\nline2\n#endregion\nline3"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selected_text() == "New Code Region"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 23); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->is_line_folded(1)); + + // Undo region creation. Line get unfolded. + code_edit->undo(); + CHECK(code_edit->get_text() == "line1\nline2\nline3"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK_FALSE(code_edit->is_line_folded(1)); + + // Redo region creation. + code_edit->redo(); + CHECK(code_edit->get_text() == "line1\n#region New Code Region\nline2\n#endregion\nline3"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selected_text() == "New Code Region"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 23); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK_FALSE(code_edit->is_line_folded(1)); // Region creation without any selection has no effect. code_edit->set_text("line1\nline2\nline3"); @@ -2925,7 +3467,7 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { code_edit->create_code_region(); CHECK(code_edit->get_text() == "line1\nline2\nline3"); - // Region creation with multiple selections. + // Region creation with multiple selections. Secondary carets are removed and the first region name is selected. code_edit->set_text("line1\nline2\nline3"); code_edit->clear_comment_delimiters(); code_edit->add_comment_delimiter("#", ""); @@ -2934,6 +3476,25 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { code_edit->select(2, 0, 2, 5, 1); code_edit->create_code_region(); CHECK(code_edit->get_text() == "#region New Code Region\nline1\n#endregion\nline2\n#region New Code Region\nline3\n#endregion"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selected_text() == "New Code Region"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 23); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 8); + + // Region creation with mixed selection and non-selection carets. Regular carets are ignored. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->select(0, 0, 0, 4, 0); + code_edit->add_caret(2, 5); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "#region New Code Region\nline1\n#endregion\nline2\nline3"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selected_text() == "New Code Region"); // Two selections on the same line create only one region. code_edit->set_text("test line1\ntest line2\ntest line3"); @@ -2960,6 +3521,10 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { code_edit->add_comment_delimiter("/*", "*/"); code_edit->create_code_region(); CHECK(code_edit->get_text() == "line1\nline2\nline3"); + } + + SUBCASE("[CodeEdit] region comment delimiters") { + code_edit->set_line_folding_enabled(true); // Choose one line comment delimiter. code_edit->set_text("//region region_name\nline2\n//endregion"); @@ -2993,6 +3558,10 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { code_edit->clear_comment_delimiters(); CHECK_FALSE(code_edit->is_line_code_region_start(0)); CHECK_FALSE(code_edit->is_line_code_region_end(2)); + } + + SUBCASE("[CodeEdit] fold region") { + code_edit->set_line_folding_enabled(true); // Fold region. code_edit->clear_comment_delimiters(); @@ -3895,10 +4464,7 @@ TEST_CASE("[SceneTree][CodeEdit] symbol lookup") { SEND_GUI_KEY_EVENT(Key::CTRL); #endif - Array signal_args; - Array arg; - arg.push_back("some"); - signal_args.push_back(arg); + Array signal_args = build_array(build_array("some")); SIGNAL_CHECK("symbol_validate", signal_args); SIGNAL_UNWATCH(code_edit, "symbol_validate"); @@ -3928,178 +4494,980 @@ TEST_CASE("[SceneTree][CodeEdit] line length guidelines") { memdelete(code_edit); } -TEST_CASE("[SceneTree][CodeEdit] Backspace delete") { +TEST_CASE("[SceneTree][CodeEdit] text manipulation") { CodeEdit *code_edit = memnew(CodeEdit); SceneTree::get_singleton()->get_root()->add_child(code_edit); code_edit->grab_focus(); - /* Backspace with selection on first line. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test backspace"); - code_edit->select(0, 0, 0, 5); - code_edit->backspace(); - CHECK(code_edit->get_line(0) == "backspace"); - - /* Backspace with selection on first line and caret at the beginning of file. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test backspace"); - code_edit->select(0, 0, 0, 5); - code_edit->set_caret_column(0); - code_edit->backspace(); - CHECK(code_edit->get_line(0) == "backspace"); - - /* Move caret up to the previous line on backspace if caret is at the first column. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("line 1\nline 2"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(0); - code_edit->backspace(); - CHECK(code_edit->get_line(0) == "line 1line 2"); - CHECK(code_edit->get_caret_line() == 0); - CHECK(code_edit->get_caret_column() == 6); - - /* Backspace delete all text if all text is selected. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("line 1\nline 2\nline 3"); - code_edit->select_all(); - code_edit->backspace(); - CHECK(code_edit->get_text().is_empty()); - - /* Backspace at the beginning without selection has no effect. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("line 1\nline 2\nline 3"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(0); - code_edit->backspace(); - CHECK(code_edit->get_text() == "line 1\nline 2\nline 3"); + SUBCASE("[SceneTree][CodeEdit] backspace") { + // Backspace with selection on first line. + code_edit->set_text("test backspace"); + code_edit->select(0, 0, 0, 5); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "backspace"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); - memdelete(code_edit); -} + // Backspace with selection on first line and caret at the beginning of file. + code_edit->set_text("test backspace"); + code_edit->select(0, 5, 0, 0); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "backspace"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); -TEST_CASE("[SceneTree][CodeEdit] New Line") { - CodeEdit *code_edit = memnew(CodeEdit); - SceneTree::get_singleton()->get_root()->add_child(code_edit); - code_edit->grab_focus(); + // Move caret up to the previous line on backspace if caret is at the first column. + code_edit->set_text("line 1\nline 2"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "line 1line 2"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 6); - /* Add a new line. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(13); - SEND_GUI_ACTION("ui_text_newline"); - CHECK(code_edit->get_line(0) == "test new line"); - CHECK(code_edit->get_line(1) == ""); - - /* Split line with new line. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(5); - SEND_GUI_ACTION("ui_text_newline"); - CHECK(code_edit->get_line(0) == "test "); - CHECK(code_edit->get_line(1) == "new line"); - - /* Delete selection and split with new line. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->select(0, 0, 0, 5); - SEND_GUI_ACTION("ui_text_newline"); - CHECK(code_edit->get_line(0) == ""); - CHECK(code_edit->get_line(1) == "new line"); - - /* Blank new line below with selection should not split. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->select(0, 0, 0, 5); - SEND_GUI_ACTION("ui_text_newline_blank"); - CHECK(code_edit->get_line(0) == "test new line"); - CHECK(code_edit->get_line(1) == ""); - - /* Blank new line above with selection should not split. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->select(0, 0, 0, 5); - SEND_GUI_ACTION("ui_text_newline_above"); - CHECK(code_edit->get_line(0) == ""); - CHECK(code_edit->get_line(1) == "test new line"); + // Multiple carets with a caret at the first column. + code_edit->set_text("line 1\nline 2"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(2); + code_edit->add_caret(1, 0); + code_edit->add_caret(1, 5); + code_edit->backspace(); + CHECK(code_edit->get_text() == "line 1lne2"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 7); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 6); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 9); + code_edit->remove_secondary_carets(); + + // Multiple carets close together. + code_edit->set_text("line 1\nline 2"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(2); + code_edit->add_caret(1, 1); + code_edit->backspace(); + CHECK(code_edit->get_text() == "line 1\nne 2"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); - memdelete(code_edit); -} + // Backspace delete all text if all text is selected. + code_edit->set_text("line 1\nline 2\nline 3"); + code_edit->select_all(); + code_edit->backspace(); + CHECK(code_edit->get_text().is_empty()); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); -TEST_CASE("[SceneTree][CodeEdit] Duplicate Lines") { - CodeEdit *code_edit = memnew(CodeEdit); - SceneTree::get_singleton()->get_root()->add_child(code_edit); - code_edit->grab_focus(); + // Backspace at the beginning without selection has no effect. + code_edit->set_text("line 1\nline 2\nline 3"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_text() == "line 1\nline 2\nline 3"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + } + + SUBCASE("[TextEdit] cut") { + DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton()); + code_edit->set_line_folding_enabled(true); + + // Cut without a selection removes the entire line. + code_edit->set_text("this is\nsome\n"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(6); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "some\n"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 3); // In the default font, this is the same position. + + // Undo restores the cut text. + code_edit->undo(); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "this is\nsome\n"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 6); + + // Redo. + code_edit->redo(); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "some\n"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 3); + + // Cut unfolds the line. + code_edit->set_text("this is\n\tsome\n"); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + + code_edit->cut(); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "\tsome\n"); + CHECK(code_edit->get_caret_line() == 0); + + // Cut with a selection removes just the selection. + code_edit->set_text("this is\nsome\n"); + code_edit->select(0, 5, 0, 7); + + SEND_GUI_ACTION("ui_cut"); + CHECK(code_edit->get_viewport()->is_input_handled()); + CHECK(DS->clipboard_get() == "is"); + CHECK(code_edit->get_text() == "this \nsome\n"); + CHECK_FALSE(code_edit->get_caret_line()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); + + // Cut does not change the text if not editable. Text is still added to clipboard. + code_edit->set_text("this is\nsome\n"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + + code_edit->set_editable(false); + code_edit->cut(); + code_edit->set_editable(true); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "this is\nsome\n"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); + + // Cut line with multiple carets. + code_edit->set_text("this is\nsome\n"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(3); + code_edit->add_caret(0, 2); + code_edit->add_caret(0, 4); + code_edit->add_caret(2, 0); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "this is\n\n"); + CHECK(code_edit->get_text() == "some"); + CHECK(code_edit->get_caret_count() == 3); + CHECK_FALSE(code_edit->has_selection(0)); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 2); // In the default font, this is the same position. + // The previous caret at index 1 was merged. + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 3); // In the default font, this is the same position. + CHECK_FALSE(code_edit->has_selection(2)); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 4); + code_edit->remove_secondary_carets(); + + // Cut on the only line removes the contents. + code_edit->set_caret_line(0); + code_edit->set_caret_column(2); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "some\n"); + CHECK(code_edit->get_text() == ""); + CHECK(code_edit->get_line_count() == 1); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Cut empty line. + code_edit->cut(); + CHECK(DS->clipboard_get() == "\n"); + CHECK(code_edit->get_text() == ""); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Cut multiple lines, in order. + code_edit->set_text("this is\nsome\ntext to\nbe\n\ncut"); + code_edit->set_caret_line(2); + code_edit->set_caret_column(7); + code_edit->add_caret(3, 0); + code_edit->add_caret(0, 2); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "this is\ntext to\nbe\n"); + CHECK(code_edit->get_text() == "some\n\ncut"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 2); + code_edit->remove_secondary_carets(); + + // Cut multiple selections, in order. Ignores regular carets. + code_edit->set_text("this is\nsome\ntext to\nbe\n\ncut"); + code_edit->add_caret(3, 0); + code_edit->add_caret(0, 2); + code_edit->add_caret(2, 0); + code_edit->select(1, 0, 1, 2, 0); + code_edit->select(3, 0, 4, 0, 1); + code_edit->select(0, 5, 0, 3, 2); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "s \nso\nbe\n"); + CHECK(code_edit->get_text() == "thiis\nme\ntext to\n\ncut"); + CHECK(code_edit->get_caret_count() == 4); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 3); + CHECK(code_edit->get_caret_column(1) == 0); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 3); + CHECK(code_edit->get_caret_line(3) == 2); + CHECK(code_edit->get_caret_column(3) == 0); + } + + SUBCASE("[SceneTree][CodeEdit] new line") { + // Add a new line. + code_edit->set_text("test new line"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(13); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "test new line"); + CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Split line with new line. + code_edit->set_text("test new line"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "test "); + CHECK(code_edit->get_line(1) == "new line"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Delete selection and split with new line. + code_edit->set_text("test new line"); + code_edit->select(0, 0, 0, 5); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "new line"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Blank new line below with selection should not split. + code_edit->set_text("test new line"); + code_edit->select(0, 0, 0, 5); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test new line"); + CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Blank new line above with selection should not split. + code_edit->set_text("test new line"); + code_edit->select(0, 0, 0, 5); + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test new line"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Multiple new lines with multiple carets. + code_edit->set_text("test new line"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 8); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "test "); + CHECK(code_edit->get_line(1) == "new"); + CHECK(code_edit->get_line(2) == " line"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 2); + CHECK(code_edit->get_caret_column(1) == 0); + + // Multiple blank new lines with multiple carets. + code_edit->set_text("test new line"); + code_edit->remove_secondary_carets(); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 8); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test new line"); + CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_line(2) == ""); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 2); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + + // Multiple new lines above with multiple carets. + code_edit->set_text("test new line"); + code_edit->remove_secondary_carets(); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 8); + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_line(2) == "test new line"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + + // See '[CodeEdit] auto indent' tests for tests about new line with indentation. + } + + SUBCASE("[SceneTree][CodeEdit] move lines up") { + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + + // Move line up with caret on it. + code_edit->set_caret_line(2); + code_edit->set_caret_column(1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Undo. + code_edit->undo(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 1); + + // Redo. + code_edit->redo(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Does nothing at the first line. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 1); + + // Works on empty line. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->set_caret_line(3); + code_edit->set_caret_column(0); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\n\nto\nmove\naround"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 0); + + // Move multiple lines up with selection. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(4, 0, 5, 1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\naround\n"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 3); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 4); + CHECK(code_edit->get_caret_column() == 1); + + // Does not affect line with selection end at column 0. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(4, 0, 5, 0); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\n\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 3); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 4); + CHECK(code_edit->get_caret_column() == 0); + + // Move multiple lines up with selection, right to left selection. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(5, 2, 4, 1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\naround\n"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 4); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 3); + CHECK(code_edit->get_caret_column() == 1); + + // Move multiple lines with multiple carets. A line with multiple carets is only moved once. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(5, 2, 5, 4); + code_edit->add_caret(4, 0); + code_edit->add_caret(4, 4); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\naround\n"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 4); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 4); + CHECK(code_edit->get_caret_column(0) == 4); + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 3); + CHECK(code_edit->get_caret_column(1) == 0); + CHECK_FALSE(code_edit->has_selection(2)); + CHECK(code_edit->get_caret_line(2) == 3); + CHECK(code_edit->get_caret_column(2) == 4); + + // Move multiple separate lines with multiple selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(2, 2, 1, 4); + code_edit->add_caret(5, 0); + code_edit->select(5, 0, 5, 1, 1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "lines\nto\ntest\n\naround\nmove"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 1); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 4); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 4); + CHECK(code_edit->get_selection_origin_column(1) == 0); + CHECK(code_edit->get_caret_line(1) == 4); + CHECK(code_edit->get_caret_column(1) == 1); + } + + SUBCASE("[SceneTree][CodeEdit] move lines down") { + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + + // Move line down with caret on it. + code_edit->set_caret_line(1); + code_edit->set_caret_column(1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 1); + + // Undo. + code_edit->undo(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Redo. + code_edit->redo(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 1); + + // Does nothing at the last line. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->set_caret_line(5); + code_edit->set_caret_column(1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 5); + CHECK(code_edit->get_caret_column() == 1); + + // Works on empty line. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->set_caret_line(3); + code_edit->set_caret_column(0); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\n\naround"); + CHECK(code_edit->get_caret_line() == 4); + CHECK(code_edit->get_caret_column() == 0); + + // Move multiple lines down with selection. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(1, 0, 2, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\n\nlines\nto\nmove\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 2); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 3); + CHECK(code_edit->get_caret_column() == 1); + + // Does not affect line with selection end at column 0. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(1, 0, 2, 0); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 2); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 3); + CHECK(code_edit->get_caret_column() == 0); + + // Move multiple lines down with selection, right to left selection. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(2, 2, 1, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\n\nlines\nto\nmove\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 3); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 1); + + // Move multiple lines with multiple carets. A line with multiple carets is only moved once. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(1, 2, 1, 4); + code_edit->add_caret(0, 0); + code_edit->add_caret(0, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "to\ntest\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 2); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 2); + CHECK(code_edit->get_caret_column(0) == 4); + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + CHECK_FALSE(code_edit->has_selection(2)); + CHECK(code_edit->get_caret_line(2) == 1); + CHECK(code_edit->get_caret_column(2) == 1); + + // Move multiple separate lines with multiple selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(0, 2, 1, 4); + code_edit->add_caret(4, 0); + code_edit->select(4, 0, 4, 2, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "to\ntest\nlines\n\naround\nmove"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 1); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 2); + CHECK(code_edit->get_caret_column(0) == 4); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 5); + CHECK(code_edit->get_selection_origin_column(1) == 0); + CHECK(code_edit->get_caret_line(1) == 5); + CHECK(code_edit->get_caret_column(1) == 2); + } + + SUBCASE("[SceneTree][CodeEdit] delete lines") { + code_edit->set_text("test\nlines\nto\n\ndelete"); + + // Delete line with caret on it. + code_edit->set_caret_line(1); + code_edit->set_caret_column(1); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "test\nto\n\ndelete"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Undo. + code_edit->undo(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\ndelete"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Redo. + code_edit->redo(); + CHECK(code_edit->get_text() == "test\nto\n\ndelete"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Delete empty line. + code_edit->set_caret_line(2); + code_edit->set_caret_column(0); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "test\nto\ndelete"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 0); + + // Deletes only one line when there are multiple carets on it. Carets move down and the column gets clamped. + code_edit->set_caret_line(0); + code_edit->set_caret_column(0); + code_edit->add_caret(0, 1); + code_edit->add_caret(0, 4); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "to\ndelete"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 1); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 2); + + // Delete multiple lines with selection. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\ndelete"); + code_edit->select(0, 1, 2, 1); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "\ndelete"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Does not affect line with selection end at column 0. + code_edit->set_text("test\nlines\nto\n\ndelete"); + code_edit->select(0, 1, 1, 0); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "lines\nto\n\ndelete"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Delete multiple lines with multiple carets. + code_edit->set_text("test\nlines\nto\n\ndelete"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(2); + code_edit->add_caret(1, 0); + code_edit->add_caret(4, 5); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "to\n"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + + // Delete multiple separate lines with multiple selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\ndelete"); + code_edit->add_caret(4, 5); + code_edit->select(0, 1, 1, 1); + code_edit->select(5, 5, 4, 0, 1); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "to\n"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + + // Deletes contents when there is only one line. + code_edit->remove_secondary_carets(); + code_edit->set_text("test"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(4); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == ""); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + } + + SUBCASE("[SceneTree][CodeEdit] duplicate selection") { + code_edit->set_text("test\nlines\nto\n\nduplicate"); + + // Duplicate selected text. + code_edit->select(0, 1, 1, 2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\nliest\nlines\nto\n\nduplicate"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 2); + + // Undo. + code_edit->undo(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nduplicate"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 1); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 2); + + // Redo. + code_edit->redo(); + CHECK(code_edit->get_text() == "test\nliest\nlines\nto\n\nduplicate"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 2); + + // Duplicate selected text, right to left selection. + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->select(1, 1, 0, 2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\nlst\nlines\nto\n\nduplicate"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 2); + CHECK(code_edit->get_selection_origin_column() == 1); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Duplicate line if there is no selection. + code_edit->deselect(); + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\nlines\nlines\nto\n\nduplicate"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 2); + + // Duplicate multiple lines. + code_edit->deselect(); + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(2); + code_edit->add_caret(5, 0); + code_edit->add_caret(0, 4); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\ntest\nlines\nlines\nto\n\nduplicate\nduplicate"); + CHECK(code_edit->get_caret_count() == 3); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line(0) == 3); + CHECK(code_edit->get_caret_column(0) == 2); + CHECK(code_edit->get_caret_line(1) == 7); + CHECK(code_edit->get_caret_column(1) == 0); + CHECK(code_edit->get_caret_line(2) == 1); + CHECK(code_edit->get_caret_column(2) == 4); + + // Duplicate multiple separate selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->add_caret(4, 4); + code_edit->add_caret(0, 1); + code_edit->add_caret(0, 4); + code_edit->select(2, 0, 2, 1, 0); + code_edit->select(3, 0, 4, 4, 1); + code_edit->select(0, 1, 0, 0, 2); + code_edit->select(0, 2, 0, 4, 3); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "ttestst\nlines\ntto\n\ndupl\nduplicate"); + CHECK(code_edit->get_caret_count() == 4); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 2); + CHECK(code_edit->get_selection_origin_column(0) == 1); + CHECK(code_edit->get_caret_line(0) == 2); + CHECK(code_edit->get_caret_column(0) == 2); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 4); + CHECK(code_edit->get_selection_origin_column(1) == 4); + CHECK(code_edit->get_caret_line(1) == 5); + CHECK(code_edit->get_caret_column(1) == 4); + CHECK(code_edit->has_selection(2)); + CHECK(code_edit->get_selection_origin_line(2) == 0); + CHECK(code_edit->get_selection_origin_column(2) == 2); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 1); + CHECK(code_edit->has_selection(3)); + CHECK(code_edit->get_selection_origin_line(3) == 0); + CHECK(code_edit->get_selection_origin_column(3) == 5); + CHECK(code_edit->get_caret_line(3) == 0); + CHECK(code_edit->get_caret_column(3) == 7); + + // Duplicate adjacent selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->add_caret(1, 2); + code_edit->select(1, 0, 1, 1, 0); + code_edit->select(1, 1, 1, 4, 1); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\nllineines\nto\n\nduplicate"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 1); + CHECK(code_edit->get_selection_origin_column(0) == 1); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 2); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 1); + CHECK(code_edit->get_selection_origin_column(1) == 5); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 8); + + // Duplicate lines then duplicate selections when there are both selections and non-selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test duplicate"); + code_edit->select(0, 14, 0, 13, 0); + code_edit->add_caret(0, 8); + code_edit->add_caret(0, 4); + code_edit->select(0, 2, 0, 4, 2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test duplicate\ntestst duplicatee"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 1); + CHECK(code_edit->get_selection_origin_column(0) == 17); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 16); + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 10); + CHECK(code_edit->has_selection(2)); + CHECK(code_edit->get_selection_origin_line(2) == 1); + CHECK(code_edit->get_selection_origin_column(2) == 4); + CHECK(code_edit->get_caret_line(2) == 1); + CHECK(code_edit->get_caret_column(2) == 6); + } + + SUBCASE("[SceneTree][CodeEdit] duplicate lines") { + String reset_text = R"(extends Node + +func _ready(): + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var c := a + b + for i in range(c): + print("This is the solution: ", sin(i)) + var pos = get_index() - 1 + print("Make sure this exits: %b" % pos) +)"; + + code_edit->set_text(reset_text); - code_edit->set_text(R"(extends Node + // Duplicate a single line without selection. + code_edit->set_caret_line(0); + code_edit->duplicate_lines(); + CHECK(code_edit->get_line(0) == "extends Node"); + CHECK(code_edit->get_line(1) == "extends Node"); + CHECK(code_edit->get_line(2) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Duplicate multiple lines with selection. + code_edit->set_text(reset_text); + code_edit->select(4, 8, 6, 15); + code_edit->duplicate_lines(); + CHECK(code_edit->get_text() == R"(extends Node + +func _ready(): + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var c := a + b + for i in range(c): + var b := get_child_count() + var c := a + b + for i in range(c): + print("This is the solution: ", sin(i)) + var pos = get_index() - 1 + print("Make sure this exits: %b" % pos) +)"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 7); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_line() == 9); + CHECK(code_edit->get_caret_column() == 15); + + // Duplicate multiple lines with right to left selection. + code_edit->set_text(reset_text); + code_edit->select(6, 15, 4, 8); + code_edit->duplicate_lines(); + CHECK(code_edit->get_text() == R"(extends Node func _ready(): var a := len(OS.get_cmdline_args()) var b := get_child_count() var c := a + b for i in range(c): + var b := get_child_count() + var c := a + b + for i in range(c): print("This is the solution: ", sin(i)) var pos = get_index() - 1 print("Make sure this exits: %b" % pos) )"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 9); + CHECK(code_edit->get_selection_origin_column() == 15); + CHECK(code_edit->get_caret_line() == 7); + CHECK(code_edit->get_caret_column() == 8); + + // Duplicate single lines with multiple carets. Multiple carets on a single line only duplicate once. + code_edit->remove_secondary_carets(); + code_edit->deselect(); + code_edit->set_text(reset_text); + code_edit->set_caret_line(3); + code_edit->set_caret_column(1); + code_edit->add_caret(5, 1); + code_edit->add_caret(5, 5); + code_edit->add_caret(4, 2); + code_edit->duplicate_lines(); + CHECK(code_edit->get_text() == R"(extends Node - /* Duplicate a single line without selection. */ - code_edit->set_caret_line(0); - code_edit->duplicate_lines(); - CHECK(code_edit->get_line(0) == "extends Node"); - CHECK(code_edit->get_line(1) == "extends Node"); - CHECK(code_edit->get_line(2) == ""); - - /* Duplicate multiple lines with selection. */ - code_edit->set_caret_line(6); - code_edit->set_caret_column(15); - code_edit->select(4, 8, 6, 15); - code_edit->duplicate_lines(); - CHECK(code_edit->get_line(6) == "\tvar c := a + b"); - CHECK(code_edit->get_line(7) == "\tvar a := len(OS.get_cmdline_args())"); - CHECK(code_edit->get_line(8) == "\tvar b := get_child_count()"); - CHECK(code_edit->get_line(9) == "\tvar c := a + b"); - CHECK(code_edit->get_line(10) == "\tfor i in range(c):"); - - /* Duplicate single lines with multiple carets. */ - code_edit->deselect(); - code_edit->set_caret_line(10); - code_edit->set_caret_column(1); - code_edit->add_caret(11, 2); - code_edit->add_caret(12, 1); - code_edit->duplicate_lines(); - CHECK(code_edit->get_line(9) == "\tvar c := a + b"); - CHECK(code_edit->get_line(10) == "\tfor i in range(c):"); - CHECK(code_edit->get_line(11) == "\tfor i in range(c):"); - CHECK(code_edit->get_line(12) == "\t\tprint(\"This is the solution: \", sin(i))"); - CHECK(code_edit->get_line(13) == "\t\tprint(\"This is the solution: \", sin(i))"); - CHECK(code_edit->get_line(14) == "\tvar pos = get_index() - 1"); - CHECK(code_edit->get_line(15) == "\tvar pos = get_index() - 1"); - CHECK(code_edit->get_line(16) == "\tprint(\"Make sure this exits: %b\" % pos)"); - - /* Duplicate multiple lines with multiple carets. */ - code_edit->select(0, 0, 1, 2, 0); - code_edit->select(3, 0, 4, 2, 1); - code_edit->select(16, 0, 17, 0, 2); - code_edit->set_caret_line(1, false, true, 0, 0); - code_edit->set_caret_column(2, false, 0); - code_edit->set_caret_line(4, false, true, 0, 1); - code_edit->set_caret_column(2, false, 1); - code_edit->set_caret_line(17, false, true, 0, 2); - code_edit->set_caret_column(0, false, 2); - code_edit->duplicate_lines(); - CHECK(code_edit->get_line(1) == "extends Node"); - CHECK(code_edit->get_line(2) == "extends Node"); - CHECK(code_edit->get_line(3) == "extends Node"); - CHECK(code_edit->get_line(4) == ""); - CHECK(code_edit->get_line(6) == "\tvar a := len(OS.get_cmdline_args())"); - CHECK(code_edit->get_line(7) == "func _ready():"); - CHECK(code_edit->get_line(8) == "\tvar a := len(OS.get_cmdline_args())"); - CHECK(code_edit->get_line(9) == "\tvar b := get_child_count()"); - CHECK(code_edit->get_line(20) == "\tprint(\"Make sure this exits: %b\" % pos)"); - CHECK(code_edit->get_line(21) == ""); - CHECK(code_edit->get_line(22) == "\tprint(\"Make sure this exits: %b\" % pos)"); - CHECK(code_edit->get_line(23) == ""); +func _ready(): + var a := len(OS.get_cmdline_args()) + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var b := get_child_count() + var c := a + b + var c := a + b + for i in range(c): + print("This is the solution: ", sin(i)) + var pos = get_index() - 1 + print("Make sure this exits: %b" % pos) +)"); + CHECK(code_edit->get_caret_count() == 4); + CHECK_FALSE(code_edit->has_selection(0)); + CHECK(code_edit->get_caret_line(0) == 4); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 8); + CHECK(code_edit->get_caret_column(1) == 1); + CHECK_FALSE(code_edit->has_selection(2)); + CHECK(code_edit->get_caret_line(2) == 8); + CHECK(code_edit->get_caret_column(2) == 5); + CHECK_FALSE(code_edit->has_selection(3)); + CHECK(code_edit->get_caret_line(3) == 6); + CHECK(code_edit->get_caret_column(3) == 2); + + // Duplicate multiple lines with multiple selections. + code_edit->remove_secondary_carets(); + code_edit->set_text(reset_text); + code_edit->add_caret(4, 2); + code_edit->add_caret(6, 0); + code_edit->add_caret(7, 8); + code_edit->select(0, 0, 2, 5, 0); + code_edit->select(3, 0, 4, 2, 1); + code_edit->select(7, 1, 6, 0, 2); + code_edit->select(7, 3, 7, 8, 3); + code_edit->duplicate_lines(); + CHECK(code_edit->get_text() == R"(extends Node + +func _ready(): +extends Node + +func _ready(): + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var c := a + b + for i in range(c): + print("This is the solution: ", sin(i)) + for i in range(c): + print("This is the solution: ", sin(i)) + var pos = get_index() - 1 + print("Make sure this exits: %b" % pos) +)"); + CHECK(code_edit->get_caret_count() == 4); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 3); + CHECK(code_edit->get_selection_origin_column(0) == 0); + CHECK(code_edit->get_caret_line(0) == 5); + CHECK(code_edit->get_caret_column(0) == 5); + + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 8); + CHECK(code_edit->get_selection_origin_column(1) == 0); + CHECK(code_edit->get_caret_line(1) == 9); + CHECK(code_edit->get_caret_column(1) == 2); + + CHECK(code_edit->has_selection(2)); + CHECK(code_edit->get_selection_origin_line(2) == 14); + CHECK(code_edit->get_selection_origin_column(2) == 1); + CHECK(code_edit->get_caret_line(2) == 13); + CHECK(code_edit->get_caret_column(2) == 0); + + CHECK(code_edit->has_selection(3)); + CHECK(code_edit->get_selection_origin_line(3) == 14); + CHECK(code_edit->get_selection_origin_column(3) == 3); + CHECK(code_edit->get_caret_line(3) == 14); + CHECK(code_edit->get_caret_column(3) == 8); + } memdelete(code_edit); } diff --git a/tests/scene/test_graph_node.h b/tests/scene/test_graph_node.h new file mode 100644 index 0000000000..72b8b682c9 --- /dev/null +++ b/tests/scene/test_graph_node.h @@ -0,0 +1,59 @@ +/**************************************************************************/ +/* test_graph_node.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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 TEST_GRAPH_NODE_H +#define TEST_GRAPH_NODE_H + +#include "scene/gui/graph_node.h" +#include "scene/main/window.h" + +#include "tests/test_macros.h" + +namespace TestGraphNode { + +TEST_CASE("[GraphNode][SceneTree]") { + SUBCASE("[GraphNode] Graph Node only child on delete should not cause error.") { + // Setup. + GraphNode *test_node = memnew(GraphNode); + test_child->set_name("Graph Node"); + Control *test_child = memnew(Control); + test_child->set_name("child"); + test_node->add_child(test_child); + + // Test. + CHECK_NOTHROW_MESSAGE(test_node->remove_child(test_child)); + + memdelete(test_node); + } +} + +} // namespace TestGraphNode + +#endif // TEST_GRAPH_NODE_H diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 8577dd7148..246d869687 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -36,6 +36,23 @@ #include "tests/test_macros.h" namespace TestTextEdit { +static inline Array build_array() { + return Array(); +} +template <typename... Targs> +static inline Array build_array(Variant item, Targs... Fargs) { + Array a = build_array(Fargs...); + a.push_front(item); + return a; +} +static inline Array reverse_nested(Array array) { + Array reversed_array = array.duplicate(true); + reversed_array.reverse(); + for (int i = 0; i < reversed_array.size(); i++) { + ((Array)reversed_array[i]).reverse(); + } + return reversed_array; +} TEST_CASE("[SceneTree][TextEdit] text entry") { SceneTree::get_singleton()->get_root()->set_physics_object_picking(false); @@ -52,12 +69,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_WATCH(text_edit, "lines_edited_from"); SIGNAL_WATCH(text_edit, "caret_changed"); - Array args1; - args1.push_back(0); - args1.push_back(0); - Array lines_edited_args; - lines_edited_args.push_back(args1); - lines_edited_args.push_back(args1.duplicate()); + Array lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); SUBCASE("[TextEdit] clear and set text") { // "text_changed" should not be emitted on clear / set. @@ -119,13 +131,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_set"); - // Clear. + // Can clear even if not editable. text_edit->set_editable(false); - Array lines_edited_clear_args; - Array new_args = args1.duplicate(); - new_args[0] = 1; - lines_edited_clear_args.push_back(new_args); + Array lines_edited_clear_args = build_array(build_array(1, 0)); text_edit->clear(); MessageQueue::get_singleton()->flush(); @@ -210,6 +219,321 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("text_changed"); } + SUBCASE("[TextEdit] insert text") { + // insert_text is 0 indexed. + ERR_PRINT_OFF; + text_edit->insert_text("test", 1, 0); + ERR_PRINT_ON; + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + // Insert text when there is no text. + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("tes", 0, 0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "tes"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Insert multiple lines. + lines_edited_args = build_array(build_array(0, 1)); + + text_edit->insert_text("t\ninserting text", 0, 3); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\ninserting text"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 14); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Can insert even if not editable. + lines_edited_args = build_array(build_array(1, 1)); + + text_edit->set_editable(false); + text_edit->insert_text("mid", 1, 2); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\ninmidserting text"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 17); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->set_editable(true); + + // Undo insert. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\ninserting text"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 14); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Redo insert. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\ninmidserting text"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 17); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Insert offsets carets after the edit. + text_edit->add_caret(1, 1); + text_edit->add_caret(1, 4); + text_edit->select(1, 4, 1, 6, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 2)); + + text_edit->insert_text("\n ", 1, 2); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nin\n midserting text"); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 16); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 5); + CHECK(text_edit->get_selection_origin_line(2) == 2); + CHECK(text_edit->get_selection_origin_column(2) == 3); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Insert text outside of selections. + text_edit->set_text("test text"); + text_edit->add_caret(0, 8); + text_edit->select(0, 1, 0, 4, 0); + text_edit->select(0, 4, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("a", 0, 4, true, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testa text"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 9); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 5); + + // Insert text to beginning of selections. + text_edit->set_text("test text"); + text_edit->add_caret(0, 8); + text_edit->select(0, 1, 0, 4, 0); + text_edit->select(0, 4, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("a", 0, 4, false, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testa text"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 9); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 4); + + // Insert text to end of selections. + text_edit->set_text("test text"); + text_edit->add_caret(0, 8); + text_edit->select(0, 1, 0, 4, 0); + text_edit->select(0, 4, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("a", 0, 4, true, true); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testa text"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 9); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 5); + + // Insert text inside of selections. + text_edit->set_text("test text"); + text_edit->add_caret(0, 8); + text_edit->select(0, 1, 0, 4, 0); + text_edit->select(0, 4, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("a", 0, 4, false, true); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testa text"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + } + + SUBCASE("[TextEdit] remove text") { + lines_edited_args = build_array(build_array(0, 0), build_array(0, 2)); + + text_edit->set_text("test\nremoveing text\nthird line"); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + // remove_text is 0 indexed. + ERR_PRINT_OFF; + text_edit->remove_text(3, 0, 3, 4); + ERR_PRINT_ON; + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremoveing text\nthird line"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + // Remove multiple lines. + text_edit->set_caret_line(2); + text_edit->set_caret_column(10); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 1)); + + text_edit->remove_text(1, 9, 2, 2); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremoveingird line"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 17); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Can remove even if not editable. + lines_edited_args = build_array(build_array(1, 1)); + + text_edit->set_editable(false); + text_edit->remove_text(1, 5, 1, 6); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremovingird line"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 16); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->set_editable(true); + + // Undo remove. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremoveingird line"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 17); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Redo remove. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremovingird line"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 16); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Remove collapses carets and offsets carets after the edit. + text_edit->set_caret_line(1); + text_edit->set_caret_column(9); + text_edit->add_caret(1, 10); + text_edit->select(1, 10, 1, 13, 1); + text_edit->add_caret(1, 14); + text_edit->add_caret(1, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + text_edit->remove_text(1, 8, 1, 11); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremoving line"); + // Caret 0 was merged into the selection. + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 10); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 8); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 11); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 2); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); + } + SUBCASE("[TextEdit] set and get line") { // Set / Get line is 0 indexed. text_edit->set_line(1, "test"); @@ -225,6 +549,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_text() == "test"); CHECK(text_edit->get_line(0) == "test"); CHECK(text_edit->get_line(1) == ""); + CHECK(text_edit->get_line_count() == 1); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); @@ -233,14 +558,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { // Setting to a longer line, caret and selections should be preserved. text_edit->select_all(); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->has_selection()); - SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_DISCARD("caret_changed"); text_edit->set_line(0, "test text"); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_line(0) == "test text"); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "test"); + CHECK(text_edit->get_selection_origin_column() == 0); + CHECK(text_edit->get_caret_column() == 4); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("caret_changed"); @@ -299,12 +625,84 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("text_set"); - ERR_PRINT_ON; + + // Both ends of selection are adjusted and deselects. + text_edit->set_text("test text"); + text_edit->select(0, 8, 0, 6); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_line(0, "test"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "test"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Multiple carets adjust to keep visual position. + text_edit->set_text("test text"); + text_edit->set_caret_column(2); + text_edit->add_caret(0, 0); + text_edit->add_caret(0, 1); + text_edit->add_caret(0, 6); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_line(0, "\tset line"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "\tset line"); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection()); + // In the default font, these are the same positions. + CHECK(text_edit->get_caret_column(0) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + // The previous caret at index 2 was merged. + CHECK(text_edit->get_caret_column(2) == 4); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); + + // Insert multiple lines. + text_edit->set_text("test text\nsecond line"); + text_edit->set_caret_column(5); + text_edit->add_caret(1, 6); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 1)); + + text_edit->set_line(0, "multiple\nlines"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "multiple\nlines\nsecond line"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 3); // In the default font, this is the same position. + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 6); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); } SUBCASE("[TextEdit] swap lines") { - ((Array)lines_edited_args[1])[1] = 1; + lines_edited_args = build_array(build_array(0, 0), build_array(0, 1)); text_edit->set_text("testing\nswap"); MessageQueue::get_singleton()->flush(); @@ -317,15 +715,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_caret_column(text_edit->get_line(0).length()); MessageQueue::get_singleton()->flush(); SIGNAL_CHECK("caret_changed", empty_signal_args); + // Emitted twice for each line. + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0), build_array(1, 1), build_array(1, 1)); - ((Array)lines_edited_args[1])[1] = 0; - Array swap_args; - swap_args.push_back(1); - swap_args.push_back(1); - lines_edited_args.push_back(swap_args); - lines_edited_args.push_back(swap_args); - - // Order does not matter. Should also work if not editable. + // Order does not matter. Works when not editable. text_edit->set_editable(false); text_edit->swap_lines(1, 0); MessageQueue::get_singleton()->flush(); @@ -336,19 +729,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("text_set"); text_edit->set_editable(true); - lines_edited_args.reverse(); - - // Single undo/redo action + // Single undo/redo action. text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "testing\nswap"); - SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); - lines_edited_args.reverse(); - text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "swap\ntesting"); @@ -361,36 +750,70 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { ERR_PRINT_OFF; text_edit->swap_lines(-1, 0); CHECK(text_edit->get_text() == "swap\ntesting"); - SIGNAL_CHECK_FALSE("lines_edited_from"); - SIGNAL_CHECK_FALSE("caret_changed"); - SIGNAL_CHECK_FALSE("text_changed"); - SIGNAL_CHECK_FALSE("text_set"); - text_edit->swap_lines(0, -1); CHECK(text_edit->get_text() == "swap\ntesting"); - SIGNAL_CHECK_FALSE("lines_edited_from"); - SIGNAL_CHECK_FALSE("caret_changed"); - SIGNAL_CHECK_FALSE("text_changed"); - SIGNAL_CHECK_FALSE("text_set"); - text_edit->swap_lines(2, 0); CHECK(text_edit->get_text() == "swap\ntesting"); + text_edit->swap_lines(0, 2); + CHECK(text_edit->get_text() == "swap\ntesting"); + MessageQueue::get_singleton()->flush(); SIGNAL_CHECK_FALSE("lines_edited_from"); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("text_set"); + ERR_PRINT_ON; + + // Carets are also swapped. + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + text_edit->select(0, 0, 0, 2); + text_edit->add_caret(1, 6); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 1), build_array(1, 1), build_array(0, 0), build_array(0, 0)); + + text_edit->swap_lines(0, 1); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nswap"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 2); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 6); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); + + // Swap non adjacent lines. + text_edit->insert_line_at(1, "new line"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(5); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nnew line\nswap"); + SIGNAL_DISCARD("caret_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + lines_edited_args = build_array(build_array(2, 2), build_array(2, 2), build_array(0, 0), build_array(0, 0)); text_edit->swap_lines(0, 2); - CHECK(text_edit->get_text() == "swap\ntesting"); - SIGNAL_CHECK_FALSE("lines_edited_from"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "swap\nnew line\ntesting"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK_FALSE("caret_changed"); - SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); - ERR_PRINT_ON; } SUBCASE("[TextEdit] insert line at") { - ((Array)lines_edited_args[1])[1] = 1; + lines_edited_args = build_array(build_array(0, 0), build_array(0, 1)); text_edit->set_text("testing\nswap"); MessageQueue::get_singleton()->flush(); @@ -407,9 +830,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_to_line() == 1); SIGNAL_CHECK("caret_changed", empty_signal_args); - // Insert before should move caret and selection, and works when not editable. + // Insert line at inserts a line before and moves caret and selection. Works when not editable. text_edit->set_editable(false); - lines_edited_args.remove_at(0); + lines_edited_args = build_array(build_array(0, 1)); text_edit->insert_line_at(0, "new"); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "new\ntesting\nswap"); @@ -417,7 +840,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_column() == text_edit->get_line(2).size() - 1); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_from_column() == 0); CHECK(text_edit->get_selection_to_line() == 2); + CHECK(text_edit->get_selection_to_column() == 4); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); @@ -425,19 +850,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(true); // Can undo/redo as single action. - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 0; text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "testing\nswap"); CHECK(text_edit->has_selection()); - SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); - ((Array)lines_edited_args[0])[0] = 0; - ((Array)lines_edited_args[0])[1] = 1; text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "new\ntesting\nswap"); @@ -454,9 +875,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_from_line() == 0); CHECK(text_edit->get_selection_to_line() == 2); SIGNAL_CHECK_FALSE("caret_changed"); + lines_edited_args = build_array(build_array(2, 3)); - ((Array)lines_edited_args[0])[0] = 2; - ((Array)lines_edited_args[0])[1] = 3; text_edit->insert_line_at(2, "after"); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); @@ -474,24 +894,222 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { ERR_PRINT_OFF; text_edit->insert_line_at(-1, "after"); CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); + text_edit->insert_line_at(4, "after"); + CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); + MessageQueue::get_singleton()->flush(); SIGNAL_CHECK_FALSE("lines_edited_from"); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("text_set"); + ERR_PRINT_ON; - text_edit->insert_line_at(4, "after"); - CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); + // Can insert multiple lines. + text_edit->select(0, 1, 2, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 4)); + + text_edit->insert_line_at(2, "multiple\nlines"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "new\ntesting\nmultiple\nlines\nafter\nswap"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 4); + CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 1); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + } + + SUBCASE("[TextEdit] remove line at") { + lines_edited_args = build_array(build_array(0, 0), build_array(0, 5)); + text_edit->set_text("testing\nremove line at\n\tremove\nlines\n\ntest"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nremove line at\n\tremove\nlines\n\ntest"); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + // Remove line handles multiple carets. + text_edit->set_caret_line(2); + text_edit->set_caret_column(0); + text_edit->add_caret(2, 7); + text_edit->select(2, 1, 2, 7, 1); + text_edit->add_caret(3, 1); + text_edit->add_caret(4, 5); + text_edit->add_caret(1, 5); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 2)); + + text_edit->remove_line_at(2, true); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nremove line at\nlines\n\ntest"); + CHECK(text_edit->get_caret_count() == 5); + CHECK_FALSE(text_edit->has_selection(0)); // Same line. + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->has_selection(1)); // Same line, clamped. + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 3); // In the default font, this is the same position. + CHECK_FALSE(text_edit->has_selection(2)); // Moved up. + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 1); + CHECK_FALSE(text_edit->has_selection(3)); // Moved up. + CHECK(text_edit->get_caret_line(3) == 3); + CHECK(text_edit->get_caret_column(3) == 0); + CHECK_FALSE(text_edit->has_selection(4)); // Didn't move. + CHECK(text_edit->get_caret_line(4) == 1); + CHECK(text_edit->get_caret_column(4) == 5); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); + + // Remove first line. + text_edit->set_caret_line(0); + text_edit->set_caret_column(5); + text_edit->add_caret(4, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0)); + + text_edit->remove_line_at(0, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines\n\ntest"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); + + // Remove empty line. + text_edit->set_caret_line(2); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 2)); + + text_edit->remove_line_at(2, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines\ntest"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Remove last line. + text_edit->set_caret_line(2); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 1)); + + text_edit->remove_line_at(2, true); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Out of bounds. + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + ERR_PRINT_OFF + text_edit->remove_line_at(2, true); + ERR_PRINT_ON + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); SIGNAL_CHECK_FALSE("lines_edited_from"); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("text_set"); - ERR_PRINT_ON; + + // Remove regular line with move caret up and not editable. + text_edit->set_editable(false); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0)); + + text_edit->remove_line_at(1, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 1); // In the default font, this is the same position. + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->set_editable(true); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 2); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 1); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Remove only line removes line content. + text_edit->set_caret_line(0); + text_edit->set_caret_column(10); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->remove_line_at(0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_line_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); } - SUBCASE("[TextEdit] insert line at caret") { - lines_edited_args.pop_back(); - ((Array)lines_edited_args[0])[1] = 1; + SUBCASE("[TextEdit] insert text at caret") { + lines_edited_args = build_array(build_array(0, 1)); + // Insert text at caret can insert multiple lines. text_edit->insert_text_at_caret("testing\nswap"); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "testing\nswap"); @@ -502,11 +1120,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); + // Text is inserted at caret. text_edit->set_caret_line(0, false); text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[1] = 0; + lines_edited_args = build_array(build_array(0, 0)); text_edit->insert_text_at_caret("mid"); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "temidsting\nswap"); @@ -517,9 +1137,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); + // Selections are deleted then text is inserted. It also works even if not editable. text_edit->select(0, 0, 0, text_edit->get_line(0).length()); CHECK(text_edit->has_selection()); - lines_edited_args.push_back(args1.duplicate()); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); text_edit->set_editable(false); text_edit->insert_text_at_caret("new line"); @@ -534,12 +1155,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("text_set"); text_edit->set_editable(true); + // Undo restores text and selection. text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "temidsting\nswap"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("caret_changed", empty_signal_args); @@ -589,24 +1213,19 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_WATCH(text_edit, "lines_edited_from"); SIGNAL_WATCH(text_edit, "caret_changed"); - Array args1; - args1.push_back(0); - args1.push_back(0); - Array lines_edited_args; - lines_edited_args.push_back(args1); - lines_edited_args.push_back(args1.duplicate()); + Array lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); SUBCASE("[TextEdit] select all") { + // Select when there is no text does not select. text_edit->select_all(); CHECK_FALSE(text_edit->has_selection()); - ERR_PRINT_OFF; - CHECK(text_edit->get_selection_from_line() == -1); - CHECK(text_edit->get_selection_from_column() == -1); - CHECK(text_edit->get_selection_to_line() == -1); - CHECK(text_edit->get_selection_to_column() == -1); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 0); + CHECK(text_edit->get_selection_to_column() == 0); CHECK(text_edit->get_selected_text() == ""); - ERR_PRINT_ON; + // Select all selects all text. text_edit->set_text("test\nselection"); SEND_GUI_ACTION("ui_text_select_all"); CHECK(text_edit->get_viewport()->is_input_handled()); @@ -618,10 +1237,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_to_line() == 1); CHECK(text_edit->get_selection_to_column() == 9); CHECK(text_edit->get_selection_mode() == TextEdit::SelectionMode::SELECTION_MODE_SHIFT); + CHECK(text_edit->is_caret_after_selection_origin()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 9); SIGNAL_CHECK("caret_changed", empty_signal_args); + // Cannot select when disabled. text_edit->set_caret_line(0); text_edit->set_caret_column(0); text_edit->set_selecting_enabled(false); @@ -654,6 +1275,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Select word under caret with multiple carets. text_edit->select_word_under_caret(); CHECK(text_edit->has_selection(0)); CHECK(text_edit->get_selected_text(0) == "test"); @@ -675,6 +1297,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_count() == 2); + // Select word under caret disables selection if there is already a selection. text_edit->select_word_under_caret(); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == ""); @@ -703,6 +1326,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selected_text() == "test\ntest"); SIGNAL_CHECK("caret_changed", empty_signal_args); + // Cannot select when disabled. text_edit->set_selecting_enabled(false); text_edit->select_word_under_caret(); CHECK_FALSE(text_edit->has_selection()); @@ -714,10 +1338,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); text_edit->set_selecting_enabled(true); - text_edit->set_caret_line(1, false, true, 0, 0); + // Select word under caret when there is no word does not select. + text_edit->set_caret_line(1, false, true, -1, 0); text_edit->set_caret_column(5, false, 0); - - text_edit->set_caret_line(2, false, true, 0, 1); + text_edit->set_caret_line(2, false, true, -1, 1); text_edit->set_caret_column(5, false, 1); text_edit->select_word_under_caret(); @@ -739,7 +1363,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_caret_column(0); text_edit->set_caret_line(1); - // First selection made by the implicit select_word_under_caret call + // First selection made by the implicit select_word_under_caret call. text_edit->add_selection_for_next_occurrence(); CHECK(text_edit->get_caret_count() == 1); CHECK(text_edit->get_selected_text(0) == "test"); @@ -780,7 +1404,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line(3) == 3); CHECK(text_edit->get_caret_column(3) == 9); - // A different word with a new manually added caret + // A different word with a new manually added caret. text_edit->add_caret(2, 1); text_edit->select(2, 0, 2, 4, 4); CHECK(text_edit->get_selected_text(4) == "rand"); @@ -795,7 +1419,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line(5) == 3); CHECK(text_edit->get_caret_column(5) == 22); - // Make sure the previous selections are still active + // Make sure the previous selections are still active. CHECK(text_edit->get_selected_text(0) == "test"); CHECK(text_edit->get_selected_text(1) == "test"); CHECK(text_edit->get_selected_text(2) == "test"); @@ -987,6 +1611,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT) CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "t"); + CHECK(text_edit->is_caret_after_selection_origin()); #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT | KeyModifierMask::ALT) @@ -995,10 +1620,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { #endif CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "test"); + CHECK(text_edit->is_caret_after_selection_origin()); SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT) CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "tes"); + CHECK(text_edit->is_caret_after_selection_origin()); #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT | KeyModifierMask::ALT) @@ -1019,11 +1646,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT) CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "t"); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); SEND_GUI_KEY_EVENT(Key::LEFT) CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == ""); + // Cannot select when disabled. text_edit->set_selecting_enabled(false); SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT) CHECK_FALSE(text_edit->has_selection()); @@ -1032,46 +1661,120 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] mouse drag select") { - /* Set size for mouse input. */ + // Set size for mouse input. text_edit->set_size(Size2(200, 200)); text_edit->set_text("this is some text\nfor selection"); text_edit->grab_focus(); MessageQueue::get_singleton()->flush(); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); - SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE); + // Click and drag to make a selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Add (2,0) to bring it past the center point of the grapheme and account for integer division flooring. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "for s"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); - CHECK(text_edit->get_selection_from_line() == 1); - CHECK(text_edit->get_selection_from_column() == 0); - CHECK(text_edit->get_selection_to_line() == 1); - CHECK(text_edit->get_selection_to_column() == 5); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 0); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->is_caret_after_selection_origin()); + CHECK(text_edit->is_dragging_cursor()); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Releasing finishes. + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for s"); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for s"); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 0); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->is_caret_after_selection_origin()); + + // Clicking clears selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + // Cannot select when disabled, but caret still moves. text_edit->set_selecting_enabled(false); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); - SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); text_edit->set_selecting_enabled(true); + + // Only last caret is moved when adding a selection. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 15); + text_edit->select(0, 11, 0, 15, 1); + MessageQueue::get_singleton()->flush(); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 11); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 15); + + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selected_text(2) == "for s"); + CHECK(text_edit->get_selection_origin_line(2) == 1); + CHECK(text_edit->get_selection_origin_column(2) == 5); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 0); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(2)); + + // Overlapping carets and selections merges them. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "s is some text\nfor s"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 5); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); + + // Entering text stops selecting. + text_edit->insert_text_at_caret("a"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "thiaelection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); } SUBCASE("[TextEdit] mouse word select") { - /* Set size for mouse input. */ + // Set size for mouse input. text_edit->set_size(Size2(200, 200)); - text_edit->set_text("this is some text\nfor selection"); + text_edit->set_text("this is some text\nfor selection\n"); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); - SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE); + // Double click to select word. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 2).get_center() + Point2i(2, 0), Key::NONE); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "for"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); @@ -1081,9 +1784,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_to_column() == 3); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 3); + CHECK(text_edit->is_caret_after_selection_origin()); SIGNAL_CHECK("caret_changed", empty_signal_args); - SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE); + // Moving mouse selects entire words at a time. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 6).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "for selection"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); @@ -1093,15 +1798,116 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_to_column() == 13); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 13); + CHECK(text_edit->is_caret_after_selection_origin()); + CHECK(text_edit->is_dragging_cursor()); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + // Moving to a word before the initial selected word reverses selection direction and keeps the initial word selected. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "some text\nfor"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_from_column() == 8); + CHECK(text_edit->get_selection_to_line() == 1); + CHECK(text_edit->get_selection_to_column() == 3); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); SIGNAL_CHECK("caret_changed", empty_signal_args); - Point2i line_0 = text_edit->get_pos_at_line_column(0, 0); - line_0.y /= 2; - SEND_GUI_MOUSE_BUTTON_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Releasing finishes. + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "some text\nfor"); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 2).get_center(), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "some text\nfor"); + text_edit->deselect(); + + // Can start word select mode on an empty line. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(2, 0).get_center() + Point2i(2, 0), Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 0); + + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "selection\n"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_selection_origin_line() == 2); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Clicking clears selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + + // Can start word select mode when not on a word. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(0, 12).get_center() + Point2i(2, 0), Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 12); + + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == " text\nfor selection"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 13); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 12); + + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "some"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 12); + + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 15).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + + // Add a new selection without affecting the old one. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 8).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "some"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 8); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 12); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "ele"); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 5); + + // Shift + double click to extend selection and start word select mode. + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 8).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + text_edit->remove_secondary_carets(); + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), Key::NONE | KeyModifierMask::SHIFT); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == " text\nfor selection"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 13); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 12); + + // Cannot select when disabled, but caret still moves to end of word. text_edit->set_selecting_enabled(false); - SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE); + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 1).get_center() + Point2i(2, 0), Key::NONE); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 3); @@ -1109,32 +1915,149 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] mouse line select") { - /* Set size for mouse input. */ + // Set size for mouse input. text_edit->set_size(Size2(200, 200)); - text_edit->set_text("this is some text\nfor selection"); + text_edit->set_text("this is some text\nfor selection\nwith 3 lines"); MessageQueue::get_singleton()->flush(); - SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Triple click to select line. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 2).get_center(), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 2).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->has_selection()); - CHECK(text_edit->get_selected_text() == "for selection"); + CHECK(text_edit->get_selected_text() == "for selection\n"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); CHECK(text_edit->get_selection_from_line() == 1); CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 2); + CHECK(text_edit->get_selection_to_column() == 0); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->is_caret_after_selection_origin()); + + // Moving mouse selects entire lines at a time. Selecting above reverses the selection direction. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "this is some text\nfor selection"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_from_column() == 0); CHECK(text_edit->get_selection_to_line() == 1); CHECK(text_edit->get_selection_to_column() == 13); - CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); + CHECK(text_edit->is_dragging_cursor()); - Point2i line_0 = text_edit->get_pos_at_line_column(0, 0); - line_0.y /= 2; - SEND_GUI_MOUSE_BUTTON_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Selecting to the last line puts the caret at end of the line. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection\nwith 3 lines"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 2); + CHECK(text_edit->get_selection_to_column() == 12); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 12); + CHECK(text_edit->is_caret_after_selection_origin()); + + // Releasing finishes. + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection\nwith 3 lines"); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 2).get_center(), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection\nwith 3 lines"); + + // Clicking clears selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + + // Can start line select mode on an empty line. + text_edit->set_text("this is some text\n\nfor selection\nwith 4 lines"); + MessageQueue::get_singleton()->flush(); + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 0).get_center() + Point2i(2, 0), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "\n"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 0); + + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(2, 9).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "\nfor selection\n"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Add a new selection without affecting the old one. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "\nfor selection\n"); + CHECK(text_edit->get_caret_line(0) == 3); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 0); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "is"); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 2); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Selecting the last line puts caret at the end. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(3, 3).get_center(), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(3, 3).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "with 4 lines"); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 12); + CHECK(text_edit->get_selection_origin_line() == 3); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Selecting above reverses direction. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection\nwith 4 lines"); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_selection_origin_line() == 3); + CHECK(text_edit->get_selection_origin_column() == 12); + + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Shift + triple click to extend selection and restart line select mode. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(0, 9).get_center() + Point2i(2, 0), Key::NONE | KeyModifierMask::SHIFT); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 9).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "this is some text\n\nfor selection\nwith 4 lines"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_selection_origin_line() == 3); + CHECK(text_edit->get_selection_origin_column() == 12); + + // Cannot select when disabled, but caret still moves to the start of the next line. text_edit->set_selecting_enabled(false); - SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(0, 2).get_center(), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); @@ -1142,30 +2065,47 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] mouse shift click select") { - /* Set size for mouse input. */ + // Set size for mouse input. text_edit->set_size(Size2(200, 200)); text_edit->set_text("this is some text\nfor selection"); MessageQueue::get_singleton()->flush(); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); + // Shift click to make a selection from the previous caret position. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 1).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); CHECK(text_edit->has_selection()); - CHECK(text_edit->get_selected_text() == "for s"); + CHECK(text_edit->get_selected_text() == "or s"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); - CHECK(text_edit->get_selection_from_line() == 1); - CHECK(text_edit->get_selection_from_column() == 0); - CHECK(text_edit->get_selection_to_line() == 1); - CHECK(text_edit->get_selection_to_column() == 5); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 1); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->is_caret_after_selection_origin()); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Shift click above to switch selection direction. Uses original selection position. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "s some text\nf"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 1); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 6); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); + + // Clicking clears selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + // Cannot select when disabled, but caret still moves. text_edit->set_selecting_enabled(false); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); @@ -1175,89 +2115,166 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SUBCASE("[TextEdit] select and deselect") { text_edit->set_text("this is some text\nfor selection"); MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + // Select clamps input to full text. text_edit->select(-1, -1, 500, 500); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "this is some text\nfor selection"); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 13); + CHECK(text_edit->get_selection_from_line(0) == text_edit->get_selection_origin_line(0)); + CHECK(text_edit->get_selection_from_column(0) == text_edit->get_selection_origin_column(0)); + CHECK(text_edit->get_selection_to_line(0) == text_edit->get_caret_line(0)); + CHECK(text_edit->get_selection_to_column(0) == text_edit->get_caret_column(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); text_edit->deselect(); + MessageQueue::get_singleton()->flush(); CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); + // Select works in the other direction. text_edit->select(500, 500, -1, -1); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "this is some text\nfor selection"); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 13); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_selection_from_line(0) == text_edit->get_caret_line(0)); + CHECK(text_edit->get_selection_from_column(0) == text_edit->get_caret_column(0)); + CHECK(text_edit->get_selection_to_line(0) == text_edit->get_selection_origin_line(0)); + CHECK(text_edit->get_selection_to_column(0) == text_edit->get_selection_origin_column(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); text_edit->deselect(); + MessageQueue::get_singleton()->flush(); CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); + // Select part of a line. text_edit->select(0, 4, 0, 8); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == " is "); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 4); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 8); + CHECK(text_edit->get_selection_from_line(0) == text_edit->get_selection_origin_line(0)); + CHECK(text_edit->get_selection_from_column(0) == text_edit->get_selection_origin_column(0)); + CHECK(text_edit->get_selection_to_line(0) == text_edit->get_caret_line(0)); + CHECK(text_edit->get_selection_to_column(0) == text_edit->get_caret_column(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); text_edit->deselect(); + MessageQueue::get_singleton()->flush(); CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); + // Select part of a line in the other direction. text_edit->select(0, 8, 0, 4); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == " is "); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 8); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_from_line(0) == text_edit->get_caret_line(0)); + CHECK(text_edit->get_selection_from_column(0) == text_edit->get_caret_column(0)); + CHECK(text_edit->get_selection_to_line(0) == text_edit->get_selection_origin_line(0)); + CHECK(text_edit->get_selection_to_column(0) == text_edit->get_selection_origin_column(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + // Cannot select when disabled. text_edit->set_selecting_enabled(false); CHECK_FALSE(text_edit->has_selection()); text_edit->select(0, 8, 0, 4); + MessageQueue::get_singleton()->flush(); CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); text_edit->set_selecting_enabled(true); + } - text_edit->select(0, 8, 0, 4); - CHECK(text_edit->has_selection()); - SEND_GUI_ACTION("ui_text_caret_right"); + SUBCASE("[TextEdit] delete selection") { + text_edit->set_text("this is some text\nfor selection"); + MessageQueue::get_singleton()->flush(); + + // Delete selection does nothing if there is no selection. + text_edit->set_caret_line(0); + text_edit->set_caret_column(8); CHECK_FALSE(text_edit->has_selection()); text_edit->delete_selection(); CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); - text_edit->select(0, 8, 0, 4); + // Backspace removes selection. + text_edit->select(0, 4, 0, 8); CHECK(text_edit->has_selection()); SEND_GUI_ACTION("ui_text_backspace"); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 4); + // Undo restores previous selection. text_edit->undo(); - CHECK(text_edit->has_selection()); CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 4); + // Redo restores caret. text_edit->redo(); - CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 4); text_edit->undo(); - CHECK(text_edit->has_selection()); CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); - text_edit->select(0, 8, 0, 4); + text_edit->select(0, 4, 0, 8); CHECK(text_edit->has_selection()); + // Delete selection removes text, deselects, and moves caret. text_edit->delete_selection(); - CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + // Undo delete works. text_edit->undo(); - CHECK(text_edit->has_selection()); CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 4); + // Redo delete works. text_edit->redo(); - CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 4); @@ -1267,19 +2284,227 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); + // Can still delete if not editable. text_edit->set_editable(false); text_edit->delete_selection(); text_edit->set_editable(false); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + // Cannot undo since it was not editable. text_edit->undo(); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + + // Delete multiple adjacent selections on the same line. + text_edit->select(0, 0, 0, 5); + text_edit->add_caret(0, 8); + text_edit->select(0, 5, 0, 8, 1); + CHECK(text_edit->get_caret_count() == 2); + text_edit->delete_selection(); + CHECK(text_edit->get_text() == " text\nfor selection"); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + + // Delete mulitline selection. Ignore non selections. + text_edit->remove_secondary_carets(); + text_edit->select(1, 3, 0, 2); + text_edit->add_caret(1, 7); + text_edit->delete_selection(); + CHECK(text_edit->get_text() == " t selection"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 6); } - // Add readonly test? SUBCASE("[TextEdit] text drag") { + text_edit->set_size(Size2(200, 200)); + text_edit->set_text("drag test\ndrop here ''"); + text_edit->grab_click_focus(); + MessageQueue::get_singleton()->flush(); + + // Drag and drop selected text to mouse position. + text_edit->select(0, 0, 0, 4); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 11).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 11).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == " test\ndrop here 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 15); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 11); + + // Undo. + text_edit->undo(); + CHECK(text_edit->get_text() == "drag test\ndrop here ''"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Redo. + text_edit->redo(); + CHECK(text_edit->get_text() == " test\ndrop here 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 15); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 11); + + // Hold control when dropping to not delete selected text. + text_edit->select(1, 10, 1, 16); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 12).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "'drag'"); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_KEY_EVENT(Key::CMD_OR_CTRL); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + SEND_GUI_KEY_UP_EVENT(Key::CMD_OR_CTRL); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' test\ndrop here 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 6); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Multiple caret drags entire selection. + text_edit->select(0, 11, 0, 7, 0); + text_edit->add_caret(1, 2); + text_edit->select(1, 2, 1, 4, 1); + text_edit->add_caret(1, 12); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection(true, 1)); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 12).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "test\nop"); + // Carets aren't removed from dragging, only dropping. + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 7); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 11); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 12); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 7); + + // Drop onto same selection should do effectively nothing. + text_edit->select(1, 3, 1, 7); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "here"); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 3); + + // Cannot drag when drag and drop selection is disabled. It becomes regular drag to select. + text_edit->set_drag_and_drop_selection_enabled(false); + text_edit->select(0, 1, 0, 5); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK_FALSE(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 2); + text_edit->set_drag_and_drop_selection_enabled(true); + + // Cancel drag and drop from Escape key. + text_edit->select(0, 1, 0, 5); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + SEND_GUI_KEY_EVENT(Key::ESCAPE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 1); + + // Cancel drag and drop from caret move key input. + text_edit->select(0, 1, 0, 5); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + SEND_GUI_KEY_EVENT(Key::RIGHT); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + + // Cancel drag and drop from text key input. + text_edit->select(0, 1, 0, 5); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + SEND_GUI_KEY_EVENT(Key::A); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'A' \ndr heretest\nop 'drag'"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 2); + } + + SUBCASE("[TextEdit] text drag to another text edit") { TextEdit *target_text_edit = memnew(TextEdit); SceneTree::get_singleton()->get_root()->add_child(target_text_edit); @@ -1292,27 +2517,223 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_text("drag me"); text_edit->select_all(); text_edit->grab_click_focus(); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); MessageQueue::get_singleton()->flush(); - Point2i line_0 = text_edit->get_pos_at_line_column(0, 0); - line_0.y /= 2; - SEND_GUI_MOUSE_BUTTON_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Drag text between text edits. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->is_mouse_over_selection()); - SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 7).get_center(), MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->get_viewport()->gui_is_dragging()); CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag me"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); - line_0 = target_text_edit->get_pos_at_line_column(0, 0); - line_0.y /= 2; - line_0.x += 401; // As empty add one. - SEND_GUI_MOUSE_MOTION_EVENT(line_0, MouseButtonMask::LEFT, Key::NONE); + Point2i target_line0 = target_text_edit->get_position() + Point2i(1, target_text_edit->get_line_height() / 2); + SEND_GUI_MOUSE_MOTION_EVENT(target_line0, MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 0); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_line0, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == ""); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK(target_text_edit->get_text() == "drag me"); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 7); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 0); - SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + // Undo is separate per TextEdit. + text_edit->undo(); + CHECK(text_edit->get_text() == "drag me"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + CHECK(target_text_edit->get_text() == "drag me"); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 7); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 0); + + target_text_edit->undo(); + CHECK(text_edit->get_text() == "drag me"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + CHECK(target_text_edit->get_text() == ""); + CHECK_FALSE(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 0); + + // Redo is also separate. + text_edit->redo(); + CHECK(text_edit->get_text() == ""); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK(target_text_edit->get_text() == ""); + CHECK_FALSE(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + target_text_edit->redo(); CHECK(text_edit->get_text() == ""); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); CHECK(target_text_edit->get_text() == "drag me"); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 7); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 0); + + // Hold control to not remove selected text. + text_edit->set_text("drag test\ndrop test"); + MessageQueue::get_singleton()->flush(); + target_text_edit->select(0, 0, 0, 3, 0); + target_text_edit->add_caret(0, 5); + text_edit->select(0, 5, 0, 7, 0); + text_edit->add_caret(0, 1); + text_edit->select(0, 1, 0, 0, 1); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection(true, 0)); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 6).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "d\nte"); + CHECK(text_edit->has_selection()); + SEND_GUI_KEY_EVENT(Key::CMD_OR_CTRL); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + SEND_GUI_KEY_UP_EVENT(Key::CMD_OR_CTRL); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag test\ndrop test"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 7); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK(target_text_edit->get_text() == "drag md\ntee"); + CHECK(target_text_edit->get_caret_count() == 1); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 1); + CHECK(target_text_edit->get_caret_column() == 2); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 6); + + // Drop onto selected text deletes the selected text first. + text_edit->set_deselect_on_focus_loss_enabled(false); + target_text_edit->set_deselect_on_focus_loss_enabled(false); + text_edit->remove_secondary_carets(); + text_edit->select(0, 5, 0, 9); + target_text_edit->select(0, 6, 0, 8); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection(true, 0)); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "test"); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag \ndrop test"); + CHECK(target_text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + CHECK(target_text_edit->get_text() == "drag mdtest\ntee"); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 11); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 7); + text_edit->set_deselect_on_focus_loss_enabled(true); + target_text_edit->set_deselect_on_focus_loss_enabled(true); + + // Can drop even when drag and drop selection is disabled. + target_text_edit->set_drag_and_drop_selection_enabled(false); + text_edit->select(0, 4, 0, 5); + target_text_edit->deselect(); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == " "); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag\ndrop test"); + CHECK(target_text_edit->get_text() == "drag md test\ntee"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 8); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 7); + target_text_edit->set_drag_and_drop_selection_enabled(true); + + // Cannot drop when not editable. + target_text_edit->set_editable(false); + text_edit->select(0, 1, 0, 4); + target_text_edit->deselect(); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "rag"); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag\ndrop test"); + CHECK(target_text_edit->get_text() == "drag md test\ntee"); + CHECK(text_edit->has_selection()); + CHECK_FALSE(target_text_edit->has_selection()); + target_text_edit->set_editable(true); + + // Can drag when not editable, but text will not be removed. + text_edit->set_editable(false); + text_edit->select(0, 0, 0, 4); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag\ndrop test"); + CHECK(target_text_edit->get_text() == "dragdrag md test\ntee"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 8); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 4); + text_edit->set_editable(true); memdelete(target_text_edit); } @@ -1324,44 +2745,41 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] overridable actions") { + DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton()); + SIGNAL_WATCH(text_edit, "text_set"); SIGNAL_WATCH(text_edit, "text_changed"); SIGNAL_WATCH(text_edit, "lines_edited_from"); SIGNAL_WATCH(text_edit, "caret_changed"); - Array args1; - args1.push_back(0); - args1.push_back(0); - Array lines_edited_args; - lines_edited_args.push_back(args1); + Array lines_edited_args = build_array(build_array(0, 0)); SUBCASE("[TextEdit] backspace") { text_edit->set_text("this is\nsome\n"); text_edit->set_caret_line(0); text_edit->set_caret_column(0); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Cannot backspace at start of text. text_edit->backspace(); MessageQueue::get_singleton()->flush(); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Backspace at start of line removes the line. text_edit->set_caret_line(2); text_edit->set_caret_column(0); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 1)); - ((Array)lines_edited_args[0])[0] = 2; - ((Array)lines_edited_args[0])[1] = 1; text_edit->backspace(); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "this is\nsome"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 4); @@ -1369,10 +2787,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[0])[0] = 1; + // Backspace removes a character. + lines_edited_args = build_array(build_array(1, 1)); text_edit->backspace(); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "this is\nsom"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 3); @@ -1380,11 +2798,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Backspace when text is selected removes the selection. text_edit->end_complex_operation(); text_edit->select(1, 0, 1, 3); text_edit->backspace(); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "this is\n"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); @@ -1392,11 +2810,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Cannot backspace if not editable. text_edit->set_editable(false); text_edit->backspace(); text_edit->set_editable(true); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "this is\n"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); @@ -1404,6 +2822,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Undo restores text to the previous end of complex operation. text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "this is\nsom"); @@ -1412,98 +2831,736 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is\n"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // See ui_text_backspace for more backspace tests. } SUBCASE("[TextEdit] cut") { + // Cut without a selection removes the entire line. text_edit->set_text("this is\nsome\n"); text_edit->set_caret_line(0); text_edit->set_caret_column(6); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0)); - ERR_PRINT_OFF; text_edit->cut(); MessageQueue::get_singleton()->flush(); - ERR_PRINT_ON; // Can't check display server content. - - ((Array)lines_edited_args[0])[0] = 1; + CHECK(DS->clipboard_get() == "this is\n"); CHECK(text_edit->get_text() == "some\n"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_caret_column() == 3); // In the default font, this is the same position. SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[0])[0] = 0; - ((Array)lines_edited_args[0])[1] = 1; + // Undo restores the cut text. text_edit->undo(); MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n"); CHECK(text_edit->get_text() == "this is\nsome\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 6); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); - SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 0; + // Redo. text_edit->redo(); MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n"); CHECK(text_edit->get_text() == "some\n"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_caret_column() == 3); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Cut with a selection removes just the selection. text_edit->set_text("this is\nsome\n"); + text_edit->select(0, 5, 0, 7); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); - ((Array)lines_edited_args[0])[0] = 0; - text_edit->select(0, 5, 0, 7); - ERR_PRINT_OFF; SEND_GUI_ACTION("ui_cut"); CHECK(text_edit->get_viewport()->is_input_handled()); MessageQueue::get_singleton()->flush(); - ERR_PRINT_ON; // Can't check display server content. + CHECK(DS->clipboard_get() == "is"); CHECK(text_edit->get_text() == "this \nsome\n"); + CHECK_FALSE(text_edit->get_caret_line()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 5); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Cut does not change the text if not editable. Text is still added to clipboard. + text_edit->set_text("this is\nsome\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(5); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + text_edit->set_editable(false); text_edit->cut(); MessageQueue::get_singleton()->flush(); text_edit->set_editable(true); - CHECK(text_edit->get_text() == "this \nsome\n"); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(text_edit->get_text() == "this is\nsome\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 5); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Cut line with multiple carets. + text_edit->set_text("this is\nsome\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + text_edit->add_caret(0, 2); + text_edit->add_caret(0, 4); + text_edit->add_caret(2, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(1, 0)); + + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n\n"); + CHECK(text_edit->get_text() == "some"); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); // In the default font, this is the same position. + // The previous caret at index 1 was merged. + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 3); // In the default font, this is the same position. + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->remove_secondary_carets(); + + // Cut on the only line removes the contents. + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "some\n"); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_line_count() == 1); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Cut empty line. + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "\n"); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK_FALSE("caret_changed"); + // These signals are emitted even if there is no change. + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Cut multiple lines, in order. + text_edit->set_text("this is\nsome\ntext to\nbe\n\ncut"); + text_edit->set_caret_line(2); + text_edit->set_caret_column(7); + text_edit->add_caret(3, 0); + text_edit->add_caret(0, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(3, 2), build_array(2, 1)); + + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\ntext to\nbe\n"); + CHECK(text_edit->get_text() == "some\n\ncut"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 2); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->remove_secondary_carets(); + + // Cut multiple selections, in order. Ignores regular carets. + text_edit->set_text("this is\nsome\ntext to\nbe\n\ncut"); + text_edit->add_caret(3, 0); + text_edit->add_caret(0, 2); + text_edit->add_caret(2, 0); + text_edit->select(1, 0, 1, 2, 0); + text_edit->select(3, 0, 4, 0, 1); + text_edit->select(0, 5, 0, 3, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 1), build_array(4, 3), build_array(0, 0)); + + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "s \nso\nbe\n"); + CHECK(text_edit->get_text() == "thiis\nme\ntext to\n\ncut"); + CHECK(text_edit->get_caret_count() == 4); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 3); + CHECK(text_edit->get_caret_line(3) == 2); + CHECK(text_edit->get_caret_column(3) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); } SUBCASE("[TextEdit] copy") { - // TODO: Cannot test need display server support. + text_edit->set_text("this is\nsome\ntest\n\ntext"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // Copy selected text. + text_edit->select(0, 0, 1, 2, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set_primary(""); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\nso"); + CHECK(DS->clipboard_get_primary() == ""); + CHECK(text_edit->get_text() == "this is\nsome\ntest\n\ntext"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 2); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Copy with GUI action. + text_edit->select(0, 0, 0, 2, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION("ui_copy"); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "th"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Can copy even if not editable. + text_edit->select(2, 4, 1, 2, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + text_edit->copy(); + text_edit->set_editable(true); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "me\ntest"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->deselect(); + + // Copy full line when there is no selection. + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Copy empty line. + text_edit->set_caret_line(3); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "\n"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->deselect(); + + // Copy full line with multiple carets on that line only copies once. + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + text_edit->add_caret(1, 0); + text_edit->add_caret(1, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "some\n"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->remove_secondary_carets(); + + // Copy selected text from all selections with `\n` in between, in order. Ignore regular carets. + text_edit->set_caret_line(2); + text_edit->set_caret_column(4); + text_edit->add_caret(4, 0); + text_edit->add_caret(0, 4); + text_edit->add_caret(1, 0); + text_edit->select(1, 3, 2, 4, 0); + text_edit->select(4, 4, 4, 0, 1); + text_edit->select(0, 5, 0, 4, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == " \ne\ntest\ntext"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Copy multiple lines with multiple carets, in order. + text_edit->set_caret_line(3); + text_edit->set_caret_column(0); + text_edit->add_caret(4, 2); + text_edit->add_caret(0, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n\ntext\n"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] paste") { - // TODO: Cannot test need display server support. + // Paste text from clipboard at caret. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 1)); + DS->clipboard_set("paste"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste"); + CHECK(text_edit->get_text() == "this is\nsopasteme\n\ntext"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste"); + CHECK(text_edit->get_text() == "this is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 2); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste"); + CHECK(text_edit->get_text() == "this is\nsopasteme\n\ntext"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste on empty line. Use GUI action. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(2); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 2)); + DS->clipboard_set("paste2"); + + SEND_GUI_ACTION("ui_paste"); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste2"); + CHECK(text_edit->get_text() == "this is\nsome\npaste2\ntext"); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 6); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste removes selection before pasting. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->select(0, 5, 1, 3); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(0, 0)); + DS->clipboard_set("paste"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste"); + CHECK(text_edit->get_text() == "this pastee\n\ntext"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 10); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste multiple lines. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 3)); + DS->clipboard_set("multi\n\nline\npaste"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "multi\n\nline\npaste"); + CHECK(text_edit->get_text() == "tmulti\n\nline\npastehis is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste full line after copying it. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 2)); + DS->clipboard_set(""); + text_edit->copy(); + text_edit->set_caret_column(3); + CHECK(DS->clipboard_get() == "some\n"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "some\n"); + CHECK(text_edit->get_text() == "this is\nsome\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Do not paste as line since it wasn't copied. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1)); + DS->clipboard_set("paste\n"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste\n"); + CHECK(text_edit->get_text() == "thispaste\n is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste text at each caret. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + text_edit->add_caret(3, 4); + text_edit->add_caret(0, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(2, 3), build_array(5, 6)); + DS->clipboard_set("paste\ntest"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste\ntest"); + CHECK(text_edit->get_text() == "thispaste\ntest is\nsopaste\ntestme\n\ntextpaste\ntest"); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 3); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 6); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->remove_secondary_carets(); + + // Paste line per caret when the amount of lines is equal to the number of carets. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + text_edit->add_caret(3, 4); + text_edit->add_caret(0, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1), build_array(3, 3)); + DS->clipboard_set("paste\ntest\n1"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste\ntest\n1"); + CHECK(text_edit->get_text() == "thispaste is\nsotestme\n\ntext1"); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 6); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 9); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->remove_secondary_carets(); + + // Cannot paste when not editable. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set("no paste"); + + text_edit->set_editable(false); + text_edit->paste(); + text_edit->set_editable(true); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "no paste"); + CHECK(text_edit->get_text() == "this is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] paste primary") { - // TODO: Cannot test need display server support. + // Set size for mouse input. + text_edit->set_size(Size2(200, 200)); + + text_edit->grab_focus(); + DS->clipboard_set(""); + DS->clipboard_set_primary(""); + CHECK(DS->clipboard_get_primary() == ""); + + // Select text with mouse to put into primary clipboard. + text_edit->set_text("this is\nsome\n\ntext"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(DS->clipboard_get() == ""); + CHECK(DS->clipboard_get_primary() == "is is\nsom"); + CHECK(text_edit->get_text() == "this is\nsome\n\ntext"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "is is\nsom"); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 2); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK_FALSE("text_set"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Middle click to paste at mouse. + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 4)); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(3, 2).get_center() + Point2i(2, 0), MouseButton::MIDDLE, MouseButtonMask::MIDDLE, Key::NONE); + CHECK(DS->clipboard_get_primary() == "is is\nsom"); + CHECK(text_edit->get_text() == "this is\nsome\n\nteis is\nsomxt"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 4); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste at mouse position if there is only one caret. + text_edit->set_text("this is\nsome\n\ntext"); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set_primary("paste"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->paste_primary_clipboard(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get_primary() == "paste"); + CHECK(text_edit->get_text() == "tpastehis is\nsome\n\ntext"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 6); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste at all carets if there are multiple carets. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(0); + text_edit->add_caret(2, 0); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set_primary("paste"); + lines_edited_args = build_array(build_array(1, 1), build_array(2, 2)); + + text_edit->paste_primary_clipboard(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get_primary() == "paste"); + CHECK(text_edit->get_text() == "this is\npastesome\npaste\ntext"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Cannot paste if not editable. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set("no paste"); + + text_edit->set_editable(false); + text_edit->paste_primary_clipboard(); + text_edit->set_editable(true); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "no paste"); + CHECK(text_edit->get_text() == "this is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SIGNAL_UNWATCH(text_edit, "text_set"); @@ -1512,60 +3569,77 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_UNWATCH(text_edit, "caret_changed"); } - // Add undo / redo tests? SUBCASE("[TextEdit] input") { SIGNAL_WATCH(text_edit, "text_set"); SIGNAL_WATCH(text_edit, "text_changed"); SIGNAL_WATCH(text_edit, "lines_edited_from"); SIGNAL_WATCH(text_edit, "caret_changed"); - Array args1; - args1.push_back(0); - args1.push_back(0); - Array lines_edited_args; - lines_edited_args.push_back(args1); + Array lines_edited_args = build_array(build_array(0, 0)); SUBCASE("[TextEdit] ui_text_newline_above") { text_edit->set_text("this is some test text.\nthis is some test text."); - text_edit->select(0, 0, 0, 4); - text_edit->set_caret_column(4); - - text_edit->add_caret(1, 4); - text_edit->select(1, 0, 1, 4, 1); - CHECK(text_edit->get_caret_count() == 2); - MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(0); - args2.push_back(1); - lines_edited_args.push_front(args2); + // Insert new line above. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(1, 4); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(2, 3)); - ((Array)lines_edited_args[1])[1] = 1; SEND_GUI_ACTION("ui_text_newline_above"); CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text."); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_caret_line(1); text_edit->set_caret_column(4); - - text_edit->set_caret_line(3, false, true, 0, 1); + text_edit->set_caret_line(3, false, true, -1, 1); text_edit->set_caret_column(4, false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); @@ -1574,31 +3648,57 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 4); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 3); CHECK(text_edit->get_caret_column(1) == 4); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); - ((Array)lines_edited_args[0])[0] = 2; - ((Array)lines_edited_args[0])[1] = 3; + // Works on first line, empty lines, and only happens at caret for selections. + text_edit->select(1, 10, 0, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(4, 5)); SEND_GUI_ACTION("ui_text_newline_above"); CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_text() == "\n\nthis is some test text.\n\n\nthis is some test text."); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 4); CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Insert multiple new lines above from one line. + text_edit->set_text("test"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 3); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(1, 2)); + + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n\ntest"); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1606,34 +3706,55 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SUBCASE("[TextEdit] ui_text_newline_blank") { text_edit->set_text("this is some test text.\nthis is some test text."); - text_edit->select(0, 0, 0, 4); - text_edit->set_caret_column(4); - - text_edit->add_caret(1, 4); - text_edit->select(1, 0, 1, 4, 1); - CHECK(text_edit->get_caret_count() == 2); - MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); - SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(1); - args2.push_back(2); - lines_edited_args.push_front(args2); + // Insert new line below. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(1, 4); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(2, 3)); - ((Array)lines_edited_args[1])[1] = 1; SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text."); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(0)); CHECK(text_edit->get_caret_line(1) == 3); CHECK(text_edit->get_caret_column(1) == 0); CHECK_FALSE(text_edit->has_selection(1)); @@ -1641,75 +3762,119 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 3); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); + + // Insert multiple new lines below from one line. + text_edit->set_text("test"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 3); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(0, 1)); + + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "test\n\n"); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); } SUBCASE("[TextEdit] ui_text_newline") { text_edit->set_text("this is some test text.\nthis is some test text."); - text_edit->select(0, 0, 0, 4); - text_edit->set_caret_column(4); - - text_edit->add_caret(1, 4); - text_edit->select(1, 0, 1, 4, 1); - CHECK(text_edit->get_caret_count() == 2); - MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(1); - args2.push_back(1); - lines_edited_args.push_front(args2); - lines_edited_args.push_front(args2.duplicate()); - ((Array)lines_edited_args[1])[1] = 2; - - lines_edited_args.push_back(lines_edited_args[2].duplicate()); - ((Array)lines_edited_args[3])[1] = 1; + // Insert new line at caret. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(1, 4); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + // Lines edited: deletion, insert line, insert line. + lines_edited_args = build_array(build_array(0, 0), build_array(0, 1), build_array(2, 3)); SEND_GUI_ACTION("ui_text_newline"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "\n is some test text.\nthis\n is some test text."); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 3); CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text."); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\n is some test text.\nthis\n is some test text."); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_newline"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "\n is some test text.\nthis\n is some test text."); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 3); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -1717,255 +3882,399 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] ui_text_backspace_all_to_left") { - text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); - text_edit->select(1, 0, 1, 4); - text_edit->set_caret_line(1); - text_edit->set_caret_column(4); - - text_edit->add_caret(3, 4); - text_edit->select(3, 0, 3, 4, 1); - CHECK(text_edit->get_caret_count() == 2); - - MessageQueue::get_singleton()->flush(); - Ref<InputEvent> tmpevent = InputEventKey::create_reference(Key::BACKSPACE | KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL); InputMap::get_singleton()->action_add_event("ui_text_backspace_all_to_left", tmpevent); + text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(3); - args2.push_back(3); - lines_edited_args.push_front(args2); - - // With selection should be a normal backspace. - ((Array)lines_edited_args[1])[0] = 1; - ((Array)lines_edited_args[1])[1] = 1; + // Remove all text to the left. + text_edit->set_caret_line(1); + text_edit->set_caret_column(5); + text_edit->add_caret(1, 2); + text_edit->add_caret(1, 8); + lines_edited_args = build_array(build_array(1, 1)); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); SEND_GUI_ACTION("ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); + CHECK(text_edit->get_text() == "\nsome test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 8); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[0])[1] = 2; - ((Array)lines_edited_args[1])[1] = 0; + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\nsome test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Acts as a normal backspace with selections. + text_edit->select(1, 5, 1, 9, 0); + text_edit->add_caret(3, 4); + text_edit->select(3, 7, 3, 4, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 3), build_array(1, 1)); - // Start of line should also be a normal backspace. SEND_GUI_ACTION("ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n is some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "\nsome text.\n\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Acts as a normal backspace when at the start of a line. + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 2), build_array(1, 0)); + SEND_GUI_ACTION("ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "some text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_caret_column(text_edit->get_line(0).length()); text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n is some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); + CHECK(text_edit->get_text() == "some text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; - ((Array)lines_edited_args[1])[0] = 0; + // Remove entire line content when at the end of the line. + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0)); SEND_GUI_ACTION("ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_text() == "\n"); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->remove_secondary_carets(); + + // Removing newline effectively happens after removing text. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(0); + text_edit->add_caret(1, 4); + + SEND_GUI_ACTION("ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "tests"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + text_edit->remove_secondary_carets(); + + // Removing newline effectively happens after removing text, reverse caret order. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + text_edit->add_caret(1, 0); + + SEND_GUI_ACTION("ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "tests"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + text_edit->remove_secondary_carets(); InputMap::get_singleton()->action_erase_event("ui_text_backspace_all_to_left", tmpevent); } SUBCASE("[TextEdit] ui_text_backspace_word") { text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); - text_edit->select(1, 0, 1, 4); - text_edit->set_caret_line(1); - text_edit->set_caret_column(4); - - text_edit->add_caret(3, 4); - text_edit->select(3, 0, 3, 4, 1); - CHECK(text_edit->get_caret_count() == 2); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); - SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(3); - args2.push_back(3); - lines_edited_args.push_front(args2); - - // With selection should be a normal backspace. - ((Array)lines_edited_args[1])[0] = 1; - ((Array)lines_edited_args[1])[1] = 1; + // Acts as a normal backspace with selections. + text_edit->select(1, 8, 1, 15); + text_edit->add_caret(3, 6); + text_edit->select(3, 10, 3, 6, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 3), build_array(1, 1)); SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "\nthis is st text.\n\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 8); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 6); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); text_edit->end_complex_operation(); - ((Array)lines_edited_args[0])[1] = 2; - ((Array)lines_edited_args[1])[1] = 0; + lines_edited_args = build_array(build_array(3, 2), build_array(1, 0)); // Start of line should also be a normal backspace. + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n is some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n is some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); + // FIXME: Remove after GH-77101 is fixed. + text_edit->start_action(TextEdit::ACTION_NONE); + + // Remove text to the start of the word to the left of the caret. text_edit->set_caret_column(text_edit->get_line(0).length()); - text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + text_edit->set_caret_column(12, false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; - ((Array)lines_edited_args[1])[0] = 0; + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0)); SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test \n is some test "); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 14); + CHECK(text_edit->get_text() == "this is st \nthis ime t text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 11); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 9); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 16); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 14); + CHECK(text_edit->get_caret_column(1) == 12); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is st \nthis ime t text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 11); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 9); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - } - SUBCASE("[TextEdit] ui_text_backspace_word same line") { - text_edit->set_text("test test test"); - text_edit->set_caret_column(4); - text_edit->add_caret(0, 9); - text_edit->add_caret(0, 15); + // Removing newline effectively happens after removing text. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(0); + text_edit->add_caret(1, 4); - // For the second caret. - Array args2; - args2.push_back(0); - lines_edited_args.push_front(args2); + SEND_GUI_ACTION("ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "tests"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + text_edit->remove_secondary_carets(); - // For the third caret. - Array args3; - args2.push_back(0); - lines_edited_args.push_front(args2); + // Removing newline effectively happens after removing text, reverse caret order. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + text_edit->add_caret(1, 0); - CHECK(text_edit->get_caret_count() == 3); - MessageQueue::get_singleton()->flush(); + SEND_GUI_ACTION("ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "tests"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + text_edit->remove_secondary_carets(); + } + SUBCASE("[TextEdit] ui_text_backspace_word same line") { + text_edit->set_text("test longwordtest test"); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Multiple carets on the same line is handled. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 11); + text_edit->add_caret(0, 15); + text_edit->add_caret(0, 9); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); + SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " "); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == " st test"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 0); CHECK(text_edit->get_caret_column(1) == 1); - CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - CHECK(text_edit->get_caret_line(2) == 0); - CHECK(text_edit->get_caret_column(2) == 2); + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test longwordtest test"); + CHECK(text_edit->get_caret_count() == 4); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 11); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 15); + CHECK_FALSE(text_edit->has_selection(3)); + CHECK(text_edit->get_caret_line(3) == 0); + CHECK(text_edit->get_caret_column(3) == 9); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " st test"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 1); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1973,130 +4282,267 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SUBCASE("[TextEdit] ui_text_backspace") { text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); - text_edit->select(1, 0, 1, 4); - text_edit->set_caret_line(1); - text_edit->set_caret_column(4); - - text_edit->add_caret(3, 4); - text_edit->select(3, 0, 3, 4, 1); - CHECK(text_edit->get_caret_count() == 2); - MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); - SIGNAL_DISCARD("caret_changed"); - - // For the second caret. - Array args2; - args2.push_back(3); - args2.push_back(3); - lines_edited_args.push_front(args2); - // With selection should be a normal backspace. - ((Array)lines_edited_args[1])[0] = 1; - ((Array)lines_edited_args[1])[1] = 1; + // Remove selected text when there are selections. + text_edit->select(1, 0, 1, 4); + text_edit->add_caret(3, 4); + text_edit->select(3, 5, 3, 2, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 3), build_array(1, 1)); SEND_GUI_ACTION("ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "\n is some test text.\n\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 2); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Undo remove selection. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK(text_edit->get_selection_origin_line(1) == 3); + CHECK(text_edit->get_selection_origin_column(1) == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo remove selection. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\n is some test text.\n\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 2); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[0])[1] = 2; - ((Array)lines_edited_args[1])[1] = 0; + // Remove the newline when at start of line. + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 2), build_array(1, 0)); - // Start of line should also be a normal backspace. SEND_GUI_ACTION("ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n is some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo remove newline. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\n is some test text.\n\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo remove newline. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_caret_column(text_edit->get_line(0).length()); - text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + text_edit->set_caret_column(15, false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n is some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length()); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; - ((Array)lines_edited_args[1])[0] = 0; + // FIXME: Remove after GH-77101 is fixed. + text_edit->start_action(TextEdit::ACTION_NONE); + + // Backspace removes character to the left. + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0)); SEND_GUI_ACTION("ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text\n is some test text"); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 18); + CHECK(text_edit->get_text() == " is some test text\nthis some testtext."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 18); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 14); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Backspace another character without changing caret. + SEND_GUI_ACTION("ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test tex\nthis some testext."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 17); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 13); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Undo both backspaces. + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0), build_array(1, 1), build_array(0, 0)); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 19); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 18); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo both backspaces. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test tex\nthis some testext."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 17); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 13); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - // Select the entire text, from right to left - text_edit->select(0, 18, 0, 0); + // Backspace with multiple carets that will overlap. + text_edit->remove_secondary_carets(); text_edit->set_caret_line(0); - text_edit->set_caret_column(0); - - text_edit->select(1, 18, 1, 0, 1); - text_edit->set_caret_line(1, false, true, 0, 1); - text_edit->set_caret_column(0, false, 1); + text_edit->set_caret_column(8); + text_edit->add_caret(0, 7); + text_edit->add_caret(0, 9); MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0), build_array(0, 0)); - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); + SEND_GUI_ACTION("ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is sotest tex\nthis some testext."); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 6); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Select each line of text, from right to left. Remove selection to column 0. + text_edit->select(0, text_edit->get_line(0).length(), 0, 0); + text_edit->add_caret(1, 0); + text_edit->select(1, text_edit->get_line(1).length(), 1, 0, 1); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0)); SEND_GUI_ACTION("ui_text_backspace"); CHECK(text_edit->get_text() == "\n"); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Backspace at start of first line does nothing. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION("ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n"); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] ui_text_delete_all_to_right") { @@ -2104,101 +4550,138 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { InputMap::get_singleton()->action_add_event("ui_text_delete_all_to_right", tmpevent); text_edit->set_text("this is some test text.\nthis is some test text.\n"); - text_edit->select(0, 0, 0, 4); - text_edit->set_caret_line(0); - text_edit->set_caret_column(4); - - text_edit->add_caret(1, 4); - text_edit->select(1, 0, 1, 4, 1); - CHECK(text_edit->get_caret_count() == 2); - MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); - SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(1); - args2.push_back(1); - lines_edited_args.push_front(args2); + // Remove all text to right of caret. + text_edit->set_caret_line(0); + text_edit->set_caret_column(18); + text_edit->add_caret(0, 16); + text_edit->add_caret(0, 20); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); - // With selection should be a normal delete. SEND_GUI_ACTION("ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n"); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "this is some tes\nthis is some test text.\n"); + CHECK(text_edit->get_caret_count() == 1); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 16); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + // Undo. + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text.\n"); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 18); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 16); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 20); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - // End of line should not do anything. - text_edit->set_caret_column(text_edit->get_line(0).length()); - text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + // Redo. + text_edit->redo(); MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some tes\nthis is some test text.\n"); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 16); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); + // Acts as a normal delete with selections. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(1, 4); + text_edit->select(1, 8, 1, 4, 1); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); SEND_GUI_ACTION("ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n"); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); + CHECK(text_edit->get_text() == " is some tes\nthissome test text.\n"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does nothing when caret is at end of line. + text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION("ui_text_delete_all_to_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some tes\nthissome test text.\n"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Does not work if not editable. text_edit->set_caret_column(0); text_edit->set_caret_column(0, false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n"); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == " is some tes\nthissome test text.\n"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); + // Delete entire line. SEND_GUI_ACTION("ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_text() == "\n\n"); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -2210,302 +4693,589 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_caret_mid_grapheme_enabled(true); CHECK(text_edit->is_caret_mid_grapheme_enabled()); - text_edit->set_text("this ffi some test text.\n\nthis ffi some test text.\n"); - text_edit->select(0, 0, 0, 4); - text_edit->set_caret_line(0); - text_edit->set_caret_column(4); - - text_edit->add_caret(2, 4); - text_edit->select(2, 0, 2, 4, 1); - CHECK(text_edit->get_caret_count() == 2); - + text_edit->set_text("this is some test text.\n\nthis is some test text.\n"); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(2); - args2.push_back(2); - lines_edited_args.push_front(args2); + // Acts as a normal delete with selections. + text_edit->select(0, 8, 0, 15); + text_edit->add_caret(2, 6); + text_edit->select(2, 10, 2, 6, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(2, 2)); - // With selection should be a normal delete. SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text.\n\n ffi some test text.\n"); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "this is st text.\n\nthis ime test text.\n"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 8); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 6); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - // With selection should be a normal delete. - ((Array)lines_edited_args[0])[0] = 3; - ((Array)lines_edited_args[1])[0] = 1; + // Removes newlines when at end of line. text_edit->set_caret_column(text_edit->get_line(0).length()); text_edit->set_caret_column(text_edit->get_line(2).length(), false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(2, 1)); SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); - CHECK_FALSE(text_edit->has_selection()); - + CHECK(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); - CHECK_FALSE(text_edit->has_selection(0)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[1])[0] = 0; - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; + // Does not work if not editable. text_edit->set_caret_column(0); - text_edit->set_caret_column(0, false, 1); + text_edit->set_caret_column(10, false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); + // FIXME: Remove after GH-77101 is fixed. + text_edit->start_action(TextEdit::ACTION_NONE); + + // Delete to the end of the word right of the caret. + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); + SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " some test text.\n some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == " is st text.\nthis ime t text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_column(1) == 10); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is st text.\nthis ime t text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - } - SUBCASE("[TextEdit] ui_text_delete") { - text_edit->set_caret_mid_grapheme_enabled(true); - CHECK(text_edit->is_caret_mid_grapheme_enabled()); + // Delete one word with multiple carets. + text_edit->remove_secondary_carets(); + text_edit->set_text("onelongword test"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(6); + text_edit->add_caret(0, 9); + text_edit->add_caret(0, 3); + lines_edited_args = build_array(build_array(0, 0)); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); - text_edit->set_text("this ffi some test text.\nthis ffi some test text."); - text_edit->select(0, 0, 0, 4); + SEND_GUI_ACTION("ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "one test"); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 3); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Removing newline effectively happens after removing text. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + text_edit->add_caret(0, 4); + + SEND_GUI_ACTION("ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "telines"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); + text_edit->remove_secondary_carets(); + + // Removing newline effectively happens after removing text, reverse caret order. + text_edit->set_text("test\nlines"); text_edit->set_caret_line(0); text_edit->set_caret_column(4); + text_edit->add_caret(0, 2); - text_edit->add_caret(1, 4); - text_edit->select(1, 0, 1, 4, 1); - CHECK(text_edit->get_caret_count() == 2); + SEND_GUI_ACTION("ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "telines"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); + text_edit->remove_secondary_carets(); + } + SUBCASE("[TextEdit] ui_text_delete_word same line") { + text_edit->set_text("test longwordtest test"); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(1); - args2.push_back(1); - lines_edited_args.push_front(args2); + // Multiple carets on the same line is handled. + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->add_caret(0, 11); + text_edit->add_caret(0, 15); + text_edit->add_caret(0, 9); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); - // With selection should be a normal delete. - SEND_GUI_ACTION("ui_text_delete"); + SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == " long test"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 5); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - // With selection should be a normal delete. - lines_edited_args.remove_at(0); - ((Array)lines_edited_args[0])[0] = 1; - text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); - text_edit->set_caret_column(text_edit->get_line(0).length()); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); + + text_edit->undo(); MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test longwordtest test"); + CHECK(text_edit->get_caret_count() == 4); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 11); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 15); + CHECK_FALSE(text_edit->has_selection(3)); + CHECK(text_edit->get_caret_line(3) == 0); + CHECK(text_edit->get_caret_column(3) == 9); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " long test"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + } + + SUBCASE("[TextEdit] ui_text_delete") { + text_edit->set_caret_mid_grapheme_enabled(true); + CHECK(text_edit->is_caret_mid_grapheme_enabled()); + + text_edit->set_text("this is some test text.\n\nthis is some test text.\n"); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Remove selected text when there are selections. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(2, 2); + text_edit->select(2, 5, 2, 2, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(2, 2)); + SEND_GUI_ACTION("ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text. ffi some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 20); + CHECK(text_edit->get_text() == " is some test text.\n\nthis some test text.\n"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 2); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - // Caret should be removed due to column preservation. - CHECK(text_edit->get_caret_count() == 1); - - // Lets add it back. - text_edit->set_caret_column(0); - text_edit->add_caret(0, 20); + // Undo remove selection. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); - ((Array)lines_edited_args[0])[0] = 0; - lines_edited_args.push_back(args2); - ((Array)lines_edited_args[1])[0] = 0; - ((Array)lines_edited_args[1])[1] = 0; + // Redo remove selection. + text_edit->redo(); MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\n\nthis some test text.\n"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 2); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); + // Remove newline when at end of line. + text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(2).length(), false, 1); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(2, 1)); - text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text. ffi some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 19); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 20); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - CHECK(text_edit->get_caret_line(1) == 0); + // Undo remove newline. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\n\nthis some test text.\n"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 19); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 20); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo remove newline. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 19); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 20); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Does not work if not editable. + text_edit->set_caret_column(0); + text_edit->set_caret_column(15, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + SEND_GUI_ACTION("ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); + // FIXME: Remove after GH-77101 is fixed. text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + // Delete removes character to the right. + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); + SEND_GUI_ACTION("ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "ffi some test text.ffi some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == "is some test text.\nthis some test ext."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - CHECK(text_edit->get_caret_line(1) == 0); - CHECK(text_edit->get_caret_column(1) == 19); + // Delete another character without changing caret. + SEND_GUI_ACTION("ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "s some test text.\nthis some test xt."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - SIGNAL_CHECK("caret_changed", empty_signal_args); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + // Undo both deletes. + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1), build_array(0, 0), build_array(1, 1)); - SEND_GUI_ACTION("ui_text_delete"); - CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "fi some test text.fi some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); - CHECK(text_edit->get_caret_line(1) == 0); - CHECK(text_edit->get_caret_column(1) == 18); + // Redo both deletes. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "s some test text.\nthis some test xt."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); - SIGNAL_CHECK("caret_changed", empty_signal_args); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Delete at end of last line does nothing. + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(1); + text_edit->set_caret_column(18); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION("ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "s some test text.\nthis some test xt."); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 18); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] ui_text_caret_word_left") { text_edit->set_text("\nthis is some test text.\nthis is some test text."); text_edit->set_caret_line(1); - text_edit->set_caret_column(7); - - text_edit->add_caret(2, 7); - CHECK(text_edit->get_caret_count() == 2); + text_edit->set_caret_column(15); + text_edit->add_caret(2, 10); + text_edit->select(1, 10, 1, 15); + text_edit->select(2, 15, 2, 10, 1); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // Shift should select. + // Deselect to start of previous word when selection is right to left. + // Select to start of next word when selection is left to right. #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); #else SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); #endif CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 5); - CHECK(text_edit->get_selected_text(0) == "is"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "me "); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 13); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 10); + CHECK(text_edit->is_caret_after_selection_origin(0)); - CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 5); - CHECK(text_edit->get_selected_text(1) == "is"); CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "some te"); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 15); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Should still move caret with selection. - SEND_GUI_ACTION("ui_text_caret_word_left"); + // Select to start of word with shift. + text_edit->deselect(); + text_edit->set_caret_column(7); + text_edit->set_caret_column(16, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#endif CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "is"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 7); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "tes"); CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_column(1) == 13); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 16); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Normal word left. + // Deselect and move caret to start of next word without shift. SEND_GUI_ACTION("ui_text_caret_word_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 8); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + // Moves to end of previous line when at start of line. Does nothing at start of text. + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION("ui_text_caret_word_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 23); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2515,249 +5285,417 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_text("\nthis is some test text.\nthis is some test text."); text_edit->set_caret_line(1); text_edit->set_caret_column(7); - text_edit->select(1, 2, 1, 7); - - text_edit->add_caret(2, 7); - text_edit->select(2, 2, 2, 7, 1); - CHECK(text_edit->get_caret_count() == 2); - + text_edit->select(1, 3, 1, 7); + text_edit->add_caret(2, 3); + text_edit->select(2, 7, 2, 3, 1); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // Normal left should deselect and place at selection start. - SEND_GUI_ACTION("ui_text_caret_left"); + // Remove one character from selection when selection is left to right. + // Add one character to selection when selection is right to left. + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "s i"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 6); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 3); + CHECK(text_edit->is_caret_after_selection_origin(0)); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 2); - CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "is is"); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 2); - CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 7); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // With shift should select. - SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT); + // Deselect and put caret at selection start without shift. + SEND_GUI_ACTION("ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 1); - CHECK(text_edit->get_selected_text(0) == "h"); - CHECK(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 3); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 1); - CHECK(text_edit->get_selected_text(1) == "h"); - CHECK(text_edit->has_selection(1)); - + CHECK(text_edit->get_caret_column(1) == 2); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // All ready at select left, should only deselect. + // Move caret one character to the left. SEND_GUI_ACTION("ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 2); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 1); - CHECK_FALSE(text_edit->has_selection(1)); - - SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Normal left. - SEND_GUI_ACTION("ui_text_caret_left"); + // Select one character to the left with shift and no existing selection. + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "h"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 1); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 2); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "t"); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 1); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Left at col 0 should go up a line. + // Moves to end of previous line when at start of line. Does nothing at start of text. + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + SEND_GUI_ACTION("ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 23); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - } - SUBCASE("[TextEdit] ui_text_caret_word_right") { - text_edit->set_text("this is some test text\n\nthis is some test text\n"); - text_edit->set_caret_line(0); - text_edit->set_caret_column(13); + // Selects to end of previous line when at start of line. + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(1); + text_edit->set_caret_column(0); + text_edit->select(1, 1, 1, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); - text_edit->add_caret(2, 13); - CHECK(text_edit->get_caret_count() == 2); + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "\nt"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + // Merge selections when they overlap. + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + text_edit->select(1, 6, 1, 4); + text_edit->add_caret(1, 8); + text_edit->select(1, 8, 1, 6, 1); MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + CHECK(text_edit->get_caret_count() == 2); + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "s is "); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 3); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 8); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_word_right") { + text_edit->set_text("this is some test text\n\nthis is some test text"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(15); + text_edit->add_caret(2, 10); + text_edit->select(0, 10, 0, 15); + text_edit->select(2, 15, 2, 10, 1); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // Shift should select. + // Select to end of next word when selection is right to left. + // Deselect to end of previous word when selection is left to right. #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); #else SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); #endif CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 17); - CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->get_caret_count() == 2); CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "me test"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 17); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 10); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == " te"); CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 17); - CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->get_caret_column(1) == 12); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 15); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Select to end of word with shift. + text_edit->deselect(); + text_edit->set_caret_column(13); + text_edit->set_caret_column(15, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#endif + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 17); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 13); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "st"); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 17); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 15); + CHECK(text_edit->is_caret_after_selection_origin(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Should still move caret with selection. + // Deselect and move caret to end of next word without shift. SEND_GUI_ACTION("ui_text_caret_word_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 22); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 22); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 22); - CHECK_FALSE(text_edit->has_selection(1)); - SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Normal word right. + // Moves to start of next line when at end of line. Does nothing at end of text. SEND_GUI_ACTION("ui_text_caret_word_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 22); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] ui_text_caret_right") { - text_edit->set_text("this is some test text\n\nthis is some test text\n"); + text_edit->set_text("this is some test text\n\nthis is some test text"); text_edit->set_caret_line(0); - text_edit->set_caret_column(16); - text_edit->select(0, 16, 0, 20); - - text_edit->add_caret(2, 16); - text_edit->select(2, 16, 2, 20, 1); - CHECK(text_edit->get_caret_count() == 2); - + text_edit->set_caret_column(19); + text_edit->select(0, 15, 0, 19); + text_edit->add_caret(2, 15); + text_edit->select(2, 19, 2, 15, 1); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // Normal right should deselect and place at selection start. - SEND_GUI_ACTION("ui_text_caret_right"); + // Remove one character from selection when selection is right to left. + // Add one character to selection when selection is left to right. + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 20); - CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "st te"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 20); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 15); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "t t"); CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 20); - CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_column(1) == 16); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 19); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // With shift should select. - SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT); + // Deselect and put caret at selection end without shift. + SEND_GUI_ACTION("ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 21); - CHECK(text_edit->get_selected_text(0) == "x"); - CHECK(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 20); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 21); - CHECK(text_edit->get_selected_text(1) == "x"); - CHECK(text_edit->has_selection(1)); - + CHECK(text_edit->get_caret_column(1) == 19); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // All ready at select right, should only deselect. + // Move caret one character to the right. SEND_GUI_ACTION("ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 21); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 21); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 20); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + // Select one character to the right with shift and no existing selection. + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "t"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 22); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 21); + CHECK(text_edit->is_caret_after_selection_origin(0)); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "x"); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 21); - CHECK_FALSE(text_edit->has_selection(1)); - SIGNAL_CHECK_FALSE("caret_changed"); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 20); + CHECK(text_edit->is_caret_after_selection_origin(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Normal right. + // Moves to start of next line when at end of line. Does nothing at end of text. + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(22); + text_edit->set_caret_column(22, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + SEND_GUI_ACTION("ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 22); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 22); - CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Right at end col should go down a line. - SEND_GUI_ACTION("ui_text_caret_right"); + // Selects to start of next line when at end of line. + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(22); + text_edit->select(0, 21, 0, 22); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "t\n"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 21); + CHECK(text_edit->is_caret_after_selection_origin(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); - CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); + // Merge selections when they overlap. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->select(0, 4, 0, 6); + text_edit->add_caret(0, 8); + text_edit->select(0, 6, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + CHECK(text_edit->get_caret_count() == 2); + + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == " is s"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 4); + CHECK(text_edit->is_caret_after_selection_origin(0)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2775,7 +5713,6 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_count() == 2); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->is_line_wrapped(0)); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); @@ -3156,11 +6093,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(1); - args2.push_back(1); - lines_edited_args.push_front(args2); + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); SEND_GUI_KEY_EVENT(Key::A); CHECK(text_edit->get_viewport()->is_input_handled()); @@ -3171,6 +6104,27 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Undo reverts both carets. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "a\na"); + CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_caret_column(1) == 1); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "aA\naA"); + CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_caret_column(1) == 2); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Does not work if not editable. text_edit->set_editable(false); SEND_GUI_KEY_EVENT(Key::A); CHECK_FALSE(text_edit->get_viewport()->is_input_handled()); // Should this be handled? @@ -3182,8 +6136,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); - lines_edited_args.push_back(lines_edited_args[1].duplicate()); - lines_edited_args.push_front(args2.duplicate()); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0), build_array(1, 1), build_array(1, 1)); text_edit->select(0, 0, 0, 1); text_edit->select(1, 0, 1, 1, 1); @@ -3220,8 +6173,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_overtype_mode_enabled(false); CHECK_FALSE(text_edit->is_overtype_mode_enabled()); - lines_edited_args.remove_at(0); - lines_edited_args.remove_at(1); + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); SEND_GUI_KEY_EVENT(Key::TAB); CHECK(text_edit->get_viewport()->is_input_handled()); @@ -3576,6 +6528,11 @@ TEST_CASE("[SceneTree][TextEdit] caret") { text_edit->set_caret_column(4); CHECK(text_edit->get_word_under_caret() == "Lorem"); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 15); + CHECK(text_edit->get_word_under_caret() == "Lorem\ndolor"); + text_edit->remove_secondary_carets(); + // Should this work? text_edit->set_caret_column(5); CHECK(text_edit->get_word_under_caret() == ""); @@ -3616,18 +6573,20 @@ TEST_CASE("[SceneTree][TextEdit] multicaret") { SIGNAL_DISCARD("caret_changed"); SUBCASE("[TextEdit] add remove caret") { - // Overlapping + // Overlapping. CHECK(text_edit->add_caret(0, 0) == -1); MessageQueue::get_singleton()->flush(); SIGNAL_CHECK_FALSE("caret_changed"); - // Selection - text_edit->select(0, 0, 2, 4); + // Select. + text_edit->select(2, 4, 0, 0); + + // Cannot add in selection. CHECK(text_edit->add_caret(0, 0) == -1); CHECK(text_edit->add_caret(2, 4) == -1); CHECK(text_edit->add_caret(1, 2) == -1); - // Out of bounds + // Cannot add when out of bounds. CHECK(text_edit->add_caret(-1, 0) == -1); CHECK(text_edit->add_caret(5, 0) == -1); CHECK(text_edit->add_caret(0, 100) == -1); @@ -3670,23 +6629,276 @@ TEST_CASE("[SceneTree][TextEdit] multicaret") { ERR_PRINT_ON; } - SUBCASE("[TextEdit] caret index edit order") { - Vector<int> caret_index_get_order; - caret_index_get_order.push_back(1); - caret_index_get_order.push_back(0); + SUBCASE("[TextEdit] sort carets") { + Vector<int> sorted_carets = { 0, 1, 2 }; - CHECK(text_edit->add_caret(1, 0)); - CHECK(text_edit->get_caret_count() == 2); - CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order); + // Ascending order. + text_edit->remove_secondary_carets(); + text_edit->add_caret(0, 1); + text_edit->add_caret(1, 0); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + // Descending order. + sorted_carets = { 2, 1, 0 }; text_edit->remove_secondary_carets(); text_edit->set_caret_line(1); - CHECK(text_edit->add_caret(0, 0)); + text_edit->add_caret(0, 1); + text_edit->add_caret(0, 0); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + + // Mixed order. + sorted_carets = { 0, 2, 1, 3 }; + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(0); + text_edit->add_caret(1, 0); + text_edit->add_caret(0, 1); + text_edit->add_caret(1, 1); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + + // Overlapping carets. + sorted_carets = { 0, 1, 3, 2 }; + text_edit->remove_secondary_carets(); + text_edit->add_caret(0, 1); + text_edit->add_caret(1, 2); + text_edit->add_caret(0, 2); + text_edit->set_caret_column(1, false, 3); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + + // Sorted by selection start. + sorted_carets = { 1, 0 }; + text_edit->remove_secondary_carets(); + text_edit->select(1, 3, 1, 5); + text_edit->add_caret(2, 0); + text_edit->select(1, 0, 2, 0, 1); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + } + + SUBCASE("[TextEdit] merge carets") { + text_edit->set_text("this is some text\nfor selection"); + MessageQueue::get_singleton()->flush(); + + // Don't merge carets that are not overlapping. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 6); + text_edit->add_caret(1, 6); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 6); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 6); + text_edit->remove_secondary_carets(); + + // Don't merge when in a multicaret edit. + text_edit->begin_multicaret_edit(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 4); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->is_in_mulitcaret_edit()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 4); + + // Merge overlapping carets. Merge at the end of the multicaret edit. + text_edit->end_multicaret_edit(); + CHECK_FALSE(text_edit->is_in_mulitcaret_edit()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + + // Don't merge selections that are not overlapping. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 2); + text_edit->add_caret(1, 4); + text_edit->select(0, 4, 1, 2, 0); + text_edit->select(0, 2, 0, 3, 1); + text_edit->select(1, 4, 1, 8, 2); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->has_selection(2)); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Don't merge selections that are only touching. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(1, 2); + text_edit->select(0, 4, 1, 2, 0); + text_edit->select(1, 2, 1, 5, 1); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->has_selection(1)); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Merge carets into selection. + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + text_edit->add_caret(0, 2); + text_edit->add_caret(1, 4); + text_edit->add_caret(1, 8); + text_edit->add_caret(1, 10); + text_edit->select(0, 2, 1, 8, 0); + text_edit->merge_overlapping_carets(); CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_from_line(0) == 0); + CHECK(text_edit->get_selection_from_column(0) == 2); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 8); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); + text_edit->remove_secondary_carets(); + text_edit->deselect(); - caret_index_get_order.write[0] = 0; - caret_index_get_order.write[1] = 1; - CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order); + // Merge partially overlapping selections. + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 2); + text_edit->add_caret(0, 3); + text_edit->select(0, 2, 0, 6, 0); + text_edit->select(0, 4, 1, 3, 1); + text_edit->select(1, 0, 1, 5, 2); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_from_line(0) == 0); + CHECK(text_edit->get_selection_from_column(0) == 2); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 5); + CHECK(text_edit->is_caret_after_selection_origin(0)); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Merge smaller overlapping selection into a bigger one. + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 2); + text_edit->add_caret(0, 3); + text_edit->select(0, 2, 0, 6, 0); + text_edit->select(0, 8, 1, 3, 1); + text_edit->select(0, 2, 1, 5, 2); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_from_line(0) == 0); + CHECK(text_edit->get_selection_from_column(0) == 2); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 5); + CHECK(text_edit->is_caret_after_selection_origin(0)); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Merge equal overlapping selections. + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 2); + text_edit->select(0, 2, 1, 6, 0); + text_edit->select(0, 2, 1, 6, 1); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_from_line(0) == 0); + CHECK(text_edit->get_selection_from_column(0) == 2); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 6); + CHECK(text_edit->is_caret_after_selection_origin(0)); + } + + SUBCASE("[TextEdit] collapse carets") { + text_edit->set_text("this is some text\nfor selection"); + + // Collapse carets in range, dont affect other carets. + text_edit->add_caret(0, 9); + text_edit->add_caret(1, 0); + text_edit->add_caret(1, 2); + text_edit->add_caret(1, 6); + text_edit->begin_multicaret_edit(); + + text_edit->collapse_carets(0, 8, 1, 2); + CHECK(text_edit->get_caret_count() == 5); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 8); + CHECK(text_edit->get_caret_line(3) == 1); + CHECK(text_edit->get_caret_column(3) == 2); + CHECK(text_edit->get_caret_line(4) == 1); + CHECK(text_edit->get_caret_column(4) == 6); + CHECK_FALSE(text_edit->multicaret_edit_ignore_caret(0)); + CHECK(text_edit->multicaret_edit_ignore_caret(1)); + CHECK(text_edit->multicaret_edit_ignore_caret(2)); + CHECK_FALSE(text_edit->multicaret_edit_ignore_caret(3)); + CHECK_FALSE(text_edit->multicaret_edit_ignore_caret(4)); + + // Collapsed carets get merged at the end of the edit. + text_edit->end_multicaret_edit(); + CHECK(text_edit->get_caret_count() == 4); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 2); + CHECK(text_edit->get_caret_line(3) == 1); + CHECK(text_edit->get_caret_column(3) == 6); + text_edit->remove_secondary_carets(); + + // Collapse inclusive. + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + text_edit->add_caret(1, 2); + text_edit->collapse_carets(0, 3, 1, 2, true); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + text_edit->remove_secondary_carets(); + + // Deselect if selection was encompassed. + text_edit->select(0, 5, 0, 7); + text_edit->collapse_carets(0, 3, 1, 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + + // Clamp only caret end of selection. + text_edit->select(0, 1, 0, 7); + text_edit->collapse_carets(0, 3, 1, 2); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 1); + text_edit->deselect(); + + // Clamp only selection origin end of selection. + text_edit->select(0, 7, 0, 1); + text_edit->collapse_carets(0, 3, 1, 2); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 3); + text_edit->deselect(); } SUBCASE("[TextEdit] add caret at carets") { @@ -3694,36 +6906,320 @@ TEST_CASE("[SceneTree][TextEdit] multicaret") { text_edit->set_caret_line(1); text_edit->set_caret_column(9); + // Add caret below. Column will clamp. text_edit->add_caret_at_carets(true); CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 9); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 4); + // Cannot add below when at last line. text_edit->add_caret_at_carets(true); CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + // Add caret above. Column will clamp. text_edit->add_caret_at_carets(false); CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); CHECK(text_edit->get_caret_line(2) == 0); CHECK(text_edit->get_caret_column(2) == 7); + // Cannot add above when at first line. + text_edit->add_caret_at_carets(false); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 7); + + // Cannot add below when at the last line for selection. + text_edit->remove_secondary_carets(); + text_edit->select(2, 1, 2, 4); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 2); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 4); + + // Cannot add above when at the first line for selection. + text_edit->select(0, 1, 0, 4); + text_edit->add_caret_at_carets(false); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + + // Add selection below. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 0); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 3); // In the default font, this is the same position. + + // Add selection below again. + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 0); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 3); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selection_origin_line(2) == 2); + CHECK(text_edit->get_selection_origin_column(2) == 0); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 4); + + text_edit->set_text("\tthis is\nsome\n\ttest text"); + MessageQueue::get_singleton()->flush(); + + // Last fit x is preserved when adding below. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(6); + text_edit->add_caret_at_carets(true); + text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 6); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 6); + + // Last fit x is preserved when adding above. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(2); + text_edit->set_caret_column(9); + text_edit->add_caret_at_carets(false); + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 8); + + // Last fit x is preserved when selection adding below. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->select(0, 8, 0, 5); + text_edit->add_caret_at_carets(true); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 8); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selection_origin_line(2) == 2); + CHECK(text_edit->get_selection_origin_column(2) == 7); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 5); + + // Last fit x is preserved when selection adding above. text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->select(2, 9, 2, 5); + text_edit->add_caret_at_carets(false); + text_edit->add_caret_at_carets(false); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 2); + CHECK(text_edit->get_selection_origin_column(0) == 9); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selection_origin_line(2) == 0); + CHECK(text_edit->get_selection_origin_column(2) == 8); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 5); + + // Selections are merged when they overlap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->select(0, 1, 0, 5); + text_edit->add_caret(1, 0); + text_edit->select(1, 1, 1, 3, 1); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 1); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selection_origin_line(2) == 2); + CHECK(text_edit->get_selection_origin_column(2) == 0); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 3); + + // Multiline selection. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->select(0, 3, 1, 1); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 3); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 1); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 3); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); + + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + text_edit->set_size(Size2(50, 100)); + // Line wraps: `\t,this, is\nso,me\n\t,test, ,text`. + CHECK(text_edit->is_line_wrapped(0)); + MessageQueue::get_singleton()->flush(); + + // Add caret below on next line wrap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); text_edit->set_caret_line(0); text_edit->set_caret_column(4); - text_edit->select(0, 0, 0, 4); text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_count() == 2); - CHECK(text_edit->get_selection_from_line(1) == 1); - CHECK(text_edit->get_selection_to_line(1) == 1); - CHECK(text_edit->get_selection_from_column(1) == 0); - CHECK(text_edit->get_selection_to_column(1) == 3); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 8); + // Add caret below from end of line wrap. text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 1); + + // Add caret below from last line and not last line wrap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(2); + text_edit->set_caret_column(5); + text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 10); + + // Cannot add caret below from last line last line wrap. + text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 10); + + // Add caret above from not first line wrap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 1); + + // Add caret above from first line wrap. + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_count() == 3); - CHECK(text_edit->get_selection_from_line(2) == 2); - CHECK(text_edit->get_selection_to_line(2) == 2); - CHECK(text_edit->get_selection_from_column(2) == 0); - CHECK(text_edit->get_selection_to_column(2) == 4); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 8); + + // Add caret above from first line and not first line wrap. + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 4); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 8); + CHECK(text_edit->get_caret_line(3) == 0); + CHECK(text_edit->get_caret_column(3) == 4); + + // Cannot add caret above from first line first line wrap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); } memdelete(text_edit); @@ -3992,7 +7488,7 @@ TEST_CASE("[SceneTree][TextEdit] viewport") { CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); - // Wrap + // Wrap. text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_total_visible_line_count() > total_visible_lines); @@ -4242,7 +7738,7 @@ TEST_CASE("[SceneTree][TextEdit] viewport") { CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); CHECK(text_edit->get_caret_wrap_index() == 0); - // Typing and undo / redo should adjust viewport + // Typing and undo / redo should adjust viewport. text_edit->set_caret_line(0); text_edit->set_caret_column(0); text_edit->set_line_as_first_visible(5); diff --git a/tests/test_macros.h b/tests/test_macros.h index a173b37a2d..927884dced 100644 --- a/tests/test_macros.h +++ b/tests/test_macros.h @@ -136,6 +136,7 @@ int register_test_command(String p_command, TestFunc p_function); // Requires Message Queue and InputMap to be setup. // SEND_GUI_ACTION - takes an input map key. e.g SEND_GUI_ACTION("ui_text_newline"). // SEND_GUI_KEY_EVENT - takes a keycode set. e.g SEND_GUI_KEY_EVENT(Key::A | KeyModifierMask::META). +// SEND_GUI_KEY_UP_EVENT - takes a keycode set. e.g SEND_GUI_KEY_UP_EVENT(Key::A | KeyModifierMask::META). // SEND_GUI_MOUSE_BUTTON_EVENT - takes a position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_EVENT(Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None); // SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT - takes a position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None); // SEND_GUI_MOUSE_MOTION_EVENT - takes a position, mouse mask and modifiers e.g SEND_GUI_MOUSE_MOTION_EVENT(Vector2(50, 50), MouseButtonMask::LEFT, KeyModifierMask::META); @@ -161,6 +162,14 @@ int register_test_command(String p_command, TestFunc p_function); MessageQueue::get_singleton()->flush(); \ } +#define SEND_GUI_KEY_UP_EVENT(m_input) \ + { \ + Ref<InputEventKey> event = InputEventKey::create_reference(m_input); \ + event->set_pressed(false); \ + _SEND_DISPLAYSERVER_EVENT(event); \ + MessageQueue::get_singleton()->flush(); \ + } + #define _UPDATE_EVENT_MODIFERS(m_event, m_modifers) \ m_event->set_shift_pressed(((m_modifers) & KeyModifierMask::SHIFT) != Key::NONE); \ m_event->set_alt_pressed(((m_modifers) & KeyModifierMask::ALT) != Key::NONE); \ diff --git a/thirdparty/README.md b/thirdparty/README.md index 4c44a9b6f6..4a7ab7314a 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -318,7 +318,7 @@ Files extracted from upstream source: ## glad - Upstream: https://github.com/Dav1dde/glad -- Version: 2.0.6 (658f48e72aee3c6582e80b05ac0f8787a64fe6bb, 2024) +- Version: 2.0.4 (d08b1aa01f8fe57498f04d47b5fa8c48725be877, 2023) - License: CC0 1.0 and Apache 2.0 Files extracted from upstream source: @@ -334,11 +334,11 @@ Files generated from [upstream web instance](https://gen.glad.sh/): - `glx.c` - `glad/glx.h` -See the permalinks in `glad/egl.h`, `glad/gl.h`, and `glad/glx.h` to regenrate -the files with a new version of the web instance. +See the permalinks in `glad/gl.h` and `glad/glx.h` to regenrate the files with +a new version of the web instance. -Some changes have been made in order to allow loading OpenGL and OpenGLES APIs -at the same time. See the patches in the `patches` directory. +Some changes have been made in order to allow loading OpenGL and OpenGLES APIs at the same time. +See the patches in the `patches` directory. ## glslang @@ -428,6 +428,18 @@ Files extracted from upstream source: - `jpge*.{c,h}` +## libbacktrace + +- Upstream: https://github.com/ianlancetaylor/libbacktrace +- Version: git (4d2dd0b172f2c9192f83ba93425f868f2a13c553, 2022) +- License: BSD-3-Clause + +Files extracted from upstream source: + +- `*.{c,h}` files for Windows platform +- `LICENSE` + + ## libktx - Upstream: https://github.com/KhronosGroup/KTX-Software @@ -667,6 +679,11 @@ Collection of single-file libraries used in Godot components. * Version: git (7bdffb428b2b19ad1c43aa44c714dcc104177e84, 2021) * Modifications: Change from STL to Godot types (see provided patch). * License: MIT +- `qoa.h` + * Upstream: https://github.com/phoboslab/qoa + * Version: git (e4c751d61af2c395ea828c5888e728c1953bf09f, 2024) + * Modifications: Inlined functions and patched compiler warnings. + * License: MIT - `r128.{c,h}` * Upstream: https://github.com/fahickman/r128 * Version: git (6fc177671c47640d5bb69af10cf4ee91050015a1, 2023) diff --git a/thirdparty/clipper2/include/clipper2/clipper.core.h b/thirdparty/clipper2/include/clipper2/clipper.core.h index a77cdad5f4..0de7c3720e 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.core.h +++ b/thirdparty/clipper2/include/clipper2/clipper.core.h @@ -138,7 +138,7 @@ namespace Clipper2Lib } template <typename T2> - explicit Point<T>(const Point<T2>& p) + explicit Point(const Point<T2>& p) { Init(p.x, p.y, p.z); } @@ -180,7 +180,7 @@ namespace Clipper2Lib Point(const T2 x_, const T2 y_) { Init(x_, y_); } template <typename T2> - explicit Point<T>(const Point<T2>& p) { Init(p.x, p.y); } + explicit Point(const Point<T2>& p) { Init(p.x, p.y); } Point operator * (const double scale) const { diff --git a/thirdparty/clipper2/patches/gcc14-warning.patch b/thirdparty/clipper2/patches/gcc14-warning.patch new file mode 100644 index 0000000000..a4f06ef37e --- /dev/null +++ b/thirdparty/clipper2/patches/gcc14-warning.patch @@ -0,0 +1,22 @@ +diff --git a/thirdparty/clipper2/include/clipper2/clipper.core.h b/thirdparty/clipper2/include/clipper2/clipper.core.h +index a77cdad5f4..0de7c3720e 100644 +--- a/thirdparty/clipper2/include/clipper2/clipper.core.h ++++ b/thirdparty/clipper2/include/clipper2/clipper.core.h +@@ -138,7 +138,7 @@ namespace Clipper2Lib + } + + template <typename T2> +- explicit Point<T>(const Point<T2>& p) ++ explicit Point(const Point<T2>& p) + { + Init(p.x, p.y, p.z); + } +@@ -180,7 +180,7 @@ namespace Clipper2Lib + Point(const T2 x_, const T2 y_) { Init(x_, y_); } + + template <typename T2> +- explicit Point<T>(const Point<T2>& p) { Init(p.x, p.y); } ++ explicit Point(const Point<T2>& p) { Init(p.x, p.y); } + + Point operator * (const double scale) const + { diff --git a/thirdparty/glad/EGL/eglplatform.h b/thirdparty/glad/EGL/eglplatform.h index 6786afd90b..99362a23de 100644 --- a/thirdparty/glad/EGL/eglplatform.h +++ b/thirdparty/glad/EGL/eglplatform.h @@ -64,12 +64,6 @@ typedef HDC EGLNativeDisplayType; typedef HBITMAP EGLNativePixmapType; typedef HWND EGLNativeWindowType; -#elif defined(__QNX__) - -typedef khronos_uintptr_t EGLNativeDisplayType; -typedef struct _screen_pixmap* EGLNativePixmapType; /* screen_pixmap_t */ -typedef struct _screen_window* EGLNativeWindowType; /* screen_window_t */ - #elif defined(__EMSCRIPTEN__) typedef int EGLNativeDisplayType; diff --git a/thirdparty/glad/gl.c b/thirdparty/glad/gl.c index 38ecb514bd..ee0cc188fc 100644 --- a/thirdparty/glad/gl.c +++ b/thirdparty/glad/gl.c @@ -453,7 +453,6 @@ PFNGLMULTITEXCOORDP3UIPROC glad_glMultiTexCoordP3ui = NULL; PFNGLMULTITEXCOORDP3UIVPROC glad_glMultiTexCoordP3uiv = NULL; PFNGLMULTITEXCOORDP4UIPROC glad_glMultiTexCoordP4ui = NULL; PFNGLMULTITEXCOORDP4UIVPROC glad_glMultiTexCoordP4uiv = NULL; -PFNGLNAMEDFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC glad_glNamedFramebufferTextureMultiviewOVR = NULL; PFNGLNEWLISTPROC glad_glNewList = NULL; PFNGLNORMAL3BPROC glad_glNormal3b = NULL; PFNGLNORMAL3BVPROC glad_glNormal3bv = NULL; @@ -2109,29 +2108,40 @@ static void glad_gl_load_GL_EXT_framebuffer_object( GLADuserptrloadfunc load, vo static void glad_gl_load_GL_OVR_multiview( GLADuserptrloadfunc load, void* userptr) { if(!GLAD_GL_OVR_multiview) return; glad_glFramebufferTextureMultiviewOVR = (PFNGLFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC) load(userptr, "glFramebufferTextureMultiviewOVR"); - glad_glNamedFramebufferTextureMultiviewOVR = (PFNGLNAMEDFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC) load(userptr, "glNamedFramebufferTextureMultiviewOVR"); } -static void glad_gl_free_extensions(char **exts_i) { - if (exts_i != NULL) { - unsigned int index; - for(index = 0; exts_i[index]; index++) { - free((void *) (exts_i[index])); - } - free((void *)exts_i); - exts_i = NULL; - } -} -static int glad_gl_get_extensions( const char **out_exts, char ***out_exts_i) { #if defined(GL_ES_VERSION_3_0) || defined(GL_VERSION_3_0) - if (glad_glGetStringi != NULL && glad_glGetIntegerv != NULL) { +#define GLAD_GL_IS_SOME_NEW_VERSION 1 +#else +#define GLAD_GL_IS_SOME_NEW_VERSION 0 +#endif + +static int glad_gl_get_extensions( int version, const char **out_exts, unsigned int *out_num_exts_i, char ***out_exts_i) { +#if GLAD_GL_IS_SOME_NEW_VERSION + if(GLAD_VERSION_MAJOR(version) < 3) { +#else + GLAD_UNUSED(version); + GLAD_UNUSED(out_num_exts_i); + GLAD_UNUSED(out_exts_i); +#endif + if (glad_glGetString == NULL) { + return 0; + } + *out_exts = (const char *)glad_glGetString(GL_EXTENSIONS); +#if GLAD_GL_IS_SOME_NEW_VERSION + } else { unsigned int index = 0; unsigned int num_exts_i = 0; char **exts_i = NULL; + if (glad_glGetStringi == NULL || glad_glGetIntegerv == NULL) { + return 0; + } glad_glGetIntegerv(GL_NUM_EXTENSIONS, (int*) &num_exts_i); - exts_i = (char **) malloc((num_exts_i + 1) * (sizeof *exts_i)); + if (num_exts_i > 0) { + exts_i = (char **) malloc(num_exts_i * (sizeof *exts_i)); + } if (exts_i == NULL) { return 0; } @@ -2140,40 +2150,31 @@ static int glad_gl_get_extensions( const char **out_exts, char ***out_exts_i) { size_t len = strlen(gl_str_tmp) + 1; char *local_str = (char*) malloc(len * sizeof(char)); - if(local_str == NULL) { - exts_i[index] = NULL; - glad_gl_free_extensions(exts_i); - return 0; + if(local_str != NULL) { + memcpy(local_str, gl_str_tmp, len * sizeof(char)); } - memcpy(local_str, gl_str_tmp, len * sizeof(char)); exts_i[index] = local_str; } - exts_i[index] = NULL; + *out_num_exts_i = num_exts_i; *out_exts_i = exts_i; - - return 1; } -#else - GLAD_UNUSED(out_exts_i); #endif - if (glad_glGetString == NULL) { - return 0; - } - *out_exts = (const char *)glad_glGetString(GL_EXTENSIONS); return 1; } -static int glad_gl_has_extension(const char *exts, char **exts_i, const char *ext) { - if(exts_i) { +static void glad_gl_free_extensions(char **exts_i, unsigned int num_exts_i) { + if (exts_i != NULL) { unsigned int index; - for(index = 0; exts_i[index]; index++) { - const char *e = exts_i[index]; - if(strcmp(e, ext) == 0) { - return 1; - } + for(index = 0; index < num_exts_i; index++) { + free((void *) (exts_i[index])); } - } else { + free((void *)exts_i); + exts_i = NULL; + } +} +static int glad_gl_has_extension(int version, const char *exts, unsigned int num_exts_i, char **exts_i, const char *ext) { + if(GLAD_VERSION_MAJOR(version) < 3 || !GLAD_GL_IS_SOME_NEW_VERSION) { const char *extensions; const char *loc; const char *terminator; @@ -2193,6 +2194,14 @@ static int glad_gl_has_extension(const char *exts, char **exts_i, const char *ex } extensions = terminator; } + } else { + unsigned int index; + for(index = 0; index < num_exts_i; index++) { + const char *e = exts_i[index]; + if(strcmp(e, ext) == 0) { + return 1; + } + } } return 0; } @@ -2201,21 +2210,22 @@ static GLADapiproc glad_gl_get_proc_from_userptr(void *userptr, const char* name return (GLAD_GNUC_EXTENSION (GLADapiproc (*)(const char *name)) userptr)(name); } -static int glad_gl_find_extensions_gl(void) { +static int glad_gl_find_extensions_gl( int version) { const char *exts = NULL; + unsigned int num_exts_i = 0; char **exts_i = NULL; - if (!glad_gl_get_extensions(&exts, &exts_i)) return 0; + if (!glad_gl_get_extensions(version, &exts, &num_exts_i, &exts_i)) return 0; - GLAD_GL_ARB_debug_output = glad_gl_has_extension(exts, exts_i, "GL_ARB_debug_output"); - GLAD_GL_ARB_framebuffer_object = glad_gl_has_extension(exts, exts_i, "GL_ARB_framebuffer_object"); - GLAD_GL_ARB_get_program_binary = glad_gl_has_extension(exts, exts_i, "GL_ARB_get_program_binary"); - GLAD_GL_EXT_framebuffer_blit = glad_gl_has_extension(exts, exts_i, "GL_EXT_framebuffer_blit"); - GLAD_GL_EXT_framebuffer_multisample = glad_gl_has_extension(exts, exts_i, "GL_EXT_framebuffer_multisample"); - GLAD_GL_EXT_framebuffer_object = glad_gl_has_extension(exts, exts_i, "GL_EXT_framebuffer_object"); - GLAD_GL_OVR_multiview = glad_gl_has_extension(exts, exts_i, "GL_OVR_multiview"); - GLAD_GL_OVR_multiview2 = glad_gl_has_extension(exts, exts_i, "GL_OVR_multiview2"); + GLAD_GL_ARB_debug_output = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_ARB_debug_output"); + GLAD_GL_ARB_framebuffer_object = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_ARB_framebuffer_object"); + GLAD_GL_ARB_get_program_binary = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_ARB_get_program_binary"); + GLAD_GL_EXT_framebuffer_blit = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_EXT_framebuffer_blit"); + GLAD_GL_EXT_framebuffer_multisample = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_EXT_framebuffer_multisample"); + GLAD_GL_EXT_framebuffer_object = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_EXT_framebuffer_object"); + GLAD_GL_OVR_multiview = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_OVR_multiview"); + GLAD_GL_OVR_multiview2 = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_OVR_multiview2"); - glad_gl_free_extensions(exts_i); + glad_gl_free_extensions(exts_i, num_exts_i); return 1; } @@ -2265,6 +2275,7 @@ int gladLoadGLUserPtr( GLADuserptrloadfunc load, void *userptr) { glad_glGetString = (PFNGLGETSTRINGPROC) load(userptr, "glGetString"); if(glad_glGetString == NULL) return 0; + if(glad_glGetString(GL_VERSION) == NULL) return 0; version = glad_gl_find_core_gl(); glad_gl_load_GL_VERSION_1_0(load, userptr); @@ -2280,7 +2291,7 @@ int gladLoadGLUserPtr( GLADuserptrloadfunc load, void *userptr) { glad_gl_load_GL_VERSION_3_2(load, userptr); glad_gl_load_GL_VERSION_3_3(load, userptr); - if (!glad_gl_find_extensions_gl()) return 0; + if (!glad_gl_find_extensions_gl(version)) return 0; glad_gl_load_GL_ARB_debug_output(load, userptr); glad_gl_load_GL_ARB_framebuffer_object(load, userptr); glad_gl_load_GL_ARB_get_program_binary(load, userptr); @@ -2299,15 +2310,16 @@ int gladLoadGL( GLADloadfunc load) { return gladLoadGLUserPtr( glad_gl_get_proc_from_userptr, GLAD_GNUC_EXTENSION (void*) load); } -static int glad_gl_find_extensions_gles2(void) { +static int glad_gl_find_extensions_gles2( int version) { const char *exts = NULL; + unsigned int num_exts_i = 0; char **exts_i = NULL; - if (!glad_gl_get_extensions(&exts, &exts_i)) return 0; + if (!glad_gl_get_extensions(version, &exts, &num_exts_i, &exts_i)) return 0; - GLAD_GL_OVR_multiview = glad_gl_has_extension(exts, exts_i, "GL_OVR_multiview"); - GLAD_GL_OVR_multiview2 = glad_gl_has_extension(exts, exts_i, "GL_OVR_multiview2"); + GLAD_GL_OVR_multiview = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_OVR_multiview"); + GLAD_GL_OVR_multiview2 = glad_gl_has_extension(version, exts, num_exts_i, exts_i, "GL_OVR_multiview2"); - glad_gl_free_extensions(exts_i); + glad_gl_free_extensions(exts_i, num_exts_i); return 1; } @@ -2349,6 +2361,7 @@ int gladLoadGLES2UserPtr( GLADuserptrloadfunc load, void *userptr) { glad_glGetString = (PFNGLGETSTRINGPROC) load(userptr, "glGetString"); if(glad_glGetString == NULL) return 0; + if(glad_glGetString(GL_VERSION) == NULL) return 0; version = glad_gl_find_core_gles2(); glad_gl_load_GL_ES_VERSION_2_0(load, userptr); @@ -2356,7 +2369,7 @@ int gladLoadGLES2UserPtr( GLADuserptrloadfunc load, void *userptr) { glad_gl_load_GL_ES_VERSION_3_1(load, userptr); glad_gl_load_GL_ES_VERSION_3_2(load, userptr); - if (!glad_gl_find_extensions_gles2()) return 0; + if (!glad_gl_find_extensions_gles2(version)) return 0; glad_gl_load_GL_OVR_multiview(load, userptr); @@ -2614,9 +2627,10 @@ static GLADapiproc glad_dlsym_handle(void* handle, const char *name) { typedef __eglMustCastToProperFunctionPointerType (GLAD_API_PTR *PFNEGLGETPROCADDRESSPROC)(const char *name); #endif extern __eglMustCastToProperFunctionPointerType emscripten_GetProcAddress(const char *name); -#elif defined(GLAD_GLES2_USE_SYSTEM_EGL) - #include <EGL/egl.h> +#elif EGL_STATIC + typedef void (*__eglMustCastToProperFunctionPointerType)(void); typedef __eglMustCastToProperFunctionPointerType (GLAD_API_PTR *PFNEGLGETPROCADDRESSPROC)(const char *name); + extern __eglMustCastToProperFunctionPointerType GLAD_API_PTR eglGetProcAddress(const char *name); #else #include <glad/egl.h> #endif @@ -2644,7 +2658,7 @@ static GLADapiproc glad_gles2_get_proc(void *vuserptr, const char* name) { return result; } -static void* _glad_GLES2_loader_handle = NULL; +static void* _glad_GL_loader_handle = NULL; static void* glad_gles2_dlopen_handle(void) { #if GLAD_PLATFORM_EMSCRIPTEN @@ -2660,11 +2674,11 @@ static void* glad_gles2_dlopen_handle(void) { GLAD_UNUSED(glad_get_dlopen_handle); return NULL; #else - if (_glad_GLES2_loader_handle == NULL) { - _glad_GLES2_loader_handle = glad_get_dlopen_handle(NAMES, sizeof(NAMES) / sizeof(NAMES[0])); + if (_glad_GL_loader_handle == NULL) { + _glad_GL_loader_handle = glad_get_dlopen_handle(NAMES, sizeof(NAMES) / sizeof(NAMES[0])); } - return _glad_GLES2_loader_handle; + return _glad_GL_loader_handle; #endif } @@ -2694,12 +2708,11 @@ int gladLoaderLoadGLES2(void) { userptr.get_proc_address_ptr = emscripten_GetProcAddress; version = gladLoadGLES2UserPtr(glad_gles2_get_proc, &userptr); #else -#ifndef GLAD_GLES2_USE_SYSTEM_EGL if (eglGetProcAddress == NULL) { return 0; } -#endif - did_load = _glad_GLES2_loader_handle == NULL; + + did_load = _glad_GL_loader_handle == NULL; handle = glad_gles2_dlopen_handle(); if (handle != NULL) { userptr = glad_gles2_build_userptr(handle); @@ -2718,9 +2731,9 @@ int gladLoaderLoadGLES2(void) { void gladLoaderUnloadGLES2(void) { - if (_glad_GLES2_loader_handle != NULL) { - glad_close_dlopen_handle(_glad_GLES2_loader_handle); - _glad_GLES2_loader_handle = NULL; + if (_glad_GL_loader_handle != NULL) { + glad_close_dlopen_handle(_glad_GL_loader_handle); + _glad_GL_loader_handle = NULL; } } diff --git a/thirdparty/glad/glad/egl.h b/thirdparty/glad/glad/egl.h index 053c5853a7..1bf35c1404 100644 --- a/thirdparty/glad/glad/egl.h +++ b/thirdparty/glad/glad/egl.h @@ -1,5 +1,5 @@ /** - * Loader generated by glad 2.0.6 on Fri Apr 5 08:17:09 2024 + * Loader generated by glad 2.0.3 on Fri Feb 3 07:06:48 2023 * * SPDX-License-Identifier: (WTFPL OR CC0-1.0) AND Apache-2.0 * @@ -141,7 +141,7 @@ extern "C" { #define GLAD_VERSION_MAJOR(version) (version / 10000) #define GLAD_VERSION_MINOR(version) (version % 10000) -#define GLAD_GENERATOR_VERSION "2.0.6" +#define GLAD_GENERATOR_VERSION "2.0.3" typedef void (*GLADapiproc)(void); diff --git a/thirdparty/glad/glad/gl.h b/thirdparty/glad/glad/gl.h index 1301d10b65..307ea4dbb8 100644 --- a/thirdparty/glad/glad/gl.h +++ b/thirdparty/glad/glad/gl.h @@ -1,5 +1,5 @@ /** - * Loader generated by glad 2.0.6 on Fri Apr 5 08:14:44 2024 + * Loader generated by glad 2.0.4 on Mon May 22 13:18:29 2023 * * SPDX-License-Identifier: (WTFPL OR CC0-1.0) AND Apache-2.0 * @@ -178,7 +178,7 @@ extern "C" { #define GLAD_VERSION_MAJOR(version) (version / 10000) #define GLAD_VERSION_MINOR(version) (version % 10000) -#define GLAD_GENERATOR_VERSION "2.0.6" +#define GLAD_GENERATOR_VERSION "2.0.4" typedef void (*GLADapiproc)(void); @@ -2394,7 +2394,6 @@ typedef void (GLAD_API_PTR *PFNGLMULTITEXCOORDP3UIPROC)(GLenum texture, GLenum t typedef void (GLAD_API_PTR *PFNGLMULTITEXCOORDP3UIVPROC)(GLenum texture, GLenum type, const GLuint * coords); typedef void (GLAD_API_PTR *PFNGLMULTITEXCOORDP4UIPROC)(GLenum texture, GLenum type, GLuint coords); typedef void (GLAD_API_PTR *PFNGLMULTITEXCOORDP4UIVPROC)(GLenum texture, GLenum type, const GLuint * coords); -typedef void (GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC)(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint baseViewIndex, GLsizei numViews); typedef void (GLAD_API_PTR *PFNGLNEWLISTPROC)(GLuint list, GLenum mode); typedef void (GLAD_API_PTR *PFNGLNORMAL3BPROC)(GLbyte nx, GLbyte ny, GLbyte nz); typedef void (GLAD_API_PTR *PFNGLNORMAL3BVPROC)(const GLbyte * v); @@ -3655,8 +3654,6 @@ GLAD_API_CALL PFNGLMULTITEXCOORDP4UIPROC glad_glMultiTexCoordP4ui; #define glMultiTexCoordP4ui glad_glMultiTexCoordP4ui GLAD_API_CALL PFNGLMULTITEXCOORDP4UIVPROC glad_glMultiTexCoordP4uiv; #define glMultiTexCoordP4uiv glad_glMultiTexCoordP4uiv -GLAD_API_CALL PFNGLNAMEDFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC glad_glNamedFramebufferTextureMultiviewOVR; -#define glNamedFramebufferTextureMultiviewOVR glad_glNamedFramebufferTextureMultiviewOVR GLAD_API_CALL PFNGLNEWLISTPROC glad_glNewList; #define glNewList glad_glNewList GLAD_API_CALL PFNGLNORMAL3BPROC glad_glNormal3b; diff --git a/thirdparty/glad/glad/glx.h b/thirdparty/glad/glad/glx.h index a2fa0dadee..cf7663a3af 100644 --- a/thirdparty/glad/glad/glx.h +++ b/thirdparty/glad/glad/glx.h @@ -1,5 +1,5 @@ /** - * Loader generated by glad 2.0.6 on Fri Apr 5 08:14:31 2024 + * Loader generated by glad 2.0.4 on Mon May 22 13:18:29 2023 * * SPDX-License-Identifier: (WTFPL OR CC0-1.0) AND Apache-2.0 * @@ -152,7 +152,7 @@ extern "C" { #define GLAD_VERSION_MAJOR(version) (version / 10000) #define GLAD_VERSION_MINOR(version) (version % 10000) -#define GLAD_GENERATOR_VERSION "2.0.6" +#define GLAD_GENERATOR_VERSION "2.0.4" typedef void (*GLADapiproc)(void); diff --git a/thirdparty/glad/patches/patch_enable_both_gl_and_gles.diff b/thirdparty/glad/patches/patch_enable_both_gl_and_gles.diff index 88c5510166..a98efe51d8 100644 --- a/thirdparty/glad/patches/patch_enable_both_gl_and_gles.diff +++ b/thirdparty/glad/patches/patch_enable_both_gl_and_gles.diff @@ -1,8 +1,8 @@ diff --git a/thirdparty/glad/gl.c b/thirdparty/glad/gl.c -index 3f0884a3dc..38ecb514bd 100644 +index a0b59dbbfb..9f10f6544a 100644 --- a/thirdparty/glad/gl.c +++ b/thirdparty/glad/gl.c -@@ -2462,7 +2462,7 @@ static GLADapiproc glad_gl_get_proc(void *vuserptr, const char *name) { +@@ -2475,7 +2475,7 @@ static GLADapiproc glad_gl_get_proc(void *vuserptr, const char *name) { return result; } @@ -11,7 +11,7 @@ index 3f0884a3dc..38ecb514bd 100644 static void* glad_gl_dlopen_handle(void) { #if GLAD_PLATFORM_APPLE -@@ -2484,11 +2484,11 @@ static void* glad_gl_dlopen_handle(void) { +@@ -2497,11 +2497,11 @@ static void* glad_gl_dlopen_handle(void) { }; #endif @@ -26,7 +26,7 @@ index 3f0884a3dc..38ecb514bd 100644 } static struct _glad_gl_userptr glad_gl_build_userptr(void *handle) { -@@ -2514,7 +2514,7 @@ int gladLoaderLoadGL(void) { +@@ -2527,7 +2527,7 @@ int gladLoaderLoadGL(void) { int did_load = 0; struct _glad_gl_userptr userptr; @@ -35,7 +35,7 @@ index 3f0884a3dc..38ecb514bd 100644 handle = glad_gl_dlopen_handle(); if (handle) { userptr = glad_gl_build_userptr(handle); -@@ -2532,9 +2532,9 @@ int gladLoaderLoadGL(void) { +@@ -2545,9 +2545,9 @@ int gladLoaderLoadGL(void) { void gladLoaderUnloadGL(void) { @@ -49,7 +49,7 @@ index 3f0884a3dc..38ecb514bd 100644 } diff --git a/thirdparty/glad/glad/gl.h b/thirdparty/glad/glad/gl.h -index 77c6f33cab..1301d10b65 100644 +index 905c16aeed..f3cb7d8cb5 100644 --- a/thirdparty/glad/glad/gl.h +++ b/thirdparty/glad/glad/gl.h @@ -67,6 +67,7 @@ diff --git a/thirdparty/libbacktrace/LICENSE b/thirdparty/libbacktrace/LICENSE new file mode 100644 index 0000000000..097d2774e5 --- /dev/null +++ b/thirdparty/libbacktrace/LICENSE @@ -0,0 +1,29 @@ +# Copyright (C) 2012-2016 Free Software Foundation, Inc. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: + +# (1) Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. + +# (2) Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. + +# (3) The name of the author may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/thirdparty/libbacktrace/alloc.c b/thirdparty/libbacktrace/alloc.c new file mode 100644 index 0000000000..ff2c8677c0 --- /dev/null +++ b/thirdparty/libbacktrace/alloc.c @@ -0,0 +1,167 @@ +/* alloc.c -- Memory allocation without mmap. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <errno.h> +#include <stdlib.h> +#include <sys/types.h> + +#include "backtrace.h" +#include "internal.h" + +/* Allocation routines to use on systems that do not support anonymous + mmap. This implementation just uses malloc, which means that the + backtrace functions may not be safely invoked from a signal + handler. */ + +/* Allocate memory like malloc. If ERROR_CALLBACK is NULL, don't + report an error. */ + +void * +backtrace_alloc (struct backtrace_state *state ATTRIBUTE_UNUSED, + size_t size, backtrace_error_callback error_callback, + void *data) +{ + void *ret; + + ret = malloc (size); + if (ret == NULL) + { + if (error_callback) + error_callback (data, "malloc", errno); + } + return ret; +} + +/* Free memory. */ + +void +backtrace_free (struct backtrace_state *state ATTRIBUTE_UNUSED, + void *p, size_t size ATTRIBUTE_UNUSED, + backtrace_error_callback error_callback ATTRIBUTE_UNUSED, + void *data ATTRIBUTE_UNUSED) +{ + free (p); +} + +/* Grow VEC by SIZE bytes. */ + +void * +backtrace_vector_grow (struct backtrace_state *state ATTRIBUTE_UNUSED, + size_t size, backtrace_error_callback error_callback, + void *data, struct backtrace_vector *vec) +{ + void *ret; + + if (size > vec->alc) + { + size_t alc; + void *base; + + if (vec->size == 0) + alc = 32 * size; + else if (vec->size >= 4096) + alc = vec->size + 4096; + else + alc = 2 * vec->size; + + if (alc < vec->size + size) + alc = vec->size + size; + + base = realloc (vec->base, alc); + if (base == NULL) + { + error_callback (data, "realloc", errno); + return NULL; + } + + vec->base = base; + vec->alc = alc - vec->size; + } + + ret = (char *) vec->base + vec->size; + vec->size += size; + vec->alc -= size; + return ret; +} + +/* Finish the current allocation on VEC. */ + +void * +backtrace_vector_finish (struct backtrace_state *state, + struct backtrace_vector *vec, + backtrace_error_callback error_callback, + void *data) +{ + void *ret; + + /* With this allocator we call realloc in backtrace_vector_grow, + which means we can't easily reuse the memory here. So just + release it. */ + if (!backtrace_vector_release (state, vec, error_callback, data)) + return NULL; + ret = vec->base; + vec->base = NULL; + vec->size = 0; + vec->alc = 0; + return ret; +} + +/* Release any extra space allocated for VEC. */ + +int +backtrace_vector_release (struct backtrace_state *state ATTRIBUTE_UNUSED, + struct backtrace_vector *vec, + backtrace_error_callback error_callback, + void *data) +{ + vec->alc = 0; + + if (vec->size == 0) + { + /* As of C17, realloc with size 0 is marked as an obsolescent feature, use + free instead. */ + free (vec->base); + vec->base = NULL; + return 1; + } + + vec->base = realloc (vec->base, vec->size); + if (vec->base == NULL) + { + error_callback (data, "realloc", errno); + return 0; + } + + return 1; +} diff --git a/thirdparty/libbacktrace/atomic.c b/thirdparty/libbacktrace/atomic.c new file mode 100644 index 0000000000..fcac485b23 --- /dev/null +++ b/thirdparty/libbacktrace/atomic.c @@ -0,0 +1,113 @@ +/* atomic.c -- Support for atomic functions if not present. + Copyright (C) 2013-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <sys/types.h> + +#include "backtrace.h" +#include "backtrace-supported.h" +#include "internal.h" + +/* This file holds implementations of the atomic functions that are + used if the host compiler has the sync functions but not the atomic + functions, as is true of versions of GCC before 4.7. */ + +#if !defined (HAVE_ATOMIC_FUNCTIONS) && defined (HAVE_SYNC_FUNCTIONS) + +/* Do an atomic load of a pointer. */ + +void * +backtrace_atomic_load_pointer (void *arg) +{ + void **pp; + void *p; + + pp = (void **) arg; + p = *pp; + while (!__sync_bool_compare_and_swap (pp, p, p)) + p = *pp; + return p; +} + +/* Do an atomic load of an int. */ + +int +backtrace_atomic_load_int (int *p) +{ + int i; + + i = *p; + while (!__sync_bool_compare_and_swap (p, i, i)) + i = *p; + return i; +} + +/* Do an atomic store of a pointer. */ + +void +backtrace_atomic_store_pointer (void *arg, void *p) +{ + void **pp; + void *old; + + pp = (void **) arg; + old = *pp; + while (!__sync_bool_compare_and_swap (pp, old, p)) + old = *pp; +} + +/* Do an atomic store of a size_t value. */ + +void +backtrace_atomic_store_size_t (size_t *p, size_t v) +{ + size_t old; + + old = *p; + while (!__sync_bool_compare_and_swap (p, old, v)) + old = *p; +} + +/* Do an atomic store of a int value. */ + +void +backtrace_atomic_store_int (int *p, int v) +{ + size_t old; + + old = *p; + while (!__sync_bool_compare_and_swap (p, old, v)) + old = *p; +} + +#endif diff --git a/thirdparty/libbacktrace/backtrace-supported.h b/thirdparty/libbacktrace/backtrace-supported.h new file mode 100644 index 0000000000..f597195f13 --- /dev/null +++ b/thirdparty/libbacktrace/backtrace-supported.h @@ -0,0 +1,66 @@ +/* backtrace-supported.h.in -- Whether stack backtrace is supported. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +/* The file backtrace-supported.h.in is used by configure to generate + the file backtrace-supported.h. The file backtrace-supported.h may + be #include'd to see whether the backtrace library will be able to + get a backtrace and produce symbolic information. */ + + +/* BACKTRACE_SUPPORTED will be #define'd as 1 if the backtrace library + should work, 0 if it will not. Libraries may #include this to make + other arrangements. */ + +#define BACKTRACE_SUPPORTED 1 + +/* BACKTRACE_USES_MALLOC will be #define'd as 1 if the backtrace + library will call malloc as it works, 0 if it will call mmap + instead. This may be used to determine whether it is safe to call + the backtrace functions from a signal handler. In general this + only applies to calls like backtrace and backtrace_pcinfo. It does + not apply to backtrace_simple, which never calls malloc. It does + not apply to backtrace_print, which always calls fprintf and + therefore malloc. */ + +#define BACKTRACE_USES_MALLOC 1 + +/* BACKTRACE_SUPPORTS_THREADS will be #define'd as 1 if the backtrace + library is configured with threading support, 0 if not. If this is + 0, the threaded parameter to backtrace_create_state must be passed + as 0. */ + +#define BACKTRACE_SUPPORTS_THREADS 1 + +/* BACKTRACE_SUPPORTS_DATA will be #defined'd as 1 if the backtrace_syminfo + will work for variables. It will always work for functions. */ + +#define BACKTRACE_SUPPORTS_DATA 0 diff --git a/thirdparty/libbacktrace/backtrace.c b/thirdparty/libbacktrace/backtrace.c new file mode 100644 index 0000000000..7b62900852 --- /dev/null +++ b/thirdparty/libbacktrace/backtrace.c @@ -0,0 +1,129 @@ +/* backtrace.c -- Entry point for stack backtrace library. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <sys/types.h> + +#include "unwind.h" +#include "backtrace.h" +#include "internal.h" + +/* The main backtrace_full routine. */ + +/* Data passed through _Unwind_Backtrace. */ + +struct backtrace_data +{ + /* Number of frames to skip. */ + int skip; + /* Library state. */ + struct backtrace_state *state; + /* Callback routine. */ + backtrace_full_callback callback; + /* Error callback routine. */ + backtrace_error_callback error_callback; + /* Data to pass to callback routines. */ + void *data; + /* Value to return from backtrace_full. */ + int ret; + /* Whether there is any memory available. */ + int can_alloc; +}; + +/* Unwind library callback routine. This is passed to + _Unwind_Backtrace. */ + +static _Unwind_Reason_Code +unwind (struct _Unwind_Context *context, void *vdata) +{ + struct backtrace_data *bdata = (struct backtrace_data *) vdata; + uintptr_t pc; + int ip_before_insn = 0; + +#ifdef HAVE_GETIPINFO + pc = _Unwind_GetIPInfo (context, &ip_before_insn); +#else + pc = _Unwind_GetIP (context); +#endif + + if (bdata->skip > 0) + { + --bdata->skip; + return _URC_NO_REASON; + } + + if (!ip_before_insn) + --pc; + + if (!bdata->can_alloc) + bdata->ret = bdata->callback (bdata->data, pc, NULL, 0, NULL); + else + bdata->ret = backtrace_pcinfo (bdata->state, pc, bdata->callback, + bdata->error_callback, bdata->data); + if (bdata->ret != 0) + return _URC_END_OF_STACK; + + return _URC_NO_REASON; +} + +/* Get a stack backtrace. */ + +int __attribute__((noinline)) +backtrace_full (struct backtrace_state *state, int skip, + backtrace_full_callback callback, + backtrace_error_callback error_callback, void *data) +{ + struct backtrace_data bdata; + void *p; + + bdata.skip = skip + 1; + bdata.state = state; + bdata.callback = callback; + bdata.error_callback = error_callback; + bdata.data = data; + bdata.ret = 0; + + /* If we can't allocate any memory at all, don't try to produce + file/line information. */ + p = backtrace_alloc (state, 4096, NULL, NULL); + if (p == NULL) + bdata.can_alloc = 0; + else + { + backtrace_free (state, p, 4096, NULL, NULL); + bdata.can_alloc = 1; + } + + _Unwind_Backtrace (unwind, &bdata); + return bdata.ret; +} diff --git a/thirdparty/libbacktrace/backtrace.h b/thirdparty/libbacktrace/backtrace.h new file mode 100644 index 0000000000..69cea4ca1e --- /dev/null +++ b/thirdparty/libbacktrace/backtrace.h @@ -0,0 +1,189 @@ +/* backtrace.h -- Public header file for stack backtrace library. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#ifndef BACKTRACE_H +#define BACKTRACE_H + +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/* The backtrace state. This struct is intentionally not defined in + the public interface. */ + +struct backtrace_state; + +/* The type of the error callback argument to backtrace functions. + This function, if not NULL, will be called for certain error cases. + The DATA argument is passed to the function that calls this one. + The MSG argument is an error message. The ERRNUM argument, if + greater than 0, holds an errno value. The MSG buffer may become + invalid after this function returns. + + As a special case, the ERRNUM argument will be passed as -1 if no + debug info can be found for the executable, or if the debug info + exists but has an unsupported version, but the function requires + debug info (e.g., backtrace_full, backtrace_pcinfo). The MSG in + this case will be something along the lines of "no debug info". + Similarly, ERRNUM will be passed as -1 if there is no symbol table, + but the function requires a symbol table (e.g., backtrace_syminfo). + This may be used as a signal that some other approach should be + tried. */ + +typedef void (*backtrace_error_callback) (void *data, const char *msg, + int errnum); + +/* Create state information for the backtrace routines. This must be + called before any of the other routines, and its return value must + be passed to all of the other routines. FILENAME is the path name + of the executable file; if it is NULL the library will try + system-specific path names. If not NULL, FILENAME must point to a + permanent buffer. If THREADED is non-zero the state may be + accessed by multiple threads simultaneously, and the library will + use appropriate atomic operations. If THREADED is zero the state + may only be accessed by one thread at a time. This returns a state + pointer on success, NULL on error. If an error occurs, this will + call the ERROR_CALLBACK routine. + + Calling this function allocates resources that cannot be freed. + There is no backtrace_free_state function. The state is used to + cache information that is expensive to recompute. Programs are + expected to call this function at most once and to save the return + value for all later calls to backtrace functions. */ + +extern struct backtrace_state *backtrace_create_state ( + const char *filename, int threaded, + backtrace_error_callback error_callback, void *data); + +/* The type of the callback argument to the backtrace_full function. + DATA is the argument passed to backtrace_full. PC is the program + counter. FILENAME is the name of the file containing PC, or NULL + if not available. LINENO is the line number in FILENAME containing + PC, or 0 if not available. FUNCTION is the name of the function + containing PC, or NULL if not available. This should return 0 to + continuing tracing. The FILENAME and FUNCTION buffers may become + invalid after this function returns. */ + +typedef int (*backtrace_full_callback) (void *data, uintptr_t pc, + const char *filename, int lineno, + const char *function); + +/* Get a full stack backtrace. SKIP is the number of frames to skip; + passing 0 will start the trace with the function calling + backtrace_full. DATA is passed to the callback routine. If any + call to CALLBACK returns a non-zero value, the stack backtrace + stops, and backtrace returns that value; this may be used to limit + the number of stack frames desired. If all calls to CALLBACK + return 0, backtrace returns 0. The backtrace_full function will + make at least one call to either CALLBACK or ERROR_CALLBACK. This + function requires debug info for the executable. */ + +extern int backtrace_full (struct backtrace_state *state, int skip, + backtrace_full_callback callback, + backtrace_error_callback error_callback, + void *data); + +/* The type of the callback argument to the backtrace_simple function. + DATA is the argument passed to simple_backtrace. PC is the program + counter. This should return 0 to continue tracing. */ + +typedef int (*backtrace_simple_callback) (void *data, uintptr_t pc); + +/* Get a simple backtrace. SKIP is the number of frames to skip, as + in backtrace. DATA is passed to the callback routine. If any call + to CALLBACK returns a non-zero value, the stack backtrace stops, + and backtrace_simple returns that value. Otherwise + backtrace_simple returns 0. The backtrace_simple function will + make at least one call to either CALLBACK or ERROR_CALLBACK. This + function does not require any debug info for the executable. */ + +extern int backtrace_simple (struct backtrace_state *state, int skip, + backtrace_simple_callback callback, + backtrace_error_callback error_callback, + void *data); + +/* Print the current backtrace in a user readable format to a FILE. + SKIP is the number of frames to skip, as in backtrace_full. Any + error messages are printed to stderr. This function requires debug + info for the executable. */ + +extern void backtrace_print (struct backtrace_state *state, int skip, FILE *); + +/* Given PC, a program counter in the current program, call the + callback function with filename, line number, and function name + information. This will normally call the callback function exactly + once. However, if the PC happens to describe an inlined call, and + the debugging information contains the necessary information, then + this may call the callback function multiple times. This will make + at least one call to either CALLBACK or ERROR_CALLBACK. This + returns the first non-zero value returned by CALLBACK, or 0. */ + +extern int backtrace_pcinfo (struct backtrace_state *state, uintptr_t pc, + backtrace_full_callback callback, + backtrace_error_callback error_callback, + void *data); + +/* The type of the callback argument to backtrace_syminfo. DATA and + PC are the arguments passed to backtrace_syminfo. SYMNAME is the + name of the symbol for the corresponding code. SYMVAL is the + value and SYMSIZE is the size of the symbol. SYMNAME will be NULL + if no error occurred but the symbol could not be found. */ + +typedef void (*backtrace_syminfo_callback) (void *data, uintptr_t pc, + const char *symname, + uintptr_t symval, + uintptr_t symsize); + +/* Given ADDR, an address or program counter in the current program, + call the callback information with the symbol name and value + describing the function or variable in which ADDR may be found. + This will call either CALLBACK or ERROR_CALLBACK exactly once. + This returns 1 on success, 0 on failure. This function requires + the symbol table but does not require the debug info. Note that if + the symbol table is present but ADDR could not be found in the + table, CALLBACK will be called with a NULL SYMNAME argument. + Returns 1 on success, 0 on error. */ + +extern int backtrace_syminfo (struct backtrace_state *state, uintptr_t addr, + backtrace_syminfo_callback callback, + backtrace_error_callback error_callback, + void *data); + +#ifdef __cplusplus +} /* End extern "C". */ +#endif + +#endif diff --git a/thirdparty/libbacktrace/config.h b/thirdparty/libbacktrace/config.h new file mode 100644 index 0000000000..0c745c191b --- /dev/null +++ b/thirdparty/libbacktrace/config.h @@ -0,0 +1,170 @@ +/* config.h. Generated from config.h.in by configure. */ +/* config.h.in. Generated from configure.ac by autoheader. */ + +/* ELF size: 32 or 64 */ +#define BACKTRACE_ELF_SIZE unused + +/* XCOFF size: 32 or 64 */ +#define BACKTRACE_XCOFF_SIZE unused + +/* Define to 1 if you have the __atomic functions */ +#define HAVE_ATOMIC_FUNCTIONS 1 + +/* Define to 1 if you have the `clock_gettime' function. */ +#define HAVE_CLOCK_GETTIME 1 + +/* Define to 1 if you have the declaration of `getpagesize', and to 0 if you + don't. */ +#define HAVE_DECL_GETPAGESIZE 0 + +/* Define to 1 if you have the declaration of `strnlen', and to 0 if you + don't. */ +#define HAVE_DECL_STRNLEN 1 + +/* Define to 1 if you have the <dlfcn.h> header file. */ +/* #undef HAVE_DLFCN_H */ + +/* Define if dl_iterate_phdr is available. */ +/* #undef HAVE_DL_ITERATE_PHDR */ + +/* Define to 1 if you have the fcntl function */ +/* #undef HAVE_FCNTL */ + +/* Define if getexecname is available. */ +/* #undef HAVE_GETEXECNAME */ + +/* Define if _Unwind_GetIPInfo is available. */ +#define HAVE_GETIPINFO 1 + +/* Define to 1 if you have the <inttypes.h> header file. */ +#define HAVE_INTTYPES_H 1 + +/* Define to 1 if you have KERN_PROC and KERN_PROC_PATHNAME in <sys/sysctl.h>. + */ +/* #undef HAVE_KERN_PROC */ + +/* Define to 1 if you have KERN_PROCARGS and KERN_PROC_PATHNAME in + <sys/sysctl.h>. */ +/* #undef HAVE_KERN_PROC_ARGS */ + +/* Define if -llzma is available. */ +#define HAVE_LIBLZMA 1 + +/* Define to 1 if you have the <link.h> header file. */ +/* #undef HAVE_LINK_H */ + +/* Define if AIX loadquery is available. */ +/* #undef HAVE_LOADQUERY */ + +/* Define to 1 if you have the `lstat' function. */ +/* #undef HAVE_LSTAT */ + +/* Define to 1 if you have the <mach-o/dyld.h> header file. */ +/* #undef HAVE_MACH_O_DYLD_H */ + +/* Define to 1 if you have the <memory.h> header file. */ +#define HAVE_MEMORY_H 1 + +/* Define to 1 if you have the `readlink' function. */ +/* #undef HAVE_READLINK */ + +/* Define to 1 if you have the <stdint.h> header file. */ +#define HAVE_STDINT_H 1 + +/* Define to 1 if you have the <stdlib.h> header file. */ +#define HAVE_STDLIB_H 1 + +/* Define to 1 if you have the <strings.h> header file. */ +#define HAVE_STRINGS_H 1 + +/* Define to 1 if you have the <string.h> header file. */ +#define HAVE_STRING_H 1 + +/* Define to 1 if you have the __sync functions */ +#define HAVE_SYNC_FUNCTIONS 1 + +/* Define to 1 if you have the <sys/ldr.h> header file. */ +/* #undef HAVE_SYS_LDR_H */ + +/* Define to 1 if you have the <sys/mman.h> header file. */ +/* #undef HAVE_SYS_MMAN_H */ + +/* Define to 1 if you have the <sys/stat.h> header file. */ +#define HAVE_SYS_STAT_H 1 + +/* Define to 1 if you have the <sys/types.h> header file. */ +#define HAVE_SYS_TYPES_H 1 + +/* Define to 1 if you have the <unistd.h> header file. */ +#define HAVE_UNISTD_H 1 + +/* Define if -lz is available. */ +#define HAVE_ZLIB 1 + +/* Define to the sub-directory in which libtool stores uninstalled libraries. + */ +#define LT_OBJDIR ".libs/" + +/* Define to the address where bug reports for this package should be sent. */ +#define PACKAGE_BUGREPORT "" + +/* Define to the full name of this package. */ +#define PACKAGE_NAME "package-unused" + +/* Define to the full name and version of this package. */ +#define PACKAGE_STRING "package-unused version-unused" + +/* Define to the one symbol short name of this package. */ +#define PACKAGE_TARNAME "libbacktrace" + +/* Define to the home page for this package. */ +#define PACKAGE_URL "" + +/* Define to the version of this package. */ +#define PACKAGE_VERSION "version-unused" + +/* Define to 1 if you have the ANSI C header files. */ +#define STDC_HEADERS 1 + +/* Enable extensions on AIX 3, Interix. */ +#ifndef _ALL_SOURCE +# define _ALL_SOURCE 1 +#endif +/* Enable GNU extensions on systems that have them. */ +#ifndef _GNU_SOURCE +# define _GNU_SOURCE 1 +#endif +/* Enable threading extensions on Solaris. */ +#ifndef _POSIX_PTHREAD_SEMANTICS +# define _POSIX_PTHREAD_SEMANTICS 1 +#endif +/* Enable extensions on HP NonStop. */ +#ifndef _TANDEM_SOURCE +# define _TANDEM_SOURCE 1 +#endif +/* Enable general extensions on Solaris. */ +#ifndef __EXTENSIONS__ +# define __EXTENSIONS__ 1 +#endif + + +/* Enable large inode numbers on Mac OS X 10.5. */ +#ifndef _DARWIN_USE_64_BIT_INODE +# define _DARWIN_USE_64_BIT_INODE 1 +#endif + +/* Number of bits in a file offset, on hosts where this is settable. */ +#define _FILE_OFFSET_BITS 64 + +/* Define for large files, on AIX-style hosts. */ +/* #undef _LARGE_FILES */ + +/* Define to 1 if on MINIX. */ +/* #undef _MINIX */ + +/* Define to 2 if the system does not provide POSIX.1 features except with + this defined. */ +/* #undef _POSIX_1_SOURCE */ + +/* Define to 1 if you need to in order for `stat' and other things to work. */ +/* #undef _POSIX_SOURCE */ diff --git a/thirdparty/libbacktrace/dwarf.c b/thirdparty/libbacktrace/dwarf.c new file mode 100644 index 0000000000..5b2724e6a7 --- /dev/null +++ b/thirdparty/libbacktrace/dwarf.c @@ -0,0 +1,4402 @@ +/* dwarf.c -- Get file/line information from DWARF for backtraces. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <errno.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> + +#include "filenames.h" + +#include "backtrace.h" +#include "internal.h" + +/* DWARF constants. */ + +enum dwarf_tag { + DW_TAG_entry_point = 0x3, + DW_TAG_compile_unit = 0x11, + DW_TAG_inlined_subroutine = 0x1d, + DW_TAG_subprogram = 0x2e, + DW_TAG_skeleton_unit = 0x4a, +}; + +enum dwarf_form { + DW_FORM_addr = 0x01, + DW_FORM_block2 = 0x03, + DW_FORM_block4 = 0x04, + DW_FORM_data2 = 0x05, + DW_FORM_data4 = 0x06, + DW_FORM_data8 = 0x07, + DW_FORM_string = 0x08, + DW_FORM_block = 0x09, + DW_FORM_block1 = 0x0a, + DW_FORM_data1 = 0x0b, + DW_FORM_flag = 0x0c, + DW_FORM_sdata = 0x0d, + DW_FORM_strp = 0x0e, + DW_FORM_udata = 0x0f, + DW_FORM_ref_addr = 0x10, + DW_FORM_ref1 = 0x11, + DW_FORM_ref2 = 0x12, + DW_FORM_ref4 = 0x13, + DW_FORM_ref8 = 0x14, + DW_FORM_ref_udata = 0x15, + DW_FORM_indirect = 0x16, + DW_FORM_sec_offset = 0x17, + DW_FORM_exprloc = 0x18, + DW_FORM_flag_present = 0x19, + DW_FORM_ref_sig8 = 0x20, + DW_FORM_strx = 0x1a, + DW_FORM_addrx = 0x1b, + DW_FORM_ref_sup4 = 0x1c, + DW_FORM_strp_sup = 0x1d, + DW_FORM_data16 = 0x1e, + DW_FORM_line_strp = 0x1f, + DW_FORM_implicit_const = 0x21, + DW_FORM_loclistx = 0x22, + DW_FORM_rnglistx = 0x23, + DW_FORM_ref_sup8 = 0x24, + DW_FORM_strx1 = 0x25, + DW_FORM_strx2 = 0x26, + DW_FORM_strx3 = 0x27, + DW_FORM_strx4 = 0x28, + DW_FORM_addrx1 = 0x29, + DW_FORM_addrx2 = 0x2a, + DW_FORM_addrx3 = 0x2b, + DW_FORM_addrx4 = 0x2c, + DW_FORM_GNU_addr_index = 0x1f01, + DW_FORM_GNU_str_index = 0x1f02, + DW_FORM_GNU_ref_alt = 0x1f20, + DW_FORM_GNU_strp_alt = 0x1f21 +}; + +enum dwarf_attribute { + DW_AT_sibling = 0x01, + DW_AT_location = 0x02, + DW_AT_name = 0x03, + DW_AT_ordering = 0x09, + DW_AT_subscr_data = 0x0a, + DW_AT_byte_size = 0x0b, + DW_AT_bit_offset = 0x0c, + DW_AT_bit_size = 0x0d, + DW_AT_element_list = 0x0f, + DW_AT_stmt_list = 0x10, + DW_AT_low_pc = 0x11, + DW_AT_high_pc = 0x12, + DW_AT_language = 0x13, + DW_AT_member = 0x14, + DW_AT_discr = 0x15, + DW_AT_discr_value = 0x16, + DW_AT_visibility = 0x17, + DW_AT_import = 0x18, + DW_AT_string_length = 0x19, + DW_AT_common_reference = 0x1a, + DW_AT_comp_dir = 0x1b, + DW_AT_const_value = 0x1c, + DW_AT_containing_type = 0x1d, + DW_AT_default_value = 0x1e, + DW_AT_inline = 0x20, + DW_AT_is_optional = 0x21, + DW_AT_lower_bound = 0x22, + DW_AT_producer = 0x25, + DW_AT_prototyped = 0x27, + DW_AT_return_addr = 0x2a, + DW_AT_start_scope = 0x2c, + DW_AT_bit_stride = 0x2e, + DW_AT_upper_bound = 0x2f, + DW_AT_abstract_origin = 0x31, + DW_AT_accessibility = 0x32, + DW_AT_address_class = 0x33, + DW_AT_artificial = 0x34, + DW_AT_base_types = 0x35, + DW_AT_calling_convention = 0x36, + DW_AT_count = 0x37, + DW_AT_data_member_location = 0x38, + DW_AT_decl_column = 0x39, + DW_AT_decl_file = 0x3a, + DW_AT_decl_line = 0x3b, + DW_AT_declaration = 0x3c, + DW_AT_discr_list = 0x3d, + DW_AT_encoding = 0x3e, + DW_AT_external = 0x3f, + DW_AT_frame_base = 0x40, + DW_AT_friend = 0x41, + DW_AT_identifier_case = 0x42, + DW_AT_macro_info = 0x43, + DW_AT_namelist_items = 0x44, + DW_AT_priority = 0x45, + DW_AT_segment = 0x46, + DW_AT_specification = 0x47, + DW_AT_static_link = 0x48, + DW_AT_type = 0x49, + DW_AT_use_location = 0x4a, + DW_AT_variable_parameter = 0x4b, + DW_AT_virtuality = 0x4c, + DW_AT_vtable_elem_location = 0x4d, + DW_AT_allocated = 0x4e, + DW_AT_associated = 0x4f, + DW_AT_data_location = 0x50, + DW_AT_byte_stride = 0x51, + DW_AT_entry_pc = 0x52, + DW_AT_use_UTF8 = 0x53, + DW_AT_extension = 0x54, + DW_AT_ranges = 0x55, + DW_AT_trampoline = 0x56, + DW_AT_call_column = 0x57, + DW_AT_call_file = 0x58, + DW_AT_call_line = 0x59, + DW_AT_description = 0x5a, + DW_AT_binary_scale = 0x5b, + DW_AT_decimal_scale = 0x5c, + DW_AT_small = 0x5d, + DW_AT_decimal_sign = 0x5e, + DW_AT_digit_count = 0x5f, + DW_AT_picture_string = 0x60, + DW_AT_mutable = 0x61, + DW_AT_threads_scaled = 0x62, + DW_AT_explicit = 0x63, + DW_AT_object_pointer = 0x64, + DW_AT_endianity = 0x65, + DW_AT_elemental = 0x66, + DW_AT_pure = 0x67, + DW_AT_recursive = 0x68, + DW_AT_signature = 0x69, + DW_AT_main_subprogram = 0x6a, + DW_AT_data_bit_offset = 0x6b, + DW_AT_const_expr = 0x6c, + DW_AT_enum_class = 0x6d, + DW_AT_linkage_name = 0x6e, + DW_AT_string_length_bit_size = 0x6f, + DW_AT_string_length_byte_size = 0x70, + DW_AT_rank = 0x71, + DW_AT_str_offsets_base = 0x72, + DW_AT_addr_base = 0x73, + DW_AT_rnglists_base = 0x74, + DW_AT_dwo_name = 0x76, + DW_AT_reference = 0x77, + DW_AT_rvalue_reference = 0x78, + DW_AT_macros = 0x79, + DW_AT_call_all_calls = 0x7a, + DW_AT_call_all_source_calls = 0x7b, + DW_AT_call_all_tail_calls = 0x7c, + DW_AT_call_return_pc = 0x7d, + DW_AT_call_value = 0x7e, + DW_AT_call_origin = 0x7f, + DW_AT_call_parameter = 0x80, + DW_AT_call_pc = 0x81, + DW_AT_call_tail_call = 0x82, + DW_AT_call_target = 0x83, + DW_AT_call_target_clobbered = 0x84, + DW_AT_call_data_location = 0x85, + DW_AT_call_data_value = 0x86, + DW_AT_noreturn = 0x87, + DW_AT_alignment = 0x88, + DW_AT_export_symbols = 0x89, + DW_AT_deleted = 0x8a, + DW_AT_defaulted = 0x8b, + DW_AT_loclists_base = 0x8c, + DW_AT_lo_user = 0x2000, + DW_AT_hi_user = 0x3fff, + DW_AT_MIPS_fde = 0x2001, + DW_AT_MIPS_loop_begin = 0x2002, + DW_AT_MIPS_tail_loop_begin = 0x2003, + DW_AT_MIPS_epilog_begin = 0x2004, + DW_AT_MIPS_loop_unroll_factor = 0x2005, + DW_AT_MIPS_software_pipeline_depth = 0x2006, + DW_AT_MIPS_linkage_name = 0x2007, + DW_AT_MIPS_stride = 0x2008, + DW_AT_MIPS_abstract_name = 0x2009, + DW_AT_MIPS_clone_origin = 0x200a, + DW_AT_MIPS_has_inlines = 0x200b, + DW_AT_HP_block_index = 0x2000, + DW_AT_HP_unmodifiable = 0x2001, + DW_AT_HP_prologue = 0x2005, + DW_AT_HP_epilogue = 0x2008, + DW_AT_HP_actuals_stmt_list = 0x2010, + DW_AT_HP_proc_per_section = 0x2011, + DW_AT_HP_raw_data_ptr = 0x2012, + DW_AT_HP_pass_by_reference = 0x2013, + DW_AT_HP_opt_level = 0x2014, + DW_AT_HP_prof_version_id = 0x2015, + DW_AT_HP_opt_flags = 0x2016, + DW_AT_HP_cold_region_low_pc = 0x2017, + DW_AT_HP_cold_region_high_pc = 0x2018, + DW_AT_HP_all_variables_modifiable = 0x2019, + DW_AT_HP_linkage_name = 0x201a, + DW_AT_HP_prof_flags = 0x201b, + DW_AT_HP_unit_name = 0x201f, + DW_AT_HP_unit_size = 0x2020, + DW_AT_HP_widened_byte_size = 0x2021, + DW_AT_HP_definition_points = 0x2022, + DW_AT_HP_default_location = 0x2023, + DW_AT_HP_is_result_param = 0x2029, + DW_AT_sf_names = 0x2101, + DW_AT_src_info = 0x2102, + DW_AT_mac_info = 0x2103, + DW_AT_src_coords = 0x2104, + DW_AT_body_begin = 0x2105, + DW_AT_body_end = 0x2106, + DW_AT_GNU_vector = 0x2107, + DW_AT_GNU_guarded_by = 0x2108, + DW_AT_GNU_pt_guarded_by = 0x2109, + DW_AT_GNU_guarded = 0x210a, + DW_AT_GNU_pt_guarded = 0x210b, + DW_AT_GNU_locks_excluded = 0x210c, + DW_AT_GNU_exclusive_locks_required = 0x210d, + DW_AT_GNU_shared_locks_required = 0x210e, + DW_AT_GNU_odr_signature = 0x210f, + DW_AT_GNU_template_name = 0x2110, + DW_AT_GNU_call_site_value = 0x2111, + DW_AT_GNU_call_site_data_value = 0x2112, + DW_AT_GNU_call_site_target = 0x2113, + DW_AT_GNU_call_site_target_clobbered = 0x2114, + DW_AT_GNU_tail_call = 0x2115, + DW_AT_GNU_all_tail_call_sites = 0x2116, + DW_AT_GNU_all_call_sites = 0x2117, + DW_AT_GNU_all_source_call_sites = 0x2118, + DW_AT_GNU_macros = 0x2119, + DW_AT_GNU_deleted = 0x211a, + DW_AT_GNU_dwo_name = 0x2130, + DW_AT_GNU_dwo_id = 0x2131, + DW_AT_GNU_ranges_base = 0x2132, + DW_AT_GNU_addr_base = 0x2133, + DW_AT_GNU_pubnames = 0x2134, + DW_AT_GNU_pubtypes = 0x2135, + DW_AT_GNU_discriminator = 0x2136, + DW_AT_GNU_locviews = 0x2137, + DW_AT_GNU_entry_view = 0x2138, + DW_AT_VMS_rtnbeg_pd_address = 0x2201, + DW_AT_use_GNAT_descriptive_type = 0x2301, + DW_AT_GNAT_descriptive_type = 0x2302, + DW_AT_GNU_numerator = 0x2303, + DW_AT_GNU_denominator = 0x2304, + DW_AT_GNU_bias = 0x2305, + DW_AT_upc_threads_scaled = 0x3210, + DW_AT_PGI_lbase = 0x3a00, + DW_AT_PGI_soffset = 0x3a01, + DW_AT_PGI_lstride = 0x3a02, + DW_AT_APPLE_optimized = 0x3fe1, + DW_AT_APPLE_flags = 0x3fe2, + DW_AT_APPLE_isa = 0x3fe3, + DW_AT_APPLE_block = 0x3fe4, + DW_AT_APPLE_major_runtime_vers = 0x3fe5, + DW_AT_APPLE_runtime_class = 0x3fe6, + DW_AT_APPLE_omit_frame_ptr = 0x3fe7, + DW_AT_APPLE_property_name = 0x3fe8, + DW_AT_APPLE_property_getter = 0x3fe9, + DW_AT_APPLE_property_setter = 0x3fea, + DW_AT_APPLE_property_attribute = 0x3feb, + DW_AT_APPLE_objc_complete_type = 0x3fec, + DW_AT_APPLE_property = 0x3fed +}; + +enum dwarf_line_number_op { + DW_LNS_extended_op = 0x0, + DW_LNS_copy = 0x1, + DW_LNS_advance_pc = 0x2, + DW_LNS_advance_line = 0x3, + DW_LNS_set_file = 0x4, + DW_LNS_set_column = 0x5, + DW_LNS_negate_stmt = 0x6, + DW_LNS_set_basic_block = 0x7, + DW_LNS_const_add_pc = 0x8, + DW_LNS_fixed_advance_pc = 0x9, + DW_LNS_set_prologue_end = 0xa, + DW_LNS_set_epilogue_begin = 0xb, + DW_LNS_set_isa = 0xc, +}; + +enum dwarf_extended_line_number_op { + DW_LNE_end_sequence = 0x1, + DW_LNE_set_address = 0x2, + DW_LNE_define_file = 0x3, + DW_LNE_set_discriminator = 0x4, +}; + +enum dwarf_line_number_content_type { + DW_LNCT_path = 0x1, + DW_LNCT_directory_index = 0x2, + DW_LNCT_timestamp = 0x3, + DW_LNCT_size = 0x4, + DW_LNCT_MD5 = 0x5, + DW_LNCT_lo_user = 0x2000, + DW_LNCT_hi_user = 0x3fff +}; + +enum dwarf_range_list_entry { + DW_RLE_end_of_list = 0x00, + DW_RLE_base_addressx = 0x01, + DW_RLE_startx_endx = 0x02, + DW_RLE_startx_length = 0x03, + DW_RLE_offset_pair = 0x04, + DW_RLE_base_address = 0x05, + DW_RLE_start_end = 0x06, + DW_RLE_start_length = 0x07 +}; + +enum dwarf_unit_type { + DW_UT_compile = 0x01, + DW_UT_type = 0x02, + DW_UT_partial = 0x03, + DW_UT_skeleton = 0x04, + DW_UT_split_compile = 0x05, + DW_UT_split_type = 0x06, + DW_UT_lo_user = 0x80, + DW_UT_hi_user = 0xff +}; + +#if !defined(HAVE_DECL_STRNLEN) || !HAVE_DECL_STRNLEN + +/* If strnlen is not declared, provide our own version. */ + +static size_t +xstrnlen (const char *s, size_t maxlen) +{ + size_t i; + + for (i = 0; i < maxlen; ++i) + if (s[i] == '\0') + break; + return i; +} + +#define strnlen xstrnlen + +#endif + +/* A buffer to read DWARF info. */ + +struct dwarf_buf +{ + /* Buffer name for error messages. */ + const char *name; + /* Start of the buffer. */ + const unsigned char *start; + /* Next byte to read. */ + const unsigned char *buf; + /* The number of bytes remaining. */ + size_t left; + /* Whether the data is big-endian. */ + int is_bigendian; + /* Error callback routine. */ + backtrace_error_callback error_callback; + /* Data for error_callback. */ + void *data; + /* Non-zero if we've reported an underflow error. */ + int reported_underflow; +}; + +/* A single attribute in a DWARF abbreviation. */ + +struct attr +{ + /* The attribute name. */ + enum dwarf_attribute name; + /* The attribute form. */ + enum dwarf_form form; + /* The attribute value, for DW_FORM_implicit_const. */ + int64_t val; +}; + +/* A single DWARF abbreviation. */ + +struct abbrev +{ + /* The abbrev code--the number used to refer to the abbrev. */ + uint64_t code; + /* The entry tag. */ + enum dwarf_tag tag; + /* Non-zero if this abbrev has child entries. */ + int has_children; + /* The number of attributes. */ + size_t num_attrs; + /* The attributes. */ + struct attr *attrs; +}; + +/* The DWARF abbreviations for a compilation unit. This structure + only exists while reading the compilation unit. Most DWARF readers + seem to a hash table to map abbrev ID's to abbrev entries. + However, we primarily care about GCC, and GCC simply issues ID's in + numerical order starting at 1. So we simply keep a sorted vector, + and try to just look up the code. */ + +struct abbrevs +{ + /* The number of abbrevs in the vector. */ + size_t num_abbrevs; + /* The abbrevs, sorted by the code field. */ + struct abbrev *abbrevs; +}; + +/* The different kinds of attribute values. */ + +enum attr_val_encoding +{ + /* No attribute value. */ + ATTR_VAL_NONE, + /* An address. */ + ATTR_VAL_ADDRESS, + /* An index into the .debug_addr section, whose value is relative to + * the DW_AT_addr_base attribute of the compilation unit. */ + ATTR_VAL_ADDRESS_INDEX, + /* A unsigned integer. */ + ATTR_VAL_UINT, + /* A sigd integer. */ + ATTR_VAL_SINT, + /* A string. */ + ATTR_VAL_STRING, + /* An index into the .debug_str_offsets section. */ + ATTR_VAL_STRING_INDEX, + /* An offset to other data in the containing unit. */ + ATTR_VAL_REF_UNIT, + /* An offset to other data within the .debug_info section. */ + ATTR_VAL_REF_INFO, + /* An offset to other data within the alt .debug_info section. */ + ATTR_VAL_REF_ALT_INFO, + /* An offset to data in some other section. */ + ATTR_VAL_REF_SECTION, + /* A type signature. */ + ATTR_VAL_REF_TYPE, + /* An index into the .debug_rnglists section. */ + ATTR_VAL_RNGLISTS_INDEX, + /* A block of data (not represented). */ + ATTR_VAL_BLOCK, + /* An expression (not represented). */ + ATTR_VAL_EXPR, +}; + +/* An attribute value. */ + +struct attr_val +{ + /* How the value is stored in the field u. */ + enum attr_val_encoding encoding; + union + { + /* ATTR_VAL_ADDRESS*, ATTR_VAL_UINT, ATTR_VAL_REF*. */ + uint64_t uint; + /* ATTR_VAL_SINT. */ + int64_t sint; + /* ATTR_VAL_STRING. */ + const char *string; + /* ATTR_VAL_BLOCK not stored. */ + } u; +}; + +/* The line number program header. */ + +struct line_header +{ + /* The version of the line number information. */ + int version; + /* Address size. */ + int addrsize; + /* The minimum instruction length. */ + unsigned int min_insn_len; + /* The maximum number of ops per instruction. */ + unsigned int max_ops_per_insn; + /* The line base for special opcodes. */ + int line_base; + /* The line range for special opcodes. */ + unsigned int line_range; + /* The opcode base--the first special opcode. */ + unsigned int opcode_base; + /* Opcode lengths, indexed by opcode - 1. */ + const unsigned char *opcode_lengths; + /* The number of directory entries. */ + size_t dirs_count; + /* The directory entries. */ + const char **dirs; + /* The number of filenames. */ + size_t filenames_count; + /* The filenames. */ + const char **filenames; +}; + +/* A format description from a line header. */ + +struct line_header_format +{ + int lnct; /* LNCT code. */ + enum dwarf_form form; /* Form of entry data. */ +}; + +/* Map a single PC value to a file/line. We will keep a vector of + these sorted by PC value. Each file/line will be correct from the + PC up to the PC of the next entry if there is one. We allocate one + extra entry at the end so that we can use bsearch. */ + +struct line +{ + /* PC. */ + uintptr_t pc; + /* File name. Many entries in the array are expected to point to + the same file name. */ + const char *filename; + /* Line number. */ + int lineno; + /* Index of the object in the original array read from the DWARF + section, before it has been sorted. The index makes it possible + to use Quicksort and maintain stability. */ + int idx; +}; + +/* A growable vector of line number information. This is used while + reading the line numbers. */ + +struct line_vector +{ + /* Memory. This is an array of struct line. */ + struct backtrace_vector vec; + /* Number of valid mappings. */ + size_t count; +}; + +/* A function described in the debug info. */ + +struct function +{ + /* The name of the function. */ + const char *name; + /* If this is an inlined function, the filename of the call + site. */ + const char *caller_filename; + /* If this is an inlined function, the line number of the call + site. */ + int caller_lineno; + /* Map PC ranges to inlined functions. */ + struct function_addrs *function_addrs; + size_t function_addrs_count; +}; + +/* An address range for a function. This maps a PC value to a + specific function. */ + +struct function_addrs +{ + /* Range is LOW <= PC < HIGH. */ + uint64_t low; + uint64_t high; + /* Function for this address range. */ + struct function *function; +}; + +/* A growable vector of function address ranges. */ + +struct function_vector +{ + /* Memory. This is an array of struct function_addrs. */ + struct backtrace_vector vec; + /* Number of address ranges present. */ + size_t count; +}; + +/* A DWARF compilation unit. This only holds the information we need + to map a PC to a file and line. */ + +struct unit +{ + /* The first entry for this compilation unit. */ + const unsigned char *unit_data; + /* The length of the data for this compilation unit. */ + size_t unit_data_len; + /* The offset of UNIT_DATA from the start of the information for + this compilation unit. */ + size_t unit_data_offset; + /* Offset of the start of the compilation unit from the start of the + .debug_info section. */ + size_t low_offset; + /* Offset of the end of the compilation unit from the start of the + .debug_info section. */ + size_t high_offset; + /* DWARF version. */ + int version; + /* Whether unit is DWARF64. */ + int is_dwarf64; + /* Address size. */ + int addrsize; + /* Offset into line number information. */ + off_t lineoff; + /* Offset of compilation unit in .debug_str_offsets. */ + uint64_t str_offsets_base; + /* Offset of compilation unit in .debug_addr. */ + uint64_t addr_base; + /* Offset of compilation unit in .debug_rnglists. */ + uint64_t rnglists_base; + /* Primary source file. */ + const char *filename; + /* Compilation command working directory. */ + const char *comp_dir; + /* Absolute file name, only set if needed. */ + const char *abs_filename; + /* The abbreviations for this unit. */ + struct abbrevs abbrevs; + + /* The fields above this point are read in during initialization and + may be accessed freely. The fields below this point are read in + as needed, and therefore require care, as different threads may + try to initialize them simultaneously. */ + + /* PC to line number mapping. This is NULL if the values have not + been read. This is (struct line *) -1 if there was an error + reading the values. */ + struct line *lines; + /* Number of entries in lines. */ + size_t lines_count; + /* PC ranges to function. */ + struct function_addrs *function_addrs; + size_t function_addrs_count; +}; + +/* An address range for a compilation unit. This maps a PC value to a + specific compilation unit. Note that we invert the representation + in DWARF: instead of listing the units and attaching a list of + ranges, we list the ranges and have each one point to the unit. + This lets us do a binary search to find the unit. */ + +struct unit_addrs +{ + /* Range is LOW <= PC < HIGH. */ + uint64_t low; + uint64_t high; + /* Compilation unit for this address range. */ + struct unit *u; +}; + +/* A growable vector of compilation unit address ranges. */ + +struct unit_addrs_vector +{ + /* Memory. This is an array of struct unit_addrs. */ + struct backtrace_vector vec; + /* Number of address ranges present. */ + size_t count; +}; + +/* A growable vector of compilation unit pointer. */ + +struct unit_vector +{ + struct backtrace_vector vec; + size_t count; +}; + +/* The information we need to map a PC to a file and line. */ + +struct dwarf_data +{ + /* The data for the next file we know about. */ + struct dwarf_data *next; + /* The data for .gnu_debugaltlink. */ + struct dwarf_data *altlink; + /* The base address for this file. */ + uintptr_t base_address; + /* A sorted list of address ranges. */ + struct unit_addrs *addrs; + /* Number of address ranges in list. */ + size_t addrs_count; + /* A sorted list of units. */ + struct unit **units; + /* Number of units in the list. */ + size_t units_count; + /* The unparsed DWARF debug data. */ + struct dwarf_sections dwarf_sections; + /* Whether the data is big-endian or not. */ + int is_bigendian; + /* A vector used for function addresses. We keep this here so that + we can grow the vector as we read more functions. */ + struct function_vector fvec; +}; + +/* Report an error for a DWARF buffer. */ + +static void +dwarf_buf_error (struct dwarf_buf *buf, const char *msg, int errnum) +{ + char b[200]; + + snprintf (b, sizeof b, "%s in %s at %d", + msg, buf->name, (int) (buf->buf - buf->start)); + buf->error_callback (buf->data, b, errnum); +} + +/* Require at least COUNT bytes in BUF. Return 1 if all is well, 0 on + error. */ + +static int +require (struct dwarf_buf *buf, size_t count) +{ + if (buf->left >= count) + return 1; + + if (!buf->reported_underflow) + { + dwarf_buf_error (buf, "DWARF underflow", 0); + buf->reported_underflow = 1; + } + + return 0; +} + +/* Advance COUNT bytes in BUF. Return 1 if all is well, 0 on + error. */ + +static int +advance (struct dwarf_buf *buf, size_t count) +{ + if (!require (buf, count)) + return 0; + buf->buf += count; + buf->left -= count; + return 1; +} + +/* Read one zero-terminated string from BUF and advance past the string. */ + +static const char * +read_string (struct dwarf_buf *buf) +{ + const char *p = (const char *)buf->buf; + size_t len = strnlen (p, buf->left); + + /* - If len == left, we ran out of buffer before finding the zero terminator. + Generate an error by advancing len + 1. + - If len < left, advance by len + 1 to skip past the zero terminator. */ + size_t count = len + 1; + + if (!advance (buf, count)) + return NULL; + + return p; +} + +/* Read one byte from BUF and advance 1 byte. */ + +static unsigned char +read_byte (struct dwarf_buf *buf) +{ + const unsigned char *p = buf->buf; + + if (!advance (buf, 1)) + return 0; + return p[0]; +} + +/* Read a signed char from BUF and advance 1 byte. */ + +static signed char +read_sbyte (struct dwarf_buf *buf) +{ + const unsigned char *p = buf->buf; + + if (!advance (buf, 1)) + return 0; + return (*p ^ 0x80) - 0x80; +} + +/* Read a uint16 from BUF and advance 2 bytes. */ + +static uint16_t +read_uint16 (struct dwarf_buf *buf) +{ + const unsigned char *p = buf->buf; + + if (!advance (buf, 2)) + return 0; + if (buf->is_bigendian) + return ((uint16_t) p[0] << 8) | (uint16_t) p[1]; + else + return ((uint16_t) p[1] << 8) | (uint16_t) p[0]; +} + +/* Read a 24 bit value from BUF and advance 3 bytes. */ + +static uint32_t +read_uint24 (struct dwarf_buf *buf) +{ + const unsigned char *p = buf->buf; + + if (!advance (buf, 3)) + return 0; + if (buf->is_bigendian) + return (((uint32_t) p[0] << 16) | ((uint32_t) p[1] << 8) + | (uint32_t) p[2]); + else + return (((uint32_t) p[2] << 16) | ((uint32_t) p[1] << 8) + | (uint32_t) p[0]); +} + +/* Read a uint32 from BUF and advance 4 bytes. */ + +static uint32_t +read_uint32 (struct dwarf_buf *buf) +{ + const unsigned char *p = buf->buf; + + if (!advance (buf, 4)) + return 0; + if (buf->is_bigendian) + return (((uint32_t) p[0] << 24) | ((uint32_t) p[1] << 16) + | ((uint32_t) p[2] << 8) | (uint32_t) p[3]); + else + return (((uint32_t) p[3] << 24) | ((uint32_t) p[2] << 16) + | ((uint32_t) p[1] << 8) | (uint32_t) p[0]); +} + +/* Read a uint64 from BUF and advance 8 bytes. */ + +static uint64_t +read_uint64 (struct dwarf_buf *buf) +{ + const unsigned char *p = buf->buf; + + if (!advance (buf, 8)) + return 0; + if (buf->is_bigendian) + return (((uint64_t) p[0] << 56) | ((uint64_t) p[1] << 48) + | ((uint64_t) p[2] << 40) | ((uint64_t) p[3] << 32) + | ((uint64_t) p[4] << 24) | ((uint64_t) p[5] << 16) + | ((uint64_t) p[6] << 8) | (uint64_t) p[7]); + else + return (((uint64_t) p[7] << 56) | ((uint64_t) p[6] << 48) + | ((uint64_t) p[5] << 40) | ((uint64_t) p[4] << 32) + | ((uint64_t) p[3] << 24) | ((uint64_t) p[2] << 16) + | ((uint64_t) p[1] << 8) | (uint64_t) p[0]); +} + +/* Read an offset from BUF and advance the appropriate number of + bytes. */ + +static uint64_t +read_offset (struct dwarf_buf *buf, int is_dwarf64) +{ + if (is_dwarf64) + return read_uint64 (buf); + else + return read_uint32 (buf); +} + +/* Read an address from BUF and advance the appropriate number of + bytes. */ + +static uint64_t +read_address (struct dwarf_buf *buf, int addrsize) +{ + switch (addrsize) + { + case 1: + return read_byte (buf); + case 2: + return read_uint16 (buf); + case 4: + return read_uint32 (buf); + case 8: + return read_uint64 (buf); + default: + dwarf_buf_error (buf, "unrecognized address size", 0); + return 0; + } +} + +/* Return whether a value is the highest possible address, given the + address size. */ + +static int +is_highest_address (uint64_t address, int addrsize) +{ + switch (addrsize) + { + case 1: + return address == (unsigned char) -1; + case 2: + return address == (uint16_t) -1; + case 4: + return address == (uint32_t) -1; + case 8: + return address == (uint64_t) -1; + default: + return 0; + } +} + +/* Read an unsigned LEB128 number. */ + +static uint64_t +read_uleb128 (struct dwarf_buf *buf) +{ + uint64_t ret; + unsigned int shift; + int overflow; + unsigned char b; + + ret = 0; + shift = 0; + overflow = 0; + do + { + const unsigned char *p; + + p = buf->buf; + if (!advance (buf, 1)) + return 0; + b = *p; + if (shift < 64) + ret |= ((uint64_t) (b & 0x7f)) << shift; + else if (!overflow) + { + dwarf_buf_error (buf, "LEB128 overflows uint64_t", 0); + overflow = 1; + } + shift += 7; + } + while ((b & 0x80) != 0); + + return ret; +} + +/* Read a signed LEB128 number. */ + +static int64_t +read_sleb128 (struct dwarf_buf *buf) +{ + uint64_t val; + unsigned int shift; + int overflow; + unsigned char b; + + val = 0; + shift = 0; + overflow = 0; + do + { + const unsigned char *p; + + p = buf->buf; + if (!advance (buf, 1)) + return 0; + b = *p; + if (shift < 64) + val |= ((uint64_t) (b & 0x7f)) << shift; + else if (!overflow) + { + dwarf_buf_error (buf, "signed LEB128 overflows uint64_t", 0); + overflow = 1; + } + shift += 7; + } + while ((b & 0x80) != 0); + + if ((b & 0x40) != 0 && shift < 64) + val |= ((uint64_t) -1) << shift; + + return (int64_t) val; +} + +/* Return the length of an LEB128 number. */ + +static size_t +leb128_len (const unsigned char *p) +{ + size_t ret; + + ret = 1; + while ((*p & 0x80) != 0) + { + ++p; + ++ret; + } + return ret; +} + +/* Read initial_length from BUF and advance the appropriate number of bytes. */ + +static uint64_t +read_initial_length (struct dwarf_buf *buf, int *is_dwarf64) +{ + uint64_t len; + + len = read_uint32 (buf); + if (len == 0xffffffff) + { + len = read_uint64 (buf); + *is_dwarf64 = 1; + } + else + *is_dwarf64 = 0; + + return len; +} + +/* Free an abbreviations structure. */ + +static void +free_abbrevs (struct backtrace_state *state, struct abbrevs *abbrevs, + backtrace_error_callback error_callback, void *data) +{ + size_t i; + + for (i = 0; i < abbrevs->num_abbrevs; ++i) + backtrace_free (state, abbrevs->abbrevs[i].attrs, + abbrevs->abbrevs[i].num_attrs * sizeof (struct attr), + error_callback, data); + backtrace_free (state, abbrevs->abbrevs, + abbrevs->num_abbrevs * sizeof (struct abbrev), + error_callback, data); + abbrevs->num_abbrevs = 0; + abbrevs->abbrevs = NULL; +} + +/* Read an attribute value. Returns 1 on success, 0 on failure. If + the value can be represented as a uint64_t, sets *VAL and sets + *IS_VALID to 1. We don't try to store the value of other attribute + forms, because we don't care about them. */ + +static int +read_attribute (enum dwarf_form form, uint64_t implicit_val, + struct dwarf_buf *buf, int is_dwarf64, int version, + int addrsize, const struct dwarf_sections *dwarf_sections, + struct dwarf_data *altlink, struct attr_val *val) +{ + /* Avoid warnings about val.u.FIELD may be used uninitialized if + this function is inlined. The warnings aren't valid but can + occur because the different fields are set and used + conditionally. */ + memset (val, 0, sizeof *val); + + switch (form) + { + case DW_FORM_addr: + val->encoding = ATTR_VAL_ADDRESS; + val->u.uint = read_address (buf, addrsize); + return 1; + case DW_FORM_block2: + val->encoding = ATTR_VAL_BLOCK; + return advance (buf, read_uint16 (buf)); + case DW_FORM_block4: + val->encoding = ATTR_VAL_BLOCK; + return advance (buf, read_uint32 (buf)); + case DW_FORM_data2: + val->encoding = ATTR_VAL_UINT; + val->u.uint = read_uint16 (buf); + return 1; + case DW_FORM_data4: + val->encoding = ATTR_VAL_UINT; + val->u.uint = read_uint32 (buf); + return 1; + case DW_FORM_data8: + val->encoding = ATTR_VAL_UINT; + val->u.uint = read_uint64 (buf); + return 1; + case DW_FORM_data16: + val->encoding = ATTR_VAL_BLOCK; + return advance (buf, 16); + case DW_FORM_string: + val->encoding = ATTR_VAL_STRING; + val->u.string = read_string (buf); + return val->u.string == NULL ? 0 : 1; + case DW_FORM_block: + val->encoding = ATTR_VAL_BLOCK; + return advance (buf, read_uleb128 (buf)); + case DW_FORM_block1: + val->encoding = ATTR_VAL_BLOCK; + return advance (buf, read_byte (buf)); + case DW_FORM_data1: + val->encoding = ATTR_VAL_UINT; + val->u.uint = read_byte (buf); + return 1; + case DW_FORM_flag: + val->encoding = ATTR_VAL_UINT; + val->u.uint = read_byte (buf); + return 1; + case DW_FORM_sdata: + val->encoding = ATTR_VAL_SINT; + val->u.sint = read_sleb128 (buf); + return 1; + case DW_FORM_strp: + { + uint64_t offset; + + offset = read_offset (buf, is_dwarf64); + if (offset >= dwarf_sections->size[DEBUG_STR]) + { + dwarf_buf_error (buf, "DW_FORM_strp out of range", 0); + return 0; + } + val->encoding = ATTR_VAL_STRING; + val->u.string = + (const char *) dwarf_sections->data[DEBUG_STR] + offset; + return 1; + } + case DW_FORM_line_strp: + { + uint64_t offset; + + offset = read_offset (buf, is_dwarf64); + if (offset >= dwarf_sections->size[DEBUG_LINE_STR]) + { + dwarf_buf_error (buf, "DW_FORM_line_strp out of range", 0); + return 0; + } + val->encoding = ATTR_VAL_STRING; + val->u.string = + (const char *) dwarf_sections->data[DEBUG_LINE_STR] + offset; + return 1; + } + case DW_FORM_udata: + val->encoding = ATTR_VAL_UINT; + val->u.uint = read_uleb128 (buf); + return 1; + case DW_FORM_ref_addr: + val->encoding = ATTR_VAL_REF_INFO; + if (version == 2) + val->u.uint = read_address (buf, addrsize); + else + val->u.uint = read_offset (buf, is_dwarf64); + return 1; + case DW_FORM_ref1: + val->encoding = ATTR_VAL_REF_UNIT; + val->u.uint = read_byte (buf); + return 1; + case DW_FORM_ref2: + val->encoding = ATTR_VAL_REF_UNIT; + val->u.uint = read_uint16 (buf); + return 1; + case DW_FORM_ref4: + val->encoding = ATTR_VAL_REF_UNIT; + val->u.uint = read_uint32 (buf); + return 1; + case DW_FORM_ref8: + val->encoding = ATTR_VAL_REF_UNIT; + val->u.uint = read_uint64 (buf); + return 1; + case DW_FORM_ref_udata: + val->encoding = ATTR_VAL_REF_UNIT; + val->u.uint = read_uleb128 (buf); + return 1; + case DW_FORM_indirect: + { + uint64_t form; + + form = read_uleb128 (buf); + if (form == DW_FORM_implicit_const) + { + dwarf_buf_error (buf, + "DW_FORM_indirect to DW_FORM_implicit_const", + 0); + return 0; + } + return read_attribute ((enum dwarf_form) form, 0, buf, is_dwarf64, + version, addrsize, dwarf_sections, altlink, + val); + } + case DW_FORM_sec_offset: + val->encoding = ATTR_VAL_REF_SECTION; + val->u.uint = read_offset (buf, is_dwarf64); + return 1; + case DW_FORM_exprloc: + val->encoding = ATTR_VAL_EXPR; + return advance (buf, read_uleb128 (buf)); + case DW_FORM_flag_present: + val->encoding = ATTR_VAL_UINT; + val->u.uint = 1; + return 1; + case DW_FORM_ref_sig8: + val->encoding = ATTR_VAL_REF_TYPE; + val->u.uint = read_uint64 (buf); + return 1; + case DW_FORM_strx: case DW_FORM_strx1: case DW_FORM_strx2: + case DW_FORM_strx3: case DW_FORM_strx4: + { + uint64_t offset; + + switch (form) + { + case DW_FORM_strx: + offset = read_uleb128 (buf); + break; + case DW_FORM_strx1: + offset = read_byte (buf); + break; + case DW_FORM_strx2: + offset = read_uint16 (buf); + break; + case DW_FORM_strx3: + offset = read_uint24 (buf); + break; + case DW_FORM_strx4: + offset = read_uint32 (buf); + break; + default: + /* This case can't happen. */ + return 0; + } + val->encoding = ATTR_VAL_STRING_INDEX; + val->u.uint = offset; + return 1; + } + case DW_FORM_addrx: case DW_FORM_addrx1: case DW_FORM_addrx2: + case DW_FORM_addrx3: case DW_FORM_addrx4: + { + uint64_t offset; + + switch (form) + { + case DW_FORM_addrx: + offset = read_uleb128 (buf); + break; + case DW_FORM_addrx1: + offset = read_byte (buf); + break; + case DW_FORM_addrx2: + offset = read_uint16 (buf); + break; + case DW_FORM_addrx3: + offset = read_uint24 (buf); + break; + case DW_FORM_addrx4: + offset = read_uint32 (buf); + break; + default: + /* This case can't happen. */ + return 0; + } + val->encoding = ATTR_VAL_ADDRESS_INDEX; + val->u.uint = offset; + return 1; + } + case DW_FORM_ref_sup4: + val->encoding = ATTR_VAL_REF_SECTION; + val->u.uint = read_uint32 (buf); + return 1; + case DW_FORM_ref_sup8: + val->encoding = ATTR_VAL_REF_SECTION; + val->u.uint = read_uint64 (buf); + return 1; + case DW_FORM_implicit_const: + val->encoding = ATTR_VAL_UINT; + val->u.uint = implicit_val; + return 1; + case DW_FORM_loclistx: + /* We don't distinguish this from DW_FORM_sec_offset. It + * shouldn't matter since we don't care about loclists. */ + val->encoding = ATTR_VAL_REF_SECTION; + val->u.uint = read_uleb128 (buf); + return 1; + case DW_FORM_rnglistx: + val->encoding = ATTR_VAL_RNGLISTS_INDEX; + val->u.uint = read_uleb128 (buf); + return 1; + case DW_FORM_GNU_addr_index: + val->encoding = ATTR_VAL_REF_SECTION; + val->u.uint = read_uleb128 (buf); + return 1; + case DW_FORM_GNU_str_index: + val->encoding = ATTR_VAL_REF_SECTION; + val->u.uint = read_uleb128 (buf); + return 1; + case DW_FORM_GNU_ref_alt: + val->u.uint = read_offset (buf, is_dwarf64); + if (altlink == NULL) + { + val->encoding = ATTR_VAL_NONE; + return 1; + } + val->encoding = ATTR_VAL_REF_ALT_INFO; + return 1; + case DW_FORM_strp_sup: case DW_FORM_GNU_strp_alt: + { + uint64_t offset; + + offset = read_offset (buf, is_dwarf64); + if (altlink == NULL) + { + val->encoding = ATTR_VAL_NONE; + return 1; + } + if (offset >= altlink->dwarf_sections.size[DEBUG_STR]) + { + dwarf_buf_error (buf, "DW_FORM_strp_sup out of range", 0); + return 0; + } + val->encoding = ATTR_VAL_STRING; + val->u.string = + (const char *) altlink->dwarf_sections.data[DEBUG_STR] + offset; + return 1; + } + default: + dwarf_buf_error (buf, "unrecognized DWARF form", -1); + return 0; + } +} + +/* If we can determine the value of a string attribute, set *STRING to + point to the string. Return 1 on success, 0 on error. If we don't + know the value, we consider that a success, and we don't change + *STRING. An error is only reported for some sort of out of range + offset. */ + +static int +resolve_string (const struct dwarf_sections *dwarf_sections, int is_dwarf64, + int is_bigendian, uint64_t str_offsets_base, + const struct attr_val *val, + backtrace_error_callback error_callback, void *data, + const char **string) +{ + switch (val->encoding) + { + case ATTR_VAL_STRING: + *string = val->u.string; + return 1; + + case ATTR_VAL_STRING_INDEX: + { + uint64_t offset; + struct dwarf_buf offset_buf; + + offset = val->u.uint * (is_dwarf64 ? 8 : 4) + str_offsets_base; + if (offset + (is_dwarf64 ? 8 : 4) + > dwarf_sections->size[DEBUG_STR_OFFSETS]) + { + error_callback (data, "DW_FORM_strx value out of range", 0); + return 0; + } + + offset_buf.name = ".debug_str_offsets"; + offset_buf.start = dwarf_sections->data[DEBUG_STR_OFFSETS]; + offset_buf.buf = dwarf_sections->data[DEBUG_STR_OFFSETS] + offset; + offset_buf.left = dwarf_sections->size[DEBUG_STR_OFFSETS] - offset; + offset_buf.is_bigendian = is_bigendian; + offset_buf.error_callback = error_callback; + offset_buf.data = data; + offset_buf.reported_underflow = 0; + + offset = read_offset (&offset_buf, is_dwarf64); + if (offset >= dwarf_sections->size[DEBUG_STR]) + { + dwarf_buf_error (&offset_buf, + "DW_FORM_strx offset out of range", + 0); + return 0; + } + *string = (const char *) dwarf_sections->data[DEBUG_STR] + offset; + return 1; + } + + default: + return 1; + } +} + +/* Set *ADDRESS to the real address for a ATTR_VAL_ADDRESS_INDEX. + Return 1 on success, 0 on error. */ + +static int +resolve_addr_index (const struct dwarf_sections *dwarf_sections, + uint64_t addr_base, int addrsize, int is_bigendian, + uint64_t addr_index, + backtrace_error_callback error_callback, void *data, + uint64_t *address) +{ + uint64_t offset; + struct dwarf_buf addr_buf; + + offset = addr_index * addrsize + addr_base; + if (offset + addrsize > dwarf_sections->size[DEBUG_ADDR]) + { + error_callback (data, "DW_FORM_addrx value out of range", 0); + return 0; + } + + addr_buf.name = ".debug_addr"; + addr_buf.start = dwarf_sections->data[DEBUG_ADDR]; + addr_buf.buf = dwarf_sections->data[DEBUG_ADDR] + offset; + addr_buf.left = dwarf_sections->size[DEBUG_ADDR] - offset; + addr_buf.is_bigendian = is_bigendian; + addr_buf.error_callback = error_callback; + addr_buf.data = data; + addr_buf.reported_underflow = 0; + + *address = read_address (&addr_buf, addrsize); + return 1; +} + +/* Compare a unit offset against a unit for bsearch. */ + +static int +units_search (const void *vkey, const void *ventry) +{ + const size_t *key = (const size_t *) vkey; + const struct unit *entry = *((const struct unit *const *) ventry); + size_t offset; + + offset = *key; + if (offset < entry->low_offset) + return -1; + else if (offset >= entry->high_offset) + return 1; + else + return 0; +} + +/* Find a unit in PU containing OFFSET. */ + +static struct unit * +find_unit (struct unit **pu, size_t units_count, size_t offset) +{ + struct unit **u; + u = bsearch (&offset, pu, units_count, sizeof (struct unit *), units_search); + return u == NULL ? NULL : *u; +} + +/* Compare function_addrs for qsort. When ranges are nested, make the + smallest one sort last. */ + +static int +function_addrs_compare (const void *v1, const void *v2) +{ + const struct function_addrs *a1 = (const struct function_addrs *) v1; + const struct function_addrs *a2 = (const struct function_addrs *) v2; + + if (a1->low < a2->low) + return -1; + if (a1->low > a2->low) + return 1; + if (a1->high < a2->high) + return 1; + if (a1->high > a2->high) + return -1; + return strcmp (a1->function->name, a2->function->name); +} + +/* Compare a PC against a function_addrs for bsearch. We always + allocate an entra entry at the end of the vector, so that this + routine can safely look at the next entry. Note that if there are + multiple ranges containing PC, which one will be returned is + unpredictable. We compensate for that in dwarf_fileline. */ + +static int +function_addrs_search (const void *vkey, const void *ventry) +{ + const uintptr_t *key = (const uintptr_t *) vkey; + const struct function_addrs *entry = (const struct function_addrs *) ventry; + uintptr_t pc; + + pc = *key; + if (pc < entry->low) + return -1; + else if (pc > (entry + 1)->low) + return 1; + else + return 0; +} + +/* Add a new compilation unit address range to a vector. This is + called via add_ranges. Returns 1 on success, 0 on failure. */ + +static int +add_unit_addr (struct backtrace_state *state, void *rdata, + uint64_t lowpc, uint64_t highpc, + backtrace_error_callback error_callback, void *data, + void *pvec) +{ + struct unit *u = (struct unit *) rdata; + struct unit_addrs_vector *vec = (struct unit_addrs_vector *) pvec; + struct unit_addrs *p; + + /* Try to merge with the last entry. */ + if (vec->count > 0) + { + p = (struct unit_addrs *) vec->vec.base + (vec->count - 1); + if ((lowpc == p->high || lowpc == p->high + 1) + && u == p->u) + { + if (highpc > p->high) + p->high = highpc; + return 1; + } + } + + p = ((struct unit_addrs *) + backtrace_vector_grow (state, sizeof (struct unit_addrs), + error_callback, data, &vec->vec)); + if (p == NULL) + return 0; + + p->low = lowpc; + p->high = highpc; + p->u = u; + + ++vec->count; + + return 1; +} + +/* Compare unit_addrs for qsort. When ranges are nested, make the + smallest one sort last. */ + +static int +unit_addrs_compare (const void *v1, const void *v2) +{ + const struct unit_addrs *a1 = (const struct unit_addrs *) v1; + const struct unit_addrs *a2 = (const struct unit_addrs *) v2; + + if (a1->low < a2->low) + return -1; + if (a1->low > a2->low) + return 1; + if (a1->high < a2->high) + return 1; + if (a1->high > a2->high) + return -1; + if (a1->u->lineoff < a2->u->lineoff) + return -1; + if (a1->u->lineoff > a2->u->lineoff) + return 1; + return 0; +} + +/* Compare a PC against a unit_addrs for bsearch. We always allocate + an entry entry at the end of the vector, so that this routine can + safely look at the next entry. Note that if there are multiple + ranges containing PC, which one will be returned is unpredictable. + We compensate for that in dwarf_fileline. */ + +static int +unit_addrs_search (const void *vkey, const void *ventry) +{ + const uintptr_t *key = (const uintptr_t *) vkey; + const struct unit_addrs *entry = (const struct unit_addrs *) ventry; + uintptr_t pc; + + pc = *key; + if (pc < entry->low) + return -1; + else if (pc > (entry + 1)->low) + return 1; + else + return 0; +} + +/* Sort the line vector by PC. We want a stable sort here to maintain + the order of lines for the same PC values. Since the sequence is + being sorted in place, their addresses cannot be relied on to + maintain stability. That is the purpose of the index member. */ + +static int +line_compare (const void *v1, const void *v2) +{ + const struct line *ln1 = (const struct line *) v1; + const struct line *ln2 = (const struct line *) v2; + + if (ln1->pc < ln2->pc) + return -1; + else if (ln1->pc > ln2->pc) + return 1; + else if (ln1->idx < ln2->idx) + return -1; + else if (ln1->idx > ln2->idx) + return 1; + else + return 0; +} + +/* Find a PC in a line vector. We always allocate an extra entry at + the end of the lines vector, so that this routine can safely look + at the next entry. Note that when there are multiple mappings for + the same PC value, this will return the last one. */ + +static int +line_search (const void *vkey, const void *ventry) +{ + const uintptr_t *key = (const uintptr_t *) vkey; + const struct line *entry = (const struct line *) ventry; + uintptr_t pc; + + pc = *key; + if (pc < entry->pc) + return -1; + else if (pc >= (entry + 1)->pc) + return 1; + else + return 0; +} + +/* Sort the abbrevs by the abbrev code. This function is passed to + both qsort and bsearch. */ + +static int +abbrev_compare (const void *v1, const void *v2) +{ + const struct abbrev *a1 = (const struct abbrev *) v1; + const struct abbrev *a2 = (const struct abbrev *) v2; + + if (a1->code < a2->code) + return -1; + else if (a1->code > a2->code) + return 1; + else + { + /* This really shouldn't happen. It means there are two + different abbrevs with the same code, and that means we don't + know which one lookup_abbrev should return. */ + return 0; + } +} + +/* Read the abbreviation table for a compilation unit. Returns 1 on + success, 0 on failure. */ + +static int +read_abbrevs (struct backtrace_state *state, uint64_t abbrev_offset, + const unsigned char *dwarf_abbrev, size_t dwarf_abbrev_size, + int is_bigendian, backtrace_error_callback error_callback, + void *data, struct abbrevs *abbrevs) +{ + struct dwarf_buf abbrev_buf; + struct dwarf_buf count_buf; + size_t num_abbrevs; + + abbrevs->num_abbrevs = 0; + abbrevs->abbrevs = NULL; + + if (abbrev_offset >= dwarf_abbrev_size) + { + error_callback (data, "abbrev offset out of range", 0); + return 0; + } + + abbrev_buf.name = ".debug_abbrev"; + abbrev_buf.start = dwarf_abbrev; + abbrev_buf.buf = dwarf_abbrev + abbrev_offset; + abbrev_buf.left = dwarf_abbrev_size - abbrev_offset; + abbrev_buf.is_bigendian = is_bigendian; + abbrev_buf.error_callback = error_callback; + abbrev_buf.data = data; + abbrev_buf.reported_underflow = 0; + + /* Count the number of abbrevs in this list. */ + + count_buf = abbrev_buf; + num_abbrevs = 0; + while (read_uleb128 (&count_buf) != 0) + { + if (count_buf.reported_underflow) + return 0; + ++num_abbrevs; + // Skip tag. + read_uleb128 (&count_buf); + // Skip has_children. + read_byte (&count_buf); + // Skip attributes. + while (read_uleb128 (&count_buf) != 0) + { + uint64_t form; + + form = read_uleb128 (&count_buf); + if ((enum dwarf_form) form == DW_FORM_implicit_const) + read_sleb128 (&count_buf); + } + // Skip form of last attribute. + read_uleb128 (&count_buf); + } + + if (count_buf.reported_underflow) + return 0; + + if (num_abbrevs == 0) + return 1; + + abbrevs->abbrevs = ((struct abbrev *) + backtrace_alloc (state, + num_abbrevs * sizeof (struct abbrev), + error_callback, data)); + if (abbrevs->abbrevs == NULL) + return 0; + abbrevs->num_abbrevs = num_abbrevs; + memset (abbrevs->abbrevs, 0, num_abbrevs * sizeof (struct abbrev)); + + num_abbrevs = 0; + while (1) + { + uint64_t code; + struct abbrev a; + size_t num_attrs; + struct attr *attrs; + + if (abbrev_buf.reported_underflow) + goto fail; + + code = read_uleb128 (&abbrev_buf); + if (code == 0) + break; + + a.code = code; + a.tag = (enum dwarf_tag) read_uleb128 (&abbrev_buf); + a.has_children = read_byte (&abbrev_buf); + + count_buf = abbrev_buf; + num_attrs = 0; + while (read_uleb128 (&count_buf) != 0) + { + uint64_t form; + + ++num_attrs; + form = read_uleb128 (&count_buf); + if ((enum dwarf_form) form == DW_FORM_implicit_const) + read_sleb128 (&count_buf); + } + + if (num_attrs == 0) + { + attrs = NULL; + read_uleb128 (&abbrev_buf); + read_uleb128 (&abbrev_buf); + } + else + { + attrs = ((struct attr *) + backtrace_alloc (state, num_attrs * sizeof *attrs, + error_callback, data)); + if (attrs == NULL) + goto fail; + num_attrs = 0; + while (1) + { + uint64_t name; + uint64_t form; + + name = read_uleb128 (&abbrev_buf); + form = read_uleb128 (&abbrev_buf); + if (name == 0) + break; + attrs[num_attrs].name = (enum dwarf_attribute) name; + attrs[num_attrs].form = (enum dwarf_form) form; + if ((enum dwarf_form) form == DW_FORM_implicit_const) + attrs[num_attrs].val = read_sleb128 (&abbrev_buf); + else + attrs[num_attrs].val = 0; + ++num_attrs; + } + } + + a.num_attrs = num_attrs; + a.attrs = attrs; + + abbrevs->abbrevs[num_abbrevs] = a; + ++num_abbrevs; + } + + backtrace_qsort (abbrevs->abbrevs, abbrevs->num_abbrevs, + sizeof (struct abbrev), abbrev_compare); + + return 1; + + fail: + free_abbrevs (state, abbrevs, error_callback, data); + return 0; +} + +/* Return the abbrev information for an abbrev code. */ + +static const struct abbrev * +lookup_abbrev (struct abbrevs *abbrevs, uint64_t code, + backtrace_error_callback error_callback, void *data) +{ + struct abbrev key; + void *p; + + /* With GCC, where abbrevs are simply numbered in order, we should + be able to just look up the entry. */ + if (code - 1 < abbrevs->num_abbrevs + && abbrevs->abbrevs[code - 1].code == code) + return &abbrevs->abbrevs[code - 1]; + + /* Otherwise we have to search. */ + memset (&key, 0, sizeof key); + key.code = code; + p = bsearch (&key, abbrevs->abbrevs, abbrevs->num_abbrevs, + sizeof (struct abbrev), abbrev_compare); + if (p == NULL) + { + error_callback (data, "invalid abbreviation code", 0); + return NULL; + } + return (const struct abbrev *) p; +} + +/* This struct is used to gather address range information while + reading attributes. We use this while building a mapping from + address ranges to compilation units and then again while mapping + from address ranges to function entries. Normally either + lowpc/highpc is set or ranges is set. */ + +struct pcrange { + uint64_t lowpc; /* The low PC value. */ + int have_lowpc; /* Whether a low PC value was found. */ + int lowpc_is_addr_index; /* Whether lowpc is in .debug_addr. */ + uint64_t highpc; /* The high PC value. */ + int have_highpc; /* Whether a high PC value was found. */ + int highpc_is_relative; /* Whether highpc is relative to lowpc. */ + int highpc_is_addr_index; /* Whether highpc is in .debug_addr. */ + uint64_t ranges; /* Offset in ranges section. */ + int have_ranges; /* Whether ranges is valid. */ + int ranges_is_index; /* Whether ranges is DW_FORM_rnglistx. */ +}; + +/* Update PCRANGE from an attribute value. */ + +static void +update_pcrange (const struct attr* attr, const struct attr_val* val, + struct pcrange *pcrange) +{ + switch (attr->name) + { + case DW_AT_low_pc: + if (val->encoding == ATTR_VAL_ADDRESS) + { + pcrange->lowpc = val->u.uint; + pcrange->have_lowpc = 1; + } + else if (val->encoding == ATTR_VAL_ADDRESS_INDEX) + { + pcrange->lowpc = val->u.uint; + pcrange->have_lowpc = 1; + pcrange->lowpc_is_addr_index = 1; + } + break; + + case DW_AT_high_pc: + if (val->encoding == ATTR_VAL_ADDRESS) + { + pcrange->highpc = val->u.uint; + pcrange->have_highpc = 1; + } + else if (val->encoding == ATTR_VAL_UINT) + { + pcrange->highpc = val->u.uint; + pcrange->have_highpc = 1; + pcrange->highpc_is_relative = 1; + } + else if (val->encoding == ATTR_VAL_ADDRESS_INDEX) + { + pcrange->highpc = val->u.uint; + pcrange->have_highpc = 1; + pcrange->highpc_is_addr_index = 1; + } + break; + + case DW_AT_ranges: + if (val->encoding == ATTR_VAL_UINT + || val->encoding == ATTR_VAL_REF_SECTION) + { + pcrange->ranges = val->u.uint; + pcrange->have_ranges = 1; + } + else if (val->encoding == ATTR_VAL_RNGLISTS_INDEX) + { + pcrange->ranges = val->u.uint; + pcrange->have_ranges = 1; + pcrange->ranges_is_index = 1; + } + break; + + default: + break; + } +} + +/* Call ADD_RANGE for a low/high PC pair. Returns 1 on success, 0 on + error. */ + +static int +add_low_high_range (struct backtrace_state *state, + const struct dwarf_sections *dwarf_sections, + uintptr_t base_address, int is_bigendian, + struct unit *u, const struct pcrange *pcrange, + int (*add_range) (struct backtrace_state *state, + void *rdata, uint64_t lowpc, + uint64_t highpc, + backtrace_error_callback error_callback, + void *data, void *vec), + void *rdata, + backtrace_error_callback error_callback, void *data, + void *vec) +{ + uint64_t lowpc; + uint64_t highpc; + + lowpc = pcrange->lowpc; + if (pcrange->lowpc_is_addr_index) + { + if (!resolve_addr_index (dwarf_sections, u->addr_base, u->addrsize, + is_bigendian, lowpc, error_callback, data, + &lowpc)) + return 0; + } + + highpc = pcrange->highpc; + if (pcrange->highpc_is_addr_index) + { + if (!resolve_addr_index (dwarf_sections, u->addr_base, u->addrsize, + is_bigendian, highpc, error_callback, data, + &highpc)) + return 0; + } + if (pcrange->highpc_is_relative) + highpc += lowpc; + + /* Add in the base address of the module when recording PC values, + so that we can look up the PC directly. */ + lowpc += base_address; + highpc += base_address; + + return add_range (state, rdata, lowpc, highpc, error_callback, data, vec); +} + +/* Call ADD_RANGE for each range read from .debug_ranges, as used in + DWARF versions 2 through 4. */ + +static int +add_ranges_from_ranges ( + struct backtrace_state *state, + const struct dwarf_sections *dwarf_sections, + uintptr_t base_address, int is_bigendian, + struct unit *u, uint64_t base, + const struct pcrange *pcrange, + int (*add_range) (struct backtrace_state *state, void *rdata, + uint64_t lowpc, uint64_t highpc, + backtrace_error_callback error_callback, void *data, + void *vec), + void *rdata, + backtrace_error_callback error_callback, void *data, + void *vec) +{ + struct dwarf_buf ranges_buf; + + if (pcrange->ranges >= dwarf_sections->size[DEBUG_RANGES]) + { + error_callback (data, "ranges offset out of range", 0); + return 0; + } + + ranges_buf.name = ".debug_ranges"; + ranges_buf.start = dwarf_sections->data[DEBUG_RANGES]; + ranges_buf.buf = dwarf_sections->data[DEBUG_RANGES] + pcrange->ranges; + ranges_buf.left = dwarf_sections->size[DEBUG_RANGES] - pcrange->ranges; + ranges_buf.is_bigendian = is_bigendian; + ranges_buf.error_callback = error_callback; + ranges_buf.data = data; + ranges_buf.reported_underflow = 0; + + while (1) + { + uint64_t low; + uint64_t high; + + if (ranges_buf.reported_underflow) + return 0; + + low = read_address (&ranges_buf, u->addrsize); + high = read_address (&ranges_buf, u->addrsize); + + if (low == 0 && high == 0) + break; + + if (is_highest_address (low, u->addrsize)) + base = high; + else + { + if (!add_range (state, rdata, + low + base + base_address, + high + base + base_address, + error_callback, data, vec)) + return 0; + } + } + + if (ranges_buf.reported_underflow) + return 0; + + return 1; +} + +/* Call ADD_RANGE for each range read from .debug_rnglists, as used in + DWARF version 5. */ + +static int +add_ranges_from_rnglists ( + struct backtrace_state *state, + const struct dwarf_sections *dwarf_sections, + uintptr_t base_address, int is_bigendian, + struct unit *u, uint64_t base, + const struct pcrange *pcrange, + int (*add_range) (struct backtrace_state *state, void *rdata, + uint64_t lowpc, uint64_t highpc, + backtrace_error_callback error_callback, void *data, + void *vec), + void *rdata, + backtrace_error_callback error_callback, void *data, + void *vec) +{ + uint64_t offset; + struct dwarf_buf rnglists_buf; + + if (!pcrange->ranges_is_index) + offset = pcrange->ranges; + else + offset = u->rnglists_base + pcrange->ranges * (u->is_dwarf64 ? 8 : 4); + if (offset >= dwarf_sections->size[DEBUG_RNGLISTS]) + { + error_callback (data, "rnglists offset out of range", 0); + return 0; + } + + rnglists_buf.name = ".debug_rnglists"; + rnglists_buf.start = dwarf_sections->data[DEBUG_RNGLISTS]; + rnglists_buf.buf = dwarf_sections->data[DEBUG_RNGLISTS] + offset; + rnglists_buf.left = dwarf_sections->size[DEBUG_RNGLISTS] - offset; + rnglists_buf.is_bigendian = is_bigendian; + rnglists_buf.error_callback = error_callback; + rnglists_buf.data = data; + rnglists_buf.reported_underflow = 0; + + if (pcrange->ranges_is_index) + { + offset = read_offset (&rnglists_buf, u->is_dwarf64); + offset += u->rnglists_base; + if (offset >= dwarf_sections->size[DEBUG_RNGLISTS]) + { + error_callback (data, "rnglists index offset out of range", 0); + return 0; + } + rnglists_buf.buf = dwarf_sections->data[DEBUG_RNGLISTS] + offset; + rnglists_buf.left = dwarf_sections->size[DEBUG_RNGLISTS] - offset; + } + + while (1) + { + unsigned char rle; + + rle = read_byte (&rnglists_buf); + if (rle == DW_RLE_end_of_list) + break; + switch (rle) + { + case DW_RLE_base_addressx: + { + uint64_t index; + + index = read_uleb128 (&rnglists_buf); + if (!resolve_addr_index (dwarf_sections, u->addr_base, + u->addrsize, is_bigendian, index, + error_callback, data, &base)) + return 0; + } + break; + + case DW_RLE_startx_endx: + { + uint64_t index; + uint64_t low; + uint64_t high; + + index = read_uleb128 (&rnglists_buf); + if (!resolve_addr_index (dwarf_sections, u->addr_base, + u->addrsize, is_bigendian, index, + error_callback, data, &low)) + return 0; + index = read_uleb128 (&rnglists_buf); + if (!resolve_addr_index (dwarf_sections, u->addr_base, + u->addrsize, is_bigendian, index, + error_callback, data, &high)) + return 0; + if (!add_range (state, rdata, low + base_address, + high + base_address, error_callback, data, + vec)) + return 0; + } + break; + + case DW_RLE_startx_length: + { + uint64_t index; + uint64_t low; + uint64_t length; + + index = read_uleb128 (&rnglists_buf); + if (!resolve_addr_index (dwarf_sections, u->addr_base, + u->addrsize, is_bigendian, index, + error_callback, data, &low)) + return 0; + length = read_uleb128 (&rnglists_buf); + low += base_address; + if (!add_range (state, rdata, low, low + length, + error_callback, data, vec)) + return 0; + } + break; + + case DW_RLE_offset_pair: + { + uint64_t low; + uint64_t high; + + low = read_uleb128 (&rnglists_buf); + high = read_uleb128 (&rnglists_buf); + if (!add_range (state, rdata, low + base + base_address, + high + base + base_address, + error_callback, data, vec)) + return 0; + } + break; + + case DW_RLE_base_address: + base = read_address (&rnglists_buf, u->addrsize); + break; + + case DW_RLE_start_end: + { + uint64_t low; + uint64_t high; + + low = read_address (&rnglists_buf, u->addrsize); + high = read_address (&rnglists_buf, u->addrsize); + if (!add_range (state, rdata, low + base_address, + high + base_address, error_callback, data, + vec)) + return 0; + } + break; + + case DW_RLE_start_length: + { + uint64_t low; + uint64_t length; + + low = read_address (&rnglists_buf, u->addrsize); + length = read_uleb128 (&rnglists_buf); + low += base_address; + if (!add_range (state, rdata, low, low + length, + error_callback, data, vec)) + return 0; + } + break; + + default: + dwarf_buf_error (&rnglists_buf, "unrecognized DW_RLE value", -1); + return 0; + } + } + + if (rnglists_buf.reported_underflow) + return 0; + + return 1; +} + +/* Call ADD_RANGE for each lowpc/highpc pair in PCRANGE. RDATA is + passed to ADD_RANGE, and is either a struct unit * or a struct + function *. VEC is the vector we are adding ranges to, and is + either a struct unit_addrs_vector * or a struct function_vector *. + Returns 1 on success, 0 on error. */ + +static int +add_ranges (struct backtrace_state *state, + const struct dwarf_sections *dwarf_sections, + uintptr_t base_address, int is_bigendian, + struct unit *u, uint64_t base, const struct pcrange *pcrange, + int (*add_range) (struct backtrace_state *state, void *rdata, + uint64_t lowpc, uint64_t highpc, + backtrace_error_callback error_callback, + void *data, void *vec), + void *rdata, + backtrace_error_callback error_callback, void *data, + void *vec) +{ + if (pcrange->have_lowpc && pcrange->have_highpc) + return add_low_high_range (state, dwarf_sections, base_address, + is_bigendian, u, pcrange, add_range, rdata, + error_callback, data, vec); + + if (!pcrange->have_ranges) + { + /* Did not find any address ranges to add. */ + return 1; + } + + if (u->version < 5) + return add_ranges_from_ranges (state, dwarf_sections, base_address, + is_bigendian, u, base, pcrange, add_range, + rdata, error_callback, data, vec); + else + return add_ranges_from_rnglists (state, dwarf_sections, base_address, + is_bigendian, u, base, pcrange, add_range, + rdata, error_callback, data, vec); +} + +/* Find the address range covered by a compilation unit, reading from + UNIT_BUF and adding values to U. Returns 1 if all data could be + read, 0 if there is some error. */ + +static int +find_address_ranges (struct backtrace_state *state, uintptr_t base_address, + struct dwarf_buf *unit_buf, + const struct dwarf_sections *dwarf_sections, + int is_bigendian, struct dwarf_data *altlink, + backtrace_error_callback error_callback, void *data, + struct unit *u, struct unit_addrs_vector *addrs, + enum dwarf_tag *unit_tag) +{ + while (unit_buf->left > 0) + { + uint64_t code; + const struct abbrev *abbrev; + struct pcrange pcrange; + struct attr_val name_val; + int have_name_val; + struct attr_val comp_dir_val; + int have_comp_dir_val; + size_t i; + + code = read_uleb128 (unit_buf); + if (code == 0) + return 1; + + abbrev = lookup_abbrev (&u->abbrevs, code, error_callback, data); + if (abbrev == NULL) + return 0; + + if (unit_tag != NULL) + *unit_tag = abbrev->tag; + + memset (&pcrange, 0, sizeof pcrange); + memset (&name_val, 0, sizeof name_val); + have_name_val = 0; + memset (&comp_dir_val, 0, sizeof comp_dir_val); + have_comp_dir_val = 0; + for (i = 0; i < abbrev->num_attrs; ++i) + { + struct attr_val val; + + if (!read_attribute (abbrev->attrs[i].form, abbrev->attrs[i].val, + unit_buf, u->is_dwarf64, u->version, + u->addrsize, dwarf_sections, altlink, &val)) + return 0; + + switch (abbrev->attrs[i].name) + { + case DW_AT_low_pc: case DW_AT_high_pc: case DW_AT_ranges: + update_pcrange (&abbrev->attrs[i], &val, &pcrange); + break; + + case DW_AT_stmt_list: + if ((abbrev->tag == DW_TAG_compile_unit + || abbrev->tag == DW_TAG_skeleton_unit) + && (val.encoding == ATTR_VAL_UINT + || val.encoding == ATTR_VAL_REF_SECTION)) + u->lineoff = val.u.uint; + break; + + case DW_AT_name: + if (abbrev->tag == DW_TAG_compile_unit + || abbrev->tag == DW_TAG_skeleton_unit) + { + name_val = val; + have_name_val = 1; + } + break; + + case DW_AT_comp_dir: + if (abbrev->tag == DW_TAG_compile_unit + || abbrev->tag == DW_TAG_skeleton_unit) + { + comp_dir_val = val; + have_comp_dir_val = 1; + } + break; + + case DW_AT_str_offsets_base: + if ((abbrev->tag == DW_TAG_compile_unit + || abbrev->tag == DW_TAG_skeleton_unit) + && val.encoding == ATTR_VAL_REF_SECTION) + u->str_offsets_base = val.u.uint; + break; + + case DW_AT_addr_base: + if ((abbrev->tag == DW_TAG_compile_unit + || abbrev->tag == DW_TAG_skeleton_unit) + && val.encoding == ATTR_VAL_REF_SECTION) + u->addr_base = val.u.uint; + break; + + case DW_AT_rnglists_base: + if ((abbrev->tag == DW_TAG_compile_unit + || abbrev->tag == DW_TAG_skeleton_unit) + && val.encoding == ATTR_VAL_REF_SECTION) + u->rnglists_base = val.u.uint; + break; + + default: + break; + } + } + + // Resolve strings after we're sure that we have seen + // DW_AT_str_offsets_base. + if (have_name_val) + { + if (!resolve_string (dwarf_sections, u->is_dwarf64, is_bigendian, + u->str_offsets_base, &name_val, + error_callback, data, &u->filename)) + return 0; + } + if (have_comp_dir_val) + { + if (!resolve_string (dwarf_sections, u->is_dwarf64, is_bigendian, + u->str_offsets_base, &comp_dir_val, + error_callback, data, &u->comp_dir)) + return 0; + } + + if (abbrev->tag == DW_TAG_compile_unit + || abbrev->tag == DW_TAG_subprogram + || abbrev->tag == DW_TAG_skeleton_unit) + { + if (!add_ranges (state, dwarf_sections, base_address, + is_bigendian, u, pcrange.lowpc, &pcrange, + add_unit_addr, (void *) u, error_callback, data, + (void *) addrs)) + return 0; + + /* If we found the PC range in the DW_TAG_compile_unit or + DW_TAG_skeleton_unit, we can stop now. */ + if ((abbrev->tag == DW_TAG_compile_unit + || abbrev->tag == DW_TAG_skeleton_unit) + && (pcrange.have_ranges + || (pcrange.have_lowpc && pcrange.have_highpc))) + return 1; + } + + if (abbrev->has_children) + { + if (!find_address_ranges (state, base_address, unit_buf, + dwarf_sections, is_bigendian, altlink, + error_callback, data, u, addrs, NULL)) + return 0; + } + } + + return 1; +} + +/* Build a mapping from address ranges to the compilation units where + the line number information for that range can be found. Returns 1 + on success, 0 on failure. */ + +static int +build_address_map (struct backtrace_state *state, uintptr_t base_address, + const struct dwarf_sections *dwarf_sections, + int is_bigendian, struct dwarf_data *altlink, + backtrace_error_callback error_callback, void *data, + struct unit_addrs_vector *addrs, + struct unit_vector *unit_vec) +{ + struct dwarf_buf info; + struct backtrace_vector units; + size_t units_count; + size_t i; + struct unit **pu; + size_t unit_offset = 0; + struct unit_addrs *pa; + + memset (&addrs->vec, 0, sizeof addrs->vec); + memset (&unit_vec->vec, 0, sizeof unit_vec->vec); + addrs->count = 0; + unit_vec->count = 0; + + /* Read through the .debug_info section. FIXME: Should we use the + .debug_aranges section? gdb and addr2line don't use it, but I'm + not sure why. */ + + info.name = ".debug_info"; + info.start = dwarf_sections->data[DEBUG_INFO]; + info.buf = info.start; + info.left = dwarf_sections->size[DEBUG_INFO]; + info.is_bigendian = is_bigendian; + info.error_callback = error_callback; + info.data = data; + info.reported_underflow = 0; + + memset (&units, 0, sizeof units); + units_count = 0; + + while (info.left > 0) + { + const unsigned char *unit_data_start; + uint64_t len; + int is_dwarf64; + struct dwarf_buf unit_buf; + int version; + int unit_type; + uint64_t abbrev_offset; + int addrsize; + struct unit *u; + enum dwarf_tag unit_tag; + + if (info.reported_underflow) + goto fail; + + unit_data_start = info.buf; + + len = read_initial_length (&info, &is_dwarf64); + unit_buf = info; + unit_buf.left = len; + + if (!advance (&info, len)) + goto fail; + + version = read_uint16 (&unit_buf); + if (version < 2 || version > 5) + { + dwarf_buf_error (&unit_buf, "unrecognized DWARF version", -1); + goto fail; + } + + if (version < 5) + unit_type = 0; + else + { + unit_type = read_byte (&unit_buf); + if (unit_type == DW_UT_type || unit_type == DW_UT_split_type) + { + /* This unit doesn't have anything we need. */ + continue; + } + } + + pu = ((struct unit **) + backtrace_vector_grow (state, sizeof (struct unit *), + error_callback, data, &units)); + if (pu == NULL) + goto fail; + + u = ((struct unit *) + backtrace_alloc (state, sizeof *u, error_callback, data)); + if (u == NULL) + goto fail; + + *pu = u; + ++units_count; + + if (version < 5) + addrsize = 0; /* Set below. */ + else + addrsize = read_byte (&unit_buf); + + memset (&u->abbrevs, 0, sizeof u->abbrevs); + abbrev_offset = read_offset (&unit_buf, is_dwarf64); + if (!read_abbrevs (state, abbrev_offset, + dwarf_sections->data[DEBUG_ABBREV], + dwarf_sections->size[DEBUG_ABBREV], + is_bigendian, error_callback, data, &u->abbrevs)) + goto fail; + + if (version < 5) + addrsize = read_byte (&unit_buf); + + switch (unit_type) + { + case 0: + break; + case DW_UT_compile: case DW_UT_partial: + break; + case DW_UT_skeleton: case DW_UT_split_compile: + read_uint64 (&unit_buf); /* dwo_id */ + break; + default: + break; + } + + u->low_offset = unit_offset; + unit_offset += len + (is_dwarf64 ? 12 : 4); + u->high_offset = unit_offset; + u->unit_data = unit_buf.buf; + u->unit_data_len = unit_buf.left; + u->unit_data_offset = unit_buf.buf - unit_data_start; + u->version = version; + u->is_dwarf64 = is_dwarf64; + u->addrsize = addrsize; + u->filename = NULL; + u->comp_dir = NULL; + u->abs_filename = NULL; + u->lineoff = 0; + u->str_offsets_base = 0; + u->addr_base = 0; + u->rnglists_base = 0; + + /* The actual line number mappings will be read as needed. */ + u->lines = NULL; + u->lines_count = 0; + u->function_addrs = NULL; + u->function_addrs_count = 0; + + if (!find_address_ranges (state, base_address, &unit_buf, dwarf_sections, + is_bigendian, altlink, error_callback, data, + u, addrs, &unit_tag)) + goto fail; + + if (unit_buf.reported_underflow) + goto fail; + } + if (info.reported_underflow) + goto fail; + + /* Add a trailing addrs entry, but don't include it in addrs->count. */ + pa = ((struct unit_addrs *) + backtrace_vector_grow (state, sizeof (struct unit_addrs), + error_callback, data, &addrs->vec)); + if (pa == NULL) + goto fail; + pa->low = 0; + --pa->low; + pa->high = pa->low; + pa->u = NULL; + + unit_vec->vec = units; + unit_vec->count = units_count; + return 1; + + fail: + if (units_count > 0) + { + pu = (struct unit **) units.base; + for (i = 0; i < units_count; i++) + { + free_abbrevs (state, &pu[i]->abbrevs, error_callback, data); + backtrace_free (state, pu[i], sizeof **pu, error_callback, data); + } + backtrace_vector_free (state, &units, error_callback, data); + } + if (addrs->count > 0) + { + backtrace_vector_free (state, &addrs->vec, error_callback, data); + addrs->count = 0; + } + return 0; +} + +/* Add a new mapping to the vector of line mappings that we are + building. Returns 1 on success, 0 on failure. */ + +static int +add_line (struct backtrace_state *state, struct dwarf_data *ddata, + uintptr_t pc, const char *filename, int lineno, + backtrace_error_callback error_callback, void *data, + struct line_vector *vec) +{ + struct line *ln; + + /* If we are adding the same mapping, ignore it. This can happen + when using discriminators. */ + if (vec->count > 0) + { + ln = (struct line *) vec->vec.base + (vec->count - 1); + if (pc == ln->pc && filename == ln->filename && lineno == ln->lineno) + return 1; + } + + ln = ((struct line *) + backtrace_vector_grow (state, sizeof (struct line), error_callback, + data, &vec->vec)); + if (ln == NULL) + return 0; + + /* Add in the base address here, so that we can look up the PC + directly. */ + ln->pc = pc + ddata->base_address; + + ln->filename = filename; + ln->lineno = lineno; + ln->idx = vec->count; + + ++vec->count; + + return 1; +} + +/* Free the line header information. */ + +static void +free_line_header (struct backtrace_state *state, struct line_header *hdr, + backtrace_error_callback error_callback, void *data) +{ + if (hdr->dirs_count != 0) + backtrace_free (state, hdr->dirs, hdr->dirs_count * sizeof (const char *), + error_callback, data); + backtrace_free (state, hdr->filenames, + hdr->filenames_count * sizeof (char *), + error_callback, data); +} + +/* Read the directories and file names for a line header for version + 2, setting fields in HDR. Return 1 on success, 0 on failure. */ + +static int +read_v2_paths (struct backtrace_state *state, struct unit *u, + struct dwarf_buf *hdr_buf, struct line_header *hdr) +{ + const unsigned char *p; + const unsigned char *pend; + size_t i; + + /* Count the number of directory entries. */ + hdr->dirs_count = 0; + p = hdr_buf->buf; + pend = p + hdr_buf->left; + while (p < pend && *p != '\0') + { + p += strnlen((const char *) p, pend - p) + 1; + ++hdr->dirs_count; + } + + /* The index of the first entry in the list of directories is 1. Index 0 is + used for the current directory of the compilation. To simplify index + handling, we set entry 0 to the compilation unit directory. */ + ++hdr->dirs_count; + hdr->dirs = ((const char **) + backtrace_alloc (state, + hdr->dirs_count * sizeof (const char *), + hdr_buf->error_callback, + hdr_buf->data)); + if (hdr->dirs == NULL) + return 0; + + hdr->dirs[0] = u->comp_dir; + i = 1; + while (*hdr_buf->buf != '\0') + { + if (hdr_buf->reported_underflow) + return 0; + + hdr->dirs[i] = read_string (hdr_buf); + if (hdr->dirs[i] == NULL) + return 0; + ++i; + } + if (!advance (hdr_buf, 1)) + return 0; + + /* Count the number of file entries. */ + hdr->filenames_count = 0; + p = hdr_buf->buf; + pend = p + hdr_buf->left; + while (p < pend && *p != '\0') + { + p += strnlen ((const char *) p, pend - p) + 1; + p += leb128_len (p); + p += leb128_len (p); + p += leb128_len (p); + ++hdr->filenames_count; + } + + /* The index of the first entry in the list of file names is 1. Index 0 is + used for the DW_AT_name of the compilation unit. To simplify index + handling, we set entry 0 to the compilation unit file name. */ + ++hdr->filenames_count; + hdr->filenames = ((const char **) + backtrace_alloc (state, + hdr->filenames_count * sizeof (char *), + hdr_buf->error_callback, + hdr_buf->data)); + if (hdr->filenames == NULL) + return 0; + hdr->filenames[0] = u->filename; + i = 1; + while (*hdr_buf->buf != '\0') + { + const char *filename; + uint64_t dir_index; + + if (hdr_buf->reported_underflow) + return 0; + + filename = read_string (hdr_buf); + if (filename == NULL) + return 0; + dir_index = read_uleb128 (hdr_buf); + if (IS_ABSOLUTE_PATH (filename) + || (dir_index < hdr->dirs_count && hdr->dirs[dir_index] == NULL)) + hdr->filenames[i] = filename; + else + { + const char *dir; + size_t dir_len; + size_t filename_len; + char *s; + + if (dir_index < hdr->dirs_count) + dir = hdr->dirs[dir_index]; + else + { + dwarf_buf_error (hdr_buf, + ("invalid directory index in " + "line number program header"), + 0); + return 0; + } + dir_len = strlen (dir); + filename_len = strlen (filename); + s = ((char *) backtrace_alloc (state, dir_len + filename_len + 2, + hdr_buf->error_callback, + hdr_buf->data)); + if (s == NULL) + return 0; + memcpy (s, dir, dir_len); + /* FIXME: If we are on a DOS-based file system, and the + directory or the file name use backslashes, then we + should use a backslash here. */ + s[dir_len] = '/'; + memcpy (s + dir_len + 1, filename, filename_len + 1); + hdr->filenames[i] = s; + } + + /* Ignore the modification time and size. */ + read_uleb128 (hdr_buf); + read_uleb128 (hdr_buf); + + ++i; + } + + return 1; +} + +/* Read a single version 5 LNCT entry for a directory or file name in a + line header. Sets *STRING to the resulting name, ignoring other + data. Return 1 on success, 0 on failure. */ + +static int +read_lnct (struct backtrace_state *state, struct dwarf_data *ddata, + struct unit *u, struct dwarf_buf *hdr_buf, + const struct line_header *hdr, size_t formats_count, + const struct line_header_format *formats, const char **string) +{ + size_t i; + const char *dir; + const char *path; + + dir = NULL; + path = NULL; + for (i = 0; i < formats_count; i++) + { + struct attr_val val; + + if (!read_attribute (formats[i].form, 0, hdr_buf, u->is_dwarf64, + u->version, hdr->addrsize, &ddata->dwarf_sections, + ddata->altlink, &val)) + return 0; + switch (formats[i].lnct) + { + case DW_LNCT_path: + if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64, + ddata->is_bigendian, u->str_offsets_base, + &val, hdr_buf->error_callback, hdr_buf->data, + &path)) + return 0; + break; + case DW_LNCT_directory_index: + if (val.encoding == ATTR_VAL_UINT) + { + if (val.u.uint >= hdr->dirs_count) + { + dwarf_buf_error (hdr_buf, + ("invalid directory index in " + "line number program header"), + 0); + return 0; + } + dir = hdr->dirs[val.u.uint]; + } + break; + default: + /* We don't care about timestamps or sizes or hashes. */ + break; + } + } + + if (path == NULL) + { + dwarf_buf_error (hdr_buf, + "missing file name in line number program header", + 0); + return 0; + } + + if (dir == NULL) + *string = path; + else + { + size_t dir_len; + size_t path_len; + char *s; + + dir_len = strlen (dir); + path_len = strlen (path); + s = (char *) backtrace_alloc (state, dir_len + path_len + 2, + hdr_buf->error_callback, hdr_buf->data); + if (s == NULL) + return 0; + memcpy (s, dir, dir_len); + /* FIXME: If we are on a DOS-based file system, and the + directory or the path name use backslashes, then we should + use a backslash here. */ + s[dir_len] = '/'; + memcpy (s + dir_len + 1, path, path_len + 1); + *string = s; + } + + return 1; +} + +/* Read a set of DWARF 5 line header format entries, setting *PCOUNT + and *PPATHS. Return 1 on success, 0 on failure. */ + +static int +read_line_header_format_entries (struct backtrace_state *state, + struct dwarf_data *ddata, + struct unit *u, + struct dwarf_buf *hdr_buf, + struct line_header *hdr, + size_t *pcount, + const char ***ppaths) +{ + size_t formats_count; + struct line_header_format *formats; + size_t paths_count; + const char **paths; + size_t i; + int ret; + + formats_count = read_byte (hdr_buf); + if (formats_count == 0) + formats = NULL; + else + { + formats = ((struct line_header_format *) + backtrace_alloc (state, + (formats_count + * sizeof (struct line_header_format)), + hdr_buf->error_callback, + hdr_buf->data)); + if (formats == NULL) + return 0; + + for (i = 0; i < formats_count; i++) + { + formats[i].lnct = (int) read_uleb128(hdr_buf); + formats[i].form = (enum dwarf_form) read_uleb128 (hdr_buf); + } + } + + paths_count = read_uleb128 (hdr_buf); + if (paths_count == 0) + { + *pcount = 0; + *ppaths = NULL; + ret = 1; + goto exit; + } + + paths = ((const char **) + backtrace_alloc (state, paths_count * sizeof (const char *), + hdr_buf->error_callback, hdr_buf->data)); + if (paths == NULL) + { + ret = 0; + goto exit; + } + for (i = 0; i < paths_count; i++) + { + if (!read_lnct (state, ddata, u, hdr_buf, hdr, formats_count, + formats, &paths[i])) + { + backtrace_free (state, paths, + paths_count * sizeof (const char *), + hdr_buf->error_callback, hdr_buf->data); + ret = 0; + goto exit; + } + } + + *pcount = paths_count; + *ppaths = paths; + + ret = 1; + + exit: + if (formats != NULL) + backtrace_free (state, formats, + formats_count * sizeof (struct line_header_format), + hdr_buf->error_callback, hdr_buf->data); + + return ret; +} + +/* Read the line header. Return 1 on success, 0 on failure. */ + +static int +read_line_header (struct backtrace_state *state, struct dwarf_data *ddata, + struct unit *u, int is_dwarf64, struct dwarf_buf *line_buf, + struct line_header *hdr) +{ + uint64_t hdrlen; + struct dwarf_buf hdr_buf; + + hdr->version = read_uint16 (line_buf); + if (hdr->version < 2 || hdr->version > 5) + { + dwarf_buf_error (line_buf, "unsupported line number version", -1); + return 0; + } + + if (hdr->version < 5) + hdr->addrsize = u->addrsize; + else + { + hdr->addrsize = read_byte (line_buf); + /* We could support a non-zero segment_selector_size but I doubt + we'll ever see it. */ + if (read_byte (line_buf) != 0) + { + dwarf_buf_error (line_buf, + "non-zero segment_selector_size not supported", + -1); + return 0; + } + } + + hdrlen = read_offset (line_buf, is_dwarf64); + + hdr_buf = *line_buf; + hdr_buf.left = hdrlen; + + if (!advance (line_buf, hdrlen)) + return 0; + + hdr->min_insn_len = read_byte (&hdr_buf); + if (hdr->version < 4) + hdr->max_ops_per_insn = 1; + else + hdr->max_ops_per_insn = read_byte (&hdr_buf); + + /* We don't care about default_is_stmt. */ + read_byte (&hdr_buf); + + hdr->line_base = read_sbyte (&hdr_buf); + hdr->line_range = read_byte (&hdr_buf); + + hdr->opcode_base = read_byte (&hdr_buf); + hdr->opcode_lengths = hdr_buf.buf; + if (!advance (&hdr_buf, hdr->opcode_base - 1)) + return 0; + + if (hdr->version < 5) + { + if (!read_v2_paths (state, u, &hdr_buf, hdr)) + return 0; + } + else + { + if (!read_line_header_format_entries (state, ddata, u, &hdr_buf, hdr, + &hdr->dirs_count, + &hdr->dirs)) + return 0; + if (!read_line_header_format_entries (state, ddata, u, &hdr_buf, hdr, + &hdr->filenames_count, + &hdr->filenames)) + return 0; + } + + if (hdr_buf.reported_underflow) + return 0; + + return 1; +} + +/* Read the line program, adding line mappings to VEC. Return 1 on + success, 0 on failure. */ + +static int +read_line_program (struct backtrace_state *state, struct dwarf_data *ddata, + const struct line_header *hdr, struct dwarf_buf *line_buf, + struct line_vector *vec) +{ + uint64_t address; + unsigned int op_index; + const char *reset_filename; + const char *filename; + int lineno; + + address = 0; + op_index = 0; + if (hdr->filenames_count > 1) + reset_filename = hdr->filenames[1]; + else + reset_filename = ""; + filename = reset_filename; + lineno = 1; + while (line_buf->left > 0) + { + unsigned int op; + + op = read_byte (line_buf); + if (op >= hdr->opcode_base) + { + unsigned int advance; + + /* Special opcode. */ + op -= hdr->opcode_base; + advance = op / hdr->line_range; + address += (hdr->min_insn_len * (op_index + advance) + / hdr->max_ops_per_insn); + op_index = (op_index + advance) % hdr->max_ops_per_insn; + lineno += hdr->line_base + (int) (op % hdr->line_range); + add_line (state, ddata, address, filename, lineno, + line_buf->error_callback, line_buf->data, vec); + } + else if (op == DW_LNS_extended_op) + { + uint64_t len; + + len = read_uleb128 (line_buf); + op = read_byte (line_buf); + switch (op) + { + case DW_LNE_end_sequence: + /* FIXME: Should we mark the high PC here? It seems + that we already have that information from the + compilation unit. */ + address = 0; + op_index = 0; + filename = reset_filename; + lineno = 1; + break; + case DW_LNE_set_address: + address = read_address (line_buf, hdr->addrsize); + break; + case DW_LNE_define_file: + { + const char *f; + unsigned int dir_index; + + f = read_string (line_buf); + if (f == NULL) + return 0; + dir_index = read_uleb128 (line_buf); + /* Ignore that time and length. */ + read_uleb128 (line_buf); + read_uleb128 (line_buf); + if (IS_ABSOLUTE_PATH (f)) + filename = f; + else + { + const char *dir; + size_t dir_len; + size_t f_len; + char *p; + + if (dir_index < hdr->dirs_count) + dir = hdr->dirs[dir_index]; + else + { + dwarf_buf_error (line_buf, + ("invalid directory index " + "in line number program"), + 0); + return 0; + } + dir_len = strlen (dir); + f_len = strlen (f); + p = ((char *) + backtrace_alloc (state, dir_len + f_len + 2, + line_buf->error_callback, + line_buf->data)); + if (p == NULL) + return 0; + memcpy (p, dir, dir_len); + /* FIXME: If we are on a DOS-based file system, + and the directory or the file name use + backslashes, then we should use a backslash + here. */ + p[dir_len] = '/'; + memcpy (p + dir_len + 1, f, f_len + 1); + filename = p; + } + } + break; + case DW_LNE_set_discriminator: + /* We don't care about discriminators. */ + read_uleb128 (line_buf); + break; + default: + if (!advance (line_buf, len - 1)) + return 0; + break; + } + } + else + { + switch (op) + { + case DW_LNS_copy: + add_line (state, ddata, address, filename, lineno, + line_buf->error_callback, line_buf->data, vec); + break; + case DW_LNS_advance_pc: + { + uint64_t advance; + + advance = read_uleb128 (line_buf); + address += (hdr->min_insn_len * (op_index + advance) + / hdr->max_ops_per_insn); + op_index = (op_index + advance) % hdr->max_ops_per_insn; + } + break; + case DW_LNS_advance_line: + lineno += (int) read_sleb128 (line_buf); + break; + case DW_LNS_set_file: + { + uint64_t fileno; + + fileno = read_uleb128 (line_buf); + if (fileno >= hdr->filenames_count) + { + dwarf_buf_error (line_buf, + ("invalid file number in " + "line number program"), + 0); + return 0; + } + filename = hdr->filenames[fileno]; + } + break; + case DW_LNS_set_column: + read_uleb128 (line_buf); + break; + case DW_LNS_negate_stmt: + break; + case DW_LNS_set_basic_block: + break; + case DW_LNS_const_add_pc: + { + unsigned int advance; + + op = 255 - hdr->opcode_base; + advance = op / hdr->line_range; + address += (hdr->min_insn_len * (op_index + advance) + / hdr->max_ops_per_insn); + op_index = (op_index + advance) % hdr->max_ops_per_insn; + } + break; + case DW_LNS_fixed_advance_pc: + address += read_uint16 (line_buf); + op_index = 0; + break; + case DW_LNS_set_prologue_end: + break; + case DW_LNS_set_epilogue_begin: + break; + case DW_LNS_set_isa: + read_uleb128 (line_buf); + break; + default: + { + unsigned int i; + + for (i = hdr->opcode_lengths[op - 1]; i > 0; --i) + read_uleb128 (line_buf); + } + break; + } + } + } + + return 1; +} + +/* Read the line number information for a compilation unit. Returns 1 + on success, 0 on failure. */ + +static int +read_line_info (struct backtrace_state *state, struct dwarf_data *ddata, + backtrace_error_callback error_callback, void *data, + struct unit *u, struct line_header *hdr, struct line **lines, + size_t *lines_count) +{ + struct line_vector vec; + struct dwarf_buf line_buf; + uint64_t len; + int is_dwarf64; + struct line *ln; + + memset (&vec.vec, 0, sizeof vec.vec); + vec.count = 0; + + memset (hdr, 0, sizeof *hdr); + + if (u->lineoff != (off_t) (size_t) u->lineoff + || (size_t) u->lineoff >= ddata->dwarf_sections.size[DEBUG_LINE]) + { + error_callback (data, "unit line offset out of range", 0); + goto fail; + } + + line_buf.name = ".debug_line"; + line_buf.start = ddata->dwarf_sections.data[DEBUG_LINE]; + line_buf.buf = ddata->dwarf_sections.data[DEBUG_LINE] + u->lineoff; + line_buf.left = ddata->dwarf_sections.size[DEBUG_LINE] - u->lineoff; + line_buf.is_bigendian = ddata->is_bigendian; + line_buf.error_callback = error_callback; + line_buf.data = data; + line_buf.reported_underflow = 0; + + len = read_initial_length (&line_buf, &is_dwarf64); + line_buf.left = len; + + if (!read_line_header (state, ddata, u, is_dwarf64, &line_buf, hdr)) + goto fail; + + if (!read_line_program (state, ddata, hdr, &line_buf, &vec)) + goto fail; + + if (line_buf.reported_underflow) + goto fail; + + if (vec.count == 0) + { + /* This is not a failure in the sense of a generating an error, + but it is a failure in that sense that we have no useful + information. */ + goto fail; + } + + /* Allocate one extra entry at the end. */ + ln = ((struct line *) + backtrace_vector_grow (state, sizeof (struct line), error_callback, + data, &vec.vec)); + if (ln == NULL) + goto fail; + ln->pc = (uintptr_t) -1; + ln->filename = NULL; + ln->lineno = 0; + ln->idx = 0; + + if (!backtrace_vector_release (state, &vec.vec, error_callback, data)) + goto fail; + + ln = (struct line *) vec.vec.base; + backtrace_qsort (ln, vec.count, sizeof (struct line), line_compare); + + *lines = ln; + *lines_count = vec.count; + + return 1; + + fail: + backtrace_vector_free (state, &vec.vec, error_callback, data); + free_line_header (state, hdr, error_callback, data); + *lines = (struct line *) (uintptr_t) -1; + *lines_count = 0; + return 0; +} + +static const char *read_referenced_name (struct dwarf_data *, struct unit *, + uint64_t, backtrace_error_callback, + void *); + +/* Read the name of a function from a DIE referenced by ATTR with VAL. */ + +static const char * +read_referenced_name_from_attr (struct dwarf_data *ddata, struct unit *u, + struct attr *attr, struct attr_val *val, + backtrace_error_callback error_callback, + void *data) +{ + switch (attr->name) + { + case DW_AT_abstract_origin: + case DW_AT_specification: + break; + default: + return NULL; + } + + if (attr->form == DW_FORM_ref_sig8) + return NULL; + + if (val->encoding == ATTR_VAL_REF_INFO) + { + struct unit *unit + = find_unit (ddata->units, ddata->units_count, + val->u.uint); + if (unit == NULL) + return NULL; + + uint64_t offset = val->u.uint - unit->low_offset; + return read_referenced_name (ddata, unit, offset, error_callback, data); + } + + if (val->encoding == ATTR_VAL_UINT + || val->encoding == ATTR_VAL_REF_UNIT) + return read_referenced_name (ddata, u, val->u.uint, error_callback, data); + + if (val->encoding == ATTR_VAL_REF_ALT_INFO) + { + struct unit *alt_unit + = find_unit (ddata->altlink->units, ddata->altlink->units_count, + val->u.uint); + if (alt_unit == NULL) + return NULL; + + uint64_t offset = val->u.uint - alt_unit->low_offset; + return read_referenced_name (ddata->altlink, alt_unit, offset, + error_callback, data); + } + + return NULL; +} + +/* Read the name of a function from a DIE referenced by a + DW_AT_abstract_origin or DW_AT_specification tag. OFFSET is within + the same compilation unit. */ + +static const char * +read_referenced_name (struct dwarf_data *ddata, struct unit *u, + uint64_t offset, backtrace_error_callback error_callback, + void *data) +{ + struct dwarf_buf unit_buf; + uint64_t code; + const struct abbrev *abbrev; + const char *ret; + size_t i; + + /* OFFSET is from the start of the data for this compilation unit. + U->unit_data is the data, but it starts U->unit_data_offset bytes + from the beginning. */ + + if (offset < u->unit_data_offset + || offset - u->unit_data_offset >= u->unit_data_len) + { + error_callback (data, + "abstract origin or specification out of range", + 0); + return NULL; + } + + offset -= u->unit_data_offset; + + unit_buf.name = ".debug_info"; + unit_buf.start = ddata->dwarf_sections.data[DEBUG_INFO]; + unit_buf.buf = u->unit_data + offset; + unit_buf.left = u->unit_data_len - offset; + unit_buf.is_bigendian = ddata->is_bigendian; + unit_buf.error_callback = error_callback; + unit_buf.data = data; + unit_buf.reported_underflow = 0; + + code = read_uleb128 (&unit_buf); + if (code == 0) + { + dwarf_buf_error (&unit_buf, + "invalid abstract origin or specification", + 0); + return NULL; + } + + abbrev = lookup_abbrev (&u->abbrevs, code, error_callback, data); + if (abbrev == NULL) + return NULL; + + ret = NULL; + for (i = 0; i < abbrev->num_attrs; ++i) + { + struct attr_val val; + + if (!read_attribute (abbrev->attrs[i].form, abbrev->attrs[i].val, + &unit_buf, u->is_dwarf64, u->version, u->addrsize, + &ddata->dwarf_sections, ddata->altlink, &val)) + return NULL; + + switch (abbrev->attrs[i].name) + { + case DW_AT_name: + /* Third name preference: don't override. A name we found in some + other way, will normally be more useful -- e.g., this name is + normally not mangled. */ + if (ret != NULL) + break; + if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64, + ddata->is_bigendian, u->str_offsets_base, + &val, error_callback, data, &ret)) + return NULL; + break; + + case DW_AT_linkage_name: + case DW_AT_MIPS_linkage_name: + /* First name preference: override all. */ + { + const char *s; + + s = NULL; + if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64, + ddata->is_bigendian, u->str_offsets_base, + &val, error_callback, data, &s)) + return NULL; + if (s != NULL) + return s; + } + break; + + case DW_AT_specification: + /* Second name preference: override DW_AT_name, don't override + DW_AT_linkage_name. */ + { + const char *name; + + name = read_referenced_name_from_attr (ddata, u, &abbrev->attrs[i], + &val, error_callback, data); + if (name != NULL) + ret = name; + } + break; + + default: + break; + } + } + + return ret; +} + +/* Add a range to a unit that maps to a function. This is called via + add_ranges. Returns 1 on success, 0 on error. */ + +static int +add_function_range (struct backtrace_state *state, void *rdata, + uint64_t lowpc, uint64_t highpc, + backtrace_error_callback error_callback, void *data, + void *pvec) +{ + struct function *function = (struct function *) rdata; + struct function_vector *vec = (struct function_vector *) pvec; + struct function_addrs *p; + + if (vec->count > 0) + { + p = (struct function_addrs *) vec->vec.base + (vec->count - 1); + if ((lowpc == p->high || lowpc == p->high + 1) + && function == p->function) + { + if (highpc > p->high) + p->high = highpc; + return 1; + } + } + + p = ((struct function_addrs *) + backtrace_vector_grow (state, sizeof (struct function_addrs), + error_callback, data, &vec->vec)); + if (p == NULL) + return 0; + + p->low = lowpc; + p->high = highpc; + p->function = function; + + ++vec->count; + + return 1; +} + +/* Read one entry plus all its children. Add function addresses to + VEC. Returns 1 on success, 0 on error. */ + +static int +read_function_entry (struct backtrace_state *state, struct dwarf_data *ddata, + struct unit *u, uint64_t base, struct dwarf_buf *unit_buf, + const struct line_header *lhdr, + backtrace_error_callback error_callback, void *data, + struct function_vector *vec_function, + struct function_vector *vec_inlined) +{ + while (unit_buf->left > 0) + { + uint64_t code; + const struct abbrev *abbrev; + int is_function; + struct function *function; + struct function_vector *vec; + size_t i; + struct pcrange pcrange; + int have_linkage_name; + + code = read_uleb128 (unit_buf); + if (code == 0) + return 1; + + abbrev = lookup_abbrev (&u->abbrevs, code, error_callback, data); + if (abbrev == NULL) + return 0; + + is_function = (abbrev->tag == DW_TAG_subprogram + || abbrev->tag == DW_TAG_entry_point + || abbrev->tag == DW_TAG_inlined_subroutine); + + if (abbrev->tag == DW_TAG_inlined_subroutine) + vec = vec_inlined; + else + vec = vec_function; + + function = NULL; + if (is_function) + { + function = ((struct function *) + backtrace_alloc (state, sizeof *function, + error_callback, data)); + if (function == NULL) + return 0; + memset (function, 0, sizeof *function); + } + + memset (&pcrange, 0, sizeof pcrange); + have_linkage_name = 0; + for (i = 0; i < abbrev->num_attrs; ++i) + { + struct attr_val val; + + if (!read_attribute (abbrev->attrs[i].form, abbrev->attrs[i].val, + unit_buf, u->is_dwarf64, u->version, + u->addrsize, &ddata->dwarf_sections, + ddata->altlink, &val)) + return 0; + + /* The compile unit sets the base address for any address + ranges in the function entries. */ + if ((abbrev->tag == DW_TAG_compile_unit + || abbrev->tag == DW_TAG_skeleton_unit) + && abbrev->attrs[i].name == DW_AT_low_pc) + { + if (val.encoding == ATTR_VAL_ADDRESS) + base = val.u.uint; + else if (val.encoding == ATTR_VAL_ADDRESS_INDEX) + { + if (!resolve_addr_index (&ddata->dwarf_sections, + u->addr_base, u->addrsize, + ddata->is_bigendian, val.u.uint, + error_callback, data, &base)) + return 0; + } + } + + if (is_function) + { + switch (abbrev->attrs[i].name) + { + case DW_AT_call_file: + if (val.encoding == ATTR_VAL_UINT) + { + if (val.u.uint >= lhdr->filenames_count) + { + dwarf_buf_error (unit_buf, + ("invalid file number in " + "DW_AT_call_file attribute"), + 0); + return 0; + } + function->caller_filename = lhdr->filenames[val.u.uint]; + } + break; + + case DW_AT_call_line: + if (val.encoding == ATTR_VAL_UINT) + function->caller_lineno = val.u.uint; + break; + + case DW_AT_abstract_origin: + case DW_AT_specification: + /* Second name preference: override DW_AT_name, don't override + DW_AT_linkage_name. */ + if (have_linkage_name) + break; + { + const char *name; + + name + = read_referenced_name_from_attr (ddata, u, + &abbrev->attrs[i], &val, + error_callback, data); + if (name != NULL) + function->name = name; + } + break; + + case DW_AT_name: + /* Third name preference: don't override. */ + if (function->name != NULL) + break; + if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64, + ddata->is_bigendian, + u->str_offsets_base, &val, + error_callback, data, &function->name)) + return 0; + break; + + case DW_AT_linkage_name: + case DW_AT_MIPS_linkage_name: + /* First name preference: override all. */ + { + const char *s; + + s = NULL; + if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64, + ddata->is_bigendian, + u->str_offsets_base, &val, + error_callback, data, &s)) + return 0; + if (s != NULL) + { + function->name = s; + have_linkage_name = 1; + } + } + break; + + case DW_AT_low_pc: case DW_AT_high_pc: case DW_AT_ranges: + update_pcrange (&abbrev->attrs[i], &val, &pcrange); + break; + + default: + break; + } + } + } + + /* If we couldn't find a name for the function, we have no use + for it. */ + if (is_function && function->name == NULL) + { + backtrace_free (state, function, sizeof *function, + error_callback, data); + is_function = 0; + } + + if (is_function) + { + if (pcrange.have_ranges + || (pcrange.have_lowpc && pcrange.have_highpc)) + { + if (!add_ranges (state, &ddata->dwarf_sections, + ddata->base_address, ddata->is_bigendian, + u, base, &pcrange, add_function_range, + (void *) function, error_callback, data, + (void *) vec)) + return 0; + } + else + { + backtrace_free (state, function, sizeof *function, + error_callback, data); + is_function = 0; + } + } + + if (abbrev->has_children) + { + if (!is_function) + { + if (!read_function_entry (state, ddata, u, base, unit_buf, lhdr, + error_callback, data, vec_function, + vec_inlined)) + return 0; + } + else + { + struct function_vector fvec; + + /* Gather any information for inlined functions in + FVEC. */ + + memset (&fvec, 0, sizeof fvec); + + if (!read_function_entry (state, ddata, u, base, unit_buf, lhdr, + error_callback, data, vec_function, + &fvec)) + return 0; + + if (fvec.count > 0) + { + struct function_addrs *p; + struct function_addrs *faddrs; + + /* Allocate a trailing entry, but don't include it + in fvec.count. */ + p = ((struct function_addrs *) + backtrace_vector_grow (state, + sizeof (struct function_addrs), + error_callback, data, + &fvec.vec)); + if (p == NULL) + return 0; + p->low = 0; + --p->low; + p->high = p->low; + p->function = NULL; + + if (!backtrace_vector_release (state, &fvec.vec, + error_callback, data)) + return 0; + + faddrs = (struct function_addrs *) fvec.vec.base; + backtrace_qsort (faddrs, fvec.count, + sizeof (struct function_addrs), + function_addrs_compare); + + function->function_addrs = faddrs; + function->function_addrs_count = fvec.count; + } + } + } + } + + return 1; +} + +/* Read function name information for a compilation unit. We look + through the whole unit looking for function tags. */ + +static void +read_function_info (struct backtrace_state *state, struct dwarf_data *ddata, + const struct line_header *lhdr, + backtrace_error_callback error_callback, void *data, + struct unit *u, struct function_vector *fvec, + struct function_addrs **ret_addrs, + size_t *ret_addrs_count) +{ + struct function_vector lvec; + struct function_vector *pfvec; + struct dwarf_buf unit_buf; + struct function_addrs *p; + struct function_addrs *addrs; + size_t addrs_count; + + /* Use FVEC if it is not NULL. Otherwise use our own vector. */ + if (fvec != NULL) + pfvec = fvec; + else + { + memset (&lvec, 0, sizeof lvec); + pfvec = &lvec; + } + + unit_buf.name = ".debug_info"; + unit_buf.start = ddata->dwarf_sections.data[DEBUG_INFO]; + unit_buf.buf = u->unit_data; + unit_buf.left = u->unit_data_len; + unit_buf.is_bigendian = ddata->is_bigendian; + unit_buf.error_callback = error_callback; + unit_buf.data = data; + unit_buf.reported_underflow = 0; + + while (unit_buf.left > 0) + { + if (!read_function_entry (state, ddata, u, 0, &unit_buf, lhdr, + error_callback, data, pfvec, pfvec)) + return; + } + + if (pfvec->count == 0) + return; + + /* Allocate a trailing entry, but don't include it in + pfvec->count. */ + p = ((struct function_addrs *) + backtrace_vector_grow (state, sizeof (struct function_addrs), + error_callback, data, &pfvec->vec)); + if (p == NULL) + return; + p->low = 0; + --p->low; + p->high = p->low; + p->function = NULL; + + addrs_count = pfvec->count; + + if (fvec == NULL) + { + if (!backtrace_vector_release (state, &lvec.vec, error_callback, data)) + return; + addrs = (struct function_addrs *) pfvec->vec.base; + } + else + { + /* Finish this list of addresses, but leave the remaining space in + the vector available for the next function unit. */ + addrs = ((struct function_addrs *) + backtrace_vector_finish (state, &fvec->vec, + error_callback, data)); + if (addrs == NULL) + return; + fvec->count = 0; + } + + backtrace_qsort (addrs, addrs_count, sizeof (struct function_addrs), + function_addrs_compare); + + *ret_addrs = addrs; + *ret_addrs_count = addrs_count; +} + +/* See if PC is inlined in FUNCTION. If it is, print out the inlined + information, and update FILENAME and LINENO for the caller. + Returns whatever CALLBACK returns, or 0 to keep going. */ + +static int +report_inlined_functions (uintptr_t pc, struct function *function, + backtrace_full_callback callback, void *data, + const char **filename, int *lineno) +{ + struct function_addrs *p; + struct function_addrs *match; + struct function *inlined; + int ret; + + if (function->function_addrs_count == 0) + return 0; + + /* Our search isn't safe if pc == -1, as that is the sentinel + value. */ + if (pc + 1 == 0) + return 0; + + p = ((struct function_addrs *) + bsearch (&pc, function->function_addrs, + function->function_addrs_count, + sizeof (struct function_addrs), + function_addrs_search)); + if (p == NULL) + return 0; + + /* Here pc >= p->low && pc < (p + 1)->low. The function_addrs are + sorted by low, so if pc > p->low we are at the end of a range of + function_addrs with the same low value. If pc == p->low walk + forward to the end of the range with that low value. Then walk + backward and use the first range that includes pc. */ + while (pc == (p + 1)->low) + ++p; + match = NULL; + while (1) + { + if (pc < p->high) + { + match = p; + break; + } + if (p == function->function_addrs) + break; + if ((p - 1)->low < p->low) + break; + --p; + } + if (match == NULL) + return 0; + + /* We found an inlined call. */ + + inlined = match->function; + + /* Report any calls inlined into this one. */ + ret = report_inlined_functions (pc, inlined, callback, data, + filename, lineno); + if (ret != 0) + return ret; + + /* Report this inlined call. */ + ret = callback (data, pc, *filename, *lineno, inlined->name); + if (ret != 0) + return ret; + + /* Our caller will report the caller of the inlined function; tell + it the appropriate filename and line number. */ + *filename = inlined->caller_filename; + *lineno = inlined->caller_lineno; + + return 0; +} + +/* Look for a PC in the DWARF mapping for one module. On success, + call CALLBACK and return whatever it returns. On error, call + ERROR_CALLBACK and return 0. Sets *FOUND to 1 if the PC is found, + 0 if not. */ + +static int +dwarf_lookup_pc (struct backtrace_state *state, struct dwarf_data *ddata, + uintptr_t pc, backtrace_full_callback callback, + backtrace_error_callback error_callback, void *data, + int *found) +{ + struct unit_addrs *entry; + int found_entry; + struct unit *u; + int new_data; + struct line *lines; + struct line *ln; + struct function_addrs *p; + struct function_addrs *fmatch; + struct function *function; + const char *filename; + int lineno; + int ret; + + *found = 1; + + /* Find an address range that includes PC. Our search isn't safe if + PC == -1, as we use that as a sentinel value, so skip the search + in that case. */ + entry = (ddata->addrs_count == 0 || pc + 1 == 0 + ? NULL + : bsearch (&pc, ddata->addrs, ddata->addrs_count, + sizeof (struct unit_addrs), unit_addrs_search)); + + if (entry == NULL) + { + *found = 0; + return 0; + } + + /* Here pc >= entry->low && pc < (entry + 1)->low. The unit_addrs + are sorted by low, so if pc > p->low we are at the end of a range + of unit_addrs with the same low value. If pc == p->low walk + forward to the end of the range with that low value. Then walk + backward and use the first range that includes pc. */ + while (pc == (entry + 1)->low) + ++entry; + found_entry = 0; + while (1) + { + if (pc < entry->high) + { + found_entry = 1; + break; + } + if (entry == ddata->addrs) + break; + if ((entry - 1)->low < entry->low) + break; + --entry; + } + if (!found_entry) + { + *found = 0; + return 0; + } + + /* We need the lines, lines_count, function_addrs, + function_addrs_count fields of u. If they are not set, we need + to set them. When running in threaded mode, we need to allow for + the possibility that some other thread is setting them + simultaneously. */ + + u = entry->u; + lines = u->lines; + + /* Skip units with no useful line number information by walking + backward. Useless line number information is marked by setting + lines == -1. */ + while (entry > ddata->addrs + && pc >= (entry - 1)->low + && pc < (entry - 1)->high) + { + if (state->threaded) + lines = (struct line *) backtrace_atomic_load_pointer (&u->lines); + + if (lines != (struct line *) (uintptr_t) -1) + break; + + --entry; + + u = entry->u; + lines = u->lines; + } + + if (state->threaded) + lines = backtrace_atomic_load_pointer (&u->lines); + + new_data = 0; + if (lines == NULL) + { + struct function_addrs *function_addrs; + size_t function_addrs_count; + struct line_header lhdr; + size_t count; + + /* We have never read the line information for this unit. Read + it now. */ + + function_addrs = NULL; + function_addrs_count = 0; + if (read_line_info (state, ddata, error_callback, data, entry->u, &lhdr, + &lines, &count)) + { + struct function_vector *pfvec; + + /* If not threaded, reuse DDATA->FVEC for better memory + consumption. */ + if (state->threaded) + pfvec = NULL; + else + pfvec = &ddata->fvec; + read_function_info (state, ddata, &lhdr, error_callback, data, + entry->u, pfvec, &function_addrs, + &function_addrs_count); + free_line_header (state, &lhdr, error_callback, data); + new_data = 1; + } + + /* Atomically store the information we just read into the unit. + If another thread is simultaneously writing, it presumably + read the same information, and we don't care which one we + wind up with; we just leak the other one. We do have to + write the lines field last, so that the acquire-loads above + ensure that the other fields are set. */ + + if (!state->threaded) + { + u->lines_count = count; + u->function_addrs = function_addrs; + u->function_addrs_count = function_addrs_count; + u->lines = lines; + } + else + { + backtrace_atomic_store_size_t (&u->lines_count, count); + backtrace_atomic_store_pointer (&u->function_addrs, function_addrs); + backtrace_atomic_store_size_t (&u->function_addrs_count, + function_addrs_count); + backtrace_atomic_store_pointer (&u->lines, lines); + } + } + + /* Now all fields of U have been initialized. */ + + if (lines == (struct line *) (uintptr_t) -1) + { + /* If reading the line number information failed in some way, + try again to see if there is a better compilation unit for + this PC. */ + if (new_data) + return dwarf_lookup_pc (state, ddata, pc, callback, error_callback, + data, found); + return callback (data, pc, NULL, 0, NULL); + } + + /* Search for PC within this unit. */ + + ln = (struct line *) bsearch (&pc, lines, entry->u->lines_count, + sizeof (struct line), line_search); + if (ln == NULL) + { + /* The PC is between the low_pc and high_pc attributes of the + compilation unit, but no entry in the line table covers it. + This implies that the start of the compilation unit has no + line number information. */ + + if (entry->u->abs_filename == NULL) + { + const char *filename; + + filename = entry->u->filename; + if (filename != NULL + && !IS_ABSOLUTE_PATH (filename) + && entry->u->comp_dir != NULL) + { + size_t filename_len; + const char *dir; + size_t dir_len; + char *s; + + filename_len = strlen (filename); + dir = entry->u->comp_dir; + dir_len = strlen (dir); + s = (char *) backtrace_alloc (state, dir_len + filename_len + 2, + error_callback, data); + if (s == NULL) + { + *found = 0; + return 0; + } + memcpy (s, dir, dir_len); + /* FIXME: Should use backslash if DOS file system. */ + s[dir_len] = '/'; + memcpy (s + dir_len + 1, filename, filename_len + 1); + filename = s; + } + entry->u->abs_filename = filename; + } + + return callback (data, pc, entry->u->abs_filename, 0, NULL); + } + + /* Search for function name within this unit. */ + + if (entry->u->function_addrs_count == 0) + return callback (data, pc, ln->filename, ln->lineno, NULL); + + p = ((struct function_addrs *) + bsearch (&pc, entry->u->function_addrs, + entry->u->function_addrs_count, + sizeof (struct function_addrs), + function_addrs_search)); + if (p == NULL) + return callback (data, pc, ln->filename, ln->lineno, NULL); + + /* Here pc >= p->low && pc < (p + 1)->low. The function_addrs are + sorted by low, so if pc > p->low we are at the end of a range of + function_addrs with the same low value. If pc == p->low walk + forward to the end of the range with that low value. Then walk + backward and use the first range that includes pc. */ + while (pc == (p + 1)->low) + ++p; + fmatch = NULL; + while (1) + { + if (pc < p->high) + { + fmatch = p; + break; + } + if (p == entry->u->function_addrs) + break; + if ((p - 1)->low < p->low) + break; + --p; + } + if (fmatch == NULL) + return callback (data, pc, ln->filename, ln->lineno, NULL); + + function = fmatch->function; + + filename = ln->filename; + lineno = ln->lineno; + + ret = report_inlined_functions (pc, function, callback, data, + &filename, &lineno); + if (ret != 0) + return ret; + + return callback (data, pc, filename, lineno, function->name); +} + + +/* Return the file/line information for a PC using the DWARF mapping + we built earlier. */ + +static int +dwarf_fileline (struct backtrace_state *state, uintptr_t pc, + backtrace_full_callback callback, + backtrace_error_callback error_callback, void *data) +{ + struct dwarf_data *ddata; + int found; + int ret; + + if (!state->threaded) + { + for (ddata = (struct dwarf_data *) state->fileline_data; + ddata != NULL; + ddata = ddata->next) + { + ret = dwarf_lookup_pc (state, ddata, pc, callback, error_callback, + data, &found); + if (ret != 0 || found) + return ret; + } + } + else + { + struct dwarf_data **pp; + + pp = (struct dwarf_data **) (void *) &state->fileline_data; + while (1) + { + ddata = backtrace_atomic_load_pointer (pp); + if (ddata == NULL) + break; + + ret = dwarf_lookup_pc (state, ddata, pc, callback, error_callback, + data, &found); + if (ret != 0 || found) + return ret; + + pp = &ddata->next; + } + } + + /* FIXME: See if any libraries have been dlopen'ed. */ + + return callback (data, pc, NULL, 0, NULL); +} + +/* Initialize our data structures from the DWARF debug info for a + file. Return NULL on failure. */ + +static struct dwarf_data * +build_dwarf_data (struct backtrace_state *state, + uintptr_t base_address, + const struct dwarf_sections *dwarf_sections, + int is_bigendian, + struct dwarf_data *altlink, + backtrace_error_callback error_callback, + void *data) +{ + struct unit_addrs_vector addrs_vec; + struct unit_addrs *addrs; + size_t addrs_count; + struct unit_vector units_vec; + struct unit **units; + size_t units_count; + struct dwarf_data *fdata; + + if (!build_address_map (state, base_address, dwarf_sections, is_bigendian, + altlink, error_callback, data, &addrs_vec, + &units_vec)) + return NULL; + + if (!backtrace_vector_release (state, &addrs_vec.vec, error_callback, data)) + return NULL; + if (!backtrace_vector_release (state, &units_vec.vec, error_callback, data)) + return NULL; + addrs = (struct unit_addrs *) addrs_vec.vec.base; + units = (struct unit **) units_vec.vec.base; + addrs_count = addrs_vec.count; + units_count = units_vec.count; + backtrace_qsort (addrs, addrs_count, sizeof (struct unit_addrs), + unit_addrs_compare); + /* No qsort for units required, already sorted. */ + + fdata = ((struct dwarf_data *) + backtrace_alloc (state, sizeof (struct dwarf_data), + error_callback, data)); + if (fdata == NULL) + return NULL; + + fdata->next = NULL; + fdata->altlink = altlink; + fdata->base_address = base_address; + fdata->addrs = addrs; + fdata->addrs_count = addrs_count; + fdata->units = units; + fdata->units_count = units_count; + fdata->dwarf_sections = *dwarf_sections; + fdata->is_bigendian = is_bigendian; + memset (&fdata->fvec, 0, sizeof fdata->fvec); + + return fdata; +} + +/* Build our data structures from the DWARF sections for a module. + Set FILELINE_FN and STATE->FILELINE_DATA. Return 1 on success, 0 + on failure. */ + +int +backtrace_dwarf_add (struct backtrace_state *state, + uintptr_t base_address, + const struct dwarf_sections *dwarf_sections, + int is_bigendian, + struct dwarf_data *fileline_altlink, + backtrace_error_callback error_callback, + void *data, fileline *fileline_fn, + struct dwarf_data **fileline_entry) +{ + struct dwarf_data *fdata; + + fdata = build_dwarf_data (state, base_address, dwarf_sections, is_bigendian, + fileline_altlink, error_callback, data); + if (fdata == NULL) + return 0; + + if (fileline_entry != NULL) + *fileline_entry = fdata; + + if (!state->threaded) + { + struct dwarf_data **pp; + + for (pp = (struct dwarf_data **) (void *) &state->fileline_data; + *pp != NULL; + pp = &(*pp)->next) + ; + *pp = fdata; + } + else + { + while (1) + { + struct dwarf_data **pp; + + pp = (struct dwarf_data **) (void *) &state->fileline_data; + + while (1) + { + struct dwarf_data *p; + + p = backtrace_atomic_load_pointer (pp); + + if (p == NULL) + break; + + pp = &p->next; + } + + if (__sync_bool_compare_and_swap (pp, NULL, fdata)) + break; + } + } + + *fileline_fn = dwarf_fileline; + + return 1; +} diff --git a/thirdparty/libbacktrace/fileline.c b/thirdparty/libbacktrace/fileline.c new file mode 100644 index 0000000000..0472f4721a --- /dev/null +++ b/thirdparty/libbacktrace/fileline.c @@ -0,0 +1,346 @@ +/* fileline.c -- Get file and line number information in a backtrace. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <unistd.h> + +#if defined (HAVE_KERN_PROC_ARGS) || defined (HAVE_KERN_PROC) +#include <sys/sysctl.h> +#endif + +#ifdef HAVE_MACH_O_DYLD_H +#include <mach-o/dyld.h> +#endif + +#include "backtrace.h" +#include "internal.h" + +#ifndef HAVE_GETEXECNAME +#define getexecname() NULL +#endif + +#if !defined (HAVE_KERN_PROC_ARGS) && !defined (HAVE_KERN_PROC) + +#define sysctl_exec_name1(state, error_callback, data) NULL +#define sysctl_exec_name2(state, error_callback, data) NULL + +#else /* defined (HAVE_KERN_PROC_ARGS) || |defined (HAVE_KERN_PROC) */ + +static char * +sysctl_exec_name (struct backtrace_state *state, + int mib0, int mib1, int mib2, int mib3, + backtrace_error_callback error_callback, void *data) +{ + int mib[4]; + size_t len; + char *name; + size_t rlen; + + mib[0] = mib0; + mib[1] = mib1; + mib[2] = mib2; + mib[3] = mib3; + + if (sysctl (mib, 4, NULL, &len, NULL, 0) < 0) + return NULL; + name = (char *) backtrace_alloc (state, len, error_callback, data); + if (name == NULL) + return NULL; + rlen = len; + if (sysctl (mib, 4, name, &rlen, NULL, 0) < 0) + { + backtrace_free (state, name, len, error_callback, data); + return NULL; + } + return name; +} + +#ifdef HAVE_KERN_PROC_ARGS + +static char * +sysctl_exec_name1 (struct backtrace_state *state, + backtrace_error_callback error_callback, void *data) +{ + /* This variant is used on NetBSD. */ + return sysctl_exec_name (state, CTL_KERN, KERN_PROC_ARGS, -1, + KERN_PROC_PATHNAME, error_callback, data); +} + +#else + +#define sysctl_exec_name1(state, error_callback, data) NULL + +#endif + +#ifdef HAVE_KERN_PROC + +static char * +sysctl_exec_name2 (struct backtrace_state *state, + backtrace_error_callback error_callback, void *data) +{ + /* This variant is used on FreeBSD. */ + return sysctl_exec_name (state, CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1, + error_callback, data); +} + +#else + +#define sysctl_exec_name2(state, error_callback, data) NULL + +#endif + +#endif /* defined (HAVE_KERN_PROC_ARGS) || |defined (HAVE_KERN_PROC) */ + +#ifdef HAVE_MACH_O_DYLD_H + +static char * +macho_get_executable_path (struct backtrace_state *state, + backtrace_error_callback error_callback, void *data) +{ + uint32_t len; + char *name; + + len = 0; + if (_NSGetExecutablePath (NULL, &len) == 0) + return NULL; + name = (char *) backtrace_alloc (state, len, error_callback, data); + if (name == NULL) + return NULL; + if (_NSGetExecutablePath (name, &len) != 0) + { + backtrace_free (state, name, len, error_callback, data); + return NULL; + } + return name; +} + +#else /* !defined (HAVE_MACH_O_DYLD_H) */ + +#define macho_get_executable_path(state, error_callback, data) NULL + +#endif /* !defined (HAVE_MACH_O_DYLD_H) */ + +/* Initialize the fileline information from the executable. Returns 1 + on success, 0 on failure. */ + +static int +fileline_initialize (struct backtrace_state *state, + backtrace_error_callback error_callback, void *data) +{ + int failed; + fileline fileline_fn; + int pass; + int called_error_callback; + int descriptor; + const char *filename; + char buf[64]; + + if (!state->threaded) + failed = state->fileline_initialization_failed; + else + failed = backtrace_atomic_load_int (&state->fileline_initialization_failed); + + if (failed) + { + error_callback (data, "failed to read executable information", -1); + return 0; + } + + if (!state->threaded) + fileline_fn = state->fileline_fn; + else + fileline_fn = backtrace_atomic_load_pointer (&state->fileline_fn); + if (fileline_fn != NULL) + return 1; + + /* We have not initialized the information. Do it now. */ + + descriptor = -1; + called_error_callback = 0; + for (pass = 0; pass < 8; ++pass) + { + int does_not_exist; + + switch (pass) + { + case 0: + filename = state->filename; + break; + case 1: + filename = getexecname (); + break; + case 2: + filename = "/proc/self/exe"; + break; + case 3: + filename = "/proc/curproc/file"; + break; + case 4: + snprintf (buf, sizeof (buf), "/proc/%ld/object/a.out", + (long) getpid ()); + filename = buf; + break; + case 5: + filename = sysctl_exec_name1 (state, error_callback, data); + break; + case 6: + filename = sysctl_exec_name2 (state, error_callback, data); + break; + case 7: + filename = macho_get_executable_path (state, error_callback, data); + break; + default: + abort (); + } + + if (filename == NULL) + continue; + + descriptor = backtrace_open (filename, error_callback, data, + &does_not_exist); + if (descriptor < 0 && !does_not_exist) + { + called_error_callback = 1; + break; + } + if (descriptor >= 0) + break; + } + + if (descriptor < 0) + { + if (!called_error_callback) + { + if (state->filename != NULL) + error_callback (data, state->filename, ENOENT); + else + error_callback (data, + "libbacktrace could not find executable to open", + 0); + } + failed = 1; + } + + if (!failed) + { + if (!backtrace_initialize (state, filename, descriptor, error_callback, + data, &fileline_fn)) + failed = 1; + } + + if (failed) + { + if (!state->threaded) + state->fileline_initialization_failed = 1; + else + backtrace_atomic_store_int (&state->fileline_initialization_failed, 1); + return 0; + } + + if (!state->threaded) + state->fileline_fn = fileline_fn; + else + { + backtrace_atomic_store_pointer (&state->fileline_fn, fileline_fn); + + /* Note that if two threads initialize at once, one of the data + sets may be leaked. */ + } + + return 1; +} + +/* Given a PC, find the file name, line number, and function name. */ + +int +backtrace_pcinfo (struct backtrace_state *state, uintptr_t pc, + backtrace_full_callback callback, + backtrace_error_callback error_callback, void *data) +{ + if (!fileline_initialize (state, error_callback, data)) + return 0; + + if (state->fileline_initialization_failed) + return 0; + + return state->fileline_fn (state, pc, callback, error_callback, data); +} + +/* Given a PC, find the symbol for it, and its value. */ + +int +backtrace_syminfo (struct backtrace_state *state, uintptr_t pc, + backtrace_syminfo_callback callback, + backtrace_error_callback error_callback, void *data) +{ + if (!fileline_initialize (state, error_callback, data)) + return 0; + + if (state->fileline_initialization_failed) + return 0; + + state->syminfo_fn (state, pc, callback, error_callback, data); + return 1; +} + +/* A backtrace_syminfo_callback that can call into a + backtrace_full_callback, used when we have a symbol table but no + debug info. */ + +void +backtrace_syminfo_to_full_callback (void *data, uintptr_t pc, + const char *symname, + uintptr_t symval ATTRIBUTE_UNUSED, + uintptr_t symsize ATTRIBUTE_UNUSED) +{ + struct backtrace_call_full *bdata = (struct backtrace_call_full *) data; + + bdata->ret = bdata->full_callback (bdata->full_data, pc, NULL, 0, symname); +} + +/* An error callback that corresponds to + backtrace_syminfo_to_full_callback. */ + +void +backtrace_syminfo_to_full_error_callback (void *data, const char *msg, + int errnum) +{ + struct backtrace_call_full *bdata = (struct backtrace_call_full *) data; + + bdata->full_error_callback (bdata->full_data, msg, errnum); +} diff --git a/thirdparty/libbacktrace/filenames.h b/thirdparty/libbacktrace/filenames.h new file mode 100644 index 0000000000..aa7bd7adff --- /dev/null +++ b/thirdparty/libbacktrace/filenames.h @@ -0,0 +1,52 @@ +/* btest.c -- Filename header for libbacktrace library + Copyright (C) 2012-2018 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#ifndef GCC_VERSION +# define GCC_VERSION (__GNUC__ * 1000 + __GNUC_MINOR__) +#endif + +#if (GCC_VERSION < 2007) +# define __attribute__(x) +#endif + +#ifndef ATTRIBUTE_UNUSED +# define ATTRIBUTE_UNUSED __attribute__ ((__unused__)) +#endif + +#if defined(__MSDOS__) || defined(_WIN32) || defined(__OS2__) || defined (__CYGWIN__) +# define IS_DIR_SEPARATOR(c) ((c) == '/' || (c) == '\\') +# define HAS_DRIVE_SPEC(f) ((f)[0] != '\0' && (f)[1] == ':') +# define IS_ABSOLUTE_PATH(f) (IS_DIR_SEPARATOR((f)[0]) || HAS_DRIVE_SPEC(f)) +#else +# define IS_DIR_SEPARATOR(c) ((c) == '/') +# define IS_ABSOLUTE_PATH(f) (IS_DIR_SEPARATOR((f)[0])) +#endif diff --git a/thirdparty/libbacktrace/internal.h b/thirdparty/libbacktrace/internal.h new file mode 100644 index 0000000000..bb481f373b --- /dev/null +++ b/thirdparty/libbacktrace/internal.h @@ -0,0 +1,380 @@ +/* internal.h -- Internal header file for stack backtrace library. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#ifndef BACKTRACE_INTERNAL_H +#define BACKTRACE_INTERNAL_H + +/* We assume that <sys/types.h> and "backtrace.h" have already been + included. */ + +#ifndef GCC_VERSION +# define GCC_VERSION (__GNUC__ * 1000 + __GNUC_MINOR__) +#endif + +#if (GCC_VERSION < 2007) +# define __attribute__(x) +#endif + +#ifndef ATTRIBUTE_UNUSED +# define ATTRIBUTE_UNUSED __attribute__ ((__unused__)) +#endif + +#ifndef ATTRIBUTE_MALLOC +# if (GCC_VERSION >= 2096) +# define ATTRIBUTE_MALLOC __attribute__ ((__malloc__)) +# else +# define ATTRIBUTE_MALLOC +# endif +#endif + +#ifndef ATTRIBUTE_FALLTHROUGH +# if (GCC_VERSION >= 7000) +# define ATTRIBUTE_FALLTHROUGH __attribute__ ((__fallthrough__)) +# else +# define ATTRIBUTE_FALLTHROUGH +# endif +#endif + +#ifndef HAVE_SYNC_FUNCTIONS + +/* Define out the sync functions. These should never be called if + they are not available. */ + +#define __sync_bool_compare_and_swap(A, B, C) (abort(), 1) +#define __sync_lock_test_and_set(A, B) (abort(), 0) +#define __sync_lock_release(A) abort() + +#endif /* !defined (HAVE_SYNC_FUNCTIONS) */ + +#ifdef HAVE_ATOMIC_FUNCTIONS + +/* We have the atomic builtin functions. */ + +#define backtrace_atomic_load_pointer(p) \ + __atomic_load_n ((p), __ATOMIC_ACQUIRE) +#define backtrace_atomic_load_int(p) \ + __atomic_load_n ((p), __ATOMIC_ACQUIRE) +#define backtrace_atomic_store_pointer(p, v) \ + __atomic_store_n ((p), (v), __ATOMIC_RELEASE) +#define backtrace_atomic_store_size_t(p, v) \ + __atomic_store_n ((p), (v), __ATOMIC_RELEASE) +#define backtrace_atomic_store_int(p, v) \ + __atomic_store_n ((p), (v), __ATOMIC_RELEASE) + +#else /* !defined (HAVE_ATOMIC_FUNCTIONS) */ +#ifdef HAVE_SYNC_FUNCTIONS + +/* We have the sync functions but not the atomic functions. Define + the atomic ones in terms of the sync ones. */ + +extern void *backtrace_atomic_load_pointer (void *); +extern int backtrace_atomic_load_int (int *); +extern void backtrace_atomic_store_pointer (void *, void *); +extern void backtrace_atomic_store_size_t (size_t *, size_t); +extern void backtrace_atomic_store_int (int *, int); + +#else /* !defined (HAVE_SYNC_FUNCTIONS) */ + +/* We have neither the sync nor the atomic functions. These will + never be called. */ + +#define backtrace_atomic_load_pointer(p) (abort(), (void *) NULL) +#define backtrace_atomic_load_int(p) (abort(), 0) +#define backtrace_atomic_store_pointer(p, v) abort() +#define backtrace_atomic_store_size_t(p, v) abort() +#define backtrace_atomic_store_int(p, v) abort() + +#endif /* !defined (HAVE_SYNC_FUNCTIONS) */ +#endif /* !defined (HAVE_ATOMIC_FUNCTIONS) */ + +/* The type of the function that collects file/line information. This + is like backtrace_pcinfo. */ + +typedef int (*fileline) (struct backtrace_state *state, uintptr_t pc, + backtrace_full_callback callback, + backtrace_error_callback error_callback, void *data); + +/* The type of the function that collects symbol information. This is + like backtrace_syminfo. */ + +typedef void (*syminfo) (struct backtrace_state *state, uintptr_t pc, + backtrace_syminfo_callback callback, + backtrace_error_callback error_callback, void *data); + +/* What the backtrace state pointer points to. */ + +struct backtrace_state +{ + /* The name of the executable. */ + const char *filename; + /* Non-zero if threaded. */ + int threaded; + /* The master lock for fileline_fn, fileline_data, syminfo_fn, + syminfo_data, fileline_initialization_failed and everything the + data pointers point to. */ + void *lock; + /* The function that returns file/line information. */ + fileline fileline_fn; + /* The data to pass to FILELINE_FN. */ + void *fileline_data; + /* The function that returns symbol information. */ + syminfo syminfo_fn; + /* The data to pass to SYMINFO_FN. */ + void *syminfo_data; + /* Whether initializing the file/line information failed. */ + int fileline_initialization_failed; + /* The lock for the freelist. */ + int lock_alloc; + /* The freelist when using mmap. */ + struct backtrace_freelist_struct *freelist; +}; + +/* Open a file for reading. Returns -1 on error. If DOES_NOT_EXIST + is not NULL, *DOES_NOT_EXIST will be set to 0 normally and set to 1 + if the file does not exist. If the file does not exist and + DOES_NOT_EXIST is not NULL, the function will return -1 and will + not call ERROR_CALLBACK. On other errors, or if DOES_NOT_EXIST is + NULL, the function will call ERROR_CALLBACK before returning. */ +extern int backtrace_open (const char *filename, + backtrace_error_callback error_callback, + void *data, + int *does_not_exist); + +/* A view of the contents of a file. This supports mmap when + available. A view will remain in memory even after backtrace_close + is called on the file descriptor from which the view was + obtained. */ + +struct backtrace_view +{ + /* The data that the caller requested. */ + const void *data; + /* The base of the view. */ + void *base; + /* The total length of the view. */ + size_t len; +}; + +/* Create a view of SIZE bytes from DESCRIPTOR at OFFSET. Store the + result in *VIEW. Returns 1 on success, 0 on error. */ +extern int backtrace_get_view (struct backtrace_state *state, int descriptor, + off_t offset, uint64_t size, + backtrace_error_callback error_callback, + void *data, struct backtrace_view *view); + +/* Release a view created by backtrace_get_view. */ +extern void backtrace_release_view (struct backtrace_state *state, + struct backtrace_view *view, + backtrace_error_callback error_callback, + void *data); + +/* Close a file opened by backtrace_open. Returns 1 on success, 0 on + error. */ + +extern int backtrace_close (int descriptor, + backtrace_error_callback error_callback, + void *data); + +/* Sort without using memory. */ + +extern void backtrace_qsort (void *base, size_t count, size_t size, + int (*compar) (const void *, const void *)); + +/* Allocate memory. This is like malloc. If ERROR_CALLBACK is NULL, + this does not report an error, it just returns NULL. */ + +extern void *backtrace_alloc (struct backtrace_state *state, size_t size, + backtrace_error_callback error_callback, + void *data) ATTRIBUTE_MALLOC; + +/* Free memory allocated by backtrace_alloc. If ERROR_CALLBACK is + NULL, this does not report an error. */ + +extern void backtrace_free (struct backtrace_state *state, void *mem, + size_t size, + backtrace_error_callback error_callback, + void *data); + +/* A growable vector of some struct. This is used for more efficient + allocation when we don't know the final size of some group of data + that we want to represent as an array. */ + +struct backtrace_vector +{ + /* The base of the vector. */ + void *base; + /* The number of bytes in the vector. */ + size_t size; + /* The number of bytes available at the current allocation. */ + size_t alc; +}; + +/* Grow VEC by SIZE bytes. Return a pointer to the newly allocated + bytes. Note that this may move the entire vector to a new memory + location. Returns NULL on failure. */ + +extern void *backtrace_vector_grow (struct backtrace_state *state, size_t size, + backtrace_error_callback error_callback, + void *data, + struct backtrace_vector *vec); + +/* Finish the current allocation on VEC. Prepare to start a new + allocation. The finished allocation will never be freed. Returns + a pointer to the base of the finished entries, or NULL on + failure. */ + +extern void* backtrace_vector_finish (struct backtrace_state *state, + struct backtrace_vector *vec, + backtrace_error_callback error_callback, + void *data); + +/* Release any extra space allocated for VEC. This may change + VEC->base. Returns 1 on success, 0 on failure. */ + +extern int backtrace_vector_release (struct backtrace_state *state, + struct backtrace_vector *vec, + backtrace_error_callback error_callback, + void *data); + +/* Free the space managed by VEC. This will reset VEC. */ + +static inline void +backtrace_vector_free (struct backtrace_state *state, + struct backtrace_vector *vec, + backtrace_error_callback error_callback, void *data) +{ + vec->alc += vec->size; + vec->size = 0; + backtrace_vector_release (state, vec, error_callback, data); +} + +/* Read initial debug data from a descriptor, and set the + fileline_data, syminfo_fn, and syminfo_data fields of STATE. + Return the fileln_fn field in *FILELN_FN--this is done this way so + that the synchronization code is only implemented once. This is + called after the descriptor has first been opened. It will close + the descriptor if it is no longer needed. Returns 1 on success, 0 + on error. There will be multiple implementations of this function, + for different file formats. Each system will compile the + appropriate one. */ + +extern int backtrace_initialize (struct backtrace_state *state, + const char *filename, + int descriptor, + backtrace_error_callback error_callback, + void *data, + fileline *fileline_fn); + +/* An enum for the DWARF sections we care about. */ + +enum dwarf_section +{ + DEBUG_INFO, + DEBUG_LINE, + DEBUG_ABBREV, + DEBUG_RANGES, + DEBUG_STR, + DEBUG_ADDR, + DEBUG_STR_OFFSETS, + DEBUG_LINE_STR, + DEBUG_RNGLISTS, + + DEBUG_MAX +}; + +/* Data for the DWARF sections we care about. */ + +struct dwarf_sections +{ + const unsigned char *data[DEBUG_MAX]; + size_t size[DEBUG_MAX]; +}; + +/* DWARF data read from a file, used for .gnu_debugaltlink. */ + +struct dwarf_data; + +/* Add file/line information for a DWARF module. */ + +extern int backtrace_dwarf_add (struct backtrace_state *state, + uintptr_t base_address, + const struct dwarf_sections *dwarf_sections, + int is_bigendian, + struct dwarf_data *fileline_altlink, + backtrace_error_callback error_callback, + void *data, fileline *fileline_fn, + struct dwarf_data **fileline_entry); + +/* A data structure to pass to backtrace_syminfo_to_full. */ + +struct backtrace_call_full +{ + backtrace_full_callback full_callback; + backtrace_error_callback full_error_callback; + void *full_data; + int ret; +}; + +/* A backtrace_syminfo_callback that can call into a + backtrace_full_callback, used when we have a symbol table but no + debug info. */ + +extern void backtrace_syminfo_to_full_callback (void *data, uintptr_t pc, + const char *symname, + uintptr_t symval, + uintptr_t symsize); + +/* An error callback that corresponds to + backtrace_syminfo_to_full_callback. */ + +extern void backtrace_syminfo_to_full_error_callback (void *, const char *, + int); + +/* A test-only hook for elf_uncompress_zdebug. */ + +extern int backtrace_uncompress_zdebug (struct backtrace_state *, + const unsigned char *compressed, + size_t compressed_size, + backtrace_error_callback, void *data, + unsigned char **uncompressed, + size_t *uncompressed_size); + +/* A test-only hook for elf_uncompress_lzma. */ + +extern int backtrace_uncompress_lzma (struct backtrace_state *, + const unsigned char *compressed, + size_t compressed_size, + backtrace_error_callback, void *data, + unsigned char **uncompressed, + size_t *uncompressed_size); + +#endif diff --git a/thirdparty/libbacktrace/pecoff.c b/thirdparty/libbacktrace/pecoff.c new file mode 100644 index 0000000000..720251900b --- /dev/null +++ b/thirdparty/libbacktrace/pecoff.c @@ -0,0 +1,935 @@ +/* pecoff.c -- Get debug data from a PE/COFFF file for backtraces. + Copyright (C) 2015-2021 Free Software Foundation, Inc. + Adapted from elf.c by Tristan Gingold, AdaCore. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> + +#include "backtrace.h" +#include "internal.h" + +/* Coff file header. */ + +typedef struct { + uint16_t machine; + uint16_t number_of_sections; + uint32_t time_date_stamp; + uint32_t pointer_to_symbol_table; + uint32_t number_of_symbols; + uint16_t size_of_optional_header; + uint16_t characteristics; +} b_coff_file_header; + +/* Coff optional header. */ + +typedef struct { + uint16_t magic; + uint8_t major_linker_version; + uint8_t minor_linker_version; + uint32_t size_of_code; + uint32_t size_of_initialized_data; + uint32_t size_of_uninitialized_data; + uint32_t address_of_entry_point; + uint32_t base_of_code; + union { + struct { + uint32_t base_of_data; + uint32_t image_base; + } pe; + struct { + uint64_t image_base; + } pep; + } u; +} b_coff_optional_header; + +/* Values of magic in optional header. */ + +#define PE_MAGIC 0x10b /* PE32 executable. */ +#define PEP_MAGIC 0x20b /* PE32+ executable (for 64bit targets). */ + +/* Coff section header. */ + +typedef struct { + char name[8]; + uint32_t virtual_size; + uint32_t virtual_address; + uint32_t size_of_raw_data; + uint32_t pointer_to_raw_data; + uint32_t pointer_to_relocations; + uint32_t pointer_to_line_numbers; + uint16_t number_of_relocations; + uint16_t number_of_line_numbers; + uint32_t characteristics; +} b_coff_section_header; + +/* Coff symbol name. */ + +typedef union { + char short_name[8]; + struct { + unsigned char zeroes[4]; + unsigned char off[4]; + } long_name; +} b_coff_name; + +/* Coff symbol (external representation which is unaligned). */ + +typedef struct { + b_coff_name name; + unsigned char value[4]; + unsigned char section_number[2]; + unsigned char type[2]; + unsigned char storage_class; + unsigned char number_of_aux_symbols; +} b_coff_external_symbol; + +/* Symbol types. */ + +#define N_TBSHFT 4 /* Shift for the derived type. */ +#define IMAGE_SYM_DTYPE_FUNCTION 2 /* Function derived type. */ + +/* Size of a coff symbol. */ + +#define SYM_SZ 18 + +/* Coff symbol, internal representation (aligned). */ + +typedef struct { + const char *name; + uint32_t value; + int16_t sec; + uint16_t type; + uint16_t sc; +} b_coff_internal_symbol; + +/* Names of sections, indexed by enum dwarf_section in internal.h. */ + +static const char * const debug_section_names[DEBUG_MAX] = +{ + ".debug_info", + ".debug_line", + ".debug_abbrev", + ".debug_ranges", + ".debug_str", + ".debug_addr", + ".debug_str_offsets", + ".debug_line_str", + ".debug_rnglists" +}; + +/* Information we gather for the sections we care about. */ + +struct debug_section_info +{ + /* Section file offset. */ + off_t offset; + /* Section size. */ + size_t size; +}; + +/* Information we keep for an coff symbol. */ + +struct coff_symbol +{ + /* The name of the symbol. */ + const char *name; + /* The address of the symbol. */ + uintptr_t address; +}; + +/* Information to pass to coff_syminfo. */ + +struct coff_syminfo_data +{ + /* Symbols for the next module. */ + struct coff_syminfo_data *next; + /* The COFF symbols, sorted by address. */ + struct coff_symbol *symbols; + /* The number of symbols. */ + size_t count; +}; + +/* A dummy callback function used when we can't find any debug info. */ + +static int +coff_nodebug (struct backtrace_state *state ATTRIBUTE_UNUSED, + uintptr_t pc ATTRIBUTE_UNUSED, + backtrace_full_callback callback ATTRIBUTE_UNUSED, + backtrace_error_callback error_callback, void *data) +{ + error_callback (data, "no debug info in PE/COFF executable", -1); + return 0; +} + +/* A dummy callback function used when we can't find a symbol + table. */ + +static void +coff_nosyms (struct backtrace_state *state ATTRIBUTE_UNUSED, + uintptr_t addr ATTRIBUTE_UNUSED, + backtrace_syminfo_callback callback ATTRIBUTE_UNUSED, + backtrace_error_callback error_callback, void *data) +{ + error_callback (data, "no symbol table in PE/COFF executable", -1); +} + +/* Read a potentially unaligned 4 byte word at P, using native endianness. */ + +static uint32_t +coff_read4 (const unsigned char *p) +{ + uint32_t res; + + memcpy (&res, p, 4); + return res; +} + +/* Read a potentially unaligned 2 byte word at P, using native endianness. + All 2 byte word in symbols are always aligned, but for coherency all + fields are declared as char arrays. */ + +static uint16_t +coff_read2 (const unsigned char *p) +{ + uint16_t res; + + memcpy (&res, p, sizeof (res)); + return res; +} + +/* Return the length (without the trailing 0) of a COFF short name. */ + +static size_t +coff_short_name_len (const char *name) +{ + int i; + + for (i = 0; i < 8; i++) + if (name[i] == 0) + return i; + return 8; +} + +/* Return true iff COFF short name CNAME is the same as NAME (a NUL-terminated + string). */ + +static int +coff_short_name_eq (const char *name, const char *cname) +{ + int i; + + for (i = 0; i < 8; i++) + { + if (name[i] != cname[i]) + return 0; + if (name[i] == 0) + return 1; + } + return name[8] == 0; +} + +/* Return true iff NAME is the same as string at offset OFF. */ + +static int +coff_long_name_eq (const char *name, unsigned int off, + struct backtrace_view *str_view) +{ + if (off >= str_view->len) + return 0; + return strcmp (name, (const char *)str_view->data + off) == 0; +} + +/* Compare struct coff_symbol for qsort. */ + +static int +coff_symbol_compare (const void *v1, const void *v2) +{ + const struct coff_symbol *e1 = (const struct coff_symbol *) v1; + const struct coff_symbol *e2 = (const struct coff_symbol *) v2; + + if (e1->address < e2->address) + return -1; + else if (e1->address > e2->address) + return 1; + else + return 0; +} + +/* Convert SYM to internal (and aligned) format ISYM, using string table + from STRTAB and STRTAB_SIZE, and number of sections SECTS_NUM. + Return -1 in case of error (invalid section number or string index). */ + +static int +coff_expand_symbol (b_coff_internal_symbol *isym, + const b_coff_external_symbol *sym, + uint16_t sects_num, + const unsigned char *strtab, size_t strtab_size) +{ + isym->type = coff_read2 (sym->type); + isym->sec = coff_read2 (sym->section_number); + isym->sc = sym->storage_class; + + if (isym->sec > 0 && (uint16_t) isym->sec > sects_num) + return -1; + if (sym->name.short_name[0] != 0) + isym->name = sym->name.short_name; + else + { + uint32_t off = coff_read4 (sym->name.long_name.off); + + if (off >= strtab_size) + return -1; + isym->name = (const char *) strtab + off; + } + return 0; +} + +/* Return true iff SYM is a defined symbol for a function. Data symbols + aren't considered because they aren't easily identified (same type as + section names, presence of symbols defined by the linker script). */ + +static int +coff_is_function_symbol (const b_coff_internal_symbol *isym) +{ + return (isym->type >> N_TBSHFT) == IMAGE_SYM_DTYPE_FUNCTION + && isym->sec > 0; +} + +/* Initialize the symbol table info for coff_syminfo. */ + +static int +coff_initialize_syminfo (struct backtrace_state *state, + uintptr_t base_address, int is_64, + const b_coff_section_header *sects, size_t sects_num, + const b_coff_external_symbol *syms, size_t syms_size, + const unsigned char *strtab, size_t strtab_size, + backtrace_error_callback error_callback, + void *data, struct coff_syminfo_data *sdata) +{ + size_t syms_count; + char *coff_symstr; + size_t coff_symstr_len; + size_t coff_symbol_count; + size_t coff_symbol_size; + struct coff_symbol *coff_symbols; + struct coff_symbol *coff_sym; + char *coff_str; + size_t i; + + syms_count = syms_size / SYM_SZ; + + /* We only care about function symbols. Count them. Also count size of + strings for in-symbol names. */ + coff_symbol_count = 0; + coff_symstr_len = 0; + for (i = 0; i < syms_count; ++i) + { + const b_coff_external_symbol *asym = &syms[i]; + b_coff_internal_symbol isym; + + if (coff_expand_symbol (&isym, asym, sects_num, strtab, strtab_size) < 0) + { + error_callback (data, "invalid section or offset in coff symbol", 0); + return 0; + } + if (coff_is_function_symbol (&isym)) + { + ++coff_symbol_count; + if (asym->name.short_name[0] != 0) + coff_symstr_len += coff_short_name_len (asym->name.short_name) + 1; + } + + i += asym->number_of_aux_symbols; + } + + coff_symbol_size = (coff_symbol_count + 1) * sizeof (struct coff_symbol); + coff_symbols = ((struct coff_symbol *) + backtrace_alloc (state, coff_symbol_size, error_callback, + data)); + if (coff_symbols == NULL) + return 0; + + /* Allocate memory for symbols strings. */ + if (coff_symstr_len > 0) + { + coff_symstr = ((char *) + backtrace_alloc (state, coff_symstr_len, error_callback, + data)); + if (coff_symstr == NULL) + { + backtrace_free (state, coff_symbols, coff_symbol_size, + error_callback, data); + return 0; + } + } + else + coff_symstr = NULL; + + /* Copy symbols. */ + coff_sym = coff_symbols; + coff_str = coff_symstr; + for (i = 0; i < syms_count; ++i) + { + const b_coff_external_symbol *asym = &syms[i]; + b_coff_internal_symbol isym; + + if (coff_expand_symbol (&isym, asym, sects_num, strtab, strtab_size)) + { + /* Should not fail, as it was already tested in the previous + loop. */ + abort (); + } + if (coff_is_function_symbol (&isym)) + { + const char *name; + int16_t secnum; + + if (asym->name.short_name[0] != 0) + { + size_t len = coff_short_name_len (isym.name); + name = coff_str; + memcpy (coff_str, isym.name, len); + coff_str[len] = 0; + coff_str += len + 1; + } + else + name = isym.name; + + if (!is_64) + { + /* Strip leading '_'. */ + if (name[0] == '_') + name++; + } + + /* Symbol value is section relative, so we need to read the address + of its section. */ + secnum = coff_read2 (asym->section_number); + + coff_sym->name = name; + coff_sym->address = (coff_read4 (asym->value) + + sects[secnum - 1].virtual_address + + base_address); + coff_sym++; + } + + i += asym->number_of_aux_symbols; + } + + /* End of symbols marker. */ + coff_sym->name = NULL; + coff_sym->address = -1; + + backtrace_qsort (coff_symbols, coff_symbol_count, + sizeof (struct coff_symbol), coff_symbol_compare); + + sdata->next = NULL; + sdata->symbols = coff_symbols; + sdata->count = coff_symbol_count; + + return 1; +} + +/* Add EDATA to the list in STATE. */ + +static void +coff_add_syminfo_data (struct backtrace_state *state, + struct coff_syminfo_data *sdata) +{ + if (!state->threaded) + { + struct coff_syminfo_data **pp; + + for (pp = (struct coff_syminfo_data **) (void *) &state->syminfo_data; + *pp != NULL; + pp = &(*pp)->next) + ; + *pp = sdata; + } + else + { + while (1) + { + struct coff_syminfo_data **pp; + + pp = (struct coff_syminfo_data **) (void *) &state->syminfo_data; + + while (1) + { + struct coff_syminfo_data *p; + + p = backtrace_atomic_load_pointer (pp); + + if (p == NULL) + break; + + pp = &p->next; + } + + if (__sync_bool_compare_and_swap (pp, NULL, sdata)) + break; + } + } +} + +/* Compare an ADDR against an elf_symbol for bsearch. We allocate one + extra entry in the array so that this can look safely at the next + entry. */ + +static int +coff_symbol_search (const void *vkey, const void *ventry) +{ + const uintptr_t *key = (const uintptr_t *) vkey; + const struct coff_symbol *entry = (const struct coff_symbol *) ventry; + uintptr_t addr; + + addr = *key; + if (addr < entry->address) + return -1; + else if (addr >= entry[1].address) + return 1; + else + return 0; +} + +/* Return the symbol name and value for an ADDR. */ + +static void +coff_syminfo (struct backtrace_state *state, uintptr_t addr, + backtrace_syminfo_callback callback, + backtrace_error_callback error_callback ATTRIBUTE_UNUSED, + void *data) +{ + struct coff_syminfo_data *sdata; + struct coff_symbol *sym = NULL; + + if (!state->threaded) + { + for (sdata = (struct coff_syminfo_data *) state->syminfo_data; + sdata != NULL; + sdata = sdata->next) + { + sym = ((struct coff_symbol *) + bsearch (&addr, sdata->symbols, sdata->count, + sizeof (struct coff_symbol), coff_symbol_search)); + if (sym != NULL) + break; + } + } + else + { + struct coff_syminfo_data **pp; + + pp = (struct coff_syminfo_data **) (void *) &state->syminfo_data; + while (1) + { + sdata = backtrace_atomic_load_pointer (pp); + if (sdata == NULL) + break; + + sym = ((struct coff_symbol *) + bsearch (&addr, sdata->symbols, sdata->count, + sizeof (struct coff_symbol), coff_symbol_search)); + if (sym != NULL) + break; + + pp = &sdata->next; + } + } + + if (sym == NULL) + callback (data, addr, NULL, 0, 0); + else + callback (data, addr, sym->name, sym->address, 0); +} + +/* Add the backtrace data for one PE/COFF file. Returns 1 on success, + 0 on failure (in both cases descriptor is closed). */ + +static int +coff_add (struct backtrace_state *state, int descriptor, + backtrace_error_callback error_callback, void *data, + fileline *fileline_fn, int *found_sym, int *found_dwarf) +{ + struct backtrace_view fhdr_view; + off_t fhdr_off; + int magic_ok; + b_coff_file_header fhdr; + off_t opt_sects_off; + size_t opt_sects_size; + unsigned int sects_num; + struct backtrace_view sects_view; + int sects_view_valid; + const b_coff_optional_header *opt_hdr; + const b_coff_section_header *sects; + struct backtrace_view str_view; + int str_view_valid; + size_t str_size; + off_t str_off; + struct backtrace_view syms_view; + off_t syms_off; + size_t syms_size; + int syms_view_valid; + unsigned int syms_num; + unsigned int i; + struct debug_section_info sections[DEBUG_MAX]; + off_t min_offset; + off_t max_offset; + struct backtrace_view debug_view; + int debug_view_valid; + int is_64; + uintptr_t image_base; + struct dwarf_sections dwarf_sections; + + *found_sym = 0; + *found_dwarf = 0; + + sects_view_valid = 0; + syms_view_valid = 0; + str_view_valid = 0; + debug_view_valid = 0; + + /* Map the MS-DOS stub (if any) and extract file header offset. */ + if (!backtrace_get_view (state, descriptor, 0, 0x40, error_callback, + data, &fhdr_view)) + goto fail; + + { + const unsigned char *vptr = fhdr_view.data; + + if (vptr[0] == 'M' && vptr[1] == 'Z') + fhdr_off = coff_read4 (vptr + 0x3c); + else + fhdr_off = 0; + } + + backtrace_release_view (state, &fhdr_view, error_callback, data); + + /* Map the coff file header. */ + if (!backtrace_get_view (state, descriptor, fhdr_off, + sizeof (b_coff_file_header) + 4, + error_callback, data, &fhdr_view)) + goto fail; + + if (fhdr_off != 0) + { + const char *magic = (const char *) fhdr_view.data; + magic_ok = memcmp (magic, "PE\0", 4) == 0; + fhdr_off += 4; + + memcpy (&fhdr, fhdr_view.data + 4, sizeof fhdr); + } + else + { + memcpy (&fhdr, fhdr_view.data, sizeof fhdr); + /* TODO: test fhdr.machine for coff but non-PE platforms. */ + magic_ok = 0; + } + backtrace_release_view (state, &fhdr_view, error_callback, data); + + if (!magic_ok) + { + error_callback (data, "executable file is not COFF", 0); + goto fail; + } + + sects_num = fhdr.number_of_sections; + syms_num = fhdr.number_of_symbols; + + opt_sects_off = fhdr_off + sizeof (fhdr); + opt_sects_size = (fhdr.size_of_optional_header + + sects_num * sizeof (b_coff_section_header)); + + /* To translate PC to file/line when using DWARF, we need to find + the .debug_info and .debug_line sections. */ + + /* Read the optional header and the section headers. */ + + if (!backtrace_get_view (state, descriptor, opt_sects_off, opt_sects_size, + error_callback, data, §s_view)) + goto fail; + sects_view_valid = 1; + opt_hdr = (const b_coff_optional_header *) sects_view.data; + sects = (const b_coff_section_header *) + (sects_view.data + fhdr.size_of_optional_header); + + is_64 = 0; + if (fhdr.size_of_optional_header > sizeof (*opt_hdr)) + { + if (opt_hdr->magic == PE_MAGIC) + image_base = opt_hdr->u.pe.image_base; + else if (opt_hdr->magic == PEP_MAGIC) + { + image_base = opt_hdr->u.pep.image_base; + is_64 = 1; + } + else + { + error_callback (data, "bad magic in PE optional header", 0); + goto fail; + } + } + else + image_base = 0; + + /* Read the symbol table and the string table. */ + + if (fhdr.pointer_to_symbol_table == 0) + { + /* No symbol table, no string table. */ + str_off = 0; + str_size = 0; + syms_num = 0; + syms_size = 0; + } + else + { + /* Symbol table is followed by the string table. The string table + starts with its length (on 4 bytes). + Map the symbol table and the length of the string table. */ + syms_off = fhdr.pointer_to_symbol_table; + syms_size = syms_num * SYM_SZ; + + if (!backtrace_get_view (state, descriptor, syms_off, syms_size + 4, + error_callback, data, &syms_view)) + goto fail; + syms_view_valid = 1; + + str_size = coff_read4 (syms_view.data + syms_size); + + str_off = syms_off + syms_size; + + if (str_size > 4) + { + /* Map string table (including the length word). */ + + if (!backtrace_get_view (state, descriptor, str_off, str_size, + error_callback, data, &str_view)) + goto fail; + str_view_valid = 1; + } + } + + memset (sections, 0, sizeof sections); + + /* Look for the symbol table. */ + for (i = 0; i < sects_num; ++i) + { + const b_coff_section_header *s = sects + i; + unsigned int str_off; + int j; + + if (s->name[0] == '/') + { + /* Extended section name. */ + str_off = atoi (s->name + 1); + } + else + str_off = 0; + + for (j = 0; j < (int) DEBUG_MAX; ++j) + { + const char *dbg_name = debug_section_names[j]; + int match; + + if (str_off != 0) + match = coff_long_name_eq (dbg_name, str_off, &str_view); + else + match = coff_short_name_eq (dbg_name, s->name); + if (match) + { + sections[j].offset = s->pointer_to_raw_data; + sections[j].size = s->virtual_size <= s->size_of_raw_data ? + s->virtual_size : s->size_of_raw_data; + break; + } + } + } + + if (syms_num != 0) + { + struct coff_syminfo_data *sdata; + + sdata = ((struct coff_syminfo_data *) + backtrace_alloc (state, sizeof *sdata, error_callback, data)); + if (sdata == NULL) + goto fail; + + if (!coff_initialize_syminfo (state, image_base, is_64, + sects, sects_num, + syms_view.data, syms_size, + str_view.data, str_size, + error_callback, data, sdata)) + { + backtrace_free (state, sdata, sizeof *sdata, error_callback, data); + goto fail; + } + + *found_sym = 1; + + coff_add_syminfo_data (state, sdata); + } + + backtrace_release_view (state, §s_view, error_callback, data); + sects_view_valid = 0; + if (syms_view_valid) + { + backtrace_release_view (state, &syms_view, error_callback, data); + syms_view_valid = 0; + } + + /* Read all the debug sections in a single view, since they are + probably adjacent in the file. We never release this view. */ + + min_offset = 0; + max_offset = 0; + for (i = 0; i < (int) DEBUG_MAX; ++i) + { + off_t end; + + if (sections[i].size == 0) + continue; + if (min_offset == 0 || sections[i].offset < min_offset) + min_offset = sections[i].offset; + end = sections[i].offset + sections[i].size; + if (end > max_offset) + max_offset = end; + } + if (min_offset == 0 || max_offset == 0) + { + if (!backtrace_close (descriptor, error_callback, data)) + goto fail; + *fileline_fn = coff_nodebug; + return 1; + } + + if (!backtrace_get_view (state, descriptor, min_offset, + max_offset - min_offset, + error_callback, data, &debug_view)) + goto fail; + debug_view_valid = 1; + + /* We've read all we need from the executable. */ + if (!backtrace_close (descriptor, error_callback, data)) + goto fail; + descriptor = -1; + + for (i = 0; i < (int) DEBUG_MAX; ++i) + { + size_t size = sections[i].size; + dwarf_sections.size[i] = size; + if (size == 0) + dwarf_sections.data[i] = NULL; + else + dwarf_sections.data[i] = ((const unsigned char *) debug_view.data + + (sections[i].offset - min_offset)); + } + + if (!backtrace_dwarf_add (state, /* base_address */ 0, &dwarf_sections, + 0, /* FIXME: is_bigendian */ + NULL, /* altlink */ + error_callback, data, fileline_fn, + NULL /* returned fileline_entry */)) + goto fail; + + *found_dwarf = 1; + + return 1; + + fail: + if (sects_view_valid) + backtrace_release_view (state, §s_view, error_callback, data); + if (str_view_valid) + backtrace_release_view (state, &str_view, error_callback, data); + if (syms_view_valid) + backtrace_release_view (state, &syms_view, error_callback, data); + if (debug_view_valid) + backtrace_release_view (state, &debug_view, error_callback, data); + if (descriptor != -1) + backtrace_close (descriptor, error_callback, data); + return 0; +} + +/* Initialize the backtrace data we need from an ELF executable. At + the ELF level, all we need to do is find the debug info + sections. */ + +int +backtrace_initialize (struct backtrace_state *state, + const char *filename ATTRIBUTE_UNUSED, int descriptor, + backtrace_error_callback error_callback, + void *data, fileline *fileline_fn) +{ + int ret; + int found_sym; + int found_dwarf; + fileline coff_fileline_fn; + + ret = coff_add (state, descriptor, error_callback, data, + &coff_fileline_fn, &found_sym, &found_dwarf); + if (!ret) + return 0; + + if (!state->threaded) + { + if (found_sym) + state->syminfo_fn = coff_syminfo; + else if (state->syminfo_fn == NULL) + state->syminfo_fn = coff_nosyms; + } + else + { + if (found_sym) + backtrace_atomic_store_pointer (&state->syminfo_fn, coff_syminfo); + else + (void) __sync_bool_compare_and_swap (&state->syminfo_fn, NULL, + coff_nosyms); + } + + if (!state->threaded) + { + if (state->fileline_fn == NULL || state->fileline_fn == coff_nodebug) + *fileline_fn = coff_fileline_fn; + } + else + { + fileline current_fn; + + current_fn = backtrace_atomic_load_pointer (&state->fileline_fn); + if (current_fn == NULL || current_fn == coff_nodebug) + *fileline_fn = coff_fileline_fn; + } + + return 1; +} diff --git a/thirdparty/libbacktrace/posix.c b/thirdparty/libbacktrace/posix.c new file mode 100644 index 0000000000..924631d2e6 --- /dev/null +++ b/thirdparty/libbacktrace/posix.c @@ -0,0 +1,104 @@ +/* posix.c -- POSIX file I/O routines for the backtrace library. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> + +#include "backtrace.h" +#include "internal.h" + +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +#ifndef O_CLOEXEC +#define O_CLOEXEC 0 +#endif + +#ifndef FD_CLOEXEC +#define FD_CLOEXEC 1 +#endif + +/* Open a file for reading. */ + +int +backtrace_open (const char *filename, backtrace_error_callback error_callback, + void *data, int *does_not_exist) +{ + int descriptor; + + if (does_not_exist != NULL) + *does_not_exist = 0; + + descriptor = open (filename, (int) (O_RDONLY | O_BINARY | O_CLOEXEC)); + if (descriptor < 0) + { + /* If DOES_NOT_EXIST is not NULL, then don't call ERROR_CALLBACK + if the file does not exist. We treat lacking permission to + open the file as the file not existing; this case arises when + running the libgo syscall package tests as root. */ + if (does_not_exist != NULL && (errno == ENOENT || errno == EACCES)) + *does_not_exist = 1; + else + error_callback (data, filename, errno); + return -1; + } + +#ifdef HAVE_FCNTL + /* Set FD_CLOEXEC just in case the kernel does not support + O_CLOEXEC. It doesn't matter if this fails for some reason. + FIXME: At some point it should be safe to only do this if + O_CLOEXEC == 0. */ + fcntl (descriptor, F_SETFD, FD_CLOEXEC); +#endif + + return descriptor; +} + +/* Close DESCRIPTOR. */ + +int +backtrace_close (int descriptor, backtrace_error_callback error_callback, + void *data) +{ + if (close (descriptor) < 0) + { + error_callback (data, "close", errno); + return 0; + } + return 1; +} diff --git a/thirdparty/libbacktrace/print.c b/thirdparty/libbacktrace/print.c new file mode 100644 index 0000000000..93d0d3abb4 --- /dev/null +++ b/thirdparty/libbacktrace/print.c @@ -0,0 +1,92 @@ +/* print.c -- Print the current backtrace. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <stdio.h> +#include <string.h> +#include <sys/types.h> + +#include "backtrace.h" +#include "internal.h" + +/* Passed to callbacks. */ + +struct print_data +{ + struct backtrace_state *state; + FILE *f; +}; + +/* Print one level of a backtrace. */ + +static int +print_callback (void *data, uintptr_t pc, const char *filename, int lineno, + const char *function) +{ + struct print_data *pdata = (struct print_data *) data; + + fprintf (pdata->f, "0x%lx %s\n\t%s:%d\n", + (unsigned long) pc, + function == NULL ? "???" : function, + filename == NULL ? "???" : filename, + lineno); + return 0; +} + +/* Print errors to stderr. */ + +static void +error_callback (void *data, const char *msg, int errnum) +{ + struct print_data *pdata = (struct print_data *) data; + + if (pdata->state->filename != NULL) + fprintf (stderr, "%s: ", pdata->state->filename); + fprintf (stderr, "libbacktrace: %s", msg); + if (errnum > 0) + fprintf (stderr, ": %s", strerror (errnum)); + fputc ('\n', stderr); +} + +/* Print a backtrace. */ + +void __attribute__((noinline)) +backtrace_print (struct backtrace_state *state, int skip, FILE *f) +{ + struct print_data data; + + data.state = state; + data.f = f; + backtrace_full (state, skip + 1, print_callback, error_callback, + (void *) &data); +} diff --git a/thirdparty/libbacktrace/read.c b/thirdparty/libbacktrace/read.c new file mode 100644 index 0000000000..1811c8d2e0 --- /dev/null +++ b/thirdparty/libbacktrace/read.c @@ -0,0 +1,110 @@ +/* read.c -- File views without mmap. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <errno.h> +#include <stdlib.h> +#include <sys/types.h> +#include <unistd.h> + +#include "backtrace.h" +#include "internal.h" + +/* This file implements file views when mmap is not available. */ + +/* Create a view of SIZE bytes from DESCRIPTOR at OFFSET. */ + +int +backtrace_get_view (struct backtrace_state *state, int descriptor, + off_t offset, uint64_t size, + backtrace_error_callback error_callback, + void *data, struct backtrace_view *view) +{ + uint64_t got; + ssize_t r; + + if ((uint64_t) (size_t) size != size) + { + error_callback (data, "file size too large", 0); + return 0; + } + + if (lseek (descriptor, offset, SEEK_SET) < 0) + { + error_callback (data, "lseek", errno); + return 0; + } + + view->base = backtrace_alloc (state, size, error_callback, data); + if (view->base == NULL) + return 0; + view->data = view->base; + view->len = size; + + got = 0; + while (got < size) + { + r = read (descriptor, view->base, size - got); + if (r < 0) + { + error_callback (data, "read", errno); + free (view->base); + return 0; + } + if (r == 0) + break; + got += (uint64_t) r; + } + + if (got < size) + { + error_callback (data, "file too short", 0); + free (view->base); + return 0; + } + + return 1; +} + +/* Release a view read by backtrace_get_view. */ + +void +backtrace_release_view (struct backtrace_state *state, + struct backtrace_view *view, + backtrace_error_callback error_callback, + void *data) +{ + backtrace_free (state, view->base, view->len, error_callback, data); + view->data = NULL; + view->base = NULL; +} diff --git a/thirdparty/libbacktrace/simple.c b/thirdparty/libbacktrace/simple.c new file mode 100644 index 0000000000..785e726e6b --- /dev/null +++ b/thirdparty/libbacktrace/simple.c @@ -0,0 +1,108 @@ +/* simple.c -- The backtrace_simple function. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include "unwind.h" +#include "backtrace.h" + +/* The simple_backtrace routine. */ + +/* Data passed through _Unwind_Backtrace. */ + +struct backtrace_simple_data +{ + /* Number of frames to skip. */ + int skip; + /* Library state. */ + struct backtrace_state *state; + /* Callback routine. */ + backtrace_simple_callback callback; + /* Error callback routine. */ + backtrace_error_callback error_callback; + /* Data to pass to callback routine. */ + void *data; + /* Value to return from backtrace. */ + int ret; +}; + +/* Unwind library callback routine. This is passed to + _Unwind_Backtrace. */ + +static _Unwind_Reason_Code +simple_unwind (struct _Unwind_Context *context, void *vdata) +{ + struct backtrace_simple_data *bdata = (struct backtrace_simple_data *) vdata; + uintptr_t pc; + int ip_before_insn = 0; + +#ifdef HAVE_GETIPINFO + pc = _Unwind_GetIPInfo (context, &ip_before_insn); +#else + pc = _Unwind_GetIP (context); +#endif + + if (bdata->skip > 0) + { + --bdata->skip; + return _URC_NO_REASON; + } + + if (!ip_before_insn) + --pc; + + bdata->ret = bdata->callback (bdata->data, pc); + + if (bdata->ret != 0) + return _URC_END_OF_STACK; + + return _URC_NO_REASON; +} + +/* Get a simple stack backtrace. */ + +int __attribute__((noinline)) +backtrace_simple (struct backtrace_state *state, int skip, + backtrace_simple_callback callback, + backtrace_error_callback error_callback, void *data) +{ + struct backtrace_simple_data bdata; + + bdata.skip = skip + 1; + bdata.state = state; + bdata.callback = callback; + bdata.error_callback = error_callback; + bdata.data = data; + bdata.ret = 0; + _Unwind_Backtrace (simple_unwind, &bdata); + return bdata.ret; +} diff --git a/thirdparty/libbacktrace/sort.c b/thirdparty/libbacktrace/sort.c new file mode 100644 index 0000000000..a60a980e65 --- /dev/null +++ b/thirdparty/libbacktrace/sort.c @@ -0,0 +1,108 @@ +/* sort.c -- Sort without allocating memory + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <stddef.h> +#include <sys/types.h> + +#include "backtrace.h" +#include "internal.h" + +/* The GNU glibc version of qsort allocates memory, which we must not + do if we are invoked by a signal handler. So provide our own + sort. */ + +static void +swap (char *a, char *b, size_t size) +{ + size_t i; + + for (i = 0; i < size; i++, a++, b++) + { + char t; + + t = *a; + *a = *b; + *b = t; + } +} + +void +backtrace_qsort (void *basearg, size_t count, size_t size, + int (*compar) (const void *, const void *)) +{ + char *base = (char *) basearg; + size_t i; + size_t mid; + + tail_recurse: + if (count < 2) + return; + + /* The symbol table and DWARF tables, which is all we use this + routine for, tend to be roughly sorted. Pick the middle element + in the array as our pivot point, so that we are more likely to + cut the array in half for each recursion step. */ + swap (base, base + (count / 2) * size, size); + + mid = 0; + for (i = 1; i < count; i++) + { + if ((*compar) (base, base + i * size) > 0) + { + ++mid; + if (i != mid) + swap (base + mid * size, base + i * size, size); + } + } + + if (mid > 0) + swap (base, base + mid * size, size); + + /* Recurse with the smaller array, loop with the larger one. That + ensures that our maximum stack depth is log count. */ + if (2 * mid < count) + { + backtrace_qsort (base, mid, size, compar); + base += (mid + 1) * size; + count -= mid + 1; + goto tail_recurse; + } + else + { + backtrace_qsort (base + (mid + 1) * size, count - (mid + 1), + size, compar); + count = mid; + goto tail_recurse; + } +} diff --git a/thirdparty/libbacktrace/state.c b/thirdparty/libbacktrace/state.c new file mode 100644 index 0000000000..0f368a2390 --- /dev/null +++ b/thirdparty/libbacktrace/state.c @@ -0,0 +1,72 @@ +/* state.c -- Create the backtrace state. + Copyright (C) 2012-2021 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. */ + +#include "config.h" + +#include <string.h> +#include <sys/types.h> + +#include "backtrace.h" +#include "backtrace-supported.h" +#include "internal.h" + +/* Create the backtrace state. This will then be passed to all the + other routines. */ + +struct backtrace_state * +backtrace_create_state (const char *filename, int threaded, + backtrace_error_callback error_callback, + void *data) +{ + struct backtrace_state init_state; + struct backtrace_state *state; + +#ifndef HAVE_SYNC_FUNCTIONS + if (threaded) + { + error_callback (data, "backtrace library does not support threads", 0); + return NULL; + } +#endif + + memset (&init_state, 0, sizeof init_state); + init_state.filename = filename; + init_state.threaded = threaded; + + state = ((struct backtrace_state *) + backtrace_alloc (&init_state, sizeof *state, error_callback, data)); + if (state == NULL) + return NULL; + *state = init_state; + + return state; +} diff --git a/thirdparty/mbedtls/include/godot_module_mbedtls_config.h b/thirdparty/mbedtls/include/godot_module_mbedtls_config.h index aed276766f..2011827b7a 100644 --- a/thirdparty/mbedtls/include/godot_module_mbedtls_config.h +++ b/thirdparty/mbedtls/include/godot_module_mbedtls_config.h @@ -49,8 +49,10 @@ #undef MBEDTLS_DES_C #undef MBEDTLS_DHM_C -#ifndef __linux__ +#if !(defined(__linux__) && defined(__aarch64__)) // ARMv8 hardware AES operations. Detection only possible on linux. +// May technically be supported on some ARM32 arches but doesn't seem +// to be in our current Linux SDK's neon-fp-armv8. #undef MBEDTLS_AESCE_C #endif diff --git a/thirdparty/misc/patches/qoa-min-fix.patch b/thirdparty/misc/patches/qoa-min-fix.patch new file mode 100644 index 0000000000..1043d8bbe7 --- /dev/null +++ b/thirdparty/misc/patches/qoa-min-fix.patch @@ -0,0 +1,155 @@ +diff --git a/qoa.h b/qoa.h +index aa8fb59434..2dde8df098 100644 +--- a/qoa.h ++++ b/qoa.h +@@ -140,14 +140,14 @@ typedef struct { + #endif + } qoa_desc; + +-unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes); +-unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes); +-void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len); ++inline unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes); ++inline unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes); ++inline void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len); + +-unsigned int qoa_max_frame_size(qoa_desc *qoa); +-unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa); +-unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len); +-short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *file); ++inline unsigned int qoa_max_frame_size(qoa_desc *qoa); ++inline unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa); ++inline unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len); ++inline short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *file); + + #ifndef QOA_NO_STDIO + +@@ -366,7 +366,7 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned + ), bytes, &p); + + +- for (int c = 0; c < channels; c++) { ++ for (unsigned int c = 0; c < channels; c++) { + /* Write the current LMS state */ + qoa_uint64_t weights = 0; + qoa_uint64_t history = 0; +@@ -380,9 +380,9 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned + + /* We encode all samples with the channels interleaved on a slice level. + E.g. for stereo: (ch-0, slice 0), (ch 1, slice 0), (ch 0, slice 1), ...*/ +- for (int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) { ++ for (unsigned int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) { + +- for (int c = 0; c < channels; c++) { ++ for (unsigned int c = 0; c < channels; c++) { + int slice_len = qoa_clamp(QOA_SLICE_LEN, 0, frame_len - sample_index); + int slice_start = sample_index * channels + c; + int slice_end = (sample_index + slice_len) * channels + c; +@@ -391,10 +391,9 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned + 16 scalefactors, encode all samples for the current slice and + meassure the total squared error. */ + qoa_uint64_t best_rank = -1; +- qoa_uint64_t best_error = -1; +- qoa_uint64_t best_slice; +- qoa_lms_t best_lms; +- int best_scalefactor; ++ qoa_uint64_t best_slice = -1; ++ qoa_lms_t best_lms = {{-1, -1, -1, -1}, {-1, -1, -1, -1}}; ++ int best_scalefactor = -1; + + for (int sfi = 0; sfi < 16; sfi++) { + /* There is a strong correlation between the scalefactors of +@@ -408,7 +407,6 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned + qoa_lms_t lms = qoa->lms[c]; + qoa_uint64_t slice = scalefactor; + qoa_uint64_t current_rank = 0; +- qoa_uint64_t current_error = 0; + + for (int si = slice_start; si < slice_end; si += channels) { + int sample = sample_data[si]; +@@ -438,7 +436,6 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned + qoa_uint64_t error_sq = error * error; + + current_rank += error_sq + weights_penalty * weights_penalty; +- current_error += error_sq; + if (current_rank > best_rank) { + break; + } +@@ -449,7 +446,6 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned + + if (current_rank < best_rank) { + best_rank = current_rank; +- best_error = current_error; + best_slice = slice; + best_lms = lms; + best_scalefactor = scalefactor; +@@ -492,9 +488,9 @@ void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len) + num_frames * QOA_LMS_LEN * 4 * qoa->channels + /* 4 * 4 bytes lms state per channel */ + num_slices * 8 * qoa->channels; /* 8 byte slices */ + +- unsigned char *bytes = QOA_MALLOC(encoded_size); ++ unsigned char *bytes = (unsigned char *)QOA_MALLOC(encoded_size); + +- for (int c = 0; c < qoa->channels; c++) { ++ for (unsigned int c = 0; c < qoa->channels; c++) { + /* Set the initial LMS weights to {0, 0, -1, 2}. This helps with the + prediction of the first few ms of a file. */ + qoa->lms[c].weights[0] = 0; +@@ -517,7 +513,7 @@ void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len) + #endif + + int frame_len = QOA_FRAME_LEN; +- for (int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) { ++ for (unsigned int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) { + frame_len = qoa_clamp(QOA_FRAME_LEN, 0, qoa->samples - sample_index); + const short *frame_samples = sample_data + sample_index * qoa->channels; + unsigned int frame_size = qoa_encode_frame(frame_samples, qoa, frame_len, bytes + p); +@@ -580,14 +576,14 @@ unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa + + /* Read and verify the frame header */ + qoa_uint64_t frame_header = qoa_read_u64(bytes, &p); +- int channels = (frame_header >> 56) & 0x0000ff; +- int samplerate = (frame_header >> 32) & 0xffffff; +- int samples = (frame_header >> 16) & 0x00ffff; +- int frame_size = (frame_header ) & 0x00ffff; ++ unsigned int channels = (frame_header >> 56) & 0x0000ff; ++ unsigned int samplerate = (frame_header >> 32) & 0xffffff; ++ unsigned int samples = (frame_header >> 16) & 0x00ffff; ++ unsigned int frame_size = (frame_header ) & 0x00ffff; + + int data_size = frame_size - 8 - QOA_LMS_LEN * 4 * channels; + int num_slices = data_size / 8; +- int max_total_samples = num_slices * QOA_SLICE_LEN; ++ unsigned int max_total_samples = num_slices * QOA_SLICE_LEN; + + if ( + channels != qoa->channels || +@@ -600,7 +596,7 @@ unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa + + + /* Read the LMS state: 4 x 2 bytes history, 4 x 2 bytes weights per channel */ +- for (int c = 0; c < channels; c++) { ++ for (unsigned int c = 0; c < channels; c++) { + qoa_uint64_t history = qoa_read_u64(bytes, &p); + qoa_uint64_t weights = qoa_read_u64(bytes, &p); + for (int i = 0; i < QOA_LMS_LEN; i++) { +@@ -613,8 +609,8 @@ unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa + + + /* Decode all slices for all channels in this frame */ +- for (int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) { +- for (int c = 0; c < channels; c++) { ++ for (unsigned int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) { ++ for (unsigned int c = 0; c < channels; c++) { + qoa_uint64_t slice = qoa_read_u64(bytes, &p); + + int scalefactor = (slice >> 60) & 0xf; +@@ -647,7 +643,7 @@ short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *qoa) { + + /* Calculate the required size of the sample buffer and allocate */ + int total_samples = qoa->samples * qoa->channels; +- short *sample_data = QOA_MALLOC(total_samples * sizeof(short)); ++ short *sample_data = (short *)QOA_MALLOC(total_samples * sizeof(short)); + + unsigned int sample_index = 0; + unsigned int frame_len; diff --git a/thirdparty/misc/qoa.h b/thirdparty/misc/qoa.h new file mode 100644 index 0000000000..2dde8df098 --- /dev/null +++ b/thirdparty/misc/qoa.h @@ -0,0 +1,728 @@ +/* + +Copyright (c) 2023, Dominic Szablewski - https://phoboslab.org +SPDX-License-Identifier: MIT + +QOA - The "Quite OK Audio" format for fast, lossy audio compression + + +-- Data Format + +QOA encodes pulse-code modulated (PCM) audio data with up to 255 channels, +sample rates from 1 up to 16777215 hertz and a bit depth of 16 bits. + +The compression method employed in QOA is lossy; it discards some information +from the uncompressed PCM data. For many types of audio signals this compression +is "transparent", i.e. the difference from the original file is often not +audible. + +QOA encodes 20 samples of 16 bit PCM data into slices of 64 bits. A single +sample therefore requires 3.2 bits of storage space, resulting in a 5x +compression (16 / 3.2). + +A QOA file consists of an 8 byte file header, followed by a number of frames. +Each frame contains an 8 byte frame header, the current 16 byte en-/decoder +state per channel and 256 slices per channel. Each slice is 8 bytes wide and +encodes 20 samples of audio data. + +All values, including the slices, are big endian. The file layout is as follows: + +struct { + struct { + char magic[4]; // magic bytes "qoaf" + uint32_t samples; // samples per channel in this file + } file_header; + + struct { + struct { + uint8_t num_channels; // no. of channels + uint24_t samplerate; // samplerate in hz + uint16_t fsamples; // samples per channel in this frame + uint16_t fsize; // frame size (includes this header) + } frame_header; + + struct { + int16_t history[4]; // most recent last + int16_t weights[4]; // most recent last + } lms_state[num_channels]; + + qoa_slice_t slices[256][num_channels]; + + } frames[ceil(samples / (256 * 20))]; +} qoa_file_t; + +Each `qoa_slice_t` contains a quantized scalefactor `sf_quant` and 20 quantized +residuals `qrNN`: + +.- QOA_SLICE -- 64 bits, 20 samples --------------------------/ /------------. +| Byte[0] | Byte[1] | Byte[2] \ \ Byte[7] | +| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | 7 6 5 / / 2 1 0 | +|------------+--------+--------+--------+---------+---------+-\ \--+---------| +| sf_quant | qr00 | qr01 | qr02 | qr03 | qr04 | / / | qr19 | +`-------------------------------------------------------------\ \------------` + +Each frame except the last must contain exactly 256 slices per channel. The last +frame may contain between 1 .. 256 (inclusive) slices per channel. The last +slice (for each channel) in the last frame may contain less than 20 samples; the +slice still must be 8 bytes wide, with the unused samples zeroed out. + +Channels are interleaved per slice. E.g. for 2 channel stereo: +slice[0] = L, slice[1] = R, slice[2] = L, slice[3] = R ... + +A valid QOA file or stream must have at least one frame. Each frame must contain +at least one channel and one sample with a samplerate between 1 .. 16777215 +(inclusive). + +If the total number of samples is not known by the encoder, the samples in the +file header may be set to 0x00000000 to indicate that the encoder is +"streaming". In a streaming context, the samplerate and number of channels may +differ from frame to frame. For static files (those with samples set to a +non-zero value), each frame must have the same number of channels and same +samplerate. + +Note that this implementation of QOA only handles files with a known total +number of samples. + +A decoder should support at least 8 channels. The channel layout for channel +counts 1 .. 8 is: + + 1. Mono + 2. L, R + 3. L, R, C + 4. FL, FR, B/SL, B/SR + 5. FL, FR, C, B/SL, B/SR + 6. FL, FR, C, LFE, B/SL, B/SR + 7. FL, FR, C, LFE, B, SL, SR + 8. FL, FR, C, LFE, BL, BR, SL, SR + +QOA predicts each audio sample based on the previously decoded ones using a +"Sign-Sign Least Mean Squares Filter" (LMS). This prediction plus the +dequantized residual forms the final output sample. + +*/ + + + +/* ----------------------------------------------------------------------------- + Header - Public functions */ + +#ifndef QOA_H +#define QOA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define QOA_MIN_FILESIZE 16 +#define QOA_MAX_CHANNELS 8 + +#define QOA_SLICE_LEN 20 +#define QOA_SLICES_PER_FRAME 256 +#define QOA_FRAME_LEN (QOA_SLICES_PER_FRAME * QOA_SLICE_LEN) +#define QOA_LMS_LEN 4 +#define QOA_MAGIC 0x716f6166 /* 'qoaf' */ + +#define QOA_FRAME_SIZE(channels, slices) \ + (8 + QOA_LMS_LEN * 4 * channels + 8 * slices * channels) + +typedef struct { + int history[QOA_LMS_LEN]; + int weights[QOA_LMS_LEN]; +} qoa_lms_t; + +typedef struct { + unsigned int channels; + unsigned int samplerate; + unsigned int samples; + qoa_lms_t lms[QOA_MAX_CHANNELS]; + #ifdef QOA_RECORD_TOTAL_ERROR + double error; + #endif +} qoa_desc; + +inline unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes); +inline unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes); +inline void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len); + +inline unsigned int qoa_max_frame_size(qoa_desc *qoa); +inline unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa); +inline unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len); +inline short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *file); + +#ifndef QOA_NO_STDIO + +int qoa_write(const char *filename, const short *sample_data, qoa_desc *qoa); +void *qoa_read(const char *filename, qoa_desc *qoa); + +#endif /* QOA_NO_STDIO */ + + +#ifdef __cplusplus +} +#endif +#endif /* QOA_H */ + + +/* ----------------------------------------------------------------------------- + Implementation */ + +#ifdef QOA_IMPLEMENTATION +#include <stdlib.h> + +#ifndef QOA_MALLOC + #define QOA_MALLOC(sz) malloc(sz) + #define QOA_FREE(p) free(p) +#endif + +typedef unsigned long long qoa_uint64_t; + + +/* The quant_tab provides an index into the dequant_tab for residuals in the +range of -8 .. 8. It maps this range to just 3bits and becomes less accurate at +the higher end. Note that the residual zero is identical to the lowest positive +value. This is mostly fine, since the qoa_div() function always rounds away +from zero. */ + +static const int qoa_quant_tab[17] = { + 7, 7, 7, 5, 5, 3, 3, 1, /* -8..-1 */ + 0, /* 0 */ + 0, 2, 2, 4, 4, 6, 6, 6 /* 1.. 8 */ +}; + + +/* We have 16 different scalefactors. Like the quantized residuals these become +less accurate at the higher end. In theory, the highest scalefactor that we +would need to encode the highest 16bit residual is (2**16)/8 = 8192. However we +rely on the LMS filter to predict samples accurately enough that a maximum +residual of one quarter of the 16 bit range is sufficient. I.e. with the +scalefactor 2048 times the quant range of 8 we can encode residuals up to 2**14. + +The scalefactor values are computed as: +scalefactor_tab[s] <- round(pow(s + 1, 2.75)) */ + +static const int qoa_scalefactor_tab[16] = { + 1, 7, 21, 45, 84, 138, 211, 304, 421, 562, 731, 928, 1157, 1419, 1715, 2048 +}; + + +/* The reciprocal_tab maps each of the 16 scalefactors to their rounded +reciprocals 1/scalefactor. This allows us to calculate the scaled residuals in +the encoder with just one multiplication instead of an expensive division. We +do this in .16 fixed point with integers, instead of floats. + +The reciprocal_tab is computed as: +reciprocal_tab[s] <- ((1<<16) + scalefactor_tab[s] - 1) / scalefactor_tab[s] */ + +static const int qoa_reciprocal_tab[16] = { + 65536, 9363, 3121, 1457, 781, 475, 311, 216, 156, 117, 90, 71, 57, 47, 39, 32 +}; + + +/* The dequant_tab maps each of the scalefactors and quantized residuals to +their unscaled & dequantized version. + +Since qoa_div rounds away from the zero, the smallest entries are mapped to 3/4 +instead of 1. The dequant_tab assumes the following dequantized values for each +of the quant_tab indices and is computed as: +float dqt[8] = {0.75, -0.75, 2.5, -2.5, 4.5, -4.5, 7, -7}; +dequant_tab[s][q] <- round_ties_away_from_zero(scalefactor_tab[s] * dqt[q]) + +The rounding employed here is "to nearest, ties away from zero", i.e. positive +and negative values are treated symmetrically. +*/ + +static const int qoa_dequant_tab[16][8] = { + { 1, -1, 3, -3, 5, -5, 7, -7}, + { 5, -5, 18, -18, 32, -32, 49, -49}, + { 16, -16, 53, -53, 95, -95, 147, -147}, + { 34, -34, 113, -113, 203, -203, 315, -315}, + { 63, -63, 210, -210, 378, -378, 588, -588}, + { 104, -104, 345, -345, 621, -621, 966, -966}, + { 158, -158, 528, -528, 950, -950, 1477, -1477}, + { 228, -228, 760, -760, 1368, -1368, 2128, -2128}, + { 316, -316, 1053, -1053, 1895, -1895, 2947, -2947}, + { 422, -422, 1405, -1405, 2529, -2529, 3934, -3934}, + { 548, -548, 1828, -1828, 3290, -3290, 5117, -5117}, + { 696, -696, 2320, -2320, 4176, -4176, 6496, -6496}, + { 868, -868, 2893, -2893, 5207, -5207, 8099, -8099}, + {1064, -1064, 3548, -3548, 6386, -6386, 9933, -9933}, + {1286, -1286, 4288, -4288, 7718, -7718, 12005, -12005}, + {1536, -1536, 5120, -5120, 9216, -9216, 14336, -14336}, +}; + + +/* The Least Mean Squares Filter is the heart of QOA. It predicts the next +sample based on the previous 4 reconstructed samples. It does so by continuously +adjusting 4 weights based on the residual of the previous prediction. + +The next sample is predicted as the sum of (weight[i] * history[i]). + +The adjustment of the weights is done with a "Sign-Sign-LMS" that adds or +subtracts the residual to each weight, based on the corresponding sample from +the history. This, surprisingly, is sufficient to get worthwhile predictions. + +This is all done with fixed point integers. Hence the right-shifts when updating +the weights and calculating the prediction. */ + +static int qoa_lms_predict(qoa_lms_t *lms) { + int prediction = 0; + for (int i = 0; i < QOA_LMS_LEN; i++) { + prediction += lms->weights[i] * lms->history[i]; + } + return prediction >> 13; +} + +static void qoa_lms_update(qoa_lms_t *lms, int sample, int residual) { + int delta = residual >> 4; + for (int i = 0; i < QOA_LMS_LEN; i++) { + lms->weights[i] += lms->history[i] < 0 ? -delta : delta; + } + + for (int i = 0; i < QOA_LMS_LEN-1; i++) { + lms->history[i] = lms->history[i+1]; + } + lms->history[QOA_LMS_LEN-1] = sample; +} + + +/* qoa_div() implements a rounding division, but avoids rounding to zero for +small numbers. E.g. 0.1 will be rounded to 1. Note that 0 itself still +returns as 0, which is handled in the qoa_quant_tab[]. +qoa_div() takes an index into the .16 fixed point qoa_reciprocal_tab as an +argument, so it can do the division with a cheaper integer multiplication. */ + +static inline int qoa_div(int v, int scalefactor) { + int reciprocal = qoa_reciprocal_tab[scalefactor]; + int n = (v * reciprocal + (1 << 15)) >> 16; + n = n + ((v > 0) - (v < 0)) - ((n > 0) - (n < 0)); /* round away from 0 */ + return n; +} + +static inline int qoa_clamp(int v, int min, int max) { + if (v < min) { return min; } + if (v > max) { return max; } + return v; +} + +/* This specialized clamp function for the signed 16 bit range improves decode +performance quite a bit. The extra if() statement works nicely with the CPUs +branch prediction as this branch is rarely taken. */ + +static inline int qoa_clamp_s16(int v) { + if ((unsigned int)(v + 32768) > 65535) { + if (v < -32768) { return -32768; } + if (v > 32767) { return 32767; } + } + return v; +} + +static inline qoa_uint64_t qoa_read_u64(const unsigned char *bytes, unsigned int *p) { + bytes += *p; + *p += 8; + return + ((qoa_uint64_t)(bytes[0]) << 56) | ((qoa_uint64_t)(bytes[1]) << 48) | + ((qoa_uint64_t)(bytes[2]) << 40) | ((qoa_uint64_t)(bytes[3]) << 32) | + ((qoa_uint64_t)(bytes[4]) << 24) | ((qoa_uint64_t)(bytes[5]) << 16) | + ((qoa_uint64_t)(bytes[6]) << 8) | ((qoa_uint64_t)(bytes[7]) << 0); +} + +static inline void qoa_write_u64(qoa_uint64_t v, unsigned char *bytes, unsigned int *p) { + bytes += *p; + *p += 8; + bytes[0] = (v >> 56) & 0xff; + bytes[1] = (v >> 48) & 0xff; + bytes[2] = (v >> 40) & 0xff; + bytes[3] = (v >> 32) & 0xff; + bytes[4] = (v >> 24) & 0xff; + bytes[5] = (v >> 16) & 0xff; + bytes[6] = (v >> 8) & 0xff; + bytes[7] = (v >> 0) & 0xff; +} + + +/* ----------------------------------------------------------------------------- + Encoder */ + +unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes) { + unsigned int p = 0; + qoa_write_u64(((qoa_uint64_t)QOA_MAGIC << 32) | qoa->samples, bytes, &p); + return p; +} + +unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes) { + unsigned int channels = qoa->channels; + + unsigned int p = 0; + unsigned int slices = (frame_len + QOA_SLICE_LEN - 1) / QOA_SLICE_LEN; + unsigned int frame_size = QOA_FRAME_SIZE(channels, slices); + int prev_scalefactor[QOA_MAX_CHANNELS] = {0}; + + /* Write the frame header */ + qoa_write_u64(( + (qoa_uint64_t)qoa->channels << 56 | + (qoa_uint64_t)qoa->samplerate << 32 | + (qoa_uint64_t)frame_len << 16 | + (qoa_uint64_t)frame_size + ), bytes, &p); + + + for (unsigned int c = 0; c < channels; c++) { + /* Write the current LMS state */ + qoa_uint64_t weights = 0; + qoa_uint64_t history = 0; + for (int i = 0; i < QOA_LMS_LEN; i++) { + history = (history << 16) | (qoa->lms[c].history[i] & 0xffff); + weights = (weights << 16) | (qoa->lms[c].weights[i] & 0xffff); + } + qoa_write_u64(history, bytes, &p); + qoa_write_u64(weights, bytes, &p); + } + + /* We encode all samples with the channels interleaved on a slice level. + E.g. for stereo: (ch-0, slice 0), (ch 1, slice 0), (ch 0, slice 1), ...*/ + for (unsigned int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) { + + for (unsigned int c = 0; c < channels; c++) { + int slice_len = qoa_clamp(QOA_SLICE_LEN, 0, frame_len - sample_index); + int slice_start = sample_index * channels + c; + int slice_end = (sample_index + slice_len) * channels + c; + + /* Brute for search for the best scalefactor. Just go through all + 16 scalefactors, encode all samples for the current slice and + meassure the total squared error. */ + qoa_uint64_t best_rank = -1; + qoa_uint64_t best_slice = -1; + qoa_lms_t best_lms = {{-1, -1, -1, -1}, {-1, -1, -1, -1}}; + int best_scalefactor = -1; + + for (int sfi = 0; sfi < 16; sfi++) { + /* There is a strong correlation between the scalefactors of + neighboring slices. As an optimization, start testing + the best scalefactor of the previous slice first. */ + int scalefactor = (sfi + prev_scalefactor[c]) % 16; + + /* We have to reset the LMS state to the last known good one + before trying each scalefactor, as each pass updates the LMS + state when encoding. */ + qoa_lms_t lms = qoa->lms[c]; + qoa_uint64_t slice = scalefactor; + qoa_uint64_t current_rank = 0; + + for (int si = slice_start; si < slice_end; si += channels) { + int sample = sample_data[si]; + int predicted = qoa_lms_predict(&lms); + + int residual = sample - predicted; + int scaled = qoa_div(residual, scalefactor); + int clamped = qoa_clamp(scaled, -8, 8); + int quantized = qoa_quant_tab[clamped + 8]; + int dequantized = qoa_dequant_tab[scalefactor][quantized]; + int reconstructed = qoa_clamp_s16(predicted + dequantized); + + + /* If the weights have grown too large, we introduce a penalty + here. This prevents pops/clicks in certain problem cases */ + int weights_penalty = (( + lms.weights[0] * lms.weights[0] + + lms.weights[1] * lms.weights[1] + + lms.weights[2] * lms.weights[2] + + lms.weights[3] * lms.weights[3] + ) >> 18) - 0x8ff; + if (weights_penalty < 0) { + weights_penalty = 0; + } + + long long error = (sample - reconstructed); + qoa_uint64_t error_sq = error * error; + + current_rank += error_sq + weights_penalty * weights_penalty; + if (current_rank > best_rank) { + break; + } + + qoa_lms_update(&lms, reconstructed, dequantized); + slice = (slice << 3) | quantized; + } + + if (current_rank < best_rank) { + best_rank = current_rank; + best_slice = slice; + best_lms = lms; + best_scalefactor = scalefactor; + } + } + + prev_scalefactor[c] = best_scalefactor; + + qoa->lms[c] = best_lms; + #ifdef QOA_RECORD_TOTAL_ERROR + qoa->error += best_error; + #endif + + /* If this slice was shorter than QOA_SLICE_LEN, we have to left- + shift all encoded data, to ensure the rightmost bits are the empty + ones. This should only happen in the last frame of a file as all + slices are completely filled otherwise. */ + best_slice <<= (QOA_SLICE_LEN - slice_len) * 3; + qoa_write_u64(best_slice, bytes, &p); + } + } + + return p; +} + +void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len) { + if ( + qoa->samples == 0 || + qoa->samplerate == 0 || qoa->samplerate > 0xffffff || + qoa->channels == 0 || qoa->channels > QOA_MAX_CHANNELS + ) { + return NULL; + } + + /* Calculate the encoded size and allocate */ + unsigned int num_frames = (qoa->samples + QOA_FRAME_LEN-1) / QOA_FRAME_LEN; + unsigned int num_slices = (qoa->samples + QOA_SLICE_LEN-1) / QOA_SLICE_LEN; + unsigned int encoded_size = 8 + /* 8 byte file header */ + num_frames * 8 + /* 8 byte frame headers */ + num_frames * QOA_LMS_LEN * 4 * qoa->channels + /* 4 * 4 bytes lms state per channel */ + num_slices * 8 * qoa->channels; /* 8 byte slices */ + + unsigned char *bytes = (unsigned char *)QOA_MALLOC(encoded_size); + + for (unsigned int c = 0; c < qoa->channels; c++) { + /* Set the initial LMS weights to {0, 0, -1, 2}. This helps with the + prediction of the first few ms of a file. */ + qoa->lms[c].weights[0] = 0; + qoa->lms[c].weights[1] = 0; + qoa->lms[c].weights[2] = -(1<<13); + qoa->lms[c].weights[3] = (1<<14); + + /* Explicitly set the history samples to 0, as we might have some + garbage in there. */ + for (int i = 0; i < QOA_LMS_LEN; i++) { + qoa->lms[c].history[i] = 0; + } + } + + + /* Encode the header and go through all frames */ + unsigned int p = qoa_encode_header(qoa, bytes); + #ifdef QOA_RECORD_TOTAL_ERROR + qoa->error = 0; + #endif + + int frame_len = QOA_FRAME_LEN; + for (unsigned int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) { + frame_len = qoa_clamp(QOA_FRAME_LEN, 0, qoa->samples - sample_index); + const short *frame_samples = sample_data + sample_index * qoa->channels; + unsigned int frame_size = qoa_encode_frame(frame_samples, qoa, frame_len, bytes + p); + p += frame_size; + } + + *out_len = p; + return bytes; +} + + + +/* ----------------------------------------------------------------------------- + Decoder */ + +unsigned int qoa_max_frame_size(qoa_desc *qoa) { + return QOA_FRAME_SIZE(qoa->channels, QOA_SLICES_PER_FRAME); +} + +unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa) { + unsigned int p = 0; + if (size < QOA_MIN_FILESIZE) { + return 0; + } + + + /* Read the file header, verify the magic number ('qoaf') and read the + total number of samples. */ + qoa_uint64_t file_header = qoa_read_u64(bytes, &p); + + if ((file_header >> 32) != QOA_MAGIC) { + return 0; + } + + qoa->samples = file_header & 0xffffffff; + if (!qoa->samples) { + return 0; + } + + /* Peek into the first frame header to get the number of channels and + the samplerate. */ + qoa_uint64_t frame_header = qoa_read_u64(bytes, &p); + qoa->channels = (frame_header >> 56) & 0x0000ff; + qoa->samplerate = (frame_header >> 32) & 0xffffff; + + if (qoa->channels == 0 || qoa->samples == 0 || qoa->samplerate == 0) { + return 0; + } + + return 8; +} + +unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len) { + unsigned int p = 0; + *frame_len = 0; + + if (size < 8 + QOA_LMS_LEN * 4 * qoa->channels) { + return 0; + } + + /* Read and verify the frame header */ + qoa_uint64_t frame_header = qoa_read_u64(bytes, &p); + unsigned int channels = (frame_header >> 56) & 0x0000ff; + unsigned int samplerate = (frame_header >> 32) & 0xffffff; + unsigned int samples = (frame_header >> 16) & 0x00ffff; + unsigned int frame_size = (frame_header ) & 0x00ffff; + + int data_size = frame_size - 8 - QOA_LMS_LEN * 4 * channels; + int num_slices = data_size / 8; + unsigned int max_total_samples = num_slices * QOA_SLICE_LEN; + + if ( + channels != qoa->channels || + samplerate != qoa->samplerate || + frame_size > size || + samples * channels > max_total_samples + ) { + return 0; + } + + + /* Read the LMS state: 4 x 2 bytes history, 4 x 2 bytes weights per channel */ + for (unsigned int c = 0; c < channels; c++) { + qoa_uint64_t history = qoa_read_u64(bytes, &p); + qoa_uint64_t weights = qoa_read_u64(bytes, &p); + for (int i = 0; i < QOA_LMS_LEN; i++) { + qoa->lms[c].history[i] = ((signed short)(history >> 48)); + history <<= 16; + qoa->lms[c].weights[i] = ((signed short)(weights >> 48)); + weights <<= 16; + } + } + + + /* Decode all slices for all channels in this frame */ + for (unsigned int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) { + for (unsigned int c = 0; c < channels; c++) { + qoa_uint64_t slice = qoa_read_u64(bytes, &p); + + int scalefactor = (slice >> 60) & 0xf; + int slice_start = sample_index * channels + c; + int slice_end = qoa_clamp(sample_index + QOA_SLICE_LEN, 0, samples) * channels + c; + + for (int si = slice_start; si < slice_end; si += channels) { + int predicted = qoa_lms_predict(&qoa->lms[c]); + int quantized = (slice >> 57) & 0x7; + int dequantized = qoa_dequant_tab[scalefactor][quantized]; + int reconstructed = qoa_clamp_s16(predicted + dequantized); + + sample_data[si] = reconstructed; + slice <<= 3; + + qoa_lms_update(&qoa->lms[c], reconstructed, dequantized); + } + } + } + + *frame_len = samples; + return p; +} + +short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *qoa) { + unsigned int p = qoa_decode_header(bytes, size, qoa); + if (!p) { + return NULL; + } + + /* Calculate the required size of the sample buffer and allocate */ + int total_samples = qoa->samples * qoa->channels; + short *sample_data = (short *)QOA_MALLOC(total_samples * sizeof(short)); + + unsigned int sample_index = 0; + unsigned int frame_len; + unsigned int frame_size; + + /* Decode all frames */ + do { + short *sample_ptr = sample_data + sample_index * qoa->channels; + frame_size = qoa_decode_frame(bytes + p, size - p, qoa, sample_ptr, &frame_len); + + p += frame_size; + sample_index += frame_len; + } while (frame_size && sample_index < qoa->samples); + + qoa->samples = sample_index; + return sample_data; +} + + + +/* ----------------------------------------------------------------------------- + File read/write convenience functions */ + +#ifndef QOA_NO_STDIO +#include <stdio.h> + +int qoa_write(const char *filename, const short *sample_data, qoa_desc *qoa) { + FILE *f = fopen(filename, "wb"); + unsigned int size; + void *encoded; + + if (!f) { + return 0; + } + + encoded = qoa_encode(sample_data, qoa, &size); + if (!encoded) { + fclose(f); + return 0; + } + + fwrite(encoded, 1, size, f); + fclose(f); + + QOA_FREE(encoded); + return size; +} + +void *qoa_read(const char *filename, qoa_desc *qoa) { + FILE *f = fopen(filename, "rb"); + int size, bytes_read; + void *data; + short *sample_data; + + if (!f) { + return NULL; + } + + fseek(f, 0, SEEK_END); + size = ftell(f); + if (size <= 0) { + fclose(f); + return NULL; + } + fseek(f, 0, SEEK_SET); + + data = QOA_MALLOC(size); + if (!data) { + fclose(f); + return NULL; + } + + bytes_read = fread(data, 1, size, f); + fclose(f); + + sample_data = qoa_decode(data, bytes_read, qoa); + QOA_FREE(data); + return sample_data; +} + +#endif /* QOA_NO_STDIO */ +#endif /* QOA_IMPLEMENTATION */ |