diff --git a/.clang-tidy b/.clang-tidy index 659b91013d47..02aa95c70923 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,7 +2,6 @@ Checks: 'clang-diagnostic-*,clang-analyzer-*,-*,cppcoreguidelines-pro-type-member-init,modernize-redundant-void-arg,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-nullptr,readability-braces-around-statements,readability-redundant-member-init' WarningsAsErrors: '' HeaderFilterRegex: '' -AnalyzeTemporaryDtors: false FormatStyle: none CheckOptions: - key: cert-dcl16-c.NewSuffixes diff --git a/.gitattributes b/.gitattributes index ccc576c2e12c..5af3e121a8cc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,12 +5,13 @@ thirdparty/* linguist-vendored # Normalize EOL for all files that Git considers text files * text=auto eol=lf -# Except for bat files, which are Windows only files +# Except for Windows-only / Visual Studio files *.bat eol=crlf +*.sln eol=crlf +*.csproj eol=crlf +misc/msvs/*.template eol=crlf # And some test files where the EOL matters *.test.txt -text -# And also the templates for Visual Studio files, which VS will always force CRLF on -/misc/msvs/*.template eol=crlf # The above only works properly for Git 2.10+, so for older versions # we need to manually list the binary files we don't want modified. diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml index 0ed7432833f2..3ff70077c428 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: | @@ -93,5 +95,5 @@ jobs: uses: codespell-project/actions-codespell@v2 with: skip: "./bin,./thirdparty,*.desktop,*.gen.*,*.po,*.pot,*.rc,./AUTHORS.md,./COPYRIGHT.txt,./DONORS.md,./core/input/gamecontrollerdb.txt,./core/string/locales.h,./editor/project_converter_3_to_4.cpp,./misc/scripts/codespell.sh,./platform/android/java/lib/src/com,./platform/web/node_modules,./platform/web/package-lock.json" - ignore_words_list: "breaked,curvelinear,doubleclick,expct,findn,gird,hel,inout,lod,mis,nd,numer,ot,requestor,te,vai" + ignore_words_list: "breaked,colour,curvelinear,doubleclick,expct,findn,gird,hel,inout,lod,mis,nd,numer,ot,requestor,te,thirdparty,vai" path: ${{ env.CHANGED_FILES }} diff --git a/.github/workflows/web_builds.yml b/.github/workflows/web_builds.yml index 47f7e4d45860..cfa1571d1f65 100644 --- a/.github/workflows/web_builds.yml +++ b/.github/workflows/web_builds.yml @@ -7,7 +7,7 @@ env: # Used for the cache key. Add version suffix to force clean build. GODOT_BASE_BRANCH: master SCONSFLAGS: verbose=yes warnings=extra werror=yes debug_symbols=no - EM_VERSION: 3.1.39 + EM_VERSION: 3.1.59 EM_CACHE_FOLDER: "emsdk-cache" concurrency: diff --git a/.mailmap b/.mailmap index 9e1ddb95e1ec..c4e7cf17432e 100644 --- a/.mailmap +++ b/.mailmap @@ -14,6 +14,7 @@ Ariel Manzur Ariel Manzur Ariel Manzur Ariel Manzur +Arman Elgudzhyan <48544263+puchik@users.noreply.github.com> A Thousand Ships <96648715+AThousandShips@users.noreply.github.com> Bastiaan Olij Benjamin @@ -77,6 +78,7 @@ Jason Knight <00jknight@gmail.com> Jean-Michel Bernard Jérôme Gully JFonS +jitspoe Juan Linietsky Juan Linietsky Juan Linietsky diff --git a/AUTHORS.md b/AUTHORS.md index f2fc58c1be82..0f000a1a75af 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 c1711a9e8121..cbb0ad179d8c 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 diff --git a/DONORS.md b/DONORS.md index f12f6f4b3a95..b62957867556 100644 --- a/DONORS.md +++ b/DONORS.md @@ -32,10 +32,8 @@ generous deed immortalized in the next stable release of Godot Engine. ## Silver sponsors - Affray Interactive Broken Rules Chasing Carrots - Gamblify Indoor Astronaut Null Orbital Knight @@ -56,6 +54,7 @@ generous deed immortalized in the next stable release of Godot Engine. Garry Newman Isaiah Smith Kenney + Libretrend Life Art Studios 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 18511ff5ee24..81ce4bca52db 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). +# +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=." + + 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=.") 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 ec4658e8ca48..91620cb075e7 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 ee20aea35d3d..a0412e91ff41 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -253,7 +253,7 @@ bool ProjectSettings::get_ignore_value_in_docs(const String &p_name) const { } void ProjectSettings::add_hidden_prefix(const String &p_prefix) { - ERR_FAIL_COND_MSG(hidden_prefixes.find(p_prefix) > -1, vformat("Hidden prefix '%s' already exists.", p_prefix)); + ERR_FAIL_COND_MSG(hidden_prefixes.has(p_prefix), vformat("Hidden prefix '%s' already exists.", p_prefix)); hidden_prefixes.push_back(p_prefix); } diff --git a/core/core_bind.cpp b/core/core_bind.cpp index 467b696eae73..0996db9d890e 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/core_constants.cpp b/core/core_constants.cpp index aaabbabfd9e7..5322e39ec0de 100644 --- a/core/core_constants.cpp +++ b/core/core_constants.cpp @@ -761,6 +761,7 @@ void register_global_constants() { BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_PACKED_VECTOR2_ARRAY", Variant::PACKED_VECTOR2_ARRAY); BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_PACKED_VECTOR3_ARRAY", Variant::PACKED_VECTOR3_ARRAY); BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_PACKED_COLOR_ARRAY", Variant::PACKED_COLOR_ARRAY); + BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_PACKED_VECTOR4_ARRAY", Variant::PACKED_VECTOR4_ARRAY); BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_MAX", Variant::VARIANT_MAX); //comparison diff --git a/core/debugger/debugger_marshalls.cpp b/core/debugger/debugger_marshalls.cpp index 3e6b7501c7ee..f4283e0ea9ce 100644 --- a/core/debugger/debugger_marshalls.cpp +++ b/core/debugger/debugger_marshalls.cpp @@ -38,10 +38,10 @@ Array DebuggerMarshalls::ScriptStackDump::serialize() { Array arr; arr.push_back(frames.size() * 3); - for (int i = 0; i < frames.size(); i++) { - arr.push_back(frames[i].file); - arr.push_back(frames[i].line); - arr.push_back(frames[i].func); + for (const ScriptLanguage::StackInfo &frame : frames) { + arr.push_back(frame.file); + arr.push_back(frame.line); + arr.push_back(frame.func); } return arr; } diff --git a/core/debugger/remote_debugger.cpp b/core/debugger/remote_debugger.cpp index 1973663c7297..bd30da3047d9 100644 --- a/core/debugger/remote_debugger.cpp +++ b/core/debugger/remote_debugger.cpp @@ -206,8 +206,7 @@ void RemoteDebugger::flush_output() { Vector joined_log_strings; Vector strings; Vector types; - for (int i = 0; i < output_strings.size(); i++) { - const OutputString &output_string = output_strings[i]; + for (const OutputString &output_string : output_strings) { if (output_string.type == MESSAGE_TYPE_ERROR) { if (!joined_log_strings.is_empty()) { strings.push_back(String("\n").join(joined_log_strings)); diff --git a/core/debugger/remote_debugger_peer.cpp b/core/debugger/remote_debugger_peer.cpp index 81ee09f51518..21a9014626db 100644 --- a/core/debugger/remote_debugger_peer.cpp +++ b/core/debugger/remote_debugger_peer.cpp @@ -45,7 +45,7 @@ bool RemoteDebuggerPeerTCP::has_message() { Array RemoteDebuggerPeerTCP::get_message() { MutexLock lock(mutex); ERR_FAIL_COND_V(!has_message(), Array()); - Array out = in_queue[0]; + Array out = in_queue.front()->get(); in_queue.pop_front(); return out; } @@ -100,7 +100,7 @@ void RemoteDebuggerPeerTCP::_write_out() { break; // Nothing left to send } mutex.lock(); - Variant var = out_queue[0]; + Variant var = out_queue.front()->get(); out_queue.pop_front(); mutex.unlock(); int size = 0; diff --git a/core/doc_data.cpp b/core/doc_data.cpp index 7549ba884ea4..672a36c35c19 100644 --- a/core/doc_data.cpp +++ b/core/doc_data.cpp @@ -152,9 +152,10 @@ void DocData::method_doc_from_methodinfo(DocData::MethodDoc &p_method, const Met return_doc_from_retinfo(p_method, p_methodinfo.return_val); - for (int i = 0; i < p_methodinfo.arguments.size(); i++) { + int i = 0; + for (List::ConstIterator itr = p_methodinfo.arguments.begin(); itr != p_methodinfo.arguments.end(); ++itr, ++i) { DocData::ArgumentDoc argument; - argument_doc_from_arginfo(argument, p_methodinfo.arguments[i]); + argument_doc_from_arginfo(argument, *itr); int default_arg_index = i - (p_methodinfo.arguments.size() - p_methodinfo.default_arguments.size()); if (default_arg_index >= 0) { Variant default_arg = p_methodinfo.default_arguments[default_arg_index]; diff --git a/core/extension/extension_api_dump.cpp b/core/extension/extension_api_dump.cpp index 69be2d2a8f69..1c9b4dc3e8ea 100644 --- a/core/extension/extension_api_dump.cpp +++ b/core/extension/extension_api_dump.cpp @@ -189,6 +189,7 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { { Variant::PACKED_VECTOR2_ARRAY, ptrsize_32 * 2, ptrsize_64 * 2, ptrsize_32 * 2, ptrsize_64 * 2 }, { Variant::PACKED_VECTOR3_ARRAY, ptrsize_32 * 2, ptrsize_64 * 2, ptrsize_32 * 2, ptrsize_64 * 2 }, { Variant::PACKED_COLOR_ARRAY, ptrsize_32 * 2, ptrsize_64 * 2, ptrsize_32 * 2, ptrsize_64 * 2 }, + { Variant::PACKED_VECTOR4_ARRAY, ptrsize_32 * 2, ptrsize_64 * 2, ptrsize_32 * 2, ptrsize_64 * 2 }, { Variant::VARIANT_MAX, sizeof(uint64_t) + sizeof(float) * 4, sizeof(uint64_t) + sizeof(float) * 4, sizeof(uint64_t) + sizeof(double) * 4, sizeof(uint64_t) + sizeof(double) * 4 }, }; @@ -230,6 +231,7 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { static_assert(type_size_array[Variant::PACKED_VECTOR2_ARRAY][sizeof(void *)] == sizeof(PackedVector2Array), "Size of PackedVector2Array mismatch"); static_assert(type_size_array[Variant::PACKED_VECTOR3_ARRAY][sizeof(void *)] == sizeof(PackedVector3Array), "Size of PackedVector3Array mismatch"); static_assert(type_size_array[Variant::PACKED_COLOR_ARRAY][sizeof(void *)] == sizeof(PackedColorArray), "Size of PackedColorArray mismatch"); + static_assert(type_size_array[Variant::PACKED_VECTOR4_ARRAY][sizeof(void *)] == sizeof(PackedVector4Array), "Size of PackedVector4Array mismatch"); static_assert(type_size_array[Variant::VARIANT_MAX][sizeof(void *)] == sizeof(Variant), "Size of Variant mismatch"); Array core_type_sizes; @@ -1016,26 +1018,34 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { d2["is_virtual"] = true; // virtual functions have no hash since no MethodBind is involved bool has_return = mi.return_val.type != Variant::NIL || (mi.return_val.usage & PROPERTY_USAGE_NIL_IS_VARIANT); - Array arguments; - for (int i = (has_return ? -1 : 0); i < mi.arguments.size(); i++) { - PropertyInfo pinfo = i == -1 ? mi.return_val : mi.arguments[i]; + if (has_return) { + PropertyInfo pinfo = mi.return_val; Dictionary d3; - if (i >= 0) { - d3["name"] = pinfo.name; + d3["type"] = get_property_info_type_name(pinfo); + + if (mi.get_argument_meta(-1) > 0) { + d3["meta"] = get_type_meta_name((GodotTypeInfo::Metadata)mi.get_argument_meta(-1)); } + d2["return_value"] = d3; + } + + Array arguments; + int i = 0; + for (List::ConstIterator itr = mi.arguments.begin(); itr != mi.arguments.end(); ++itr, ++i) { + const PropertyInfo &pinfo = *itr; + Dictionary d3; + + d3["name"] = pinfo.name; + d3["type"] = get_property_info_type_name(pinfo); if (mi.get_argument_meta(i) > 0) { d3["meta"] = get_type_meta_name((GodotTypeInfo::Metadata)mi.get_argument_meta(i)); } - if (i == -1) { - d2["return_value"] = d3; - } else { - arguments.push_back(d3); - } + arguments.push_back(d3); } if (arguments.size()) { @@ -1149,10 +1159,11 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { Array arguments; - for (int i = 0; i < F.arguments.size(); i++) { + int i = 0; + for (List::ConstIterator itr = F.arguments.begin(); itr != F.arguments.end(); ++itr, ++i) { Dictionary d3; - d3["name"] = F.arguments[i].name; - d3["type"] = get_property_info_type_name(F.arguments[i]); + d3["name"] = itr->name; + d3["type"] = get_property_info_type_name(*itr); if (F.get_argument_meta(i) > 0) { d3["meta"] = get_type_meta_name((GodotTypeInfo::Metadata)F.get_argument_meta(i)); } diff --git a/core/extension/gdextension.cpp b/core/extension/gdextension.cpp index 22a5df9935be..a26bb3e8f3c2 100644 --- a/core/extension/gdextension.cpp +++ b/core/extension/gdextension.cpp @@ -211,14 +211,14 @@ class GDExtensionMethodBind : public MethodBind { if (p_arg < 0) { return return_value_info.type; } else { - return arguments_info[p_arg].type; + return arguments_info.get(p_arg).type; } } virtual PropertyInfo _gen_argument_type_info(int p_arg) const override { if (p_arg < 0) { return return_value_info; } else { - return arguments_info[p_arg]; + return arguments_info.get(p_arg); } } @@ -232,7 +232,7 @@ class GDExtensionMethodBind : public MethodBind { if (p_arg < 0) { return return_value_metadata; } else { - return arguments_metadata[p_arg]; + return arguments_metadata.get(p_arg); } } #endif @@ -319,8 +319,9 @@ class GDExtensionMethodBind : public MethodBind { return false; } - for (uint32_t i = 0; i < p_method_info->argument_count; i++) { - if (arguments_info[i].type != (Variant::Type)p_method_info->arguments_info[i].type) { + List::ConstIterator itr = arguments_info.begin(); + for (uint32_t i = 0; i < p_method_info->argument_count; ++itr, ++i) { + if (itr->type != (Variant::Type)p_method_info->arguments_info[i].type) { return false; } } @@ -387,7 +388,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 +407,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 +422,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 +440,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 +520,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 23b1f5120827..3b1563989052 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.cpp b/core/extension/gdextension_interface.cpp index 9b4aa983572d..98f5cb4d0258 100644 --- a/core/extension/gdextension_interface.cpp +++ b/core/extension/gdextension_interface.cpp @@ -595,6 +595,8 @@ static GDExtensionVariantFromTypeConstructorFunc gdextension_get_variant_from_ty return VariantTypeConstructor::variant_from_type; case GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR3_ARRAY: return VariantTypeConstructor::variant_from_type; + case GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR4_ARRAY: + return VariantTypeConstructor::variant_from_type; case GDEXTENSION_VARIANT_TYPE_PACKED_COLOR_ARRAY: return VariantTypeConstructor::variant_from_type; case GDEXTENSION_VARIANT_TYPE_NIL: @@ -678,6 +680,8 @@ static GDExtensionTypeFromVariantConstructorFunc gdextension_get_variant_to_type return VariantTypeConstructor::type_from_variant; case GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR3_ARRAY: return VariantTypeConstructor::type_from_variant; + case GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR4_ARRAY: + return VariantTypeConstructor::type_from_variant; case GDEXTENSION_VARIANT_TYPE_PACKED_COLOR_ARRAY: return VariantTypeConstructor::type_from_variant; case GDEXTENSION_VARIANT_TYPE_NIL: @@ -1116,6 +1120,22 @@ static GDExtensionTypePtr gdextension_packed_vector3_array_operator_index_const( return (GDExtensionTypePtr)&self->ptr()[p_index]; } +static GDExtensionTypePtr gdextension_packed_vector4_array_operator_index(GDExtensionTypePtr p_self, GDExtensionInt p_index) { + PackedVector4Array *self = (PackedVector4Array *)p_self; + if (unlikely(p_index < 0 || p_index >= self->size())) { + return nullptr; + } + return (GDExtensionTypePtr)&self->ptrw()[p_index]; +} + +static GDExtensionTypePtr gdextension_packed_vector4_array_operator_index_const(GDExtensionConstTypePtr p_self, GDExtensionInt p_index) { + const PackedVector4Array *self = (const PackedVector4Array *)p_self; + if (unlikely(p_index < 0 || p_index >= self->size())) { + return nullptr; + } + return (GDExtensionTypePtr)&self->ptr()[p_index]; +} + static GDExtensionVariantPtr gdextension_array_operator_index(GDExtensionTypePtr p_self, GDExtensionInt p_index) { Array *self = (Array *)p_self; if (unlikely(p_index < 0 || p_index >= self->size())) { @@ -1620,6 +1640,8 @@ void gdextension_setup_interface() { REGISTER_INTERFACE_FUNC(packed_vector2_array_operator_index_const); REGISTER_INTERFACE_FUNC(packed_vector3_array_operator_index); REGISTER_INTERFACE_FUNC(packed_vector3_array_operator_index_const); + REGISTER_INTERFACE_FUNC(packed_vector4_array_operator_index); + REGISTER_INTERFACE_FUNC(packed_vector4_array_operator_index_const); REGISTER_INTERFACE_FUNC(array_operator_index); REGISTER_INTERFACE_FUNC(array_operator_index_const); REGISTER_INTERFACE_FUNC(array_ref); diff --git a/core/extension/gdextension_interface.h b/core/extension/gdextension_interface.h index e9c570e994a0..6fe6b8df208f 100644 --- a/core/extension/gdextension_interface.h +++ b/core/extension/gdextension_interface.h @@ -96,6 +96,7 @@ typedef enum { GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR2_ARRAY, GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR3_ARRAY, GDEXTENSION_VARIANT_TYPE_PACKED_COLOR_ARRAY, + GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR4_ARRAY, GDEXTENSION_VARIANT_TYPE_VARIANT_MAX } GDExtensionVariantType; @@ -256,6 +257,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 +335,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; @@ -1962,32 +1964,6 @@ typedef uint8_t *(*GDExtensionInterfacePackedByteArrayOperatorIndex)(GDExtension */ typedef const uint8_t *(*GDExtensionInterfacePackedByteArrayOperatorIndexConst)(GDExtensionConstTypePtr p_self, GDExtensionInt p_index); -/** - * @name packed_color_array_operator_index - * @since 4.1 - * - * Gets a pointer to a color in a PackedColorArray. - * - * @param p_self A pointer to a PackedColorArray object. - * @param p_index The index of the Color to get. - * - * @return A pointer to the requested Color. - */ -typedef GDExtensionTypePtr (*GDExtensionInterfacePackedColorArrayOperatorIndex)(GDExtensionTypePtr p_self, GDExtensionInt p_index); - -/** - * @name packed_color_array_operator_index_const - * @since 4.1 - * - * Gets a const pointer to a color in a PackedColorArray. - * - * @param p_self A const pointer to a const PackedColorArray object. - * @param p_index The index of the Color to get. - * - * @return A const pointer to the requested Color. - */ -typedef GDExtensionTypePtr (*GDExtensionInterfacePackedColorArrayOperatorIndexConst)(GDExtensionConstTypePtr p_self, GDExtensionInt p_index); - /** * @name packed_float32_array_operator_index * @since 4.1 @@ -2170,6 +2146,58 @@ typedef GDExtensionTypePtr (*GDExtensionInterfacePackedVector3ArrayOperatorIndex */ typedef GDExtensionTypePtr (*GDExtensionInterfacePackedVector3ArrayOperatorIndexConst)(GDExtensionConstTypePtr p_self, GDExtensionInt p_index); +/** + * @name packed_vector4_array_operator_index + * @since 4.3 + * + * Gets a pointer to a Vector4 in a PackedVector4Array. + * + * @param p_self A pointer to a PackedVector4Array object. + * @param p_index The index of the Vector4 to get. + * + * @return A pointer to the requested Vector4. + */ +typedef GDExtensionTypePtr (*GDExtensionInterfacePackedVector4ArrayOperatorIndex)(GDExtensionTypePtr p_self, GDExtensionInt p_index); + +/** + * @name packed_vector4_array_operator_index_const + * @since 4.3 + * + * Gets a const pointer to a Vector4 in a PackedVector4Array. + * + * @param p_self A const pointer to a PackedVector4Array object. + * @param p_index The index of the Vector4 to get. + * + * @return A const pointer to the requested Vector4. + */ +typedef GDExtensionTypePtr (*GDExtensionInterfacePackedVector4ArrayOperatorIndexConst)(GDExtensionConstTypePtr p_self, GDExtensionInt p_index); + +/** + * @name packed_color_array_operator_index + * @since 4.1 + * + * Gets a pointer to a color in a PackedColorArray. + * + * @param p_self A pointer to a PackedColorArray object. + * @param p_index The index of the Color to get. + * + * @return A pointer to the requested Color. + */ +typedef GDExtensionTypePtr (*GDExtensionInterfacePackedColorArrayOperatorIndex)(GDExtensionTypePtr p_self, GDExtensionInt p_index); + +/** + * @name packed_color_array_operator_index_const + * @since 4.1 + * + * Gets a const pointer to a color in a PackedColorArray. + * + * @param p_self A const pointer to a PackedColorArray object. + * @param p_index The index of the Color to get. + * + * @return A const pointer to the requested Color. + */ +typedef GDExtensionTypePtr (*GDExtensionInterfacePackedColorArrayOperatorIndexConst)(GDExtensionConstTypePtr p_self, GDExtensionInt p_index); + /** * @name array_operator_index * @since 4.1 @@ -2847,7 +2875,7 @@ typedef void (*GDExtensionInterfaceEditorRemovePlugin)(GDExtensionConstStringNam * * The provided pointer can be immediately freed once the function returns. * - * @param p_data A pointer to an UTF-8 encoded C string (null terminated). + * @param p_data A pointer to a UTF-8 encoded C string (null terminated). */ typedef void (*GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8Chars)(const char *p_data); @@ -2859,7 +2887,7 @@ typedef void (*GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8Chars)(const char * * * The provided pointer can be immediately freed once the function returns. * - * @param p_data A pointer to an UTF-8 encoded C string. + * @param p_data A pointer to a UTF-8 encoded C string. * @param p_size The number of bytes (not code units). */ typedef void (*GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8CharsAndLen)(const char *p_data, GDExtensionInt p_size); diff --git a/core/input/input.compat.inc b/core/input/input.compat.inc new file mode 100644 index 000000000000..cbc8b1df0f50 --- /dev/null +++ b/core/input/input.compat.inc @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* input.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 Input::_vibrate_handheld_bind_compat_91143(int p_duration_ms) { + vibrate_handheld(p_duration_ms, -1.0); +} + +void Input::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("vibrate_handheld", "duration_ms"), &Input::_vibrate_handheld_bind_compat_91143, DEFVAL(500)); +} + +#endif // DISABLE_DEPRECATED diff --git a/core/input/input.cpp b/core/input/input.cpp index c24a59203fd7..a8409cc06d3e 100644 --- a/core/input/input.cpp +++ b/core/input/input.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "input.h" +#include "input.compat.inc" #include "core/config/project_settings.h" #include "core/input/default_controller_mappings.h" @@ -120,7 +121,7 @@ void Input::_bind_methods() { ClassDB::bind_method(D_METHOD("get_joy_vibration_duration", "device"), &Input::get_joy_vibration_duration); ClassDB::bind_method(D_METHOD("start_joy_vibration", "device", "weak_magnitude", "strong_magnitude", "duration"), &Input::start_joy_vibration, DEFVAL(0)); ClassDB::bind_method(D_METHOD("stop_joy_vibration", "device"), &Input::stop_joy_vibration); - ClassDB::bind_method(D_METHOD("vibrate_handheld", "duration_ms"), &Input::vibrate_handheld, DEFVAL(500)); + ClassDB::bind_method(D_METHOD("vibrate_handheld", "duration_ms", "amplitude"), &Input::vibrate_handheld, DEFVAL(500), DEFVAL(-1.0)); ClassDB::bind_method(D_METHOD("get_gravity"), &Input::get_gravity); ClassDB::bind_method(D_METHOD("get_accelerometer"), &Input::get_accelerometer); ClassDB::bind_method(D_METHOD("get_magnetometer"), &Input::get_magnetometer); @@ -803,8 +804,8 @@ void Input::stop_joy_vibration(int p_device) { joy_vibration[p_device] = vibration; } -void Input::vibrate_handheld(int p_duration_ms) { - OS::get_singleton()->vibrate_handheld(p_duration_ms); +void Input::vibrate_handheld(int p_duration_ms, float p_amplitude) { + OS::get_singleton()->vibrate_handheld(p_duration_ms, p_amplitude); } void Input::set_gravity(const Vector3 &p_gravity) { @@ -857,7 +858,7 @@ void Input::warp_mouse(const Vector2 &p_position) { warp_mouse_func(p_position); } -Point2i Input::warp_mouse_motion(const Ref &p_motion, const Rect2 &p_rect) { +Point2 Input::warp_mouse_motion(const Ref &p_motion, const Rect2 &p_rect) { // The relative distance reported for the next event after a warp is in the boundaries of the // size of the rect on that axis, but it may be greater, in which case there's no problem as fmod() // will warp it, but if the pointer has moved in the opposite direction between the pointer relocation @@ -867,14 +868,14 @@ Point2i Input::warp_mouse_motion(const Ref &p_motion, con // detect the warp: if the relative distance is greater than the half of the size of the relevant rect // (checked per each axis), it will be considered as the consequence of a former pointer warp. - const Point2i rel_sign(p_motion->get_relative().x >= 0.0f ? 1 : -1, p_motion->get_relative().y >= 0.0 ? 1 : -1); - const Size2i warp_margin = p_rect.size * 0.5f; - const Point2i rel_warped( + const Point2 rel_sign(p_motion->get_relative().x >= 0.0f ? 1 : -1, p_motion->get_relative().y >= 0.0 ? 1 : -1); + const Size2 warp_margin = p_rect.size * 0.5f; + const Point2 rel_warped( Math::fmod(p_motion->get_relative().x + rel_sign.x * warp_margin.x, p_rect.size.x) - rel_sign.x * warp_margin.x, Math::fmod(p_motion->get_relative().y + rel_sign.y * warp_margin.y, p_rect.size.y) - rel_sign.y * warp_margin.y); - const Point2i pos_local = p_motion->get_global_position() - p_rect.position; - const Point2i pos_warped(Math::fposmod(pos_local.x, p_rect.size.x), Math::fposmod(pos_local.y, p_rect.size.y)); + const Point2 pos_local = p_motion->get_global_position() - p_rect.position; + const Point2 pos_warped(Math::fposmod(pos_local.x, p_rect.size.x), Math::fposmod(pos_local.y, p_rect.size.y)); if (pos_warped != pos_local) { warp_mouse(pos_warped + p_rect.position); } diff --git a/core/input/input.h b/core/input/input.h index d1f284e8f7fc..6e7ab4308214 100644 --- a/core/input/input.h +++ b/core/input/input.h @@ -264,6 +264,11 @@ class Input : public Object { EventDispatchFunc event_dispatch_function = nullptr; +#ifndef DISABLE_DEPRECATED + void _vibrate_handheld_bind_compat_91143(int p_duration_ms = 500); + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + protected: static void _bind_methods(); @@ -311,7 +316,7 @@ class Input : public Object { BitField get_mouse_button_mask() const; void warp_mouse(const Vector2 &p_position); - Point2i warp_mouse_motion(const Ref &p_motion, const Rect2 &p_rect); + Point2 warp_mouse_motion(const Ref &p_motion, const Rect2 &p_rect); void parse_input_event(const Ref &p_event); @@ -323,7 +328,7 @@ class Input : public Object { void start_joy_vibration(int p_device, float p_weak_magnitude, float p_strong_magnitude, float p_duration = 0); void stop_joy_vibration(int p_device); - void vibrate_handheld(int p_duration_ms = 500); + void vibrate_handheld(int p_duration_ms = 500, float p_amplitude = -1.0); void set_mouse_position(const Point2 &p_posf); diff --git a/core/io/dir_access.cpp b/core/io/dir_access.cpp index e99885befab5..5df67b1103cd 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/image.cpp b/core/io/image.cpp index 6096211cff0d..5498b448d71a 100644 --- a/core/io/image.cpp +++ b/core/io/image.cpp @@ -521,7 +521,7 @@ void Image::convert(Format p_new_format) { // Includes the main image. const int mipmap_count = get_mipmap_count() + 1; - if (format > FORMAT_RGBE9995 || p_new_format > FORMAT_RGBE9995) { + if (Image::is_format_compressed(format) || Image::is_format_compressed(p_new_format)) { ERR_FAIL_MSG("Cannot convert to <-> from compressed formats. Use compress() and decompress() instead."); } else if (format > FORMAT_RGBA8 || p_new_format > FORMAT_RGBA8) { @@ -1662,7 +1662,7 @@ int Image::_get_dst_image_size(int p_width, int p_height, Format p_format, int & } bool Image::_can_modify(Format p_format) const { - return p_format <= FORMAT_RGBE9995; + return !Image::is_format_compressed(p_format); } template FORMAT_RGBE9995; + return is_format_compressed(format); +} + +bool Image::is_format_compressed(Format p_format) { + return p_format > FORMAT_RGBE9995; } Error Image::decompress() { diff --git a/core/io/image.h b/core/io/image.h index 2cabbb767a5e..daddfac59d7d 100644 --- a/core/io/image.h +++ b/core/io/image.h @@ -376,6 +376,7 @@ class Image : public Resource { Error compress_from_channels(CompressMode p_mode, UsedChannels p_channels, ASTCFormat p_astc_format = ASTC_FORMAT_4x4); Error decompress(); bool is_compressed() const; + static bool is_format_compressed(Format p_format); void fix_alpha_edges(); void premultiply_alpha(); diff --git a/core/io/ip.cpp b/core/io/ip.cpp index ec861049264b..f20d65bef901 100644 --- a/core/io/ip.cpp +++ b/core/io/ip.cpp @@ -148,8 +148,8 @@ PackedStringArray IP::resolve_hostname_addresses(const String &p_hostname, Type resolver->mutex.unlock(); PackedStringArray result; - for (int i = 0; i < res.size(); ++i) { - result.push_back(String(res[i])); + for (const IPAddress &E : res) { + result.push_back(String(E)); } return result; } @@ -206,9 +206,9 @@ IPAddress IP::get_resolve_item_address(ResolverID p_id) const { List res = resolver->queue[p_id].response; - for (int i = 0; i < res.size(); ++i) { - if (res[i].is_valid()) { - return res[i]; + for (const IPAddress &E : res) { + if (E.is_valid()) { + return E; } } return IPAddress(); @@ -226,9 +226,9 @@ Array IP::get_resolve_item_addresses(ResolverID p_id) const { List res = resolver->queue[p_id].response; Array result; - for (int i = 0; i < res.size(); ++i) { - if (res[i].is_valid()) { - result.push_back(String(res[i])); + for (const IPAddress &E : res) { + if (E.is_valid()) { + result.push_back(String(E)); } } return result; diff --git a/core/io/marshalls.cpp b/core/io/marshalls.cpp index 18dbac991cbb..2cdfacae1751 100644 --- a/core/io/marshalls.cpp +++ b/core/io/marshalls.cpp @@ -1178,6 +1178,73 @@ Error decode_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int r_variant = carray; + } break; + + case Variant::PACKED_VECTOR4_ARRAY: { + ERR_FAIL_COND_V(len < 4, ERR_INVALID_DATA); + int32_t count = decode_uint32(buf); + buf += 4; + len -= 4; + + Vector varray; + + if (header & HEADER_DATA_FLAG_64) { + ERR_FAIL_MUL_OF(count, sizeof(double) * 4, ERR_INVALID_DATA); + ERR_FAIL_COND_V(count < 0 || count * sizeof(double) * 4 > (size_t)len, ERR_INVALID_DATA); + + if (r_len) { + (*r_len) += 4; // Size of count number. + } + + if (count) { + varray.resize(count); + Vector4 *w = varray.ptrw(); + + for (int32_t i = 0; i < count; i++) { + w[i].x = decode_double(buf + i * sizeof(double) * 4 + sizeof(double) * 0); + w[i].y = decode_double(buf + i * sizeof(double) * 4 + sizeof(double) * 1); + w[i].z = decode_double(buf + i * sizeof(double) * 4 + sizeof(double) * 2); + w[i].w = decode_double(buf + i * sizeof(double) * 4 + sizeof(double) * 3); + } + + int adv = sizeof(double) * 4 * count; + + if (r_len) { + (*r_len) += adv; + } + len -= adv; + buf += adv; + } + } else { + ERR_FAIL_MUL_OF(count, sizeof(float) * 4, ERR_INVALID_DATA); + ERR_FAIL_COND_V(count < 0 || count * sizeof(float) * 4 > (size_t)len, ERR_INVALID_DATA); + + if (r_len) { + (*r_len) += 4; // Size of count number. + } + + if (count) { + varray.resize(count); + Vector4 *w = varray.ptrw(); + + for (int32_t i = 0; i < count; i++) { + w[i].x = decode_float(buf + i * sizeof(float) * 4 + sizeof(float) * 0); + w[i].y = decode_float(buf + i * sizeof(float) * 4 + sizeof(float) * 1); + w[i].z = decode_float(buf + i * sizeof(float) * 4 + sizeof(float) * 2); + w[i].w = decode_float(buf + i * sizeof(float) * 4 + sizeof(float) * 3); + } + + int adv = sizeof(float) * 4 * count; + + if (r_len) { + (*r_len) += adv; + } + len -= adv; + buf += adv; + } + } + r_variant = varray; + } break; default: { ERR_FAIL_V(ERR_BUG); @@ -1263,6 +1330,7 @@ Error encode_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len, bo case Variant::VECTOR4: case Variant::PACKED_VECTOR2_ARRAY: case Variant::PACKED_VECTOR3_ARRAY: + case Variant::PACKED_VECTOR4_ARRAY: case Variant::TRANSFORM2D: case Variant::TRANSFORM3D: case Variant::PROJECTION: @@ -1946,6 +2014,32 @@ Error encode_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len, bo r_len += 4 * 4 * len; + } break; + case Variant::PACKED_VECTOR4_ARRAY: { + Vector data = p_variant; + int len = data.size(); + + if (buf) { + encode_uint32(len, buf); + buf += 4; + } + + r_len += 4; + + if (buf) { + for (int i = 0; i < len; i++) { + Vector4 v = data.get(i); + + encode_real(v.x, &buf[0]); + encode_real(v.y, &buf[sizeof(real_t)]); + encode_real(v.z, &buf[sizeof(real_t) * 2]); + encode_real(v.w, &buf[sizeof(real_t) * 3]); + buf += sizeof(real_t) * 4; + } + } + + r_len += sizeof(real_t) * 4 * len; + } break; default: { ERR_FAIL_V(ERR_BUG); diff --git a/core/io/packed_data_container.cpp b/core/io/packed_data_container.cpp index 11b0c697748e..b20279c9ace2 100644 --- a/core/io/packed_data_container.cpp +++ b/core/io/packed_data_container.cpp @@ -244,6 +244,7 @@ uint32_t PackedDataContainer::_pack(const Variant &p_data, Vector &tmpd case Variant::PACKED_VECTOR2_ARRAY: case Variant::PACKED_VECTOR3_ARRAY: case Variant::PACKED_COLOR_ARRAY: + case Variant::PACKED_VECTOR4_ARRAY: case Variant::STRING_NAME: case Variant::NODE_PATH: { uint32_t pos = tmpdata.size(); diff --git a/core/io/resource.cpp b/core/io/resource.cpp index 74f18ceee197..4c3cfeaa5869 100644 --- a/core/io/resource.cpp +++ b/core/io/resource.cpp @@ -383,7 +383,8 @@ Ref Resource::duplicate(bool p_subresources) const { case Variant::Type::PACKED_FLOAT64_ARRAY: case Variant::Type::PACKED_STRING_ARRAY: case Variant::Type::PACKED_VECTOR2_ARRAY: - case Variant::Type::PACKED_VECTOR3_ARRAY: { + case Variant::Type::PACKED_VECTOR3_ARRAY: + case Variant::Type::PACKED_VECTOR4_ARRAY: { r->set(E.name, p.duplicate(p_subresources)); } break; diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp index d0a820054608..ab460c5f4c57 100644 --- a/core/io/resource_format_binary.cpp +++ b/core/io/resource_format_binary.cpp @@ -85,15 +85,17 @@ enum { VARIANT_VECTOR4 = 50, VARIANT_VECTOR4I = 51, VARIANT_PROJECTION = 52, + VARIANT_PACKED_VECTOR4_ARRAY = 53, OBJECT_EMPTY = 0, OBJECT_EXTERNAL_RESOURCE = 1, OBJECT_INTERNAL_RESOURCE = 2, OBJECT_EXTERNAL_RESOURCE_INDEX = 3, - // Version 2: added 64 bits support for float and int. - // Version 3: changed nodepath encoding. - // Version 4: new string ID for ext/subresources, breaks forward compat. + // Version 2: Added 64-bit support for float and int. + // Version 3: Changed NodePath encoding. + // Version 4: New string ID for ext/subresources, breaks forward compat. // Version 5: Ability to store script class in the header. - FORMAT_VERSION = 5, + // Version 6: Added PackedVector4Array Variant type. + FORMAT_VERSION = 6, FORMAT_VERSION_CAN_RENAME_DEPS = 1, FORMAT_VERSION_NO_NODEPATH_PROPERTY = 3, }; @@ -653,6 +655,19 @@ Error ResourceLoaderBinary::parse_variant(Variant &r_v) { r_v = array; } break; + case VARIANT_PACKED_VECTOR4_ARRAY: { + uint32_t len = f->get_32(); + + Vector array; + array.resize(len); + Vector4 *w = array.ptrw(); + static_assert(sizeof(Vector4) == 4 * sizeof(real_t)); + const Error err = read_reals(reinterpret_cast(w), f, len * 4); + ERR_FAIL_COND_V(err != OK, err); + + r_v = array; + + } break; default: { ERR_FAIL_V(ERR_FILE_CORRUPT); } break; @@ -1912,33 +1927,33 @@ void ResourceFormatSaverBinaryInstance::write_variant(Ref f, const V for (int i = 0; i < len; i++) { save_unicode_string(f, r[i]); } - } break; - case Variant::PACKED_VECTOR3_ARRAY: { - f->store_32(VARIANT_PACKED_VECTOR3_ARRAY); - Vector arr = p_property; + + case Variant::PACKED_VECTOR2_ARRAY: { + f->store_32(VARIANT_PACKED_VECTOR2_ARRAY); + Vector arr = p_property; int len = arr.size(); f->store_32(len); - const Vector3 *r = arr.ptr(); + const Vector2 *r = arr.ptr(); for (int i = 0; i < len; i++) { f->store_real(r[i].x); f->store_real(r[i].y); - f->store_real(r[i].z); } - } break; - case Variant::PACKED_VECTOR2_ARRAY: { - f->store_32(VARIANT_PACKED_VECTOR2_ARRAY); - Vector arr = p_property; + + case Variant::PACKED_VECTOR3_ARRAY: { + f->store_32(VARIANT_PACKED_VECTOR3_ARRAY); + Vector arr = p_property; int len = arr.size(); f->store_32(len); - const Vector2 *r = arr.ptr(); + const Vector3 *r = arr.ptr(); for (int i = 0; i < len; i++) { f->store_real(r[i].x); f->store_real(r[i].y); + f->store_real(r[i].z); } - } break; + case Variant::PACKED_COLOR_ARRAY: { f->store_32(VARIANT_PACKED_COLOR_ARRAY); Vector arr = p_property; @@ -1952,6 +1967,20 @@ void ResourceFormatSaverBinaryInstance::write_variant(Ref f, const V f->store_float(r[i].a); } + } break; + case Variant::PACKED_VECTOR4_ARRAY: { + f->store_32(VARIANT_PACKED_VECTOR4_ARRAY); + Vector arr = p_property; + int len = arr.size(); + f->store_32(len); + const Vector4 *r = arr.ptr(); + for (int i = 0; i < len; i++) { + f->store_real(r[i].x); + f->store_real(r[i].y); + f->store_real(r[i].z); + f->store_real(r[i].w); + } + } break; default: { ERR_FAIL_MSG("Invalid variant."); diff --git a/core/io/udp_server.cpp b/core/io/udp_server.cpp index 215c6903a65d..75ba784dbd16 100644 --- a/core/io/udp_server.cpp +++ b/core/io/udp_server.cpp @@ -161,7 +161,7 @@ Ref UDPServer::take_connection() { return conn; } - Peer peer = pending[0]; + Peer peer = pending.front()->get(); pending.pop_front(); peers.push_back(peer); return peer.peer; diff --git a/core/math/aabb.h b/core/math/aabb.h index 48a883e64c95..c2945a3ef1bb 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 25bd4e8d892c..4f21a665de7f 100644 --- a/core/math/delaunay_3d.h +++ b/core/math/delaunay_3d.h @@ -278,7 +278,7 @@ class Delaunay3D { } 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::Element *E = acceleration_grid[grid_pos.x][grid_pos.y][grid_pos.z].front(); E;) { List::Element *N = E->next(); //may be deleted @@ -335,8 +335,8 @@ class Delaunay3D { 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 4483f61bc4c1..6a60a5925d44 100644 --- a/core/math/quick_hull.cpp +++ b/core/math/quick_hull.cpp @@ -55,7 +55,7 @@ Error QuickHull::build(const Vector &p_points, Geometry3D::MeshData &r_ HashSet 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 7f410feb1cbc..b4069ae86a4f 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 64806414c771..a1338da0bbcc 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 0da1b8c7ad7a..01b733183dc9 100644 --- a/core/math/triangle_mesh.cpp +++ b/core/math/triangle_mesh.cpp @@ -133,7 +133,7 @@ void TriangleMesh::create(const Vector &p_faces, const Vector 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::Iterator E = db.find(vs); if (E) { vidx = E->value; diff --git a/core/math/vector2.cpp b/core/math/vector2.cpp index 198fd85d205b..e86b97d6a80b 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 6ad003edd16e..8851942cdd2c 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 ba79d439dde5..790f56473492 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 aa29263a652a..aca9ae827204 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 fad5f2c0fb88..1e900026651d 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 f5d16984d97a..2313eb557a40 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 f41460e62337..93f9d15ac1bd 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 a9f298bff1e4..035cfcf9e20e 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 e6f6dee42c49..b6b914f36dab 100644 --- a/core/math/vector4.cpp +++ b/core/math/vector4.cpp @@ -30,6 +30,8 @@ #include "vector4.h" +#include "core/math/math_funcs.h" +#include "core/math/vector4i.h" #include "core/string/ustring.h" Vector4::Axis Vector4::min_axis_index() const { @@ -171,12 +173,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,8 +204,20 @@ 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) + ")"; } static_assert(sizeof(Vector4) == 4 * sizeof(real_t)); + +Vector4::operator Vector4i() const { + return Vector4i(x, y, z, w); +} diff --git a/core/math/vector4.h b/core/math/vector4.h index 4dba3126cb26..f69b4752bbf7 100644 --- a/core/math/vector4.h +++ b/core/math/vector4.h @@ -32,9 +32,11 @@ #define VECTOR4_H #include "core/error/error_macros.h" -#include "core/math/math_funcs.h" +#include "core/math/math_defs.h" +#include "core/typedefs.h" class String; +struct Vector4i; struct _NO_DISCARD_ Vector4 { static const int AXIS_COUNT = 4; @@ -72,10 +74,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 +111,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; @@ -129,28 +142,14 @@ struct _NO_DISCARD_ Vector4 { _FORCE_INLINE_ bool operator<=(const Vector4 &p_vec4) const; operator String() const; + operator Vector4i() const; _FORCE_INLINE_ Vector4() {} - - _FORCE_INLINE_ Vector4(real_t p_x, real_t p_y, real_t p_z, real_t p_w) : - x(p_x), - y(p_y), - z(p_z), - w(p_w) { - } - - Vector4(const Vector4 &p_vec4) : - x(p_vec4.x), - y(p_vec4.y), - z(p_vec4.z), - w(p_vec4.w) { - } - - void operator=(const Vector4 &p_vec4) { - x = p_vec4.x; - y = p_vec4.y; - z = p_vec4.z; - w = p_vec4.w; + _FORCE_INLINE_ Vector4(real_t p_x, real_t p_y, real_t p_z, real_t p_w) { + x = p_x; + y = p_y; + z = p_z; + w = p_w; } }; diff --git a/core/math/vector4i.cpp b/core/math/vector4i.cpp index 8e36c6b53423..afa77a4988dc 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 5a96d98d188d..8a9c580bc11b 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 7ef1ce74eddc..6b84dfcee9be 100644 --- a/core/object/class_db.cpp +++ b/core/object/class_db.cpp @@ -138,7 +138,7 @@ class PlaceholderExtensionInstance { 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) { @@ -425,8 +425,8 @@ uint32_t ClassDB::get_api_hash(APIType p_api) { for (const StringName &F : snames) { MethodInfo &mi = t->signal_map[F]; hash = hash_murmur3_one_64(F.hash(), hash); - for (int i = 0; i < mi.arguments.size(); i++) { - hash = hash_murmur3_one_64(mi.arguments[i].type, hash); + for (const PropertyInfo &pi : mi.arguments) { + hash = hash_murmur3_one_64(pi.type, hash); } } } @@ -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; @@ -1855,8 +1856,9 @@ void ClassDB::add_virtual_method(const StringName &p_class, const MethodInfo &p_ if (p_arg_names.size() != mi.arguments.size()) { WARN_PRINT("Mismatch argument name count for virtual method: " + String(p_class) + "::" + p_method.name); } else { - for (int i = 0; i < p_arg_names.size(); i++) { - mi.arguments[i].name = p_arg_names[i]; + List::Iterator itr = mi.arguments.begin(); + for (int i = 0; i < p_arg_names.size(); ++itr, ++i) { + itr->name = p_arg_names[i]; } } } diff --git a/core/object/method_bind.h b/core/object/method_bind.h index e97f4abc6a2c..2f9a2d1679bb 100644 --- a/core/object/method_bind.h +++ b/core/object/method_bind.h @@ -152,7 +152,7 @@ class MethodBindVarArgBase : public MethodBind { if (p_arg < 0) { return _gen_return_type_info(); } else if (p_arg < method_info.arguments.size()) { - return method_info.arguments[p_arg]; + return method_info.arguments.get(p_arg); } else { return PropertyInfo(Variant::NIL, "arg_" + itos(p_arg), PROPERTY_HINT_NONE, String(), PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_NIL_IS_VARIANT); } @@ -193,10 +193,11 @@ class MethodBindVarArgBase : public MethodBind { Vector names; names.resize(method_info.arguments.size()); #endif - for (int i = 0; i < method_info.arguments.size(); i++) { - at[i + 1] = method_info.arguments[i].type; + int i = 0; + for (List::ConstIterator itr = method_info.arguments.begin(); itr != method_info.arguments.end(); ++itr, ++i) { + at[i + 1] = itr->type; #ifdef DEBUG_METHODS_ENABLED - names.write[i] = method_info.arguments[i].name; + names.write[i] = itr->name; #endif } diff --git a/core/object/object.cpp b/core/object/object.cpp index f8d2feb5a82f..ab89f96a0de4 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -503,9 +503,14 @@ void Object::get_property_list(List *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 @@ -1297,9 +1302,8 @@ TypedArray Object::_get_signal_connection_list(const StringName &p_s TypedArray Object::_get_incoming_connections() const { TypedArray ret; - int connections_amount = connections.size(); - for (int idx_conn = 0; idx_conn < connections_amount; idx_conn++) { - ret.push_back(connections[idx_conn]); + for (const Object::Connection &connection : connections) { + ret.push_back(connection); } return ret; @@ -1455,7 +1459,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 +1482,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 915c3a8c2581..adb50268d2ad 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/script_language.cpp b/core/object/script_language.cpp index bd3199ca0af5..820296e66d03 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -537,6 +537,7 @@ void ScriptLanguage::get_core_type_words(List *p_core_type_words) const p_core_type_words->push_back("PackedVector2Array"); p_core_type_words->push_back("PackedVector3Array"); p_core_type_words->push_back("PackedColorArray"); + p_core_type_words->push_back("PackedVector4Array"); } void ScriptLanguage::frame() { diff --git a/core/object/script_language_extension.h b/core/object/script_language_extension.h index cc6b729ae8ec..8fd26c3d2c83 100644 --- a/core/object/script_language_extension.h +++ b/core/object/script_language_extension.h @@ -389,7 +389,16 @@ class ScriptLanguageExtension : public ScriptLanguage { EXBIND0RC(bool, can_make_function) EXBIND3R(Error, open_in_external_editor, const Ref @@ -140,58 +100,25 @@ 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 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 @@ 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 000000000000..bc4a893d587a --- /dev/null +++ b/misc/dist/ios_xcode/PrivacyInfo.xcprivacy @@ -0,0 +1,10 @@ + + + + + NSPrivacyAccessedAPITypes + $priv_api_types + $priv_tracking + $priv_collection + + diff --git a/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj b/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj index e9efea88094a..a5de7e887276 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 = ""; }; $pbx_locale_file_reference D0BCFE7718AEBFEB004A7AAE /* $binary.pck */ = {isa = PBXFileReference; lastKnownFileType = file; path = "$binary.pck"; sourceTree = ""; }; + F965960C2BC2C3A800579C7E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; $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 = ""; @@ -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 4706ed37f0c8..55a4e0b18c32 100644 --- a/misc/extension_api_validation/4.2-stable.expected +++ b/misc/extension_api_validation/4.2-stable.expected @@ -315,3 +315,32 @@ 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. + + +GH-91098 +-------- +Validate extension JSON: Error: Field 'classes/RichTextLabel/methods/remove_paragraph/arguments': size changed value in new API, from 1 to 2. + +Added optional argument. Compatibility method registered. + + +GH-91143 +-------- +Validate extension JSON: Error: Field 'classes/Input/methods/vibrate_handheld/arguments': size changed value in new API, from 1 to 2. + +Added optional argument. Compatibility method registered. \ No newline at end of file diff --git a/misc/scripts/codespell.sh b/misc/scripts/codespell.sh index 7dad25fa2395..ef361a649555 100755 --- a/misc/scripts/codespell.sh +++ b/misc/scripts/codespell.sh @@ -3,6 +3,6 @@ SKIP_LIST="./.*,./**/.*,./bin,./thirdparty,*.desktop,*.gen.*,*.po,*.pot,*.rc,./A SKIP_LIST+="./core/input/gamecontrollerdb.txt,./core/string/locales.h,./editor/renames_map_3_to_4.cpp,./misc/scripts/codespell.sh," SKIP_LIST+="./platform/android/java/lib/src/com,./platform/web/node_modules,./platform/web/package-lock.json," -IGNORE_LIST="breaked,cancelled,curvelinear,doubleclick,expct,findn,gird,hel,inout,lod,mis,nd,numer,ot,requestor,te,vai" +IGNORE_LIST="breaked,cancelled,colour,curvelinear,doubleclick,expct,findn,gird,hel,inout,lod,mis,nd,numer,ot,requestor,te,thirdparty,vai" codespell -w -q 3 -S "${SKIP_LIST}" -L "${IGNORE_LIST}" --builtin "clear,rare,en-GB_to_en-US" diff --git a/modules/astcenc/image_compress_astcenc.cpp b/modules/astcenc/image_compress_astcenc.cpp index 31df83efae82..941c1f44be1a 100644 --- a/modules/astcenc/image_compress_astcenc.cpp +++ b/modules/astcenc/image_compress_astcenc.cpp @@ -41,7 +41,7 @@ void _compress_astc(Image *r_img, Image::ASTCFormat p_format) { // TODO: See how to handle lossy quality. Image::Format img_format = r_img->get_format(); - if (img_format >= Image::FORMAT_DXT1) { + if (Image::is_format_compressed(img_format)) { return; // Do not compress, already compressed. } diff --git a/modules/basis_universal/image_compress_basisu.cpp b/modules/basis_universal/image_compress_basisu.cpp index 72e7977eefb7..d8ef1c041486 100644 --- a/modules/basis_universal/image_compress_basisu.cpp +++ b/modules/basis_universal/image_compress_basisu.cpp @@ -96,17 +96,74 @@ Vector basis_universal_packer(const Ref &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 image_data = image->get_data(); basisu::vector basisu_mipmaps; + // Buffer for storing padded mipmap data. + Vector 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(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(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 basis_universal_packer(const Ref &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,12 +295,11 @@ Ref 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); - uint32_t mip_block_or_pixel_count = image_format >= Image::FORMAT_DXT1 ? basisu_level.m_total_blocks : basisu_level.m_orig_width * basisu_level.m_orig_height; + uint32_t mip_block_or_pixel_count = Image::is_format_compressed(image_format) ? basisu_level.m_total_blocks : basisu_level.m_orig_width * basisu_level.m_orig_height; int ofs = Image::get_image_mipmap_offset(basisu_info.m_width, basisu_info.m_height, image_format, i); bool result = transcoder.transcode_image_level(src_ptr, src_size, 0, i, dst + ofs, mip_block_or_pixel_count, basisu_format); diff --git a/modules/csg/csg.cpp b/modules/csg/csg.cpp index 878664bb9c0d..cec03b7246de 100644 --- a/modules/csg/csg.cpp +++ b/modules/csg/csg.cpp @@ -1098,7 +1098,7 @@ void CSGBrushOperation::Build2DFaces::_find_edge_intersections(const Vector2 p_s }; // Check if edge has already been processed. - if (processed_edges.find(edge_points_and_uvs) != -1) { + if (processed_edges.has(edge_points_and_uvs)) { continue; } diff --git a/modules/csg/editor/csg_gizmos.h b/modules/csg/editor/csg_gizmos.h index 6281db0a215a..de19b33e7da1 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/cvtt/image_compress_cvtt.cpp b/modules/cvtt/image_compress_cvtt.cpp index e9a7009d7ca3..7335315c512c 100644 --- a/modules/cvtt/image_compress_cvtt.cpp +++ b/modules/cvtt/image_compress_cvtt.cpp @@ -142,7 +142,7 @@ static void _digest_job_queue(void *p_job_queue, uint32_t p_index) { } void image_compress_cvtt(Image *p_image, Image::UsedChannels p_channels) { - if (p_image->get_format() >= Image::FORMAT_BPTC_RGBA) { + if (p_image->is_compressed()) { return; //do not compress, already compressed } int w = p_image->get_width(); diff --git a/modules/enet/enet_multiplayer_peer.cpp b/modules/enet/enet_multiplayer_peer.cpp index 910c4ed242ba..6f87a398c764 100644 --- a/modules/enet/enet_multiplayer_peer.cpp +++ b/modules/enet/enet_multiplayer_peer.cpp @@ -124,9 +124,9 @@ Error ENetMultiplayerPeer::add_mesh_peer(int p_id, Ref p_host) { ERR_FAIL_COND_V_MSG(active_mode != MODE_MESH, ERR_UNCONFIGURED, "The multiplayer instance is not configured as a mesh. Call 'create_mesh' first."); List> host_peers; p_host->get_peers(host_peers); - ERR_FAIL_COND_V_MSG(host_peers.size() != 1 || host_peers[0]->get_state() != ENetPacketPeer::STATE_CONNECTED, ERR_INVALID_PARAMETER, "The provided host must have exactly one peer in the connected state."); + ERR_FAIL_COND_V_MSG(host_peers.size() != 1 || host_peers.front()->get()->get_state() != ENetPacketPeer::STATE_CONNECTED, ERR_INVALID_PARAMETER, "The provided host must have exactly one peer in the connected state."); hosts[p_id] = p_host; - peers[p_id] = host_peers[0]; + peers[p_id] = host_peers.front()->get(); emit_signal(SNAME("peer_connected"), p_id); return OK; } diff --git a/modules/etcpak/image_compress_etcpak.cpp b/modules/etcpak/image_compress_etcpak.cpp index dcd73101c207..4ce0cf50d9ad 100644 --- a/modules/etcpak/image_compress_etcpak.cpp +++ b/modules/etcpak/image_compress_etcpak.cpp @@ -92,7 +92,7 @@ void _compress_etcpak(EtcpakType p_compresstype, Image *r_img) { uint64_t start_time = OS::get_singleton()->get_ticks_msec(); Image::Format img_format = r_img->get_format(); - if (img_format >= Image::FORMAT_DXT1) { + if (Image::is_format_compressed(img_format)) { return; // Do not compress, already compressed. } if (img_format > Image::FORMAT_RGBA8) { diff --git a/modules/fbx/fbx_document.cpp b/modules/fbx/fbx_document.cpp index 5f94a8056604..1361e871de03 100644 --- a/modules/fbx/fbx_document.cpp +++ b/modules/fbx/fbx_document.cpp @@ -217,6 +217,16 @@ static ufbx_skin_deformer *_find_skin_deformer(ufbx_skin_cluster *p_cluster) { return nullptr; } +static String _find_element_name(ufbx_element *p_element) { + if (p_element->name.length > 0) { + return FBXDocument::_as_string(p_element->name); + } else if (p_element->instances.count > 0) { + return _find_element_name(&p_element->instances[0]->element); + } else { + return ""; + } +} + struct ThreadPoolFBX { struct Group { ufbx_thread_pool_context ctx = {}; @@ -693,10 +703,7 @@ Error FBXDocument::_parse_meshes(Ref p_state) { // Find the first imported skin deformer for (ufbx_skin_deformer *fbx_skin : fbx_mesh->skin_deformers) { - if (!p_state->skin_indices.has(fbx_skin->typed_id)) { - continue; - } - GLTFSkinIndex skin_i = p_state->skin_indices[fbx_skin->typed_id]; + GLTFSkinIndex skin_i = p_state->original_skin_indices[fbx_skin->typed_id]; if (skin_i < 0) { continue; } @@ -2029,7 +2036,7 @@ Error FBXDocument::_parse(Ref p_state, String p_path, Ref opts.space_conversion = UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY; if (!p_state->get_allow_geometry_helper_nodes()) { opts.geometry_transform_handling = UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY_NO_FALLBACK; - opts.inherit_mode_handling = UFBX_INHERIT_MODE_HANDLING_IGNORE; + opts.inherit_mode_handling = UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK; } else { opts.geometry_transform_handling = UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES; opts.inherit_mode_handling = UFBX_INHERIT_MODE_HANDLING_COMPENSATE; @@ -2071,6 +2078,32 @@ Error FBXDocument::_parse(Ref p_state, String p_path, Ref ERR_FAIL_V_MSG(ERR_PARSE_ERROR, err_buf); } + const int max_warning_count = 10; + int warning_count[UFBX_WARNING_TYPE_COUNT] = {}; + int ignored_warning_count = 0; + for (const ufbx_warning &warning : p_state->scene->metadata.warnings) { + if (warning_count[warning.type]++ < max_warning_count) { + if (warning.count > 1) { + WARN_PRINT(vformat("FBX: ufbx warning: %s (x%d)", _as_string(warning.description), (int)warning.count)); + } else { + String element_name; + if (warning.element_id != UFBX_NO_INDEX) { + element_name = _find_element_name(p_state->scene->elements[warning.element_id]); + } + if (!element_name.is_empty()) { + WARN_PRINT(vformat("FBX: ufbx warning in '%s': %s", element_name, _as_string(warning.description))); + } else { + WARN_PRINT(vformat("FBX: ufbx warning: %s", _as_string(warning.description))); + } + } + } else { + ignored_warning_count++; + } + } + if (ignored_warning_count > 0) { + WARN_PRINT(vformat("FBX: ignored %d further ufbx warnings", ignored_warning_count)); + } + err = _parse_fbx_state(p_state, p_path); ERR_FAIL_COND_V(err != OK, err); @@ -2341,7 +2374,7 @@ Error FBXDocument::_parse_skins(Ref p_state) { HashMap joint_mapping; for (const ufbx_skin_deformer *fbx_skin : fbx_scene->skin_deformers) { - if (fbx_skin->clusters.count == 0) { + if (fbx_skin->clusters.count == 0 || fbx_skin->weights.count == 0) { p_state->skin_indices.push_back(-1); continue; } @@ -2387,8 +2420,9 @@ Error FBXDocument::_parse_skins(Ref p_state) { } } } + p_state->original_skin_indices = p_state->skin_indices.duplicate(); Error err = SkinTool::_asset_parse_skins( - p_state->skin_indices.duplicate(), + p_state->original_skin_indices, p_state->skins.duplicate(), p_state->nodes.duplicate(), p_state->skin_indices, diff --git a/modules/fbx/fbx_state.h b/modules/fbx/fbx_state.h index d10550c2282b..e52f47e0db33 100644 --- a/modules/fbx/fbx_state.h +++ b/modules/fbx/fbx_state.h @@ -53,6 +53,7 @@ class FBXState : public GLTFState { HashMap, GLTFTextureIndex, PairHash> albedo_transparency_textures; Vector skin_indices; + Vector original_skin_indices; HashMap skeleton3d_to_fbx_skeleton; HashMap> skin_and_skeleton3d_to_fbx_skin; HashSet unique_mesh_names; // Not in GLTFState because GLTFState prefixes mesh names with the scene name (or _) diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 921ed0b416c6..19b264d7640b 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -678,6 +678,27 @@ Error GDScript::_static_init() { #ifdef TOOLS_ENABLED +void GDScript::_static_default_init() { + for (const KeyValue &E : static_variables_indices) { + const GDScriptDataType &type = E.value.data_type; + // Only initialize builtin types, which are not expected to be `null`. + if (!type.has_type || type.kind != GDScriptDataType::BUILTIN) { + continue; + } + if (type.builtin_type == Variant::ARRAY && type.has_container_element_type(0)) { + Array default_value; + const GDScriptDataType &element_type = type.get_container_element_type(0); + default_value.set_typed(element_type.builtin_type, element_type.native_type, element_type.script_type); + static_variables.write[E.value.index] = default_value; + } else { + Variant default_value; + Callable::CallError err; + Variant::construct(type.builtin_type, default_value, nullptr, 0, err); + static_variables.write[E.value.index] = default_value; + } + } +} + void GDScript::_save_old_static_data() { old_static_variables_indices = static_variables_indices; old_static_variables = static_variables; @@ -841,6 +862,9 @@ Error GDScript::reload(bool p_keep_state) { #ifdef TOOLS_ENABLED if (can_run && p_keep_state) { _restore_old_static_data(); + } else if (!can_run) { + // Initialize static variables with sane default values even if the constructor isn't called. + _static_default_init(); } #endif @@ -1958,19 +1982,22 @@ int GDScriptInstance::get_method_argument_count(const StringName &p_method, bool return 0; } +void GDScriptInstance::_call_implicit_ready_recursively(GDScript *p_script) { + // Call base class first. + if (p_script->_base) { + _call_implicit_ready_recursively(p_script->_base); + } + if (p_script->implicit_ready) { + Callable::CallError err; + p_script->implicit_ready->call(this, nullptr, 0, err); + } +} + Variant GDScriptInstance::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { GDScript *sptr = script.ptr(); if (unlikely(p_method == SNAME("_ready"))) { - // Call implicit ready first, including for the super classes. - while (sptr) { - if (sptr->implicit_ready) { - sptr->implicit_ready->call(this, nullptr, 0, r_error); - } - sptr = sptr->_base; - } - - // Reset this back for the regular call. - sptr = script.ptr(); + // Call implicit ready first, including for the super classes recursively. + _call_implicit_ready_recursively(sptr); } while (sptr) { HashMap::Iterator E = sptr->member_functions.find(p_method); @@ -2892,7 +2919,7 @@ String ResourceFormatLoaderGDScript::get_resource_type(const String &p_path) con return ""; } -void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List *p_dependencies, bool p_add_types) { +void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List *r_dependencies, bool p_add_types) { Ref file = FileAccess::open(p_path, FileAccess::READ); ERR_FAIL_COND_MSG(file.is_null(), "Cannot open file '" + p_path + "'."); @@ -2906,8 +2933,13 @@ void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, Listpush_back(E); + r_dependencies->push_back(E); } } diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 781e284bfc81..f63b0da745cd 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -169,6 +169,9 @@ class GDScript : public Script { GDScriptFunction *static_initializer = nullptr; Error _static_init(); +#ifdef TOOLS_ENABLED + void _static_default_init(); // Initialize static variables with default values based on their types. +#endif int subclass_count = 0; RBSet instances; @@ -365,6 +368,8 @@ class GDScriptInstance : public ScriptInstance { SelfList::List pending_func_states; + void _call_implicit_ready_recursively(GDScript *p_script); + public: virtual Object *get_owner() { return owner; } @@ -633,7 +638,7 @@ class ResourceFormatLoaderGDScript : public ResourceFormatLoader { virtual void get_recognized_extensions(List *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 *p_dependencies, bool p_add_types = false) override; + virtual void get_dependencies(const String &p_path, List *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 0636ac508316..4b6cc4721816 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 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(constant); + if (node != nullptr) { + Ref 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 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; } @@ -3001,6 +3030,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a case Variant::PACKED_VECTOR2_ARRAY: case Variant::PACKED_VECTOR3_ARRAY: case Variant::PACKED_COLOR_ARRAY: + case Variant::PACKED_VECTOR4_ARRAY: safe_to_fold = false; break; default: @@ -3099,24 +3129,28 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a bool types_match = true; - for (int i = 0; i < p_call->arguments.size(); i++) { - GDScriptParser::DataType par_type = type_from_property(info.arguments[i], true); - GDScriptParser::DataType arg_type = p_call->arguments[i]->get_datatype(); - if (!is_type_compatible(par_type, arg_type, true)) { - types_match = false; - break; + { + List::ConstIterator arg_itr = info.arguments.begin(); + for (int i = 0; i < p_call->arguments.size(); ++arg_itr, ++i) { + GDScriptParser::DataType par_type = type_from_property(*arg_itr, true); + GDScriptParser::DataType arg_type = p_call->arguments[i]->get_datatype(); + if (!is_type_compatible(par_type, arg_type, true)) { + types_match = false; + break; #ifdef DEBUG_ENABLED - } else { - if (par_type.builtin_type == Variant::INT && arg_type.builtin_type == Variant::FLOAT && builtin_type != Variant::INT) { - parser->push_warning(p_call, GDScriptWarning::NARROWING_CONVERSION, function_name); - } + } else { + if (par_type.builtin_type == Variant::INT && arg_type.builtin_type == Variant::FLOAT && builtin_type != Variant::INT) { + parser->push_warning(p_call, GDScriptWarning::NARROWING_CONVERSION, function_name); + } #endif + } } } if (types_match) { - for (int i = 0; i < p_call->arguments.size(); i++) { - GDScriptParser::DataType par_type = type_from_property(info.arguments[i], true); + List::ConstIterator arg_itr = info.arguments.begin(); + for (int i = 0; i < p_call->arguments.size(); ++arg_itr, ++i) { + GDScriptParser::DataType par_type = type_from_property(*arg_itr, true); if (p_call->arguments[i]->is_constant) { update_const_expression_builtin_type(p_call->arguments[i], par_type, "pass"); } @@ -3336,8 +3370,8 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a // If the function requires typed arrays we must make literals be typed. for (const KeyValue &E : arrays) { int index = E.key; - if (index < par_types.size() && par_types[index].is_hard_type() && par_types[index].has_container_element_type(0)) { - update_array_literal_element_type(E.value, par_types[index].get_container_element_type(0)); + if (index < par_types.size() && par_types.get(index).is_hard_type() && par_types.get(index).has_container_element_type(0)) { + update_array_literal_element_type(E.value, par_types.get(index).get_container_element_type(0)); } } validate_call_arg(par_types, default_arg_count, method_flags.has_flag(METHOD_FLAG_VARARG), p_call); @@ -3355,7 +3389,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a } if (parent_function) { - push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parent_function->identifier->name), p_call); + push_error(vformat(R"*(Cannot call non-static function "%s()" from the static function "%s()".)*", p_call->function_name, parent_function->identifier->name), p_call); } else { push_error(vformat(R"*(Cannot call non-static function "%s()" from a static variable initializer.)*", p_call->function_name), p_call); } @@ -3769,9 +3803,11 @@ 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; + p_identifier->function_source = member.function; + p_identifier->function_source_is_static = member.function->is_static; return; } } break; @@ -3820,6 +3856,7 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod if (method_info.name == p_identifier->name) { p_identifier->set_datatype(make_callable_type(method_info)); p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_FUNCTION; + p_identifier->function_source_is_static = method_info.flags & METHOD_FLAG_STATIC; return; } @@ -4000,25 +4037,37 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident } if (found_source) { - bool source_is_variable = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_VARIABLE || p_identifier->source == GDScriptParser::IdentifierNode::INHERITED_VARIABLE; - bool source_is_signal = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_SIGNAL; - if ((source_is_variable || source_is_signal) && static_context) { + const bool source_is_instance_variable = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_VARIABLE || p_identifier->source == GDScriptParser::IdentifierNode::INHERITED_VARIABLE; + const bool source_is_instance_function = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_FUNCTION && !p_identifier->function_source_is_static; + const bool source_is_signal = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_SIGNAL; + + if (static_context && (source_is_instance_variable || source_is_instance_function || source_is_signal)) { // Get the parent function above any lambda. GDScriptParser::FunctionNode *parent_function = parser->current_function; while (parent_function && parent_function->source_lambda) { parent_function = parent_function->source_lambda->parent_function; } + String source_type; + if (source_is_instance_variable) { + source_type = "non-static variable"; + } else if (source_is_instance_function) { + source_type = "non-static function"; + } else { // source_is_signal + source_type = "signal"; + } + if (parent_function) { - push_error(vformat(R"*(Cannot access %s "%s" from the static function "%s()".)*", source_is_signal ? "signal" : "instance variable", p_identifier->name, parent_function->identifier->name), p_identifier); + push_error(vformat(R"*(Cannot access %s "%s" from the static function "%s()".)*", source_type, p_identifier->name, parent_function->identifier->name), p_identifier); } else { - push_error(vformat(R"*(Cannot access %s "%s" from a static variable initializer.)*", source_is_signal ? "signal" : "instance variable", p_identifier->name), p_identifier); + push_error(vformat(R"*(Cannot access %s "%s" from a static variable initializer.)*", source_type, p_identifier->name), p_identifier); } } if (current_lambda != nullptr) { - // If the identifier is a member variable (including the native class properties) or a signal, we consider the lambda to be using `self`, so we keep a reference to the current instance. - if (source_is_variable || source_is_signal) { + // If the identifier is a member variable (including the native class properties), member function, or a signal, + // we consider the lambda to be using `self`, so we keep a reference to the current instance. + if (source_is_instance_variable || source_is_instance_function || source_is_signal) { mark_lambda_use_self(); return; // No need to capture. } @@ -4063,6 +4112,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 +4155,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 +4275,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 +4305,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); } } @@ -4378,7 +4430,6 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri switch (base_type.builtin_type) { // Expect int or real as index. case Variant::PACKED_BYTE_ARRAY: - case Variant::PACKED_COLOR_ARRAY: case Variant::PACKED_FLOAT32_ARRAY: case Variant::PACKED_FLOAT64_ARRAY: case Variant::PACKED_INT32_ARRAY: @@ -4386,6 +4437,8 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri case Variant::PACKED_STRING_ARRAY: case Variant::PACKED_VECTOR2_ARRAY: case Variant::PACKED_VECTOR3_ARRAY: + case Variant::PACKED_COLOR_ARRAY: + case Variant::PACKED_VECTOR4_ARRAY: case Variant::ARRAY: case Variant::STRING: error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::FLOAT; @@ -4484,10 +4537,6 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri case Variant::QUATERNION: result_type.builtin_type = Variant::FLOAT; break; - // Return Color. - case Variant::PACKED_COLOR_ARRAY: - result_type.builtin_type = Variant::COLOR; - break; // Return String. case Variant::PACKED_STRING_ARRAY: case Variant::STRING: @@ -4509,6 +4558,14 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri case Variant::BASIS: result_type.builtin_type = Variant::VECTOR3; break; + // Return Color. + case Variant::PACKED_COLOR_ARRAY: + result_type.builtin_type = Variant::COLOR; + break; + // Return Vector4. + case Variant::PACKED_VECTOR4_ARRAY: + result_type.builtin_type = Variant::VECTOR4; + break; // Depends on the index. case Variant::TRANSFORM3D: case Variant::PROJECTION: @@ -5172,12 +5229,13 @@ void GDScriptAnalyzer::validate_call_arg(const List &p push_error(vformat(R"*(Too many arguments for "%s()" call. Expected at most %d but received %d.)*", p_call->function_name, p_par_types.size(), p_call->arguments.size()), p_call->arguments[p_par_types.size()]); } - for (int i = 0; i < p_call->arguments.size(); i++) { + List::ConstIterator par_itr = p_par_types.begin(); + for (int i = 0; i < p_call->arguments.size(); ++par_itr, ++i) { if (i >= p_par_types.size()) { // Already on vararg place. break; } - GDScriptParser::DataType par_type = p_par_types[i]; + GDScriptParser::DataType par_type = *par_itr; if (par_type.is_hard_type() && p_call->arguments[i]->is_constant) { update_const_expression_builtin_type(p_call->arguments[i], par_type, "pass"); diff --git a/modules/gdscript/gdscript_byte_codegen.cpp b/modules/gdscript/gdscript_byte_codegen.cpp index bfe090edb03d..4cda3d303753 100644 --- a/modules/gdscript/gdscript_byte_codegen.cpp +++ b/modules/gdscript/gdscript_byte_codegen.cpp @@ -109,6 +109,7 @@ uint32_t GDScriptByteCodeGenerator::add_temporary(const GDScriptDataType &p_type case Variant::PACKED_VECTOR2_ARRAY: case Variant::PACKED_VECTOR3_ARRAY: case Variant::PACKED_COLOR_ARRAY: + case Variant::PACKED_VECTOR4_ARRAY: case Variant::VARIANT_MAX: // Arrays, dictionaries, and objects are reference counted, so we don't use the pool for them. temp_type = Variant::NIL; @@ -543,6 +544,9 @@ void GDScriptByteCodeGenerator::write_type_adjust(const Address &p_target, Varia case Variant::PACKED_COLOR_ARRAY: append_opcode(GDScriptFunction::OPCODE_TYPE_ADJUST_PACKED_COLOR_ARRAY); break; + case Variant::PACKED_VECTOR4_ARRAY: + append_opcode(GDScriptFunction::OPCODE_TYPE_ADJUST_PACKED_VECTOR4_ARRAY); + break; case Variant::NIL: case Variant::VARIANT_MAX: return; @@ -1196,23 +1200,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
&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 &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
&p_arguments) { @@ -1547,6 +1577,10 @@ void GDScriptByteCodeGenerator::write_for(const Address &p_variable, bool p_use_ begin_opcode = GDScriptFunction::OPCODE_ITERATE_BEGIN_PACKED_COLOR_ARRAY; iterate_opcode = GDScriptFunction::OPCODE_ITERATE_PACKED_COLOR_ARRAY; break; + case Variant::PACKED_VECTOR4_ARRAY: + begin_opcode = GDScriptFunction::OPCODE_ITERATE_BEGIN_PACKED_VECTOR4_ARRAY; + iterate_opcode = GDScriptFunction::OPCODE_ITERATE_PACKED_VECTOR4_ARRAY; + break; default: break; } diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h index 5a736b2554a9..34f56a2f5c02 100644 --- a/modules/gdscript/gdscript_byte_codegen.h +++ b/modules/gdscript/gdscript_byte_codegen.h @@ -518,6 +518,7 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { virtual void write_call_builtin_type(const Address &p_target, const Address &p_base, Variant::Type p_type, const StringName &p_method, const Vector
&p_arguments) override; virtual void write_call_builtin_type_static(const Address &p_target, Variant::Type p_type, const StringName &p_method, const Vector
&p_arguments) override; virtual void write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector
&p_arguments) override; + virtual void write_call_native_static_validated(const Address &p_target, MethodBind *p_method, const Vector
&p_arguments) override; virtual void write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector
&p_arguments) override; virtual void write_call_method_bind_validated(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector
&p_arguments) override; virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector
&p_arguments) override; diff --git a/modules/gdscript/gdscript_codegen.h b/modules/gdscript/gdscript_codegen.h index 4c33ed499a68..c1c0b61395d5 100644 --- a/modules/gdscript/gdscript_codegen.h +++ b/modules/gdscript/gdscript_codegen.h @@ -131,6 +131,7 @@ class GDScriptCodeGenerator { virtual void write_call_builtin_type(const Address &p_target, const Address &p_base, Variant::Type p_type, const StringName &p_method, const Vector
&p_arguments) = 0; virtual void write_call_builtin_type_static(const Address &p_target, Variant::Type p_type, const StringName &p_method, const Vector
&p_arguments) = 0; virtual void write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector
&p_arguments) = 0; + virtual void write_call_native_static_validated(const Address &p_target, MethodBind *p_method, const Vector
&p_arguments) = 0; virtual void write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector
&p_arguments) = 0; virtual void write_call_method_bind_validated(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector
&p_arguments) = 0; virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector
&p_arguments) = 0; diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index 734e37bc0914..01e5751dc8e0 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -241,9 +241,9 @@ static bool _can_use_validate_call(const MethodBind *p_method, const Vectorget_instance_class(), p_method->get_name(), &info); - for (int i = 0; i < p_arguments.size(); i++) { - const PropertyInfo &prop = info.arguments[i]; - if (!_is_exact_type(prop, p_arguments[i].type)) { + int i = 0; + for (List::ConstIterator itr = info.arguments.begin(); itr != info.arguments.end(); ++itr, ++i) { + if (!_is_exact_type(*itr, p_arguments[i].type)) { return false; } } @@ -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(subscript->base)->name) && !Engine::get_singleton()->has_singleton(static_cast(subscript->base)->name)) { // It's a static native method call. - gen->write_call_native_static(result, static_cast(subscript->base)->name, subscript->attribute->name, arguments); + StringName class_name = static_cast(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) { @@ -2658,7 +2666,10 @@ Error GDScriptCompiler::_prepare_compilation(GDScript *p_script, const GDScriptP GDScriptDataType base_type = _gdtype_from_datatype(p_class->base_type, p_script, false); + ERR_FAIL_COND_V_MSG(base_type.native_type == StringName(), ERR_BUG, vformat(R"(Failed to get base class for "%s")", p_script->path)); + int native_idx = GDScriptLanguage::get_singleton()->get_global_map()[base_type.native_type]; + p_script->native = GDScriptLanguage::get_singleton()->get_global_array()[native_idx]; ERR_FAIL_COND_V(p_script->native.is_null(), ERR_BUG); diff --git a/modules/gdscript/gdscript_disassembler.cpp b/modules/gdscript/gdscript_disassembler.cpp index c7873dcd528c..03310450780e 100644 --- a/modules/gdscript/gdscript_disassembler.cpp +++ b/modules/gdscript/gdscript_disassembler.cpp @@ -678,6 +678,50 @@ void GDScriptFunction::disassemble(const Vector &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) "; @@ -1002,6 +1046,7 @@ void GDScriptFunction::disassemble(const Vector &p_code_lines) const { m_macro(PACKED_VECTOR2_ARRAY); \ m_macro(PACKED_VECTOR3_ARRAY); \ m_macro(PACKED_COLOR_ARRAY); \ + m_macro(PACKED_VECTOR4_ARRAY); \ m_macro(OBJECT) case OPCODE_ITERATE_BEGIN: { @@ -1106,6 +1151,7 @@ void GDScriptFunction::disassemble(const Vector &p_code_lines) const { DISASSEMBLE_TYPE_ADJUST(PACKED_VECTOR2_ARRAY); DISASSEMBLE_TYPE_ADJUST(PACKED_VECTOR3_ARRAY); DISASSEMBLE_TYPE_ADJUST(PACKED_COLOR_ARRAY); + DISASSEMBLE_TYPE_ADJUST(PACKED_VECTOR4_ARRAY); case OPCODE_ASSERT: { text += "assert ("; diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 74d383c57f93..cae85d7c701c 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -1695,6 +1695,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, if (full) { // If not fully constant, setting this value is detrimental to the inference. r_type.value = a; + r_type.type.is_constant = true; } r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; r_type.type.kind = GDScriptParser::DataType::BUILTIN; @@ -2733,9 +2734,9 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c // Handle user preference. if (opt.is_quoted()) { opt = opt.unquote().quote(quote_style); - if (use_string_names && info.arguments[p_argidx].type == Variant::STRING_NAME) { + if (use_string_names && info.arguments.get(p_argidx).type == Variant::STRING_NAME) { opt = opt.indent("&"); - } else if (use_node_paths && info.arguments[p_argidx].type == Variant::NODE_PATH) { + } else if (use_node_paths && info.arguments.get(p_argidx).type == Variant::NODE_PATH) { opt = opt.indent("^"); } } @@ -2746,7 +2747,7 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c } if (p_argidx < method_args) { - PropertyInfo arg_info = info.arguments[p_argidx]; + const PropertyInfo &arg_info = info.arguments.get(p_argidx); if (arg_info.usage & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)) { _find_enumeration_candidates(p_context, arg_info.class_name, r_result); } @@ -3334,19 +3335,17 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa } method_hint += "("; - if (mi.arguments.size()) { - for (int i = 0; i < mi.arguments.size(); i++) { - if (i > 0) { - method_hint += ", "; - } - String arg = mi.arguments[i].name; - if (arg.contains(":")) { - arg = arg.substr(0, arg.find(":")); - } - method_hint += arg; - if (use_type_hint) { - method_hint += ": " + _get_visual_datatype(mi.arguments[i], true, class_name); - } + for (List::ConstIterator arg_itr = mi.arguments.begin(); arg_itr != mi.arguments.end(); ++arg_itr) { + if (arg_itr != mi.arguments.begin()) { + method_hint += ", "; + } + String arg = arg_itr->name; + if (arg.contains(":")) { + arg = arg.substr(0, arg.find(":")); + } + method_hint += arg; + if (use_type_hint) { + method_hint += ": " + _get_visual_datatype(*arg_itr, true, class_name); } } method_hint += ")"; diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index 184d256bcd8d..759e92d68ce5 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -264,6 +264,8 @@ class GDScriptFunction { 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, @@ -299,6 +301,7 @@ class GDScriptFunction { OPCODE_ITERATE_BEGIN_PACKED_VECTOR2_ARRAY, OPCODE_ITERATE_BEGIN_PACKED_VECTOR3_ARRAY, OPCODE_ITERATE_BEGIN_PACKED_COLOR_ARRAY, + OPCODE_ITERATE_BEGIN_PACKED_VECTOR4_ARRAY, OPCODE_ITERATE_BEGIN_OBJECT, OPCODE_ITERATE, OPCODE_ITERATE_INT, @@ -319,6 +322,7 @@ class GDScriptFunction { OPCODE_ITERATE_PACKED_VECTOR2_ARRAY, OPCODE_ITERATE_PACKED_VECTOR3_ARRAY, OPCODE_ITERATE_PACKED_COLOR_ARRAY, + OPCODE_ITERATE_PACKED_VECTOR4_ARRAY, OPCODE_ITERATE_OBJECT, OPCODE_STORE_GLOBAL, OPCODE_STORE_NAMED_GLOBAL, @@ -359,6 +363,7 @@ class GDScriptFunction { OPCODE_TYPE_ADJUST_PACKED_VECTOR2_ARRAY, OPCODE_TYPE_ADJUST_PACKED_VECTOR3_ARRAY, OPCODE_TYPE_ADJUST_PACKED_COLOR_ARRAY, + OPCODE_TYPE_ADJUST_PACKED_VECTOR4_ARRAY, OPCODE_ASSERT, OPCODE_BREAKPOINT, OPCODE_LINE, diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 9799c6e610ec..9be9307b8a41 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -3199,6 +3199,9 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_call(ExpressionNode *p_pre } GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p_previous_operand, bool p_can_assign) { + // We want code completion after a DOLLAR even if the current code is invalid. + make_completion_context(COMPLETION_GET_NODE, nullptr, -1, true); + if (!current.is_node_name() && !check(GDScriptTokenizer::Token::LITERAL) && !check(GDScriptTokenizer::Token::SLASH) && !check(GDScriptTokenizer::Token::PERCENT)) { push_error(vformat(R"(Expected node path as string or identifier after "%s".)", previous.get_name())); return nullptr; @@ -3254,7 +3257,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p path_state = PATH_STATE_SLASH; } - make_completion_context(COMPLETION_GET_NODE, get_node, context_argument++); + make_completion_context(COMPLETION_GET_NODE, get_node, context_argument++, true); if (match(GDScriptTokenizer::Token::LITERAL)) { if (previous.literal.get_type() != Variant::STRING) { @@ -4129,6 +4132,9 @@ static String _get_annotation_error_string(const StringName &p_annotation_name, case Variant::COLOR: types.push_back("PackedColorArray"); break; + case Variant::VECTOR4: + types.push_back("PackedVector4Array"); + break; default: break; } @@ -4827,6 +4833,8 @@ static Variant::Type _variant_type_to_typed_array_element_type(Variant::Type p_t return Variant::VECTOR3; case Variant::PACKED_COLOR_ARRAY: return Variant::COLOR; + case Variant::PACKED_VECTOR4_ARRAY: + return Variant::VECTOR4; default: return Variant::NIL; } diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 77cb8136f8d5..1e67e2d496a5 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -902,8 +902,11 @@ class GDScriptParser { VariableNode *variable_source; ConstantNode *constant_source; SignalNode *signal_source; + FunctionNode *function_source; }; - FunctionNode *source_function = nullptr; + bool function_source_is_static = false; // For non-GDScript scripts. + + FunctionNode *source_function = nullptr; // TODO: Rename to disambiguate `function_source`. int usages = 0; // Useful for binds/iterator variable. @@ -1429,6 +1432,8 @@ class GDScriptParser { void reset_extents(Node *p_node, GDScriptTokenizer::Token p_token); void reset_extents(Node *p_node, Node *p_from); + HashSet dependencies; + template T *alloc_node() { T *node = memnew(T); @@ -1572,9 +1577,11 @@ class GDScriptParser { bool annotation_exists(const String &p_annotation_name) const; const List &get_errors() const { return errors; } - const List get_dependencies() const { - // TODO: Keep track of deps. - return List(); + const HashSet &get_dependencies() const { + return dependencies; + } + void add_dependency(const String &p_dependency) { + dependencies.insert(p_dependency); } #ifdef DEBUG_ENABLED const List &get_warnings() const { return warnings; } diff --git a/modules/gdscript/gdscript_utility_functions.cpp b/modules/gdscript/gdscript_utility_functions.cpp index e5b0f55df8ae..3e1de628d2df 100644 --- a/modules/gdscript/gdscript_utility_functions.cpp +++ b/modules/gdscript/gdscript_utility_functions.cpp @@ -519,6 +519,10 @@ struct GDScriptUtilityFunctionsDefinitions { Vector d = *p_args[0]; *r_ret = d.size(); } break; + case Variant::PACKED_VECTOR4_ARRAY: { + Vector d = *p_args[0]; + *r_ret = d.size(); + } break; default: { r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT; r_error.argument = 0; @@ -756,7 +760,7 @@ Variant::Type GDScriptUtilityFunctions::get_function_argument_type(const StringN GDScriptUtilityFunctionInfo *info = utility_function_table.lookup_ptr(p_function); ERR_FAIL_NULL_V(info, Variant::NIL); ERR_FAIL_COND_V(p_arg >= info->info.arguments.size(), Variant::NIL); - return info->info.arguments[p_arg].type; + return info->info.arguments.get(p_arg).type; } int GDScriptUtilityFunctions::get_function_argument_count(const StringName &p_function) { diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp index 842975698b8d..11bd1d224a86 100644 --- a/modules/gdscript/gdscript_vm.cpp +++ b/modules/gdscript/gdscript_vm.cpp @@ -208,159 +208,165 @@ void (*type_init_function_table[])(Variant *) = { &VariantInitializer::init, // PACKED_VECTOR2_ARRAY. &VariantInitializer::init, // PACKED_VECTOR3_ARRAY. &VariantInitializer::init, // PACKED_COLOR_ARRAY. + &VariantInitializer::init, // PACKED_VECTOR4_ARRAY. }; #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_PACKED_VECTOR4_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_PACKED_VECTOR4_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_TYPE_ADJUST_PACKED_VECTOR4_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) \ @@ -428,6 +434,7 @@ void (*type_init_function_table[])(Variant *) = { #define OP_GET_PACKED_VECTOR2_ARRAY get_vector2_array #define OP_GET_PACKED_VECTOR3_ARRAY get_vector3_array #define OP_GET_PACKED_COLOR_ARRAY get_color_array +#define OP_GET_PACKED_VECTOR4_ARRAY get_vector4_array #define OP_GET_TRANSFORM3D get_transform #define OP_GET_TRANSFORM2D get_transform2d #define OP_GET_PROJECTION get_projection @@ -882,23 +889,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 +935,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 +971,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 +1109,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 +1979,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); @@ -2969,6 +3064,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a OPCODE_ITERATE_BEGIN_PACKED_ARRAY(VECTOR2, Vector2, get_vector2_array, VECTOR2, Vector2, get_vector2); OPCODE_ITERATE_BEGIN_PACKED_ARRAY(VECTOR3, Vector3, get_vector3_array, VECTOR3, Vector3, get_vector3); OPCODE_ITERATE_BEGIN_PACKED_ARRAY(COLOR, Color, get_color_array, COLOR, Color, get_color); + OPCODE_ITERATE_BEGIN_PACKED_ARRAY(VECTOR4, Vector4, get_vector4_array, VECTOR4, Vector4, get_vector4); OPCODE(OPCODE_ITERATE_BEGIN_OBJECT) { CHECK_SPACE(4); @@ -3304,6 +3400,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a OPCODE_ITERATE_PACKED_ARRAY(VECTOR2, Vector2, get_vector2_array, get_vector2); OPCODE_ITERATE_PACKED_ARRAY(VECTOR3, Vector3, get_vector3_array, get_vector3); OPCODE_ITERATE_PACKED_ARRAY(COLOR, Color, get_color_array, get_color); + OPCODE_ITERATE_PACKED_ARRAY(VECTOR4, Vector4, get_vector4_array, get_vector4); OPCODE(OPCODE_ITERATE_OBJECT) { CHECK_SPACE(4); @@ -3435,6 +3532,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a OPCODE_TYPE_ADJUST(PACKED_VECTOR2_ARRAY, PackedVector2Array); OPCODE_TYPE_ADJUST(PACKED_VECTOR3_ARRAY, PackedVector3Array); OPCODE_TYPE_ADJUST(PACKED_COLOR_ARRAY, PackedColorArray); + OPCODE_TYPE_ADJUST(PACKED_VECTOR4_ARRAY, PackedVector4Array); OPCODE(OPCODE_ASSERT) { CHECK_SPACE(3); diff --git a/modules/gdscript/language_server/gdscript_extend_parser.cpp b/modules/gdscript/language_server/gdscript_extend_parser.cpp index ad7af34bf113..2a3db4f508d5 100644 --- a/modules/gdscript/language_server/gdscript_extend_parser.cpp +++ b/modules/gdscript/language_server/gdscript_extend_parser.cpp @@ -510,7 +510,7 @@ void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionN node_stack.push_back(p_func->body); while (!node_stack.is_empty()) { - GDScriptParser::Node *node = node_stack[0]; + GDScriptParser::Node *node = node_stack.front()->get(); node_stack.pop_front(); switch (node->type) { diff --git a/modules/gdscript/language_server/gdscript_language_server.h b/modules/gdscript/language_server/gdscript_language_server.h index 2ace5ca446fd..4ae5ab6cbf22 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 853a8e0f193b..819611099e96 100644 --- a/modules/gdscript/language_server/gdscript_workspace.cpp +++ b/modules/gdscript/language_server/gdscript_workspace.cpp @@ -223,7 +223,7 @@ void GDScriptWorkspace::reload_all_workspace_scripts() { HashMap::Iterator S = parse_results.find(path); String err_msg = "Failed parse script " + path; if (S) { - err_msg += "\n" + S->value->get_errors()[0].message; + err_msg += "\n" + S->value->get_errors().front()->get().message; } ERR_CONTINUE_MSG(err != OK, err_msg); } @@ -233,18 +233,25 @@ void GDScriptWorkspace::reload_all_workspace_scripts() { void GDScriptWorkspace::list_script_files(const String &p_root_dir, List &r_files) { Error err; Ref 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(); } } @@ -612,8 +619,8 @@ Node *GDScriptWorkspace::_get_owner_scene_node(String p_path) { _get_owners(EditorFileSystem::get_singleton()->get_filesystem(), p_path, owners); - for (int i = 0; i < owners.size(); i++) { - NodePath owner_path = owners[i]; + for (const String &owner : owners) { + NodePath owner_path = owner; Ref owner_res = ResourceLoader::load(owner_path); if (Object::cast_to(owner_res.ptr())) { Ref owner_packed_scene = Ref(Object::cast_to(*owner_res)); diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp index e3d16eaf4267..a949c44d787c 100644 --- a/modules/gdscript/tests/gdscript_test_runner.cpp +++ b/modules/gdscript/tests/gdscript_test_runner.cpp @@ -573,7 +573,7 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) { const List &errors = parser.get_errors(); if (!errors.is_empty()) { // Only the first error since the following might be cascading. - result.output += errors[0].message + "\n"; // TODO: line, column? + result.output += errors.front()->get().message + "\n"; // TODO: line, column? } if (!p_is_generating) { result.passed = check_output(result.output); @@ -592,7 +592,7 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) { const List &errors = parser.get_errors(); if (!errors.is_empty()) { // Only the first error since the following might be cascading. - result.output += errors[0].message + "\n"; // TODO: line, column? + result.output += errors.front()->get().message + "\n"; // TODO: line, column? } if (!p_is_generating) { result.passed = check_output(result.output); diff --git a/modules/gdscript/tests/gdscript_test_runner_suite.h b/modules/gdscript/tests/gdscript_test_runner_suite.h index b2289ef9cce9..d6befd2db3f5 100644 --- a/modules/gdscript/tests/gdscript_test_runner_suite.h +++ b/modules/gdscript/tests/gdscript_test_runner_suite.h @@ -81,11 +81,10 @@ TEST_CASE("[Modules][GDScript] Validate built-in API") { SUBCASE("[Modules][GDScript] Validate built-in methods") { for (const MethodInfo &mi : builtin_methods) { - for (int j = 0; j < mi.arguments.size(); j++) { - PropertyInfo arg = mi.arguments[j]; - - TEST_COND((arg.name.is_empty() || arg.name.begins_with("_unnamed_arg")), - vformat("Unnamed argument in position %d of built-in method '%s'.", j, mi.name)); + int i = 0; + for (List::ConstIterator itr = mi.arguments.begin(); itr != mi.arguments.end(); ++itr, ++i) { + TEST_COND((itr->name.is_empty() || itr->name.begins_with("_unnamed_arg")), + vformat("Unnamed argument in position %d of built-in method '%s'.", i, mi.name)); } } } @@ -96,11 +95,10 @@ TEST_CASE("[Modules][GDScript] Validate built-in API") { SUBCASE("[Modules][GDScript] Validate built-in annotations") { for (const MethodInfo &ai : builtin_annotations) { - for (int j = 0; j < ai.arguments.size(); j++) { - PropertyInfo arg = ai.arguments[j]; - - TEST_COND((arg.name.is_empty() || arg.name.begins_with("_unnamed_arg")), - vformat("Unnamed argument in position %d of built-in annotation '%s'.", j, ai.name)); + int i = 0; + for (List::ConstIterator itr = ai.arguments.begin(); itr != ai.arguments.end(); ++itr, ++i) { + TEST_COND((itr->name.is_empty() || itr->name.begins_with("_unnamed_arg")), + vformat("Unnamed argument in position %d of built-in annotation '%s'.", i, ai.name)); } } } diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static.gd new file mode 100644 index 000000000000..e041aeb914d9 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static.gd @@ -0,0 +1,10 @@ +# GH-91403 + +static func static_func(): + print(non_static_func) + +func non_static_func(): + pass + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static.out b/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static.out new file mode 100644 index 000000000000..d8d6c8bc1b77 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot access non-static function "non_static_func" from the static function "static_func()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static_in_lambda_param.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static_in_lambda_param.gd new file mode 100644 index 000000000000..36bc9dbf15a7 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static_in_lambda_param.gd @@ -0,0 +1,15 @@ +# GH-91403 + +func non_static_func(): + pass + +static func static_func( + f := func (): + var g := func (): + print(non_static_func) + g.call() +): + f.call() + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static_in_lambda_param.out b/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static_in_lambda_param.out new file mode 100644 index 000000000000..d8d6c8bc1b77 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_access_non_static_in_lambda_param.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot access non-static function "non_static_func" from the static function "static_func()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static.out b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static.out index b78f13134532..c094c08cd8a7 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot call non-static function "non_static_func()" from static function "static_func()". +Cannot call non-static function "non_static_func()" from the static function "static_func()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.out b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.out index b78f13134532..c094c08cd8a7 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot call non-static function "non_static_func()" from static function "static_func()". +Cannot call non-static function "non_static_func()" from the static function "static_func()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.out b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.out index b78f13134532..c094c08cd8a7 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot call non-static function "non_static_func()" from static function "static_func()". +Cannot call non-static function "non_static_func()" from the static function "static_func()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda.gd new file mode 100644 index 000000000000..7ae5bea7d772 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda.gd @@ -0,0 +1,14 @@ +# GH-91403 + +func non_static_func(): + pass + +static var static_var = func (): + var f := func (): + var g := func (): + print(non_static_func) + g.call() + f.call() + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda.out b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda.out new file mode 100644 index 000000000000..153e81b4053f --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot access non-static function "non_static_func" from a static variable initializer. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda_setter.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda_setter.gd new file mode 100644 index 000000000000..7479afc53272 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda_setter.gd @@ -0,0 +1,15 @@ +# GH-91403 + +func non_static_func(): + pass + +static var static_var: + set(_value): + var f := func (): + var g := func (): + print(non_static_func) + g.call() + f.call() + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda_setter.out b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda_setter.out new file mode 100644 index 000000000000..de43f2d3c44c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_access_non_static_in_lambda_setter.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot access non-static function "non_static_func" from the static function "@static_var_setter()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.out b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.out index cdf3ab2aebdc..a285b800252c 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot call non-static function "non_static_func()" from static function "@static_var_setter()". +Cannot call non-static function "non_static_func()" from the static function "@static_var_setter()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_access.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_access.gd new file mode 100644 index 000000000000..a21b2c4470b6 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_access.gd @@ -0,0 +1,11 @@ +# GH-91403 + +@static_unload + +func non_static(): + return "non static" + +static var static_var = Callable(non_static) + +func test(): + print("does not run") diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_access.out b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_access.out new file mode 100644 index 000000000000..a95069dc4f97 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_access.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot access non-static function "non_static" from a static variable initializer. diff --git a/modules/gdscript/tests/scripts/analyzer/features/boolean_operators_for_all_types.gd b/modules/gdscript/tests/scripts/analyzer/features/boolean_operators_for_all_types.gd index 73d0f9096c67..18675e5725f4 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/boolean_operators_for_all_types.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/boolean_operators_for_all_types.gd @@ -345,3 +345,12 @@ func test(): prints(x and true) prints(x or false) prints(x or true) + + # TYPE_PACKED_VECTOR4_ARRAY + x = PackedVector4Array([Vector4.ONE]) + prints("TYPE_PACKED_VECTOR4_ARRAY") + prints(not x) + prints(x and false) + prints(x and true) + prints(x or false) + prints(x or true) diff --git a/modules/gdscript/tests/scripts/analyzer/features/boolean_operators_for_all_types.out b/modules/gdscript/tests/scripts/analyzer/features/boolean_operators_for_all_types.out index e2945c910aed..47f9d7548bd6 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/boolean_operators_for_all_types.out +++ b/modules/gdscript/tests/scripts/analyzer/features/boolean_operators_for_all_types.out @@ -227,3 +227,9 @@ false true true true +TYPE_PACKED_VECTOR4_ARRAY +false +false +true +true +true diff --git a/modules/gdscript/tests/scripts/analyzer/features/static_non_static_access.gd b/modules/gdscript/tests/scripts/analyzer/features/static_non_static_access.gd new file mode 100644 index 000000000000..80ceb6d1a971 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/static_non_static_access.gd @@ -0,0 +1,75 @@ +@static_unload + +static var static_var +var non_static_var + +signal my_signal() + +static func static_func(): + pass + +func non_static_func(): + pass + +static var test_static_var_lambda = func (): + static_func() + print(static_func) + static_var = 1 + print(static_var) + +var test_non_static_var_lambda = func (): + static_func() + print(static_func) + static_var = 1 + print(static_var) + + non_static_func() + print(non_static_func) + non_static_var = 1 + print(non_static_var) + my_signal.emit() + print(my_signal) + +static var test_static_var_setter: + set(_value): + static_func() + print(static_func) + static_var = 1 + print(static_var) + +var test_non_static_var_setter: + set(_value): + static_func() + print(static_func) + static_var = 1 + print(static_var) + + non_static_func() + print(non_static_func) + non_static_var = 1 + print(non_static_var) + my_signal.emit() + print(my_signal) + +static func test_static_func(): + static_func() + print(static_func) + static_var = 1 + print(static_var) + +func test_non_static_func(): + static_func() + print(static_func) + static_var = 1 + print(static_var) + + non_static_func() + print(non_static_func) + non_static_var = 1 + print(non_static_var) + my_signal.emit() + print(my_signal) + +func test(): + test_static_var_lambda = null + test_non_static_var_lambda = null diff --git a/modules/gdscript/tests/scripts/analyzer/features/static_non_static_access.out b/modules/gdscript/tests/scripts/analyzer/features/static_non_static_access.out new file mode 100644 index 000000000000..d73c5eb7cde3 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/static_non_static_access.out @@ -0,0 +1 @@ +GDTEST_OK diff --git a/modules/gdscript/tests/scripts/parser/features/export_arrays.gd b/modules/gdscript/tests/scripts/parser/features/export_arrays.gd index ddfb186aa48c..0d97135a7b8f 100644 --- a/modules/gdscript/tests/scripts/parser/features/export_arrays.gd +++ b/modules/gdscript/tests/scripts/parser/features/export_arrays.gd @@ -63,6 +63,7 @@ var temp_packed_float64_array: PackedFloat64Array var temp_packed_color_array: PackedColorArray var temp_packed_vector2_array: PackedVector2Array var temp_packed_vector3_array: PackedVector3Array +var temp_packed_vector4_array: PackedVector4Array @export var test_weak_packed_byte_array = temp_packed_byte_array @export var test_weak_packed_int32_array = temp_packed_int32_array @@ -72,6 +73,7 @@ var temp_packed_vector3_array: PackedVector3Array @export var test_weak_packed_color_array = temp_packed_color_array @export var test_weak_packed_vector2_array = temp_packed_vector2_array @export var test_weak_packed_vector3_array = temp_packed_vector3_array +@export var test_weak_packed_vector4_array = temp_packed_vector4_array @export_range(1, 10) var test_range_weak_packed_byte_array = temp_packed_byte_array @export_range(1, 10) var test_range_weak_packed_int32_array = temp_packed_int32_array diff --git a/modules/gdscript/tests/scripts/parser/features/export_arrays.out b/modules/gdscript/tests/scripts/parser/features/export_arrays.out index 00e75fcc4364..acbf389645e8 100644 --- a/modules/gdscript/tests/scripts/parser/features/export_arrays.out +++ b/modules/gdscript/tests/scripts/parser/features/export_arrays.out @@ -123,6 +123,8 @@ var test_weak_packed_vector2_array: PackedVector2Array hint=TYPE_STRING hint_string="Vector2:Vector2" usage=DEFAULT|SCRIPT_VARIABLE var test_weak_packed_vector3_array: PackedVector3Array hint=TYPE_STRING hint_string="Vector3:Vector3" usage=DEFAULT|SCRIPT_VARIABLE +var test_weak_packed_vector4_array: PackedVector4Array + hint=TYPE_STRING hint_string="Vector4:Vector4" usage=DEFAULT|SCRIPT_VARIABLE var test_range_weak_packed_byte_array: PackedByteArray hint=TYPE_STRING hint_string="int/RANGE:1,10" usage=DEFAULT|SCRIPT_VARIABLE var test_range_weak_packed_int32_array: PackedInt32Array 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 c524a1ae6b9b..350d5d1d4585 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 cf51b0262d06..5f1f372b0a39 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 000000000000..2f31ecc52f04 --- /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 000000000000..f7d531e1192b --- /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/argument_count.gd b/modules/gdscript/tests/scripts/runtime/features/argument_count.gd index c67ce25cbe87..104489cfe606 100644 --- a/modules/gdscript/tests/scripts/runtime/features/argument_count.gd +++ b/modules/gdscript/tests/scripts/runtime/features/argument_count.gd @@ -57,7 +57,7 @@ func test(): var lambda_callable_2 : Callable = func(_foo, _bar, _baz): pass print(lambda_callable_2.get_argument_count()) # Should print 3. - # Test lambas with self. + # Test lambdas with self. var lambda_self_callable_1 : Callable = func(_foo, _bar): return self print(lambda_self_callable_1.get_argument_count()) # Should print 2. var lambda_self_callable_2 : Callable = func(_foo, _bar, _baz): return self 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 000000000000..35e4dbd6a08c --- /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 000000000000..44302c81378b --- /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/compare_builtin_equals_null.gd b/modules/gdscript/tests/scripts/runtime/features/compare_builtin_equals_null.gd index 809d0d28a9fd..5d8dafc4a1fd 100644 --- a/modules/gdscript/tests/scripts/runtime/features/compare_builtin_equals_null.gd +++ b/modules/gdscript/tests/scripts/runtime/features/compare_builtin_equals_null.gd @@ -140,3 +140,7 @@ func test(): # PackedColorArray value = PackedColorArray() print(value == null) + + # PackedVector4Array + value = PackedVector4Array() + print(value == null) diff --git a/modules/gdscript/tests/scripts/runtime/features/compare_builtin_equals_null.out b/modules/gdscript/tests/scripts/runtime/features/compare_builtin_equals_null.out index 27423ab8e7a8..e0e222eccc99 100644 --- a/modules/gdscript/tests/scripts/runtime/features/compare_builtin_equals_null.out +++ b/modules/gdscript/tests/scripts/runtime/features/compare_builtin_equals_null.out @@ -34,3 +34,4 @@ false false false false +false diff --git a/modules/gdscript/tests/scripts/runtime/features/compare_builtin_not_equals_null.gd b/modules/gdscript/tests/scripts/runtime/features/compare_builtin_not_equals_null.gd index f46afb0f18c2..88286ede0338 100644 --- a/modules/gdscript/tests/scripts/runtime/features/compare_builtin_not_equals_null.gd +++ b/modules/gdscript/tests/scripts/runtime/features/compare_builtin_not_equals_null.gd @@ -140,3 +140,7 @@ func test(): # PackedColorArray value = PackedColorArray() print(value != null) + + # PackedVector4Array + value = PackedVector4Array() + print(value != null) diff --git a/modules/gdscript/tests/scripts/runtime/features/compare_builtin_not_equals_null.out b/modules/gdscript/tests/scripts/runtime/features/compare_builtin_not_equals_null.out index a11c47854a17..f6e72aedd56f 100644 --- a/modules/gdscript/tests/scripts/runtime/features/compare_builtin_not_equals_null.out +++ b/modules/gdscript/tests/scripts/runtime/features/compare_builtin_not_equals_null.out @@ -34,3 +34,4 @@ true true true true +true diff --git a/modules/gdscript/tests/scripts/runtime/features/compare_null_equals_builtin.gd b/modules/gdscript/tests/scripts/runtime/features/compare_null_equals_builtin.gd index 7649062fdaa8..6ca1b3e490ae 100644 --- a/modules/gdscript/tests/scripts/runtime/features/compare_null_equals_builtin.gd +++ b/modules/gdscript/tests/scripts/runtime/features/compare_null_equals_builtin.gd @@ -136,3 +136,7 @@ func test(): # PackedColorArray value = PackedColorArray() print(null == value) + + # PackedVector4Array + value = PackedVector4Array() + print(null == value) diff --git a/modules/gdscript/tests/scripts/runtime/features/compare_null_equals_builtin.out b/modules/gdscript/tests/scripts/runtime/features/compare_null_equals_builtin.out index 639f6027b96a..27423ab8e7a8 100644 --- a/modules/gdscript/tests/scripts/runtime/features/compare_null_equals_builtin.out +++ b/modules/gdscript/tests/scripts/runtime/features/compare_null_equals_builtin.out @@ -33,3 +33,4 @@ false false false false +false diff --git a/modules/gdscript/tests/scripts/runtime/features/compare_null_not_equals_builtin.gd b/modules/gdscript/tests/scripts/runtime/features/compare_null_not_equals_builtin.gd index 8d5f9df1b82c..d7addfa3909c 100644 --- a/modules/gdscript/tests/scripts/runtime/features/compare_null_not_equals_builtin.gd +++ b/modules/gdscript/tests/scripts/runtime/features/compare_null_not_equals_builtin.gd @@ -136,3 +136,7 @@ func test(): # PackedColorArray value = PackedColorArray() print(null != value) + + # PackedVector4Array + value = PackedVector4Array() + print(null != value) diff --git a/modules/gdscript/tests/scripts/runtime/features/compare_null_not_equals_builtin.out b/modules/gdscript/tests/scripts/runtime/features/compare_null_not_equals_builtin.out index d1e332afbaae..a11c47854a17 100644 --- a/modules/gdscript/tests/scripts/runtime/features/compare_null_not_equals_builtin.out +++ b/modules/gdscript/tests/scripts/runtime/features/compare_null_not_equals_builtin.out @@ -33,3 +33,4 @@ true true true true +true diff --git a/modules/gdscript/tests/scripts/runtime/features/onready_base_before_subclass.gd b/modules/gdscript/tests/scripts/runtime/features/onready_base_before_subclass.gd new file mode 100644 index 000000000000..99156adb2865 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/onready_base_before_subclass.gd @@ -0,0 +1,18 @@ +#GH-63329 +class A extends Node: + @onready var a := get_value("a") + + func get_value(var_name: String) -> String: + print(var_name) + return var_name + +class B extends A: + @onready var b := get_value("b") + + func _ready(): + pass + +func test(): + var node := B.new() + node._ready() + node.free() diff --git a/modules/gdscript/tests/scripts/runtime/features/onready_base_before_subclass.out b/modules/gdscript/tests/scripts/runtime/features/onready_base_before_subclass.out new file mode 100644 index 000000000000..b417ce67caec --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/onready_base_before_subclass.out @@ -0,0 +1,3 @@ +GDTEST_OK +a +b diff --git a/modules/gdscript/tests/scripts/runtime/features/stringify.gd b/modules/gdscript/tests/scripts/runtime/features/stringify.gd index 0dbb252b0e3f..8579baf8766d 100644 --- a/modules/gdscript/tests/scripts/runtime/features/stringify.gd +++ b/modules/gdscript/tests/scripts/runtime/features/stringify.gd @@ -40,3 +40,4 @@ func test(): print(PackedVector2Array([Vector2.ONE, Vector2.ZERO])) print(PackedVector3Array([Vector3.ONE, Vector3.ZERO])) print(PackedColorArray([Color.RED, Color.BLUE, Color.GREEN])) + print(PackedVector4Array([Vector4.ONE, Vector4.ZERO])) diff --git a/modules/gdscript/tests/scripts/runtime/features/stringify.out b/modules/gdscript/tests/scripts/runtime/features/stringify.out index 1f33de00ccee..7833b6e213a8 100644 --- a/modules/gdscript/tests/scripts/runtime/features/stringify.out +++ b/modules/gdscript/tests/scripts/runtime/features/stringify.out @@ -32,3 +32,4 @@ Node::[signal]property_list_changed [(1, 1), (0, 0)] [(1, 1, 1), (0, 0, 0)] [(1, 0, 0, 1), (0, 0, 1, 1), (0, 1, 0, 1)] +[(1, 1, 1, 1), (0, 0, 0, 0)] diff --git a/modules/gdscript/tests/test_gdscript.cpp b/modules/gdscript/tests/test_gdscript.cpp index f6965cf7cfbe..fbc72a050899 100644 --- a/modules/gdscript/tests/test_gdscript.cpp +++ b/modules/gdscript/tests/test_gdscript.cpp @@ -188,11 +188,11 @@ static void recursively_disassemble_functions(const Ref script, const const MethodInfo &mi = func->get_method_info(); String signature = "Disassembling " + mi.name + "("; - for (int i = 0; i < mi.arguments.size(); i++) { - if (i > 0) { + for (List::ConstIterator arg_itr = mi.arguments.begin(); arg_itr != mi.arguments.end(); ++arg_itr) { + if (arg_itr != mi.arguments.begin()) { signature += ", "; } - signature += mi.arguments[i].name; + signature += arg_itr->name; } print_line(signature + ")"); #ifdef TOOLS_ENABLED diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h index 683ff6d4f66b..6bc61605716c 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/gridmap/editor/grid_map_editor_plugin.h b/modules/gridmap/editor/grid_map_editor_plugin.h index 924e21aef564..cfa0f0c35ce2 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 730d1ca83bc4..3c50b0d5cce9 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/doc_classes/MobileVRInterface.xml b/modules/mobile_vr/doc_classes/MobileVRInterface.xml index 1be8cc828dad..0dbe06d22077 100644 --- a/modules/mobile_vr/doc_classes/MobileVRInterface.xml +++ b/modules/mobile_vr/doc_classes/MobileVRInterface.xml @@ -34,9 +34,20 @@ The k2 lens factor, see k1. + + Set the offset rect relative to the area being rendered. A length of 1 represents the whole rendering area on that axis. + The oversample setting. Because of the lens distortion we have to render our buffers at a higher resolution then the screen can natively handle. A value between 1.5 and 2.0 often provides good results but at the cost of performance. + + The minimum radius around the focal point where full quality is guaranteed if VRS is used as a percentage of screen size. + [b]Note:[/b] Mobile and Forward+ renderers only. Requires [member Viewport.vrs_mode] to be set to [constant Viewport.VRS_XR]. + + + The strength used to calculate the VRS density map. The greater this value, the more noticeable VRS is. This improves performance at the cost of quality. + [b]Note:[/b] Mobile and Forward+ renderers only. Requires [member Viewport.vrs_mode] to be set to [constant Viewport.VRS_XR]. + diff --git a/modules/mobile_vr/mobile_vr_interface.cpp b/modules/mobile_vr/mobile_vr_interface.cpp index 94a3f0777ea1..f27281866a58 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; @@ -230,6 +230,9 @@ void MobileVRInterface::_bind_methods() { ClassDB::bind_method(D_METHOD("set_display_to_lens", "display_to_lens"), &MobileVRInterface::set_display_to_lens); ClassDB::bind_method(D_METHOD("get_display_to_lens"), &MobileVRInterface::get_display_to_lens); + ClassDB::bind_method(D_METHOD("set_offset_rect", "offset_rect"), &MobileVRInterface::set_offset_rect); + ClassDB::bind_method(D_METHOD("get_offset_rect"), &MobileVRInterface::get_offset_rect); + ClassDB::bind_method(D_METHOD("set_oversample", "oversample"), &MobileVRInterface::set_oversample); ClassDB::bind_method(D_METHOD("get_oversample"), &MobileVRInterface::get_oversample); @@ -243,9 +246,20 @@ void MobileVRInterface::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "iod", PROPERTY_HINT_RANGE, "4.0,10.0,0.1"), "set_iod", "get_iod"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "display_width", PROPERTY_HINT_RANGE, "5.0,25.0,0.1"), "set_display_width", "get_display_width"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "display_to_lens", PROPERTY_HINT_RANGE, "5.0,25.0,0.1"), "set_display_to_lens", "get_display_to_lens"); + ADD_PROPERTY(PropertyInfo(Variant::RECT2, "offset_rect"), "set_offset_rect", "get_offset_rect"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "oversample", PROPERTY_HINT_RANGE, "1.0,2.0,0.1"), "set_oversample", "get_oversample"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "k1", PROPERTY_HINT_RANGE, "0.1,10.0,0.0001"), "set_k1", "get_k1"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "k2", PROPERTY_HINT_RANGE, "0.1,10.0,0.0001"), "set_k2", "get_k2"); + + ClassDB::bind_method(D_METHOD("get_vrs_min_radius"), &MobileVRInterface::get_vrs_min_radius); + ClassDB::bind_method(D_METHOD("set_vrs_min_radius", "radius"), &MobileVRInterface::set_vrs_min_radius); + + ClassDB::bind_method(D_METHOD("get_vrs_strength"), &MobileVRInterface::get_vrs_strength); + ClassDB::bind_method(D_METHOD("set_vrs_strength", "strength"), &MobileVRInterface::set_vrs_strength); + + ADD_GROUP("Vulkan VRS", "vrs_"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "vrs_min_radius", PROPERTY_HINT_RANGE, "1.0,100.0,1.0"), "set_vrs_min_radius", "get_vrs_min_radius"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "vrs_strength", PROPERTY_HINT_RANGE, "0.1,10.0,0.1"), "set_vrs_strength", "get_vrs_strength"); } void MobileVRInterface::set_eye_height(const double p_eye_height) { @@ -256,6 +270,14 @@ double MobileVRInterface::get_eye_height() const { return eye_height; } +void MobileVRInterface::set_offset_rect(const Rect2 &p_offset_rect) { + offset_rect = p_offset_rect; +} + +Rect2 MobileVRInterface::get_offset_rect() const { + return offset_rect; +} + void MobileVRInterface::set_iod(const double p_iod) { intraocular_dist = p_iod; }; @@ -304,6 +326,22 @@ double MobileVRInterface::get_k2() const { return k2; }; +float MobileVRInterface::get_vrs_min_radius() const { + return xr_vrs.get_vrs_min_radius(); +} + +void MobileVRInterface::set_vrs_min_radius(float p_vrs_min_radius) { + xr_vrs.set_vrs_min_radius(p_vrs_min_radius); +} + +float MobileVRInterface::get_vrs_strength() const { + return xr_vrs.get_vrs_strength(); +} + +void MobileVRInterface::set_vrs_strength(float p_vrs_strength) { + xr_vrs.set_vrs_strength(p_vrs_strength); +} + uint32_t MobileVRInterface::get_view_count() { // needs stereo... return 2; @@ -477,11 +515,18 @@ Vector MobileVRInterface::post_draw_viewport(RID p_render_target, Vector blit_to_screen; - // We must have a valid render target + // We must have a valid render target. ERR_FAIL_COND_V(!p_render_target.is_valid(), blit_to_screen); - // Because we are rendering to our device we must use our main viewport! - ERR_FAIL_COND_V(p_screen_rect == Rect2(), blit_to_screen); + // We will only output to screen if this is our main viewport. + if (p_screen_rect == Rect2()) { + // Warn the developer once, it's up to the developer to output to screen. + WARN_PRINT_ONCE("SubViewport used with MobileVRInterface, no output to screen"); + + return blit_to_screen; + } + + Rect2 modified_screen_rect = Rect2(p_screen_rect.position + offset_rect.position * p_screen_rect.size, p_screen_rect.size * offset_rect.size); // and add our blits BlitToScreen blit; @@ -494,16 +539,16 @@ Vector MobileVRInterface::post_draw_viewport(RID p_render_target, blit.lens_distortion.aspect_ratio = aspect; // left eye - blit.dst_rect = p_screen_rect; + blit.dst_rect = modified_screen_rect; blit.dst_rect.size.width *= 0.5; blit.multi_view.layer = 0; blit.lens_distortion.eye_center.x = ((-intraocular_dist / 2.0) + (display_width / 4.0)) / (display_width / 2.0); blit_to_screen.push_back(blit); // right eye - blit.dst_rect = p_screen_rect; + blit.dst_rect = modified_screen_rect; blit.dst_rect.size.width *= 0.5; - blit.dst_rect.position.x = blit.dst_rect.size.width; + blit.dst_rect.position.x += blit.dst_rect.size.width; blit.multi_view.layer = 1; blit.lens_distortion.eye_center.x = ((intraocular_dist / 2.0) - (display_width / 4.0)) / (display_width / 2.0); blit_to_screen.push_back(blit); @@ -528,6 +573,23 @@ void MobileVRInterface::process() { }; }; +RID MobileVRInterface::get_vrs_texture() { + PackedVector2Array eye_foci; + + Size2 target_size = get_render_target_size(); + real_t aspect_ratio = target_size.x / target_size.y; + uint32_t view_count = get_view_count(); + + for (uint32_t v = 0; v < view_count; v++) { + Projection cm = get_projection_for_view(v, aspect_ratio, 0.1, 1000.0); + Vector3 center = cm.xform(Vector3(0.0, 0.0, 999.0)); + + eye_foci.push_back(Vector2(center.x, center.y)); + } + + return xr_vrs.make_vrs_texture(target_size, eye_foci); +} + MobileVRInterface::MobileVRInterface() {} MobileVRInterface::~MobileVRInterface() { diff --git a/modules/mobile_vr/mobile_vr_interface.h b/modules/mobile_vr/mobile_vr_interface.h index f680d8aa1156..490b1c393ce2 100644 --- a/modules/mobile_vr/mobile_vr_interface.h +++ b/modules/mobile_vr/mobile_vr_interface.h @@ -33,6 +33,7 @@ #include "servers/xr/xr_interface.h" #include "servers/xr/xr_positional_tracker.h" +#include "servers/xr/xr_vrs.h" /** The mobile interface is a native VR interface that can be used on Android and iOS phones. @@ -62,6 +63,8 @@ class MobileVRInterface : public XRInterface { double display_to_lens = 4.0; double oversample = 1.5; + Rect2 offset_rect = Rect2(0, 0, 1, 1); // Full screen rect. + double k1 = 0.215; double k2 = 0.215; double aspect = 1.0; @@ -70,6 +73,8 @@ class MobileVRInterface : public XRInterface { Ref head; Transform3D head_transform; + XRVRS xr_vrs; + /* logic for processing our sensor data, this was originally in our positional tracker logic but I think that doesn't make sense in hindsight. It only makes marginally more sense to park it here for now, @@ -121,6 +126,9 @@ class MobileVRInterface : public XRInterface { void set_display_width(const double p_display_width); double get_display_width() const; + void set_offset_rect(const Rect2 &p_offset_rect); + Rect2 get_offset_rect() const; + void set_display_to_lens(const double p_display_to_lens); double get_display_to_lens() const; @@ -133,6 +141,12 @@ class MobileVRInterface : public XRInterface { void set_k2(const double p_k2); double get_k2() const; + float get_vrs_min_radius() const; + void set_vrs_min_radius(float p_vrs_min_radius); + + float get_vrs_strength() const; + void set_vrs_strength(float p_vrs_strength); + virtual StringName get_name() const override; virtual uint32_t get_capabilities() const override; @@ -156,6 +170,8 @@ class MobileVRInterface : public XRInterface { virtual void process() override; + virtual RID get_vrs_texture() override; + MobileVRInterface(); ~MobileVRInterface(); }; diff --git a/modules/mono/.editorconfig b/modules/mono/.editorconfig index 1e5ae2839662..fcd10461ad4b 100644 --- a/modules/mono/.editorconfig +++ b/modules/mono/.editorconfig @@ -1,3 +1,7 @@ +[*.{sln,csproj}] +end_of_line = crlf +charset = utf-8-bom + [*.sln] indent_style = tab diff --git a/modules/mono/class_db_api_json.cpp b/modules/mono/class_db_api_json.cpp index c4aba577db6a..04af60e22ffa 100644 --- a/modules/mono/class_db_api_json.cpp +++ b/modules/mono/class_db_api_json.cpp @@ -166,10 +166,10 @@ void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) { Array arguments; signal_dict["arguments"] = arguments; - for (int i = 0; i < mi.arguments.size(); i++) { + for (const PropertyInfo &pi : mi.arguments) { Dictionary argument_dict; arguments.push_back(argument_dict); - argument_dict["type"] = mi.arguments[i].type; + argument_dict["type"] = pi.type; } } diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index e3f39c50f451..17df3988eea7 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.sln b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln index 9674626183ad..2c7864549336 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln @@ -1,5 +1,8 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "Godot.SourceGenerators\Godot.SourceGenerators.csproj", "{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}" @@ -37,4 +40,10 @@ Global {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {49831022-16BD-41E0-A5F3-EDE1279F4176} + EndGlobalSection EndGlobal 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 b396a5b0c7e3..74623a60ba6e 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 @@ -1,4 +1,4 @@ - + netstandard2.0 false diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj index d0907c1cd4b0..fc887313d5e8 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj @@ -1,4 +1,4 @@ - + net6.0 diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj index e5a81c0e1c15..31a255dcdfcd 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj @@ -1,4 +1,4 @@ - + net6.0 diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs index 3cc58410975f..724fb164e022 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs @@ -57,4 +57,13 @@ await CSharpSourceGeneratorVerifier.Verify( "ScriptBoilerplate_ScriptProperties.generated.cs", "OuterClass.NestedClass_ScriptProperties.generated.cs" ); } + + [Fact] + public async void AbstractGenericNode() + { + await CSharpSourceGeneratorVerifier.Verify( + "AbstractGenericNode.cs", + "AbstractGenericNode(Of T)_ScriptProperties.generated.cs" + ); + } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode(Of T)_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode(Of T)_ScriptProperties.generated.cs new file mode 100644 index 000000000000..a561c5fc0db6 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode(Of T)_ScriptProperties.generated.cs @@ -0,0 +1,49 @@ +using Godot; +using Godot.NativeInterop; + +partial class AbstractGenericNode +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// + /// Cached StringNames for the properties and fields contained in this class, for fast lookup. + /// + public new class PropertyName : global::Godot.Node.PropertyName { + /// + /// Cached name for the 'MyArray' property. + /// + public new static readonly global::Godot.StringName MyArray = "MyArray"; + } + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value) + { + if (name == PropertyName.MyArray) { + this.MyArray = global::Godot.NativeInterop.VariantUtils.ConvertToArray(value); + return true; + } + return base.SetGodotClassPropertyValue(name, value); + } + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value) + { + if (name == PropertyName.MyArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFromArray(this.MyArray); + return true; + } + return base.GetGodotClassPropertyValue(name, out value); + } + /// + /// Get the property information for all the properties declared in this class. + /// This method is used by Godot to register the available properties in the editor. + /// Do not call this method. + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List GetGodotPropertyList() + { + var properties = new global::System.Collections.Generic.List(); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.MyArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + return properties; + } +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AbstractGenericNode.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AbstractGenericNode.cs new file mode 100644 index 000000000000..cee4f6792147 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AbstractGenericNode.cs @@ -0,0 +1,7 @@ +using Godot; + +public abstract partial class AbstractGenericNode<[MustBeVariant] T> : Node +{ + [Export] // This should be included, but without type hints. + public Godot.Collections.Array MyArray { get; set; } = new(); +} 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 462da31d6680..2b5eecab8a06 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 void MethodCallsOk() Method(); } + public void MethodCallDynamic() + { + dynamic self = this; + self.Method(); + } + 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 fbabed50d0a6..1aa2979e76a5 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 @@ -1,4 +1,4 @@ - + netstandard2.0 10 diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs index 834beaa1311c..4cf6a9f431e5 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs @@ -44,7 +44,8 @@ internal enum VariantType PackedVector2Array = 35, PackedVector3Array = 36, PackedColorArray = 37, - Max = 38 + PackedVector4Array = 38, + Max = 39 } internal enum PropertyHint diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs index be6af117ebaf..bfb735e72ff2 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs @@ -51,6 +51,7 @@ public enum MarshalType StringArray, Vector2Array, Vector3Array, + Vector4Array, ColorArray, GodotObjectOrDerivedArray, SystemArrayOfStringName, diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs index 0258f53062fa..d27283295052 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs @@ -66,6 +66,7 @@ INamedTypeSymbol GetTypeByMetadataNameOrThrow(string fullyQualifiedMetadataName) MarshalType.StringArray => VariantType.PackedStringArray, MarshalType.Vector2Array => VariantType.PackedVector2Array, MarshalType.Vector3Array => VariantType.PackedVector3Array, + MarshalType.Vector4Array => VariantType.PackedVector4Array, MarshalType.ColorArray => VariantType.PackedColorArray, MarshalType.GodotObjectOrDerivedArray => VariantType.Array, MarshalType.SystemArrayOfStringName => VariantType.Array, @@ -190,6 +191,8 @@ INamedTypeSymbol GetTypeByMetadataNameOrThrow(string fullyQualifiedMetadataName) return MarshalType.Vector2Array; case { Name: "Vector3" }: return MarshalType.Vector3Array; + case { Name: "Vector4" }: + return MarshalType.Vector4Array; case { Name: "Color" }: return MarshalType.ColorArray; case { Name: "StringName" }: 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 95eaca4d3d3f..e894e7a86cf4 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 @@ private void AnalyzeNode(SyntaxNodeAnalysisContext context) 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/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs index a0e410e31ab5..21223654f366 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -658,7 +658,10 @@ static bool GetStringArrayEnumHint(VariantType elementVariantType, var elementType = MarshalUtils.GetArrayElementType(type); if (elementType == null) - return false; // Non-generic Array, so there's no hint to add + return false; // Non-generic Array, so there's no hint to add. + + if (elementType.TypeKind == TypeKind.TypeParameter) + return false; // The generic is not constructed, we can't really hint anything. var elementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementType, typeCache)!.Value; var elementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(elementMarshalType)!.Value; diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj index fd836f9ad243..f807132509e9 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj @@ -1,4 +1,4 @@ - + {6CE9A984-37B1-4F8A-8FE9-609F05F071B3} net6.0 diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj index 075548446585..f692f26a8b00 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj @@ -1,4 +1,4 @@ - + {639E48BD-44E5-4091-8EDD-22D36DC0768D} net6.0 diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj index 3bf678e9f9e0..0174b25b3ffa 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj @@ -1,4 +1,4 @@ - + {B06C2951-C8E3-4F28-80B2-717CF327EB19} Exe diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj index f68122889205..2ee28715bfdc 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj @@ -1,4 +1,4 @@ - + {92600954-25F0-4291-8E11-1FEE9FC4BE20} netstandard2.0 diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj index 09908c85a17f..7360118ee4a5 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj @@ -1,4 +1,4 @@ - + {EAFFF236-FA96-4A4D-BD23-0E51EF988277} Exe diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index 623475e11a3d..5547ffcf192d 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -1,4 +1,4 @@ - + {A8CDAD94-C6D4-4B19-A7E7-76C53CC92984} net6.0 diff --git a/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj b/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj index d60e6343ea7b..7c19fabaf6eb 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj @@ -1,8 +1,8 @@ - - - net6.0 - - false - - + + + net6.0 + + false + + diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index 564775635d82..1182e2467bc7 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -1,6 +1,8 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.ProjectEditor", "GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj", "{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools", "GodotTools\GodotTools.csproj", "{27B00618-A6F2-4828-B922-05CAEB08C286}" @@ -62,4 +64,10 @@ Global {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.Release|Any CPU.ActiveCfg = Release|Any CPU {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {521EC35A-F7F0-46A9-92CE-680D2F5B02B8} + EndGlobalSection EndGlobal diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 35b3f5a7105b..96d2c5c75f22 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -1,4 +1,4 @@ - + {27B00618-A6F2-4828-B922-05CAEB08C286} net6.0 diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index e1ce41edd5e2..eb75f05a2353 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -330,6 +330,8 @@ String BindingsGenerator::bbcode_to_text(const String &p_bbcode, const TypeInter output.append("'" BINDINGS_NAMESPACE ".Vector3[]'"); } else if (tag == "PackedColorArray") { output.append("'" BINDINGS_NAMESPACE ".Color[]'"); + } else if (tag == "PackedVector4Array") { + output.append("'" BINDINGS_NAMESPACE ".Vector4[]'"); } else { const TypeInterface *target_itype = _get_type_or_null(TypeReference(tag)); @@ -646,6 +648,8 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf xml_output.append("[]"); } else if (tag == "PackedColorArray") { xml_output.append("[]"); + } else if (tag == "PackedVector4Array") { + xml_output.append("[]"); } else { const TypeInterface *target_itype = _get_type_or_null(TypeReference(tag)); @@ -2304,8 +2308,9 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output << imethod.proxy_name << "("; - for (int i = 0; i < imethod.arguments.size(); i++) { - const ArgumentInterface &iarg = imethod.arguments[i]; + int i = 0; + for (List::ConstIterator itr = imethod.arguments.begin(); itr != imethod.arguments.end(); ++itr, ++i) { + const ArgumentInterface &iarg = *itr; const TypeInterface *arg_type = _get_type_or_null(iarg.type); ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Argument type not found @@ -3516,6 +3521,7 @@ bool BindingsGenerator::_arg_default_value_is_assignable_to_type(const Variant & case Variant::PACKED_STRING_ARRAY: case Variant::PACKED_VECTOR2_ARRAY: case Variant::PACKED_VECTOR3_ARRAY: + case Variant::PACKED_VECTOR4_ARRAY: case Variant::PACKED_COLOR_ARRAY: case Variant::CALLABLE: case Variant::SIGNAL: @@ -3722,8 +3728,6 @@ bool BindingsGenerator::_populate_object_type_interfaces() { const MethodInfo &method_info = E.first; const uint32_t hash = E.second; - int argc = method_info.arguments.size(); - if (method_info.name.is_empty()) { continue; } @@ -3815,8 +3819,9 @@ bool BindingsGenerator::_populate_object_type_interfaces() { imethod.return_type.cname = _get_type_name_from_meta(return_info.type, m ? m->get_argument_meta(-1) : (GodotTypeInfo::Metadata)method_info.return_val_metadata); } - for (int i = 0; i < argc; i++) { - PropertyInfo arginfo = method_info.arguments[i]; + int idx = 0; + for (List::ConstIterator itr = method_info.arguments.begin(); itr != method_info.arguments.end(); ++itr, ++idx) { + const PropertyInfo &arginfo = *itr; String orig_arg_name = arginfo.name; @@ -3836,13 +3841,13 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } else if (arginfo.type == Variant::NIL) { iarg.type.cname = name_cache.type_Variant; } else { - iarg.type.cname = _get_type_name_from_meta(arginfo.type, m ? m->get_argument_meta(i) : (GodotTypeInfo::Metadata)method_info.get_argument_meta(i)); + iarg.type.cname = _get_type_name_from_meta(arginfo.type, m ? m->get_argument_meta(idx) : (GodotTypeInfo::Metadata)method_info.get_argument_meta(idx)); } iarg.name = escape_csharp_keyword(snake_to_camel_case(iarg.name)); - if (m && m->has_default_argument(i)) { - bool defval_ok = _arg_default_value_from_variant(m->get_default_argument(i), iarg); + if (m && m->has_default_argument(idx)) { + bool defval_ok = _arg_default_value_from_variant(m->get_default_argument(idx), iarg); ERR_FAIL_COND_V_MSG(!defval_ok, false, "Cannot determine default value for argument '" + orig_arg_name + "' of method '" + itype.name + "." + imethod.name + "'."); } @@ -3941,10 +3946,9 @@ bool BindingsGenerator::_populate_object_type_interfaces() { isignal.name = method_info.name; isignal.cname = method_info.name; - int argc = method_info.arguments.size(); - - for (int i = 0; i < argc; i++) { - PropertyInfo arginfo = method_info.arguments[i]; + int idx = 0; + for (List::ConstIterator itr = method_info.arguments.begin(); itr != method_info.arguments.end(); ++itr, ++idx) { + const PropertyInfo &arginfo = *itr; String orig_arg_name = arginfo.name; @@ -3964,7 +3968,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } else if (arginfo.type == Variant::NIL) { iarg.type.cname = name_cache.type_Variant; } else { - iarg.type.cname = _get_type_name_from_meta(arginfo.type, (GodotTypeInfo::Metadata)method_info.get_argument_meta(i)); + iarg.type.cname = _get_type_name_from_meta(arginfo.type, (GodotTypeInfo::Metadata)method_info.get_argument_meta(idx)); } iarg.name = escape_csharp_keyword(snake_to_camel_case(iarg.name)); @@ -4246,6 +4250,7 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar case Variant::PACKED_STRING_ARRAY: case Variant::PACKED_VECTOR2_ARRAY: case Variant::PACKED_VECTOR3_ARRAY: + case Variant::PACKED_VECTOR4_ARRAY: case Variant::PACKED_COLOR_ARRAY: r_iarg.default_argument = "Array.Empty<%s>()"; r_iarg.def_param_mode = ArgumentInterface::NULLABLE_REF; @@ -4585,6 +4590,7 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { INSERT_ARRAY(PackedColorArray, godot_packed_color_array, Color); INSERT_ARRAY(PackedVector2Array, godot_packed_vector2_array, Vector2); INSERT_ARRAY(PackedVector3Array, godot_packed_vector3_array, Vector3); + INSERT_ARRAY(PackedVector4Array, godot_packed_vector4_array, Vector4); #undef INSERT_ARRAY @@ -4763,9 +4769,11 @@ bool BindingsGenerator::_method_has_conflicting_signature(const MethodInterface return false; } - for (int i = 0; i < p_imethod_left.arguments.size(); i++) { - const ArgumentInterface &iarg_left = p_imethod_left.arguments[i]; - const ArgumentInterface &iarg_right = p_imethod_right.arguments[i]; + List::ConstIterator left_itr = p_imethod_left.arguments.begin(); + List::ConstIterator right_itr = p_imethod_right.arguments.begin(); + for (; left_itr != p_imethod_left.arguments.end(); ++left_itr, ++right_itr) { + const ArgumentInterface &iarg_left = *left_itr; + const ArgumentInterface &iarg_right = *right_itr; if (iarg_left.type.cname != iarg_right.type.cname) { // Different types for arguments in the same position, so no conflict. @@ -4852,7 +4860,7 @@ static void handle_cmdline_options(String glue_dir_path) { } static void cleanup_and_exit_godot() { - // Exit once done + // Exit once done. Main::cleanup(true); ::exit(0); } @@ -4871,7 +4879,7 @@ void BindingsGenerator::handle_cmdline_args(const List &p_cmdline_args) elem = elem->next(); } else { ERR_PRINT(generate_all_glue_option + ": No output directory specified (expected path to '{GODOT_ROOT}/modules/mono/glue')."); - // Exit once done with invalid command line arguments + // Exit once done with invalid command line arguments. cleanup_and_exit_godot(); } @@ -4882,8 +4890,14 @@ void BindingsGenerator::handle_cmdline_args(const List &p_cmdline_args) } if (glue_dir_path.length()) { - handle_cmdline_options(glue_dir_path); - // Exit once done + if (Engine::get_singleton()->is_editor_hint() || + Engine::get_singleton()->is_project_manager_hint()) { + handle_cmdline_options(glue_dir_path); + } else { + // Running from a project folder, which doesn't make sense and crashes. + ERR_PRINT(generate_all_glue_option + ": Cannot generate Mono glue while running a game project. Change current directory or enable --editor."); + } + // Exit once done. cleanup_and_exit_godot(); } } diff --git a/modules/mono/editor/bindings_generator.h b/modules/mono/editor/bindings_generator.h index a397dcb02667..556d287af4ef 100644 --- a/modules/mono/editor/bindings_generator.h +++ b/modules/mono/editor/bindings_generator.h @@ -705,7 +705,7 @@ class BindingsGenerator { StringName type_Vector4i = StaticCString::create("Vector4i"); // Object not included as it must be checked for all derived classes - static constexpr int nullable_types_count = 18; + static constexpr int nullable_types_count = 19; StringName nullable_types[nullable_types_count] = { type_String, type_StringName, @@ -727,6 +727,7 @@ class BindingsGenerator { StaticCString::create(_STR(PackedVector2Array)), StaticCString::create(_STR(PackedVector3Array)), StaticCString::create(_STR(PackedColorArray)), + StaticCString::create(_STR(PackedVector4Array)), }; bool is_nullable_type(const StringName &p_type) const { diff --git a/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/Godot.SourceGenerators.Internal.csproj b/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/Godot.SourceGenerators.Internal.csproj index 4d1a5bb76c0f..81add0e44f12 100644 --- a/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/Godot.SourceGenerators.Internal.csproj +++ b/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/Godot.SourceGenerators.Internal.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 10 diff --git a/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/UnmanagedCallbacksGenerator.cs b/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/UnmanagedCallbacksGenerator.cs index f3f6759e1de4..08e293afcc27 100644 --- a/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/UnmanagedCallbacksGenerator.cs +++ b/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/UnmanagedCallbacksGenerator.cs @@ -468,6 +468,7 @@ private static void AppendCopyToStackAndGetPointer(StringBuilder source, IParame "Godot.NativeInterop.godot_packed_string_array", "Godot.NativeInterop.godot_packed_vector2_array", "Godot.NativeInterop.godot_packed_vector3_array", + "Godot.NativeInterop.godot_packed_vector4_array", "Godot.NativeInterop.godot_packed_color_array", }; } diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/GodotPlugins.csproj b/modules/mono/glue/GodotSharp/GodotPlugins/GodotPlugins.csproj index e58d730ee3cf..1e60743fb1ef 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/GodotPlugins.csproj +++ b/modules/mono/glue/GodotSharp/GodotPlugins/GodotPlugins.csproj @@ -1,18 +1,18 @@ - + - - net6.0 - 10 - enable - true + + net6.0 + 10 + enable + true - - true - LatestMajor - + + true + LatestMajor + - - - + + + diff --git a/modules/mono/glue/GodotSharp/GodotSharp.sln b/modules/mono/glue/GodotSharp/GodotSharp.sln index 8db42c2d1a4f..81ff7d75505e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp.sln +++ b/modules/mono/glue/GodotSharp/GodotSharp.sln @@ -1,5 +1,8 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotSharp", "GodotSharp\GodotSharp.csproj", "{AEBF0036-DA76-4341-B651-A3F2856AB2FA}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotSharpEditor", "GodotSharpEditor\GodotSharpEditor.csproj", "{8FBEC238-D944-4074-8548-B3B524305905}" @@ -10,8 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Inte EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -31,4 +34,10 @@ Global {7749662B-E30C-419A-B745-13852573360A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7749662B-E30C-419A-B745-13852573360A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {371B0F03-042D-45FD-A270-F3141F2480CD} + EndGlobalSection EndGlobal diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs index feaa1d07dadb..ab7f8ede44a5 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs @@ -69,7 +69,7 @@ public readonly real_t Volume public readonly Aabb Abs() { Vector3 end = End; - Vector3 topLeft = new Vector3(Mathf.Min(_position.X, end.X), Mathf.Min(_position.Y, end.Y), Mathf.Min(_position.Z, end.Z)); + Vector3 topLeft = end.Min(_position); return new Aabb(topLeft, _size.Abs()); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/CustomUnsafe.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/CustomUnsafe.cs index afef20a7f227..171cf86edb1d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/CustomUnsafe.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/CustomUnsafe.cs @@ -295,6 +295,22 @@ public static unsafe ref godot_packed_vector3_array AsRef(godot_packed_vector3_a public static unsafe ref godot_packed_vector3_array AsRef(in godot_packed_vector3_array source) => ref *ReadOnlyRefAsPointer(in source); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe godot_packed_vector4_array* AsPointer(ref godot_packed_vector4_array value) + => value.GetUnsafeAddress(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe godot_packed_vector4_array* ReadOnlyRefAsPointer(in godot_packed_vector4_array value) + => value.GetUnsafeAddress(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe ref godot_packed_vector4_array AsRef(godot_packed_vector4_array* source) + => ref *source; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe ref godot_packed_vector4_array AsRef(in godot_packed_vector4_array source) + => ref *ReadOnlyRefAsPointer(in source); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe godot_packed_color_array* AsPointer(ref godot_packed_color_array value) => value.GetUnsafeAddress(); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs index a019dd351372..7e5c01d0f883 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs @@ -1134,6 +1134,39 @@ public readonly unsafe int Size } [StructLayout(LayoutKind.Sequential)] + // ReSharper disable once InconsistentNaming + public ref struct godot_packed_vector4_array + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly unsafe godot_packed_vector4_array* GetUnsafeAddress() + => (godot_packed_vector4_array*)Unsafe.AsPointer(ref Unsafe.AsRef(in _writeProxy)); + + private IntPtr _writeProxy; + private unsafe Vector4* _ptr; + + public unsafe void Dispose() + { + if (_ptr == null) + return; + NativeFuncs.godotsharp_packed_vector4_array_destroy(ref this); + _ptr = null; + } + + public readonly unsafe Vector4* Buffer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _ptr; + } + + public readonly unsafe int Size + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; + } + } + + [StructLayout(LayoutKind.Sequential)] + // ReSharper disable once InconsistentNaming public ref struct godot_packed_color_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs index 9f7fa53e24eb..15b7ce7c73f4 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs @@ -133,6 +133,9 @@ internal static Variant.Type ConvertManagedTypeToVariantType(Type type, out bool if (type == typeof(Vector3[])) return Variant.Type.PackedVector3Array; + if (type == typeof(Vector4[])) + return Variant.Type.PackedVector4Array; + if (type == typeof(Color[])) return Variant.Type.PackedColorArray; @@ -574,6 +577,30 @@ public static unsafe godot_packed_vector3_array ConvertSystemArrayToNativePacked return NativeFuncs.godotsharp_packed_vector3_array_new_mem_copy(src, p_array.Length); } + // PackedVector4Array + + public static unsafe Vector4[] ConvertNativePackedVector4ArrayToSystemArray(godot_packed_vector4_array p_array) + { + Vector4* buffer = p_array.Buffer; + int size = p_array.Size; + if (size == 0) + return Array.Empty(); + int sizeInBytes = size * sizeof(Vector4); + var array = new Vector4[size]; + fixed (Vector4* dest = array) + Buffer.MemoryCopy(buffer, dest, sizeInBytes, sizeInBytes); + return array; + } + + public static unsafe godot_packed_vector4_array ConvertSystemArrayToNativePackedVector4Array( + Span p_array) + { + if (p_array.IsEmpty) + return new godot_packed_vector4_array(); + fixed (Vector4* src = p_array) + return NativeFuncs.godotsharp_packed_vector4_array_new_mem_copy(src, p_array.Length); + } + // PackedColorArray public static unsafe Color[] ConvertNativePackedColorArrayToSystemArray(godot_packed_color_array p_array) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index fef21fae468d..c4fd639ccedd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -144,6 +144,9 @@ public static partial godot_packed_vector2_array godotsharp_packed_vector2_array public static partial godot_packed_vector3_array godotsharp_packed_vector3_array_new_mem_copy(Vector3* p_src, int p_length); + public static partial godot_packed_vector4_array godotsharp_packed_vector4_array_new_mem_copy(Vector4* p_src, + int p_length); + public static partial godot_packed_color_array godotsharp_packed_color_array_new_mem_copy(Color* p_src, int p_length); @@ -224,6 +227,9 @@ public static partial void godotsharp_variant_new_packed_vector2_array(out godot public static partial void godotsharp_variant_new_packed_vector3_array(out godot_variant r_dest, in godot_packed_vector3_array p_pv3a); + public static partial void godotsharp_variant_new_packed_vector4_array(out godot_variant r_dest, + in godot_packed_vector4_array p_pv4a); + public static partial void godotsharp_variant_new_packed_color_array(out godot_variant r_dest, in godot_packed_color_array p_pca); @@ -302,6 +308,9 @@ public static partial godot_packed_vector2_array godotsharp_variant_as_packed_ve public static partial godot_packed_vector3_array godotsharp_variant_as_packed_vector3_array( in godot_variant p_self); + public static partial godot_packed_vector4_array godotsharp_variant_as_packed_vector4_array( + in godot_variant p_self); + public static partial godot_packed_color_array godotsharp_variant_as_packed_color_array(in godot_variant p_self); public static partial godot_bool godotsharp_variant_equals(in godot_variant p_a, in godot_variant p_b); @@ -352,6 +361,8 @@ public static partial void godotsharp_dictionary_new_copy(out godot_dictionary r public static partial void godotsharp_packed_vector3_array_destroy(ref godot_packed_vector3_array p_self); + public static partial void godotsharp_packed_vector4_array_destroy(ref godot_packed_vector4_array p_self); + public static partial void godotsharp_packed_color_array_destroy(ref godot_packed_color_array p_self); public static partial void godotsharp_variant_destroy(ref godot_variant p_self); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.cs index 94609984ac02..dc151e2c3eee 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.cs @@ -165,6 +165,12 @@ public static godot_variant CreateFromPackedVector3Array(in godot_packed_vector3 return ret; } + public static godot_variant CreateFromPackedVector4Array(in godot_packed_vector4_array from) + { + NativeFuncs.godotsharp_variant_new_packed_vector4_array(out godot_variant ret, from); + return ret; + } + public static godot_variant CreateFromPackedColorArray(in godot_packed_color_array from) { NativeFuncs.godotsharp_variant_new_packed_color_array(out godot_variant ret, from); @@ -227,6 +233,13 @@ public static godot_variant CreateFromPackedVector3Array(Span from) return CreateFromPackedVector3Array(nativePackedArray); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static godot_variant CreateFromPackedVector4Array(Span from) + { + using var nativePackedArray = Marshaling.ConvertSystemArrayToNativePackedVector4Array(from); + return CreateFromPackedVector4Array(nativePackedArray); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static godot_variant CreateFromPackedColorArray(Span from) { @@ -605,6 +618,12 @@ public static Vector3[] ConvertAsPackedVector3ArrayToSystemArray(in godot_varian return Marshaling.ConvertNativePackedVector3ArrayToSystemArray(packedArray); } + public static Vector4[] ConvertAsPackedVector4ArrayToSystemArray(in godot_variant p_var) + { + using var packedArray = NativeFuncs.godotsharp_variant_as_packed_vector4_array(p_var); + return Marshaling.ConvertNativePackedVector4ArrayToSystemArray(packedArray); + } + public static Color[] ConvertAsPackedColorArrayToSystemArray(in godot_variant p_var) { using var packedArray = NativeFuncs.godotsharp_variant_as_packed_color_array(p_var); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs index d8f7214c2fc9..2897cc4199e6 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs @@ -155,6 +155,9 @@ public static godot_variant CreateFrom<[MustBeVariant] T>(in T from) if (typeof(T) == typeof(Vector3[])) return CreateFromPackedVector3Array(UnsafeAs(from)); + if (typeof(T) == typeof(Vector4[])) + return CreateFromPackedVector4Array(UnsafeAs(from)); + if (typeof(T) == typeof(Color[])) return CreateFromPackedColorArray(UnsafeAs(from)); @@ -343,6 +346,9 @@ public static T ConvertTo<[MustBeVariant] T>(in godot_variant variant) if (typeof(T) == typeof(Vector3[])) return UnsafeAsT(ConvertAsPackedVector3ArrayToSystemArray(variant)); + if (typeof(T) == typeof(Vector4[])) + return UnsafeAsT(ConvertAsPackedVector4ArrayToSystemArray(variant)); + if (typeof(T) == typeof(Color[])) return UnsafeAsT(ConvertAsPackedColorArrayToSystemArray(variant)); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs index 9d9065911ee6..19721b6ccaf4 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs @@ -69,7 +69,7 @@ public readonly real_t Area public readonly Rect2 Abs() { Vector2 end = End; - Vector2 topLeft = new Vector2(Mathf.Min(_position.X, end.X), Mathf.Min(_position.Y, end.Y)); + Vector2 topLeft = end.Min(_position); return new Rect2(topLeft, _size.Abs()); } @@ -91,14 +91,12 @@ public readonly Rect2 Intersection(Rect2 b) return new Rect2(); } - newRect._position.X = Mathf.Max(b._position.X, _position.X); - newRect._position.Y = Mathf.Max(b._position.Y, _position.Y); + newRect._position = b._position.Max(_position); Vector2 bEnd = b._position + b._size; Vector2 end = _position + _size; - newRect._size.X = Mathf.Min(bEnd.X, end.X) - newRect._position.X; - newRect._size.Y = Mathf.Min(bEnd.Y, end.Y) - newRect._position.Y; + newRect._size = bEnd.Min(end) - newRect._position; return newRect; } @@ -338,11 +336,9 @@ public readonly Rect2 Merge(Rect2 b) { Rect2 newRect; - newRect._position.X = Mathf.Min(b._position.X, _position.X); - newRect._position.Y = Mathf.Min(b._position.Y, _position.Y); + newRect._position = b._position.Min(_position); - newRect._size.X = Mathf.Max(b._position.X + b._size.X, _position.X + _size.X); - newRect._size.Y = Mathf.Max(b._position.Y + b._size.Y, _position.Y + _size.Y); + newRect._size = (b._position + b._size).Max(_position + _size); newRect._size -= newRect._position; // Make relative again diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs index 65704b3da7b1..7ee9ff85527e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs @@ -69,7 +69,7 @@ public readonly int Area public readonly Rect2I Abs() { Vector2I end = End; - Vector2I topLeft = new Vector2I(Mathf.Min(_position.X, end.X), Mathf.Min(_position.Y, end.Y)); + Vector2I topLeft = end.Min(_position); return new Rect2I(topLeft, _size.Abs()); } @@ -91,14 +91,12 @@ public readonly Rect2I Intersection(Rect2I b) return new Rect2I(); } - newRect._position.X = Mathf.Max(b._position.X, _position.X); - newRect._position.Y = Mathf.Max(b._position.Y, _position.Y); + newRect._position = b._position.Max(_position); Vector2I bEnd = b._position + b._size; Vector2I end = _position + _size; - newRect._size.X = Mathf.Min(bEnd.X, end.X) - newRect._position.X; - newRect._size.Y = Mathf.Min(bEnd.Y, end.Y) - newRect._position.Y; + newRect._size = bEnd.Min(end) - newRect._position; return newRect; } @@ -295,11 +293,9 @@ public readonly Rect2I Merge(Rect2I b) { Rect2I newRect; - newRect._position.X = Mathf.Min(b._position.X, _position.X); - newRect._position.Y = Mathf.Min(b._position.Y, _position.Y); + newRect._position = b._position.Min(_position); - newRect._size.X = Mathf.Max(b._position.X + b._size.X, _position.X + _size.X); - newRect._size.Y = Mathf.Max(b._position.Y + b._size.Y, _position.Y + _size.Y); + newRect._size = (b._position + b._size).Max(_position + _size); newRect._size -= newRect._position; // Make relative again diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs index 37f319b69714..c805b68c94e2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs @@ -417,76 +417,31 @@ private static string CamelcaseToUnderscore(this string instance, bool lowerCase } /// - /// Performs a case-sensitive comparison to another string, return -1 if less, 0 if equal and +1 if greater. + /// Performs a case-sensitive comparison to another string and returns an integer that indicates their relative position in the sort order. /// /// /// /// The string to compare. /// The other string to compare. - /// -1 if less, 0 if equal and +1 if greater. + /// An integer that indicates the lexical relationship between the two comparands. public static int CasecmpTo(this string instance, string to) { return instance.CompareTo(to, caseSensitive: true); } /// - /// Performs a comparison to another string, return -1 if less, 0 if equal and +1 if greater. + /// Performs a comparison to another string and returns an integer that indicates their relative position in the sort order. /// /// The string to compare. /// The other string to compare. /// /// If , the comparison will be case sensitive. /// - /// -1 if less, 0 if equal and +1 if greater. + /// An integer that indicates the lexical relationship between the two comparands. + [Obsolete("Use string.Compare instead.")] public static int CompareTo(this string instance, string to, bool caseSensitive = true) { - if (string.IsNullOrEmpty(instance)) - return string.IsNullOrEmpty(to) ? 0 : -1; - - if (string.IsNullOrEmpty(to)) - return 1; - - int instanceIndex = 0; - int toIndex = 0; - - if (caseSensitive) // Outside while loop to avoid checking multiple times, despite some code duplication. - { - while (true) - { - if (to[toIndex] == 0 && instance[instanceIndex] == 0) - return 0; // We're equal - if (instance[instanceIndex] == 0) - return -1; // If this is empty, and the other one is not, then we're less... I think? - if (to[toIndex] == 0) - return 1; // Otherwise the other one is smaller... - if (instance[instanceIndex] < to[toIndex]) // More than - return -1; - if (instance[instanceIndex] > to[toIndex]) // Less than - return 1; - - instanceIndex++; - toIndex++; - } - } - else - { - while (true) - { - if (to[toIndex] == 0 && instance[instanceIndex] == 0) - return 0; // We're equal - if (instance[instanceIndex] == 0) - return -1; // If this is empty, and the other one is not, then we're less... I think? - if (to[toIndex] == 0) - return 1; // Otherwise the other one is smaller.. - if (char.ToUpperInvariant(instance[instanceIndex]) < char.ToUpperInvariant(to[toIndex])) // More than - return -1; - if (char.ToUpperInvariant(instance[instanceIndex]) > char.ToUpperInvariant(to[toIndex])) // Less than - return 1; - - instanceIndex++; - toIndex++; - } - } + return string.Compare(instance, to, !caseSensitive); } /// @@ -1297,13 +1252,13 @@ public static string Md5Text(this string instance) } /// - /// Perform a case-insensitive comparison to another string, return -1 if less, 0 if equal and +1 if greater. + /// Performs a case-insensitive comparison to another string and returns an integer that indicates their relative position in the sort order. /// /// /// /// The string to compare. /// The other string to compare. - /// -1 if less, 0 if equal and +1 if greater. + /// An integer that indicates the lexical relationship between the two comparands. public static int NocasecmpTo(this string instance, string to) { return instance.CompareTo(to, caseSensitive: false); @@ -1739,7 +1694,7 @@ public static int ToInt(this string instance) } /// - /// Converts the string (which is an array of characters) to an UTF-16 encoded array of bytes. + /// Converts the string (which is an array of characters) to a UTF-16 encoded array of bytes. /// /// /// @@ -1752,7 +1707,7 @@ public static byte[] ToUtf16Buffer(this string instance) } /// - /// Converts the string (which is an array of characters) to an UTF-32 encoded array of bytes. + /// Converts the string (which is an array of characters) to a UTF-32 encoded array of bytes. /// /// /// @@ -1765,7 +1720,7 @@ public static byte[] ToUtf32Buffer(this string instance) } /// - /// Converts the string (which is an array of characters) to an UTF-8 encoded array of bytes. + /// Converts the string (which is an array of characters) to a UTF-8 encoded array of bytes. /// The conversion is a bit slower than , /// but supports all UTF-8 characters. Therefore, you should prefer this function /// over . diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Variant.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Variant.cs index c2d3050adcb0..b40f524859db 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Variant.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Variant.cs @@ -149,6 +149,7 @@ public void Dispose() Type.PackedStringArray => AsStringArray(), Type.PackedVector2Array => AsVector2Array(), Type.PackedVector3Array => AsVector3Array(), + Type.PackedVector4Array => AsVector4Array(), Type.PackedColorArray => AsColorArray(), Type.Nil => null, Type.Max or _ => @@ -319,6 +320,10 @@ public Vector2[] AsVector2Array() => public Vector3[] AsVector3Array() => VariantUtils.ConvertAsPackedVector3ArrayToSystemArray((godot_variant)NativeVar); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4[] AsVector4Array() => + VariantUtils.ConvertAsPackedVector4ArrayToSystemArray((godot_variant)NativeVar); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Color[] AsColorArray() => VariantUtils.ConvertAsPackedColorArrayToSystemArray((godot_variant)NativeVar); @@ -491,6 +496,9 @@ public Collections.Array AsGodotArray() => [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator Vector3[](Variant from) => from.AsVector3Array(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Vector4[](Variant from) => from.AsVector4Array(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator Color[](Variant from) => from.AsColorArray(); @@ -641,6 +649,9 @@ public Collections.Array AsGodotArray() => [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Variant CreateFrom(Span from) => from; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Variant CreateFrom(Span from) => from; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Variant CreateFrom(Span from) => from; @@ -840,6 +851,10 @@ public static implicit operator Variant(Vector2[] from) => public static implicit operator Variant(Vector3[] from) => (Variant)from.AsSpan(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Variant(Vector4[] from) => + (Variant)from.AsSpan(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Variant(Color[] from) => (Variant)from.AsSpan(); @@ -892,6 +907,10 @@ public static implicit operator Variant(Span from) => public static implicit operator Variant(Span from) => CreateTakingOwnershipOfDisposableValue(VariantUtils.CreateFromPackedVector3Array(from)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Variant(Span from) => + CreateTakingOwnershipOfDisposableValue(VariantUtils.CreateFromPackedVector4Array(from)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Variant(Span from) => CreateTakingOwnershipOfDisposableValue(VariantUtils.CreateFromPackedColorArray(from)); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs index 856fd54352f1..50bf56d8320c 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs @@ -191,6 +191,23 @@ public readonly Vector2 Clamp(Vector2 min, Vector2 max) ); } + /// + /// Returns a new vector with all components clamped between the + /// and using + /// . + /// + /// The minimum allowed value. + /// The maximum allowed value. + /// The vector with all components clamped. + public readonly Vector2 Clamp(real_t min, real_t max) + { + return new Vector2 + ( + Mathf.Clamp(X, min, max), + Mathf.Clamp(Y, min, max) + ); + } + /// /// Returns the cross product of this vector and . /// @@ -412,6 +429,70 @@ public readonly Vector2 LimitLength(real_t length = 1.0f) return v; } + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector2(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y)). + /// + /// The other vector to use. + /// The resulting maximum vector. + public readonly Vector2 Max(Vector2 with) + { + return new Vector2 + ( + Mathf.Max(X, with.X), + Mathf.Max(Y, with.Y) + ); + } + + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector2(Mathf.Max(X, with), Mathf.Max(Y, with)). + /// + /// The other value to use. + /// The resulting maximum vector. + public readonly Vector2 Max(real_t with) + { + return new Vector2 + ( + Mathf.Max(X, with), + Mathf.Max(Y, with) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector2(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y)). + /// + /// The other vector to use. + /// The resulting minimum vector. + public readonly Vector2 Min(Vector2 with) + { + return new Vector2 + ( + Mathf.Min(X, with.X), + Mathf.Min(Y, with.Y) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector2(Mathf.Min(X, with), Mathf.Min(Y, with)). + /// + /// The other value to use. + /// The resulting minimum vector. + public readonly Vector2 Min(real_t with) + { + return new Vector2 + ( + Mathf.Min(X, with), + Mathf.Min(Y, with) + ); + } + /// /// Returns the axis of the vector's highest value. See . /// If both components are equal, this method returns . @@ -600,7 +681,7 @@ public readonly Vector2 Slide(Vector2 normal) } /// - /// Returns this vector with each component snapped to the nearest multiple of . + /// Returns a new vector with each component snapped to the nearest multiple of the corresponding component in . /// This can also be used to round to an arbitrary number of decimals. /// /// A vector value representing the step size to snap to. @@ -610,6 +691,17 @@ public readonly Vector2 Snapped(Vector2 step) return new Vector2(Mathf.Snapped(X, step.X), Mathf.Snapped(Y, step.Y)); } + /// + /// Returns a new vector with each component snapped to the nearest multiple of . + /// This can also be used to round to an arbitrary number of decimals. + /// + /// The step size to snap to. + /// The snapped vector. + public readonly Vector2 Snapped(real_t step) + { + return new Vector2(Mathf.Snapped(X, step), Mathf.Snapped(Y, step)); + } + /// /// Returns a perpendicular vector rotated 90 degrees counter-clockwise /// compared to the original, with the same length. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs index 511cc7971cf3..9442db4d860e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs @@ -124,6 +124,23 @@ public readonly Vector2I Clamp(Vector2I min, Vector2I max) ); } + /// + /// Returns a new vector with all components clamped between the + /// and using + /// . + /// + /// The minimum allowed value. + /// The maximum allowed value. + /// The vector with all components clamped. + public readonly Vector2I Clamp(int min, int max) + { + return new Vector2I + ( + Mathf.Clamp(X, min, max), + Mathf.Clamp(Y, min, max) + ); + } + /// /// Returns the squared distance between this vector and . /// This method runs faster than , so prefer it if @@ -174,6 +191,70 @@ public readonly int LengthSquared() return x2 + y2; } + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector2I(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y)). + /// + /// The other vector to use. + /// The resulting maximum vector. + public readonly Vector2I Max(Vector2I with) + { + return new Vector2I + ( + Mathf.Max(X, with.X), + Mathf.Max(Y, with.Y) + ); + } + + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector2I(Mathf.Max(X, with), Mathf.Max(Y, with)). + /// + /// The other value to use. + /// The resulting maximum vector. + public readonly Vector2I Max(int with) + { + return new Vector2I + ( + Mathf.Max(X, with), + Mathf.Max(Y, with) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector2I(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y)). + /// + /// The other vector to use. + /// The resulting minimum vector. + public readonly Vector2I Min(Vector2I with) + { + return new Vector2I + ( + Mathf.Min(X, with.X), + Mathf.Min(Y, with.Y) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector2I(Mathf.Min(X, with), Mathf.Min(Y, with)). + /// + /// The other value to use. + /// The resulting minimum vector. + public readonly Vector2I Min(int with) + { + return new Vector2I + ( + Mathf.Min(X, with), + Mathf.Min(Y, with) + ); + } + /// /// Returns the axis of the vector's highest value. See . /// If both components are equal, this method returns . @@ -208,6 +289,34 @@ public readonly Vector2I Sign() return v; } + /// + /// Returns a new vector with each component snapped to the closest multiple of the corresponding component in . + /// + /// A vector value representing the step size to snap to. + /// The snapped vector. + 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) + ); + } + + /// + /// Returns a new vector with each component snapped to the closest multiple of . + /// + /// The step size to snap to. + /// The snapped vector. + 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 6300705107fb..27f2713efa79 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs @@ -178,6 +178,24 @@ public readonly Vector3 Clamp(Vector3 min, Vector3 max) ); } + /// + /// Returns a new vector with all components clamped between the + /// and using + /// . + /// + /// The minimum allowed value. + /// The maximum allowed value. + /// The vector with all components clamped. + 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) + ); + } + /// /// Returns the cross product of this vector and . /// @@ -418,6 +436,57 @@ public readonly Vector3 LimitLength(real_t length = 1.0f) return v; } + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector3(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y), Mathf.Max(Z, with.Z)). + /// + /// The other vector to use. + /// The resulting maximum vector. + public readonly Vector3 Max(Vector3 with) + { + return new Vector3 + ( + Mathf.Max(X, with.X), + Mathf.Max(Y, with.Y), + Mathf.Max(Z, with.Z) + ); + } + + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector3(Mathf.Max(X, with), Mathf.Max(Y, with), Mathf.Max(Z, with)). + /// + /// The other value to use. + /// The resulting maximum vector. + public readonly Vector3 Max(real_t with) + { + return new Vector3 + ( + Mathf.Max(X, with), + Mathf.Max(Y, with), + Mathf.Max(Z, with) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector3(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y), Mathf.Min(Z, with.Z)). + /// + /// The other vector to use. + /// The resulting minimum vector. + public readonly Vector3 Min(Vector3 with) + { + return new Vector3 + ( + Mathf.Min(X, with.X), + Mathf.Min(Y, with.Y), + Mathf.Min(Z, with.Z) + ); + } + /// /// Returns the axis of the vector's highest value. See . /// If all components are equal, this method returns . @@ -643,7 +712,7 @@ public readonly Vector3 Slide(Vector3 normal) } /// - /// Returns this vector with each component snapped to the nearest multiple of . + /// Returns a new vector with each component snapped to the nearest multiple of the corresponding component in . /// This can also be used to round to an arbitrary number of decimals. /// /// A vector value representing the step size to snap to. @@ -658,6 +727,22 @@ public readonly Vector3 Snapped(Vector3 step) ); } + /// + /// Returns a new vector with each component snapped to the nearest multiple of . + /// This can also be used to round to an arbitrary number of decimals. + /// + /// The step size to snap to. + /// The snapped vector. + 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 aea46efc5ba5..8312e2c231dd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs @@ -132,6 +132,24 @@ public readonly Vector3I Clamp(Vector3I min, Vector3I max) ); } + /// + /// Returns a new vector with all components clamped between the + /// and using + /// . + /// + /// The minimum allowed value. + /// The maximum allowed value. + /// The vector with all components clamped. + 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) + ); + } + /// /// Returns the squared distance between this vector and . /// This method runs faster than , so prefer it if @@ -184,6 +202,74 @@ public readonly int LengthSquared() return x2 + y2 + z2; } + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector3I(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y), Mathf.Max(Z, with.Z)). + /// + /// The other vector to use. + /// The resulting maximum vector. + public readonly Vector3I Max(Vector3I with) + { + return new Vector3I + ( + Mathf.Max(X, with.X), + Mathf.Max(Y, with.Y), + Mathf.Max(Z, with.Z) + ); + } + + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector3I(Mathf.Max(X, with), Mathf.Max(Y, with), Mathf.Max(Z, with)). + /// + /// The other value to use. + /// The resulting maximum vector. + public readonly Vector3I Max(int with) + { + return new Vector3I + ( + Mathf.Max(X, with), + Mathf.Max(Y, with), + Mathf.Max(Z, with) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector3I(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y), Mathf.Min(Z, with.Z)). + /// + /// The other vector to use. + /// The resulting minimum vector. + public readonly Vector3I Min(Vector3I with) + { + return new Vector3I + ( + Mathf.Min(X, with.X), + Mathf.Min(Y, with.Y), + Mathf.Min(Z, with.Z) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector3I(Mathf.Min(X, with), Mathf.Min(Y, with), Mathf.Min(Z, with)). + /// + /// The other value to use. + /// The resulting minimum vector. + public readonly Vector3I Min(int with) + { + return new Vector3I + ( + Mathf.Min(X, with), + Mathf.Min(Y, with), + Mathf.Min(Z, with) + ); + } + /// /// Returns the axis of the vector's highest value. See . /// If all components are equal, this method returns . @@ -219,6 +305,36 @@ public readonly Vector3I Sign() return v; } + /// + /// Returns a new vector with each component snapped to the closest multiple of the corresponding component in . + /// + /// A vector value representing the step size to snap to. + /// The snapped vector. + 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) + ); + } + + /// + /// Returns a new vector with each component snapped to the closest multiple of . + /// + /// The step size to snap to. + /// The snapped vector. + 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 7c4832943c05..ec59197fa33f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs @@ -176,6 +176,25 @@ public readonly Vector4 Clamp(Vector4 min, Vector4 max) ); } + /// + /// Returns a new vector with all components clamped between the + /// and using + /// . + /// + /// The minimum allowed value. + /// The maximum allowed value. + /// The vector with all components clamped. + 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) + ); + } + /// /// Performs a cubic interpolation between vectors , this vector, /// , and , by the given amount . @@ -351,6 +370,78 @@ public readonly Vector4 Lerp(Vector4 to, real_t weight) ); } + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector4(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y), Mathf.Max(Z, with.Z), Mathf.Max(W, with.W)). + /// + /// The other vector to use. + /// The resulting maximum vector. + public readonly Vector4 Max(Vector4 with) + { + return new Vector4 + ( + Mathf.Max(X, with.X), + Mathf.Max(Y, with.Y), + Mathf.Max(Z, with.Z), + Mathf.Max(W, with.W) + ); + } + + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector4(Mathf.Max(X, with), Mathf.Max(Y, with), Mathf.Max(Z, with), Mathf.Max(W, with)). + /// + /// The other value to use. + /// The resulting maximum vector. + public readonly Vector4 Max(real_t with) + { + return new Vector4 + ( + Mathf.Max(X, with), + Mathf.Max(Y, with), + Mathf.Max(Z, with), + Mathf.Max(W, with) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector4(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y), Mathf.Min(Z, with.Z), Mathf.Min(W, with.W)). + /// + /// The other vector to use. + /// The resulting minimum vector. + public readonly Vector4 Min(Vector4 with) + { + return new Vector4 + ( + Mathf.Min(X, with.X), + Mathf.Min(Y, with.Y), + Mathf.Min(Z, with.Z), + Mathf.Min(W, with.W) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector4(Mathf.Min(X, with), Mathf.Min(Y, with), Mathf.Min(Z, with), Mathf.Min(W, with)). + /// + /// The other value to use. + /// The resulting minimum vector. + public readonly Vector4 Min(real_t with) + { + return new Vector4 + ( + Mathf.Min(X, with), + Mathf.Min(Y, with), + Mathf.Min(Z, with), + Mathf.Min(W, with) + ); + } + /// /// Returns the axis of the vector's highest value. See . /// If all components are equal, this method returns . @@ -465,7 +556,7 @@ public readonly Vector4 Sign() } /// - /// Returns this vector with each component snapped to the nearest multiple of . + /// Returns a new vector with each component snapped to the nearest multiple of the corresponding component in . /// This can also be used to round to an arbitrary number of decimals. /// /// A vector value representing the step size to snap to. @@ -480,6 +571,22 @@ public readonly Vector4 Snapped(Vector4 step) ); } + /// + /// Returns a new vector with each component snapped to the nearest multiple of . + /// This can also be used to round to an arbitrary number of decimals. + /// + /// The step size to snap to. + /// The snapped vector. + 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 27aa86b7e47e..ba8e54b88b91 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs @@ -149,6 +149,25 @@ public readonly Vector4I Clamp(Vector4I min, Vector4I max) ); } + /// + /// Returns a new vector with all components clamped between + /// and using + /// . + /// + /// The minimum allowed value. + /// The maximum allowed value. + /// The vector with all components clamped. + 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) + ); + } + /// /// Returns the squared distance between this vector and . /// This method runs faster than , so prefer it if @@ -203,6 +222,78 @@ public readonly int LengthSquared() return x2 + y2 + z2 + w2; } + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector4I(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y), Mathf.Max(Z, with.Z), Mathf.Max(W, with.W)). + /// + /// The other vector to use. + /// The resulting maximum vector. + public readonly Vector4I Max(Vector4I with) + { + return new Vector4I + ( + Mathf.Max(X, with.X), + Mathf.Max(Y, with.Y), + Mathf.Max(Z, with.Z), + Mathf.Max(W, with.W) + ); + } + + /// + /// Returns the result of the component-wise maximum between + /// this vector and . + /// Equivalent to new Vector4I(Mathf.Max(X, with), Mathf.Max(Y, with), Mathf.Max(Z, with), Mathf.Max(W, with)). + /// + /// The other value to use. + /// The resulting maximum vector. + public readonly Vector4I Max(int with) + { + return new Vector4I + ( + Mathf.Max(X, with), + Mathf.Max(Y, with), + Mathf.Max(Z, with), + Mathf.Max(W, with) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector4I(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y), Mathf.Min(Z, with.Z), Mathf.Min(W, with.W)). + /// + /// The other vector to use. + /// The resulting minimum vector. + public readonly Vector4I Min(Vector4I with) + { + return new Vector4I + ( + Mathf.Min(X, with.X), + Mathf.Min(Y, with.Y), + Mathf.Min(Z, with.Z), + Mathf.Min(W, with.W) + ); + } + + /// + /// Returns the result of the component-wise minimum between + /// this vector and . + /// Equivalent to new Vector4I(Mathf.Min(X, with), Mathf.Min(Y, with), Mathf.Min(Z, with), Mathf.Min(W, with)). + /// + /// The other value to use. + /// The resulting minimum vector. + public readonly Vector4I Min(int with) + { + return new Vector4I + ( + Mathf.Min(X, with), + Mathf.Min(Y, with), + Mathf.Min(Z, with), + Mathf.Min(W, with) + ); + } + /// /// Returns the axis of the vector's highest value. See . /// If all components are equal, this method returns . @@ -254,6 +345,36 @@ public readonly Vector4I Sign() return new Vector4I(Mathf.Sign(X), Mathf.Sign(Y), Mathf.Sign(Z), Mathf.Sign(W)); } + /// + /// Returns a new vector with each component snapped to the closest multiple of the corresponding component in . + /// + /// A vector value representing the step size to snap to. + /// The snapped vector. + 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) + ); + } + + /// + /// Returns a new vector with each component snapped to the closest multiple of . + /// + /// The step size to snap to. + /// The snapped vector. + 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 67282416ed4d..6b25087c93ac 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -1,4 +1,4 @@ - + {AEBF0036-DA76-4341-B651-A3F2856AB2FA} bin/$(Configuration) diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj index 8373edb9bf7b..4561fdaf2b6f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj @@ -1,4 +1,4 @@ - + {8FBEC238-D944-4074-8548-B3B524305905} bin/$(Configuration) diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index 4bb324c0eef9..1af462dafd2e 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -461,6 +461,16 @@ godot_packed_array godotsharp_packed_vector3_array_new_mem_copy(const Vector3 *p return ret; } +godot_packed_array godotsharp_packed_vector4_array_new_mem_copy(const Vector4 *p_src, int32_t p_length) { + godot_packed_array ret; + memnew_placement(&ret, PackedVector4Array); + PackedVector4Array *array = reinterpret_cast(&ret); + array->resize(p_length); + Vector4 *dst = array->ptrw(); + memcpy(dst, p_src, p_length * sizeof(Vector4)); + return ret; +} + godot_packed_array godotsharp_packed_color_array_new_mem_copy(const Color *p_src, int32_t p_length) { godot_packed_array ret; memnew_placement(&ret, PackedColorArray); @@ -646,6 +656,10 @@ void godotsharp_variant_new_packed_vector3_array(godot_variant *r_dest, const Pa memnew_placement(r_dest, Variant(*p_pv3a)); } +void godotsharp_variant_new_packed_vector4_array(godot_variant *r_dest, const PackedVector4Array *p_pv4a) { + memnew_placement(r_dest, Variant(*p_pv4a)); +} + void godotsharp_variant_new_packed_color_array(godot_variant *r_dest, const PackedColorArray *p_pca) { memnew_placement(r_dest, Variant(*p_pca)); } @@ -886,6 +900,13 @@ godot_packed_array godotsharp_variant_as_packed_vector3_array(const Variant *p_s return raw_dest; } +godot_packed_array godotsharp_variant_as_packed_vector4_array(const Variant *p_self) { + godot_packed_array raw_dest; + PackedVector4Array *dest = (PackedVector4Array *)&raw_dest; + memnew_placement(dest, PackedVector4Array(p_self->operator PackedVector4Array())); + return raw_dest; +} + godot_packed_array godotsharp_variant_as_packed_color_array(const Variant *p_self) { godot_packed_array raw_dest; PackedColorArray *dest = (PackedColorArray *)&raw_dest; @@ -974,6 +995,10 @@ void godotsharp_packed_vector3_array_destroy(PackedVector3Array *p_self) { p_self->~PackedVector3Array(); } +void godotsharp_packed_vector4_array_destroy(PackedVector4Array *p_self) { + p_self->~PackedVector4Array(); +} + void godotsharp_packed_color_array_destroy(PackedColorArray *p_self) { p_self->~PackedColorArray(); } @@ -1456,6 +1481,7 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_packed_float64_array_new_mem_copy, (void *)godotsharp_packed_vector2_array_new_mem_copy, (void *)godotsharp_packed_vector3_array_new_mem_copy, + (void *)godotsharp_packed_vector4_array_new_mem_copy, (void *)godotsharp_packed_color_array_new_mem_copy, (void *)godotsharp_packed_string_array_add, (void *)godotsharp_callable_new_with_delegate, @@ -1484,6 +1510,7 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_variant_new_packed_string_array, (void *)godotsharp_variant_new_packed_vector2_array, (void *)godotsharp_variant_new_packed_vector3_array, + (void *)godotsharp_variant_new_packed_vector4_array, (void *)godotsharp_variant_new_packed_color_array, (void *)godotsharp_variant_as_bool, (void *)godotsharp_variant_as_int, @@ -1520,6 +1547,7 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_variant_as_packed_string_array, (void *)godotsharp_variant_as_packed_vector2_array, (void *)godotsharp_variant_as_packed_vector3_array, + (void *)godotsharp_variant_as_packed_vector4_array, (void *)godotsharp_variant_as_packed_color_array, (void *)godotsharp_variant_equals, (void *)godotsharp_string_new_with_utf16_chars, @@ -1538,6 +1566,7 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_packed_string_array_destroy, (void *)godotsharp_packed_vector2_array_destroy, (void *)godotsharp_packed_vector3_array_destroy, + (void *)godotsharp_packed_vector4_array_destroy, (void *)godotsharp_packed_color_array_destroy, (void *)godotsharp_variant_destroy, (void *)godotsharp_string_destroy, diff --git a/modules/multiplayer/editor/multiplayer_editor_plugin.h b/modules/multiplayer/editor/multiplayer_editor_plugin.h index a22144cdcfd8..e8ade539a775 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.cpp b/modules/multiplayer/editor/replication_editor.cpp index 8453a4147372..73e53a6a07e2 100644 --- a/modules/multiplayer/editor/replication_editor.cpp +++ b/modules/multiplayer/editor/replication_editor.cpp @@ -234,7 +234,8 @@ ReplicationEditor::ReplicationEditor() { Variant::PACKED_STRING_ARRAY, Variant::PACKED_VECTOR2_ARRAY, Variant::PACKED_VECTOR3_ARRAY, - Variant::PACKED_COLOR_ARRAY + Variant::PACKED_COLOR_ARRAY, + Variant::PACKED_VECTOR4_ARRAY, }; prop_selector->set_type_filter(types); prop_selector->connect("selected", callable_mp(this, &ReplicationEditor::_pick_node_property_selected)); diff --git a/modules/multiplayer/editor/replication_editor.h b/modules/multiplayer/editor/replication_editor.h index 80c1892ec31f..8f1177429234 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/multiplayer/scene_replication_config.cpp b/modules/multiplayer/scene_replication_config.cpp index 733540a0e425..ab92f7d90f5c 100644 --- a/modules/multiplayer/scene_replication_config.cpp +++ b/modules/multiplayer/scene_replication_config.cpp @@ -48,7 +48,7 @@ bool SceneReplicationConfig::_set(const StringName &p_name, const Variant &p_val return true; } ERR_FAIL_INDEX_V(idx, properties.size(), false); - ReplicationProperty &prop = properties[idx]; + const ReplicationProperty &prop = properties.get(idx); if (what == "replication_mode") { ERR_FAIL_COND_V(p_value.get_type() != Variant::INT, false); ReplicationMode mode = (ReplicationMode)p_value.operator int(); @@ -80,7 +80,7 @@ bool SceneReplicationConfig::_get(const StringName &p_name, Variant &r_ret) cons int idx = prop_name.get_slicec('/', 1).to_int(); String what = prop_name.get_slicec('/', 2); ERR_FAIL_INDEX_V(idx, properties.size(), false); - const ReplicationProperty &prop = properties[idx]; + const ReplicationProperty &prop = properties.get(idx); if (what == "path") { r_ret = prop.name; return true; @@ -147,8 +147,8 @@ void SceneReplicationConfig::remove_property(const NodePath &p_path) { } bool SceneReplicationConfig::has_property(const NodePath &p_path) const { - for (int i = 0; i < properties.size(); i++) { - if (properties[i].name == p_path) { + for (const ReplicationProperty &property : properties) { + if (property.name == p_path) { return true; } } @@ -156,8 +156,9 @@ bool SceneReplicationConfig::has_property(const NodePath &p_path) const { } int SceneReplicationConfig::property_get_index(const NodePath &p_path) const { - for (int i = 0; i < properties.size(); i++) { - if (properties[i].name == p_path) { + int i = 0; + for (List::ConstIterator itr = properties.begin(); itr != properties.end(); ++itr, ++i) { + if (itr->name == p_path) { return i; } } diff --git a/modules/multiplayer/scene_replication_interface.cpp b/modules/multiplayer/scene_replication_interface.cpp index bb32eed1a901..182e9b455cd9 100644 --- a/modules/multiplayer/scene_replication_interface.cpp +++ b/modules/multiplayer/scene_replication_interface.cpp @@ -243,7 +243,7 @@ Error SceneReplicationInterface::on_replication_start(Object *p_obj, Variant p_c // Try to apply synchronizer Net ID ERR_FAIL_COND_V_MSG(pending_sync_net_ids.is_empty(), ERR_INVALID_DATA, vformat("The MultiplayerSynchronizer at path \"%s\" is unable to process the pending spawn since it has no network ID. This might happen when changing the multiplayer authority during the \"_ready\" callback. Make sure to only change the authority of multiplayer synchronizers during \"_enter_tree\" or the \"_spawn_custom\" callback of their multiplayer spawner.", sync->get_path())); ERR_FAIL_COND_V(!peers_info.has(pending_spawn_remote), ERR_INVALID_DATA); - uint32_t net_id = pending_sync_net_ids[0]; + uint32_t net_id = pending_sync_net_ids.front()->get(); pending_sync_net_ids.pop_front(); peers_info[pending_spawn_remote].recv_sync_ids[net_id] = sync->get_instance_id(); sync->set_net_id(net_id); diff --git a/modules/navigation/2d/nav_mesh_generator_2d.cpp b/modules/navigation/2d/nav_mesh_generator_2d.cpp index 15a645816cd3..2198158f9ca3 100644 --- a/modules/navigation/2d/nav_mesh_generator_2d.cpp +++ b/modules/navigation/2d/nav_mesh_generator_2d.cpp @@ -243,7 +243,7 @@ void NavMeshGenerator2D::generator_parse_geometry_node(Ref p_ generator_parse_multimeshinstance2d_node(p_navigation_mesh, p_source_geometry_data, p_node); generator_parse_polygon2d_node(p_navigation_mesh, p_source_geometry_data, p_node); generator_parse_staticbody2d_node(p_navigation_mesh, p_source_geometry_data, p_node); - generator_parse_tilemap_node(p_navigation_mesh, p_source_geometry_data, p_node); + generator_parse_tile_map_layer_node(p_navigation_mesh, p_source_geometry_data, p_node); generator_parse_navigationobstacle_node(p_navigation_mesh, p_source_geometry_data, p_node); generator_rid_rwlock.read_lock(); @@ -259,6 +259,14 @@ void NavMeshGenerator2D::generator_parse_geometry_node(Ref p_ for (int i = 0; i < p_node->get_child_count(); i++) { generator_parse_geometry_node(p_navigation_mesh, p_source_geometry_data, p_node->get_child(i), p_recurse_children); } + } else if (Object::cast_to(p_node)) { + // Special case for TileMap, so that internal layer get parsed even if p_recurse_children is false. + for (int i = 0; i < p_node->get_child_count(); i++) { + TileMapLayer *tile_map_layer = Object::cast_to(p_node->get_child(i)); + if (tile_map_layer->get_index_in_tile_map() >= 0) { + generator_parse_tile_map_layer_node(p_navigation_mesh, p_source_geometry_data, tile_map_layer); + } + } } } @@ -580,141 +588,102 @@ void NavMeshGenerator2D::generator_parse_staticbody2d_node(const Ref &p_navigation_mesh, Ref p_source_geometry_data, Node *p_node) { - TileMap *tilemap = Object::cast_to(p_node); - - if (tilemap == nullptr) { +void NavMeshGenerator2D::generator_parse_tile_map_layer_node(const Ref &p_navigation_mesh, Ref p_source_geometry_data, Node *p_node) { + TileMapLayer *tile_map_layer = Object::cast_to(p_node); + if (tile_map_layer == nullptr) { return; } - NavigationPolygon::ParsedGeometryType parsed_geometry_type = p_navigation_mesh->get_parsed_geometry_type(); - uint32_t parsed_collision_mask = p_navigation_mesh->get_parsed_collision_mask(); - - if (tilemap->get_layers_count() <= 0) { - return; - } - - Ref tile_set = tilemap->get_tileset(); + Ref tile_set = tile_map_layer->get_tile_set(); if (!tile_set.is_valid()) { return; } int physics_layers_count = tile_set->get_physics_layers_count(); int navigation_layers_count = tile_set->get_navigation_layers_count(); - if (physics_layers_count <= 0 && navigation_layers_count <= 0) { return; } - HashSet cells_with_navigation_polygon; - HashSet cells_with_collision_polygon; + NavigationPolygon::ParsedGeometryType parsed_geometry_type = p_navigation_mesh->get_parsed_geometry_type(); + uint32_t parsed_collision_mask = p_navigation_mesh->get_parsed_collision_mask(); + + const Transform2D tilemap_xform = p_source_geometry_data->root_node_transform * tile_map_layer->get_global_transform(); - const Transform2D tilemap_xform = p_source_geometry_data->root_node_transform * tilemap->get_global_transform(); + TypedArray used_cells = tile_map_layer->get_used_cells(); + for (int used_cell_index = 0; used_cell_index < used_cells.size(); used_cell_index++) { + const Vector2i &cell = used_cells[used_cell_index]; -#ifdef DEBUG_ENABLED - int error_print_counter = 0; - int error_print_max = 10; -#endif // DEBUG_ENABLED + const TileData *tile_data = tile_map_layer->get_cell_tile_data(cell); + if (tile_data == nullptr) { + continue; + } - for (int tilemap_layer = 0; tilemap_layer < tilemap->get_layers_count(); tilemap_layer++) { - TypedArray used_cells = tilemap->get_used_cells(tilemap_layer); + // Transform flags. + const int alternative_id = tile_map_layer->get_cell_alternative_tile(cell); + bool flip_h = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_H); + bool flip_v = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_V); + bool transpose = (alternative_id & TileSetAtlasSource::TRANSFORM_TRANSPOSE); + + Transform2D tile_transform; + tile_transform.set_origin(tile_map_layer->map_to_local(cell)); + + const Transform2D tile_transform_offset = tilemap_xform * tile_transform; + + // Parse traversable polygons. + for (int navigation_layer = 0; navigation_layer < navigation_layers_count; navigation_layer++) { + Ref navigation_polygon = tile_data->get_navigation_polygon(navigation_layer, flip_h, flip_v, transpose); + if (navigation_polygon.is_valid()) { + for (int outline_index = 0; outline_index < navigation_polygon->get_outline_count(); outline_index++) { + const Vector &navigation_polygon_outline = navigation_polygon->get_outline(outline_index); + if (navigation_polygon_outline.is_empty()) { + continue; + } - for (int used_cell_index = 0; used_cell_index < used_cells.size(); used_cell_index++) { - const Vector2i &cell = used_cells[used_cell_index]; + Vector traversable_outline; + traversable_outline.resize(navigation_polygon_outline.size()); - const TileData *tile_data = tilemap->get_cell_tile_data(tilemap_layer, cell, false); - if (tile_data == nullptr) { - continue; - } + const Vector2 *navigation_polygon_outline_ptr = navigation_polygon_outline.ptr(); + Vector2 *traversable_outline_ptrw = traversable_outline.ptrw(); - // Transform flags. - const int alternative_id = tilemap->get_cell_alternative_tile(tilemap_layer, cell, false); - bool flip_h = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_H); - bool flip_v = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_V); - bool transpose = (alternative_id & TileSetAtlasSource::TRANSFORM_TRANSPOSE); - - Transform2D tile_transform; - tile_transform.set_origin(tilemap->map_to_local(cell)); - - const Transform2D tile_transform_offset = tilemap_xform * tile_transform; - - if (navigation_layers_count > 0) { - Ref navigation_polygon = tile_data->get_navigation_polygon(tilemap_layer, flip_h, flip_v, transpose); - if (navigation_polygon.is_valid()) { - if (cells_with_navigation_polygon.has(cell)) { -#ifdef DEBUG_ENABLED - error_print_counter++; - if (error_print_counter <= error_print_max) { - WARN_PRINT(vformat("TileMap navigation mesh baking error. The TileMap cell key Vector2i(%s, %s) has navigation mesh from 2 or more different TileMap layers assigned. This can cause unexpected navigation mesh baking results. The duplicated cell data was ignored.", cell.x, cell.y)); - } -#endif // DEBUG_ENABLED - } else { - cells_with_navigation_polygon.insert(cell); - - for (int outline_index = 0; outline_index < navigation_polygon->get_outline_count(); outline_index++) { - const Vector &navigation_polygon_outline = navigation_polygon->get_outline(outline_index); - if (navigation_polygon_outline.size() == 0) { - continue; - } - - Vector traversable_outline; - traversable_outline.resize(navigation_polygon_outline.size()); - - const Vector2 *navigation_polygon_outline_ptr = navigation_polygon_outline.ptr(); - Vector2 *traversable_outline_ptrw = traversable_outline.ptrw(); - - for (int traversable_outline_index = 0; traversable_outline_index < traversable_outline.size(); traversable_outline_index++) { - traversable_outline_ptrw[traversable_outline_index] = tile_transform_offset.xform(navigation_polygon_outline_ptr[traversable_outline_index]); - } - - p_source_geometry_data->_add_traversable_outline(traversable_outline); - } + for (int traversable_outline_index = 0; traversable_outline_index < traversable_outline.size(); traversable_outline_index++) { + traversable_outline_ptrw[traversable_outline_index] = tile_transform_offset.xform(navigation_polygon_outline_ptr[traversable_outline_index]); } + + p_source_geometry_data->_add_traversable_outline(traversable_outline); } } + } - if (physics_layers_count > 0 && (parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_STATIC_COLLIDERS || parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_BOTH) && (tile_set->get_physics_layer_collision_layer(tilemap_layer) & parsed_collision_mask)) { - if (cells_with_collision_polygon.has(cell)) { -#ifdef DEBUG_ENABLED - error_print_counter++; - if (error_print_counter <= error_print_max) { - WARN_PRINT(vformat("TileMap navigation mesh baking error. The cell key Vector2i(%s, %s) has collision polygons from 2 or more different TileMap layers assigned that all match the parsed collision mask. This can cause unexpected navigation mesh baking results. The duplicated cell data was ignored.", cell.x, cell.y)); + // Parse obstacles. + for (int physics_layer = 0; physics_layer < physics_layers_count; physics_layer++) { + if ((parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_STATIC_COLLIDERS || parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_BOTH) && + (tile_set->get_physics_layer_collision_layer(physics_layer) & parsed_collision_mask)) { + for (int collision_polygon_index = 0; collision_polygon_index < tile_data->get_collision_polygons_count(physics_layer); collision_polygon_index++) { + PackedVector2Array collision_polygon_points = tile_data->get_collision_polygon_points(physics_layer, collision_polygon_index); + if (collision_polygon_points.is_empty()) { + continue; } -#endif // DEBUG_ENABLED - } else { - cells_with_collision_polygon.insert(cell); - - for (int collision_polygon_index = 0; collision_polygon_index < tile_data->get_collision_polygons_count(tilemap_layer); collision_polygon_index++) { - PackedVector2Array collision_polygon_points = tile_data->get_collision_polygon_points(tilemap_layer, collision_polygon_index); - if (collision_polygon_points.size() == 0) { - continue; - } - - if (flip_h || flip_v || transpose) { - collision_polygon_points = TileData::get_transformed_vertices(collision_polygon_points, flip_h, flip_v, transpose); - } - Vector obstruction_outline; - obstruction_outline.resize(collision_polygon_points.size()); + if (flip_h || flip_v || transpose) { + collision_polygon_points = TileData::get_transformed_vertices(collision_polygon_points, flip_h, flip_v, transpose); + } - const Vector2 *collision_polygon_points_ptr = collision_polygon_points.ptr(); - Vector2 *obstruction_outline_ptrw = obstruction_outline.ptrw(); + Vector obstruction_outline; + obstruction_outline.resize(collision_polygon_points.size()); - for (int obstruction_outline_index = 0; obstruction_outline_index < obstruction_outline.size(); obstruction_outline_index++) { - obstruction_outline_ptrw[obstruction_outline_index] = tile_transform_offset.xform(collision_polygon_points_ptr[obstruction_outline_index]); - } + const Vector2 *collision_polygon_points_ptr = collision_polygon_points.ptr(); + Vector2 *obstruction_outline_ptrw = obstruction_outline.ptrw(); - p_source_geometry_data->_add_obstruction_outline(obstruction_outline); + for (int obstruction_outline_index = 0; obstruction_outline_index < obstruction_outline.size(); obstruction_outline_index++) { + obstruction_outline_ptrw[obstruction_outline_index] = tile_transform_offset.xform(collision_polygon_points_ptr[obstruction_outline_index]); } + + p_source_geometry_data->_add_obstruction_outline(obstruction_outline); } } } } -#ifdef DEBUG_ENABLED - if (error_print_counter > error_print_max) { - ERR_PRINT(vformat("TileMap navigation mesh baking error. A total of %s cells with navigation or collision polygons from 2 or more different TileMap layers overlap. This can cause unexpected navigation mesh baking results. The duplicated cell data was ignored.", error_print_counter)); - } -#endif // DEBUG_ENABLED } void NavMeshGenerator2D::generator_parse_navigationobstacle_node(const Ref &p_navigation_mesh, Ref p_source_geometry_data, Node *p_node) { diff --git a/modules/navigation/2d/nav_mesh_generator_2d.h b/modules/navigation/2d/nav_mesh_generator_2d.h index 235a84d54879..d5f9694242aa 100644 --- a/modules/navigation/2d/nav_mesh_generator_2d.h +++ b/modules/navigation/2d/nav_mesh_generator_2d.h @@ -89,7 +89,7 @@ class NavMeshGenerator2D : public Object { static void generator_parse_multimeshinstance2d_node(const Ref &p_navigation_mesh, Ref p_source_geometry_data, Node *p_node); static void generator_parse_polygon2d_node(const Ref &p_navigation_mesh, Ref p_source_geometry_data, Node *p_node); static void generator_parse_staticbody2d_node(const Ref &p_navigation_mesh, Ref p_source_geometry_data, Node *p_node); - static void generator_parse_tilemap_node(const Ref &p_navigation_mesh, Ref p_source_geometry_data, Node *p_node); + static void generator_parse_tile_map_layer_node(const Ref &p_navigation_mesh, Ref p_source_geometry_data, Node *p_node); static void generator_parse_navigationobstacle_node(const Ref &p_navigation_mesh, Ref p_source_geometry_data, Node *p_node); static bool generator_emit_callback(const Callable &p_callback); diff --git a/modules/navigation/3d/godot_navigation_server_3d.cpp b/modules/navigation/3d/godot_navigation_server_3d.cpp index e942ff622efc..eab6ced4acfd 100644 --- a/modules/navigation/3d/godot_navigation_server_3d.cpp +++ b/modules/navigation/3d/godot_navigation_server_3d.cpp @@ -136,7 +136,7 @@ bool GodotNavigationServer3D::map_is_active(RID p_map) const { NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, false); - return active_maps.find(map) >= 0; + return active_maps.has(map); } COMMAND_2(map_set_up, RID, p_map, Vector3, p_up) { diff --git a/modules/navigation/editor/navigation_mesh_editor_plugin.h b/modules/navigation/editor/navigation_mesh_editor_plugin.h index b73d8d2e69d4..6114c62ebfd6 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/navigation/nav_map.cpp b/modules/navigation/nav_map.cpp index c70fa8c9d508..a6f64bba5ade 100644 --- a/modules/navigation/nav_map.cpp +++ b/modules/navigation/nav_map.cpp @@ -734,7 +734,7 @@ void NavMap::remove_link(NavLink *p_link) { } bool NavMap::has_agent(NavAgent *agent) const { - return (agents.find(agent) >= 0); + return agents.has(agent); } void NavMap::add_agent(NavAgent *agent) { @@ -754,7 +754,7 @@ void NavMap::remove_agent(NavAgent *agent) { } bool NavMap::has_obstacle(NavObstacle *obstacle) const { - return (obstacles.find(obstacle) >= 0); + return obstacles.has(obstacle); } void NavMap::add_obstacle(NavObstacle *obstacle) { diff --git a/modules/noise/editor/noise_editor_plugin.h b/modules/noise/editor/noise_editor_plugin.h index 948ccba29bee..aa94cf4d234e 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 29eb42522fc5..363b7bdc3112 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 bbcb63a7e6f1..ba0e4f6cdd38 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 action_set = OpenXRActionSet::new_action_set("godot", "Godot action set"); add_action_set(action_set); - // Create our actions + // Create our actions. Ref trigger = action_set->add_new_action("trigger", "Trigger", OpenXRAction::OPENXR_ACTION_FLOAT, "/user/hand/left,/user/hand/right"); Ref trigger_click = action_set->add_new_action("trigger_click", "Trigger click", OpenXRAction::OPENXR_ACTION_BOOL, "/user/hand/left,/user/hand/right"); Ref 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 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 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 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 f737f3b64226..4419d24dd306 100644 --- a/modules/openxr/doc_classes/OpenXRAPIExtension.xml +++ b/modules/openxr/doc_classes/OpenXRAPIExtension.xml @@ -54,7 +54,7 @@ - Returns the timing for the next frame. + Returns the predicted display timing for the next frame. @@ -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. + + + + Returns the predicted display timing for the current frame. + + diff --git a/modules/openxr/doc_classes/OpenXRCompositionLayer.xml b/modules/openxr/doc_classes/OpenXRCompositionLayer.xml index b9c69075e14c..6f4ac00f3ed2 100644 --- a/modules/openxr/doc_classes/OpenXRCompositionLayer.xml +++ b/modules/openxr/doc_classes/OpenXRCompositionLayer.xml @@ -32,6 +32,10 @@ Enables the blending the layer using its alpha channel. Can be combined with [member Viewport.transparent_bg] to give the layer a transparent background. + + Enables a technique called "hole punching", which allows putting the composition layer behind the main projection layer (i.e. setting [member sort_order] to a negative value) while "punching a hole" through everything rendered by Godot so that the layer is still visible. + This can be used to create the illusion that the composition layer exists in the same 3D space as everything rendered by Godot, allowing objects to appear to pass both behind or in front of the composition layer. + The [SubViewport] to render on the composition layer. diff --git a/modules/openxr/doc_classes/OpenXRInterface.xml b/modules/openxr/doc_classes/OpenXRInterface.xml index 05dff7d6ae01..309cbe0d722c 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. + + + + 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. + + @@ -145,8 +152,21 @@ The render size multiplier for the current HMD. Must be set before the interface has been initialized. + + The minimum radius around the focal point where full quality is guaranteed if VRS is used as a percentage of screen size. + [b]Note:[/b] Mobile and Forward+ renderers only. Requires [member Viewport.vrs_mode] to be set to [constant Viewport.VRS_XR]. + + + The strength used to calculate the VRS density map. The greater this value, the more noticeable VRS is. This improves performance at the cost of quality. + [b]Note:[/b] Mobile and Forward+ renderers only. Requires [member Viewport.vrs_mode] to be set to [constant Viewport.VRS_XR]. + + + + Informs our OpenXR instance is exiting. + + Informs the user queued a recenter of the player position. @@ -169,6 +189,11 @@ Informs our OpenXR session now has focus. + + + Informs our OpenXR session is in the process of being lost. + + 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 22e8853c8cca..cfe5fed0955e 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 b80f20d0492d..672df0de28a6 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 1fba8e5f8b4d..9a00cecab1b2 100644 --- a/modules/openxr/extensions/openxr_composition_layer_extension.cpp +++ b/modules/openxr/extensions/openxr_composition_layer_extension.cpp @@ -187,7 +187,7 @@ void OpenXRViewportCompositionLayerProvider::on_pre_render() { XrCompositionLayerBaseHeader *OpenXRViewportCompositionLayerProvider::get_composition_layer() { if (openxr_api == nullptr || composition_layer_extension == nullptr) { - // OpenXR not initialised or we're in the editor? + // OpenXR not initialized or we're in the editor? return nullptr; } @@ -260,7 +260,7 @@ XrCompositionLayerBaseHeader *OpenXRViewportCompositionLayerProvider::get_compos bool OpenXRViewportCompositionLayerProvider::update_and_acquire_swapchain(bool p_static_image) { if (openxr_api == nullptr || composition_layer_extension == nullptr) { - // OpenXR not initialised or we're in the editor? + // OpenXR not initialized or we're in the editor? return false; } if (!composition_layer_extension->is_available(composition_layer->type)) { @@ -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_eye_gaze_interaction.cpp b/modules/openxr/extensions/openxr_eye_gaze_interaction.cpp index 477a1c260957..eea996edd939 100644 --- a/modules/openxr/extensions/openxr_eye_gaze_interaction.cpp +++ b/modules/openxr/extensions/openxr_eye_gaze_interaction.cpp @@ -34,6 +34,7 @@ #include "core/os/os.h" #include "../action_map/openxr_interaction_profile_metadata.h" +#include "../openxr_api.h" OpenXREyeGazeInteractionExtension *OpenXREyeGazeInteractionExtension::singleton = nullptr; @@ -106,3 +107,38 @@ void OpenXREyeGazeInteractionExtension::on_register_metadata() { metadata->register_interaction_profile("Eye gaze", "/interaction_profiles/ext/eye_gaze_interaction", XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME); metadata->register_io_path("/interaction_profiles/ext/eye_gaze_interaction", "Gaze pose", "/user/eyes_ext", "/user/eyes_ext/input/gaze_ext/pose", "", OpenXRAction::OPENXR_ACTION_POSE); } + +bool OpenXREyeGazeInteractionExtension::get_eye_gaze_pose(double p_dist, Vector3 &r_eye_pose) { + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL_V(openxr_api, false); + + if (!init_eye_gaze_pose) { + init_eye_gaze_pose = true; + + eye_tracker = openxr_api->find_tracker("/user/eyes_ext"); + if (eye_tracker.is_null()) { + WARN_PRINT("Couldn't obtain eye tracker"); + } + + eye_action = openxr_api->find_action("eye_gaze_pose"); + if (eye_action.is_null()) { + WARN_PRINT("Couldn't obtain pose action for `eye_gaze_pose`, make sure to add this to your action map."); + } + } + + if (eye_tracker.is_null() || eye_action.is_null()) { + return false; + } + + Transform3D eye_transform; + Vector3 linear_velocity; + Vector3 angular_velocity; + XRPose::TrackingConfidence confidence = openxr_api->get_action_pose(eye_action, eye_tracker, eye_transform, linear_velocity, angular_velocity); + if (confidence == XRPose::XR_TRACKING_CONFIDENCE_NONE) { + return false; + } + + r_eye_pose = eye_transform.origin + eye_transform.basis[2] * p_dist; + + return true; +} diff --git a/modules/openxr/extensions/openxr_eye_gaze_interaction.h b/modules/openxr/extensions/openxr_eye_gaze_interaction.h index 2b99f8edff0b..114c1aacc7ec 100644 --- a/modules/openxr/extensions/openxr_eye_gaze_interaction.h +++ b/modules/openxr/extensions/openxr_eye_gaze_interaction.h @@ -50,11 +50,17 @@ class OpenXREyeGazeInteractionExtension : public OpenXRExtensionWrapper { virtual void on_register_metadata() override; + bool get_eye_gaze_pose(double p_dist, Vector3 &r_eye_pose); + private: static OpenXREyeGazeInteractionExtension *singleton; bool available = false; XrSystemEyeGazeInteractionPropertiesEXT properties; + + bool init_eye_gaze_pose = false; + RID eye_tracker; + RID eye_action; }; #endif // OPENXR_EYE_GAZE_INTERACTION_H 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 000000000000..65de4b23c4a2 --- /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 OpenXRHandInteractionExtension::get_requested_extensions() { + HashMap 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 000000000000..789e300c0b7c --- /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 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 12fa3bed7e88..b8a2f5893591 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; diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index 1fe402341ba7..0742cae26a5b 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -54,6 +54,7 @@ #endif #include "extensions/openxr_composition_layer_depth_extension.h" +#include "extensions/openxr_eye_gaze_interaction.h" #include "extensions/openxr_fb_display_refresh_rate_extension.h" #include "extensions/openxr_fb_foveation_extension.h" #include "extensions/openxr_fb_update_swapchain_extension.h" @@ -160,7 +161,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 +194,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 +215,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 +771,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 +923,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 +935,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 +1019,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 +1045,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 +1142,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 +1161,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 +1176,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 +1187,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 +1224,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 +1265,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 +1316,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(); @@ -1316,8 +1335,8 @@ bool OpenXRAPI::on_state_synchronized() { // Just in case, see if we already have active trackers... List trackers; tracker_owner.get_owned_list(&trackers); - for (int i = 0; i < trackers.size(); i++) { - tracker_check_profile(trackers[i]); + for (const RID &tracker : trackers) { + tracker_check_profile(tracker); } for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { @@ -1374,34 +1393,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 +1441,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 +1644,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 +1659,8 @@ bool OpenXRAPI::initialize_session() { return false; } + allocate_view_buffers(view_count, submit_depth_buffer); + return true; } @@ -1696,12 +1712,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 +1735,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 +1784,87 @@ 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); +} + +Vector2 OpenXRAPI::get_eye_focus(uint32_t p_view, float p_aspect) { + ERR_FAIL_NULL_V(graphics_extension, Vector2()); + + if (!render_state.running) { + return Vector2(); + } + + // xrWaitFrame not run yet + if (render_state.predicted_display_time == 0) { + return Vector2(); + } + + // we don't have valid view info + if (render_state.views == nullptr || !render_state.view_pose_valid) { + return Vector2(); + } + + Projection cm; + if (!graphics_extension->create_projection_fov(render_state.views[p_view].fov, 0.1, 1000.0, cm)) { + return Vector2(); + } + + // Default focus to center... + Vector3 focus = cm.xform(Vector3(0.0, 0.0, 999.9)); + + // Lets check for eye tracking... + OpenXREyeGazeInteractionExtension *eye_gaze_interaction = OpenXREyeGazeInteractionExtension::get_singleton(); + if (eye_gaze_interaction && eye_gaze_interaction->supports_eye_gaze_interaction()) { + Vector3 eye_gaze_pose; + if (eye_gaze_interaction->get_eye_gaze_pose(1.0, eye_gaze_pose)) { + Transform3D view_transform = transform_from_pose(render_state.views[p_view].pose); + + eye_gaze_pose = view_transform.xform_inv(eye_gaze_pose); + focus = cm.xform(eye_gaze_pose); + } + } + + return Vector2(focus.x, focus.y); } bool OpenXRAPI::poll_events() { @@ -1910,8 +1963,8 @@ bool OpenXRAPI::poll_events() { List trackers; tracker_owner.get_owned_list(&trackers); - for (int i = 0; i < trackers.size(); i++) { - tracker_check_profile(trackers[i], event->session); + for (const RID &tracker : trackers) { + tracker_check_profile(tracker, event->session); } } break; @@ -1934,53 +1987,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; + } + + if (!running) { + 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 + // 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 +2080,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,11 +2091,53 @@ 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 +2157,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 +2166,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 +2179,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 +2200,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 +2231,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 +2269,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 +2312,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 ordered_layers_list; @@ -2210,9 +2352,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 +2370,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(layers_list.size()), // layerCount layers_list.ptr() // layers @@ -2271,6 +2413,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 +2557,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() { @@ -2535,10 +2674,23 @@ bool OpenXRAPI::xr_result(XrResult result, const char *format, Array args) const RID OpenXRAPI::get_tracker_rid(XrPath p_path) { List current; tracker_owner.get_owned_list(¤t); - for (int i = 0; i < current.size(); i++) { - Tracker *tracker = tracker_owner.get_or_null(current[i]); + for (const RID &E : current) { + Tracker *tracker = tracker_owner.get_or_null(E); if (tracker && tracker->toplevel_path == p_path) { - return current[i]; + return E; + } + } + + return RID(); +} + +RID OpenXRAPI::find_tracker(const String &p_name) { + List current; + tracker_owner.get_owned_list(¤t); + for (const RID &E : current) { + Tracker *tracker = tracker_owner.get_or_null(E); + if (tracker && tracker->name == p_name) { + return E; } } @@ -2729,10 +2881,23 @@ void OpenXRAPI::action_set_free(RID p_action_set) { RID OpenXRAPI::get_action_rid(XrAction p_action) { List current; action_owner.get_owned_list(¤t); - for (int i = 0; i < current.size(); i++) { - Action *action = action_owner.get_or_null(current[i]); + for (const RID &E : current) { + Action *action = action_owner.get_or_null(E); if (action && action->handle == p_action) { - return current[i]; + return E; + } + } + + return RID(); +} + +RID OpenXRAPI::find_action(const String &p_name) { + List current; + action_owner.get_owned_list(¤t); + for (const RID &E : current) { + Action *action = action_owner.get_or_null(E); + if (action && action->name == p_name) { + return E; } } @@ -2833,10 +2998,10 @@ void OpenXRAPI::action_free(RID p_action) { RID OpenXRAPI::get_interaction_profile_rid(XrPath p_path) { List current; interaction_profile_owner.get_owned_list(¤t); - for (int i = 0; i < current.size(); i++) { - InteractionProfile *ip = interaction_profile_owner.get_or_null(current[i]); + for (const RID &E : current) { + InteractionProfile *ip = interaction_profile_owner.get_or_null(E); if (ip && ip->path == p_path) { - return current[i]; + return E; } } @@ -3132,7 +3297,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 e83536620040..f9d2e6014836 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 -// 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 @@ class OpenXRAPI { 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 @@ class OpenXRAPI { 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 @@ class OpenXRAPI { 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 @@ class OpenXRAPI { // 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 @@ class OpenXRAPI { 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); @@ -394,6 +456,7 @@ class OpenXRAPI { XRPose::TrackingConfidence get_head_center(Transform3D &r_transform, Vector3 &r_linear_velocity, Vector3 &r_angular_velocity); bool get_view_transform(uint32_t p_view, Transform3D &r_transform); bool get_view_projection(uint32_t p_view, double p_z_near, double p_z_far, Projection &p_camera_matrix); + Vector2 get_eye_focus(uint32_t p_view, float p_aspect); bool process(); void pre_render(); @@ -453,6 +516,9 @@ class OpenXRAPI { bool interaction_profile_suggest_bindings(RID p_interaction_profile); void interaction_profile_free(RID p_interaction_profile); + RID find_tracker(const String &p_name); + RID find_action(const String &p_name); + bool sync_action_sets(const Vector p_active_sets); bool get_action_bool(RID p_action, RID p_tracker); float get_action_float(RID p_action, RID p_tracker); diff --git a/modules/openxr/openxr_api_extension.cpp b/modules/openxr/openxr_api_extension.cpp index fae0fc13d35f..a1744fa1dbc2 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 576e497798f8..cff2c4738e04 100644 --- a/modules/openxr/openxr_api_extension.h +++ b/modules/openxr/openxr_api_extension.h @@ -69,6 +69,7 @@ class OpenXRAPIExtension : public RefCounted { 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 48d0f57e3617..96eb884b022b 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,8 +94,19 @@ 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); + // VRS + ClassDB::bind_method(D_METHOD("get_vrs_min_radius"), &OpenXRInterface::get_vrs_min_radius); + ClassDB::bind_method(D_METHOD("set_vrs_min_radius", "radius"), &OpenXRInterface::set_vrs_min_radius); + ClassDB::bind_method(D_METHOD("get_vrs_strength"), &OpenXRInterface::get_vrs_strength); + ClassDB::bind_method(D_METHOD("set_vrs_strength", "strength"), &OpenXRInterface::set_vrs_strength); + + ADD_GROUP("Vulkan VRS", "vrs_"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "vrs_min_radius", PROPERTY_HINT_RANGE, "1.0,100.0,1.0"), "set_vrs_min_radius", "get_vrs_min_radius"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "vrs_strength", PROPERTY_HINT_RANGE, "0.1,10.0,0.1"), "set_vrs_strength", "get_vrs_strength"); + BIND_ENUM_CONSTANT(HAND_LEFT); BIND_ENUM_CONSTANT(HAND_RIGHT); BIND_ENUM_CONSTANT(HAND_MAX); @@ -807,6 +821,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; @@ -854,6 +883,22 @@ Array OpenXRInterface::get_action_sets() const { return arr; } +float OpenXRInterface::get_vrs_min_radius() const { + return xr_vrs.get_vrs_min_radius(); +} + +void OpenXRInterface::set_vrs_min_radius(float p_vrs_min_radius) { + xr_vrs.set_vrs_min_radius(p_vrs_min_radius); +} + +float OpenXRInterface::get_vrs_strength() const { + return xr_vrs.get_vrs_strength(); +} + +void OpenXRInterface::set_vrs_strength(float p_vrs_strength) { + xr_vrs.set_vrs_strength(p_vrs_strength); +} + double OpenXRInterface::get_render_target_size_multiplier() const { if (openxr_api == nullptr) { return 1.0; @@ -1259,6 +1304,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")); } @@ -1409,6 +1462,24 @@ Vector3 OpenXRInterface::get_hand_joint_angular_velocity(Hand p_hand, HandJoints return Vector3(); } +RID OpenXRInterface::get_vrs_texture() { + if (!openxr_api) { + return RID(); + } + + PackedVector2Array eye_foci; + + Size2 target_size = get_render_target_size(); + real_t aspect_ratio = target_size.x / target_size.y; + uint32_t view_count = get_view_count(); + + for (uint32_t v = 0; v < view_count; v++) { + eye_foci.push_back(openxr_api->get_eye_focus(v, aspect_ratio)); + } + + return xr_vrs.make_vrs_texture(target_size, eye_foci); +} + OpenXRInterface::OpenXRInterface() { openxr_api = OpenXRAPI::get_singleton(); if (openxr_api) { diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h index e916c7dac205..f0ee0dc3c4ed 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" @@ -57,6 +80,8 @@ class OpenXRInterface : public XRInterface { XRPose::TrackingConfidence head_confidence; Transform3D transform_for_view[2]; // We currently assume 2, but could be 4 for VARJO which we do not support yet + XRVRS xr_vrs; + void _load_action_map(); struct Action { // An action we've registered with OpenXR @@ -110,6 +135,7 @@ class OpenXRInterface : public XRInterface { 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; @@ -144,6 +170,12 @@ class OpenXRInterface : public XRInterface { bool get_foveation_dynamic() const; void set_foveation_dynamic(bool p_foveation_dynamic); + float get_vrs_min_radius() const; + void set_vrs_min_radius(float p_vrs_min_radius); + + float get_vrs_strength() const; + void set_vrs_strength(float p_vrs_strength); + virtual Size2 get_render_target_size() override; virtual uint32_t get_view_count() override; virtual Transform3D get_camera_transform() override; @@ -173,6 +205,8 @@ class OpenXRInterface : public XRInterface { 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); @@ -250,6 +284,8 @@ class OpenXRInterface : public XRInterface { Vector3 get_hand_joint_linear_velocity(Hand p_hand, HandJoints p_joint) const; Vector3 get_hand_joint_angular_velocity(Hand p_hand, HandJoints p_joint) const; + virtual RID get_vrs_texture() override; + OpenXRInterface(); ~OpenXRInterface(); }; diff --git a/modules/openxr/register_types.cpp b/modules/openxr/register_types.cpp index eb0527f07c4f..85514737f269 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/openxr/scene/openxr_composition_layer.cpp b/modules/openxr/scene/openxr_composition_layer.cpp index ce883b79b33b..219f176479a0 100644 --- a/modules/openxr/scene/openxr_composition_layer.cpp +++ b/modules/openxr/scene/openxr_composition_layer.cpp @@ -40,6 +40,13 @@ HashSet OpenXRCompositionLayer::viewports_in_use; +static const char *HOLE_PUNCH_SHADER_CODE = + "shader_type spatial;\n" + "render_mode blend_mix, depth_draw_opaque, cull_back, shadow_to_opacity, shadows_disabled;\n" + "void fragment() {\n" + "\tALBEDO = vec3(0.0, 0.0, 0.0);\n" + "}\n"; + OpenXRCompositionLayer::OpenXRCompositionLayer() { openxr_api = OpenXRAPI::get_singleton(); composition_layer_extension = OpenXRCompositionLayerExtension::get_singleton(); @@ -80,6 +87,9 @@ void OpenXRCompositionLayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_layer_viewport", "viewport"), &OpenXRCompositionLayer::set_layer_viewport); ClassDB::bind_method(D_METHOD("get_layer_viewport"), &OpenXRCompositionLayer::get_layer_viewport); + ClassDB::bind_method(D_METHOD("set_enable_hole_punch", "enable"), &OpenXRCompositionLayer::set_enable_hole_punch); + ClassDB::bind_method(D_METHOD("get_enable_hole_punch"), &OpenXRCompositionLayer::get_enable_hole_punch); + ClassDB::bind_method(D_METHOD("set_sort_order", "order"), &OpenXRCompositionLayer::set_sort_order); ClassDB::bind_method(D_METHOD("get_sort_order"), &OpenXRCompositionLayer::get_sort_order); @@ -93,6 +103,16 @@ void OpenXRCompositionLayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "layer_viewport", PROPERTY_HINT_NODE_TYPE, "SubViewport"), "set_layer_viewport", "get_layer_viewport"); ADD_PROPERTY(PropertyInfo(Variant::INT, "sort_order", PROPERTY_HINT_NONE, ""), "set_sort_order", "get_sort_order"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "alpha_blend", PROPERTY_HINT_NONE, ""), "set_alpha_blend", "get_alpha_blend"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enable_hole_punch", PROPERTY_HINT_NONE, ""), "set_enable_hole_punch", "get_enable_hole_punch"); +} + +bool OpenXRCompositionLayer::_should_use_fallback_node() { + if (Engine::get_singleton()->is_editor_hint()) { + return true; + } else if (openxr_session_running) { + return enable_hole_punch || !is_natively_supported(); + } + return false; } void OpenXRCompositionLayer::_create_fallback_node() { @@ -103,21 +123,27 @@ void OpenXRCompositionLayer::_create_fallback_node() { should_update_fallback_mesh = true; } +void OpenXRCompositionLayer::_remove_fallback_node() { + ERR_FAIL_COND(fallback != nullptr); + remove_child(fallback); + fallback->queue_free(); + fallback = nullptr; +} + void OpenXRCompositionLayer::_on_openxr_session_begun() { - if (!is_natively_supported()) { - if (!fallback) { - _create_fallback_node(); - } - } else if (layer_viewport && is_visible() && is_inside_tree()) { + openxr_session_running = true; + if (layer_viewport && is_natively_supported() && is_visible() && is_inside_tree()) { openxr_layer_provider->set_viewport(layer_viewport->get_viewport_rid(), layer_viewport->get_size()); } + if (!fallback && _should_use_fallback_node()) { + _create_fallback_node(); + } } void OpenXRCompositionLayer::_on_openxr_session_stopping() { - if (fallback && !Engine::get_singleton()->is_editor_hint()) { - fallback->queue_free(); - remove_child(fallback); - fallback = nullptr; + openxr_session_running = false; + if (fallback && !_should_use_fallback_node()) { + _remove_fallback_node(); } else { openxr_layer_provider->set_viewport(RID(), Size2i()); } @@ -152,7 +178,7 @@ void OpenXRCompositionLayer::set_layer_viewport(SubViewport *p_viewport) { if (fallback) { _reset_fallback_material(); - } else if (openxr_api && openxr_api->is_running() && is_visible() && is_inside_tree()) { + } else if (openxr_session_running && is_visible() && is_inside_tree()) { if (layer_viewport) { openxr_layer_provider->set_viewport(layer_viewport->get_viewport_rid(), layer_viewport->get_size()); } else { @@ -165,9 +191,33 @@ SubViewport *OpenXRCompositionLayer::get_layer_viewport() const { return layer_viewport; } +void OpenXRCompositionLayer::set_enable_hole_punch(bool p_enable) { + if (enable_hole_punch == p_enable) { + return; + } + + enable_hole_punch = p_enable; + if (_should_use_fallback_node()) { + if (fallback) { + _reset_fallback_material(); + } else { + _create_fallback_node(); + } + } else if (fallback) { + _remove_fallback_node(); + } + + update_configuration_warnings(); +} + +bool OpenXRCompositionLayer::get_enable_hole_punch() const { + return enable_hole_punch; +} + void OpenXRCompositionLayer::set_sort_order(int p_order) { if (openxr_layer_provider) { openxr_layer_provider->set_sort_order(p_order); + update_configuration_warnings(); } } @@ -212,15 +262,28 @@ void OpenXRCompositionLayer::_reset_fallback_material() { return; } - if (layer_viewport) { + if (enable_hole_punch && !Engine::get_singleton()->is_editor_hint() && is_natively_supported()) { + Ref material = fallback->get_surface_override_material(0); + if (material.is_null()) { + Ref shader; + shader.instantiate(); + shader->set_code(HOLE_PUNCH_SHADER_CODE); + + material.instantiate(); + material->set_shader(shader); + + fallback->set_surface_override_material(0, material); + } + } else if (layer_viewport) { Ref material = fallback->get_surface_override_material(0); if (material.is_null()) { material.instantiate(); material->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED); - material->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true); material->set_local_to_scene(true); fallback->set_surface_override_material(0, material); } + + material->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, !enable_hole_punch); material->set_transparency(get_alpha_blend() ? StandardMaterial3D::TRANSPARENCY_ALPHA : StandardMaterial3D::TRANSPARENCY_DISABLED); Ref texture = material->get_texture(StandardMaterial3D::TEXTURE_ALBEDO); @@ -260,7 +323,7 @@ void OpenXRCompositionLayer::_notification(int p_what) { } } break; case NOTIFICATION_VISIBILITY_CHANGED: { - if (!fallback && openxr_api && openxr_api->is_running() && is_inside_tree()) { + if (!fallback && openxr_session_running && is_inside_tree()) { if (layer_viewport && is_visible()) { openxr_layer_provider->set_viewport(layer_viewport->get_viewport_rid(), layer_viewport->get_size()); } else { @@ -277,7 +340,7 @@ void OpenXRCompositionLayer::_notification(int p_what) { composition_layer_extension->register_viewport_composition_layer_provider(openxr_layer_provider); } - if (!fallback && layer_viewport && openxr_api && openxr_api->is_running() && is_visible()) { + if (!fallback && layer_viewport && openxr_session_running && is_visible()) { openxr_layer_provider->set_viewport(layer_viewport->get_viewport_rid(), layer_viewport->get_size()); } } break; @@ -347,5 +410,9 @@ PackedStringArray OpenXRCompositionLayer::get_configuration_warnings() const { warnings.push_back(RTR("OpenXR composition layers must have orthonormalized transforms (ie. no scale or shearing).")); } + if (enable_hole_punch && get_sort_order() >= 0) { + warnings.push_back(RTR("Hole punching won't work as expected unless the sort order is less than zero.")); + } + return warnings; } diff --git a/modules/openxr/scene/openxr_composition_layer.h b/modules/openxr/scene/openxr_composition_layer.h index 9f064379d398..7998d096b6df 100644 --- a/modules/openxr/scene/openxr_composition_layer.h +++ b/modules/openxr/scene/openxr_composition_layer.h @@ -46,13 +46,17 @@ class OpenXRCompositionLayer : public Node3D { GDCLASS(OpenXRCompositionLayer, Node3D); SubViewport *layer_viewport = nullptr; + bool enable_hole_punch = false; MeshInstance3D *fallback = nullptr; bool should_update_fallback_mesh = false; + bool openxr_session_running = false; Dictionary extension_property_values; + bool _should_use_fallback_node(); void _create_fallback_node(); void _reset_fallback_material(); + void _remove_fallback_node(); protected: OpenXRAPI *openxr_api = nullptr; @@ -79,6 +83,9 @@ class OpenXRCompositionLayer : public Node3D { void set_layer_viewport(SubViewport *p_viewport); SubViewport *get_layer_viewport() const; + void set_enable_hole_punch(bool p_enable); + bool get_enable_hole_punch() const; + void set_sort_order(int p_order); int get_sort_order() const; diff --git a/modules/raycast/config.py b/modules/raycast/config.py index 26329d813af0..0fd35af528f2 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 1d9d36fbbfa8..fcf3f643152d 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). +# +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 32dbc59fd4c9..3453c3e8f02f 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_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 1ed335fe9943..09a037fd28da 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -7342,6 +7342,16 @@ bool TextServerAdvanced::_is_valid_identifier(const String &p_string) const { return true; } +bool TextServerAdvanced::_is_valid_letter(char32_t p_unicode) const { +#ifndef ICU_STATIC_DATA + if (!icu_data_loaded) { + return TextServer::is_valid_letter(p_unicode); + } +#endif + + return u_isalpha(p_unicode); +} + TextServerAdvanced::TextServerAdvanced() { _insert_num_systems_lang(); _insert_feature_sets(); diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index 1cd73a69998b..7e29f984c166 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -988,6 +988,7 @@ class TextServerAdvanced : public TextServerExtension { MODBIND1RC(String, strip_diacritics, const String &); MODBIND1RC(bool, is_valid_identifier, const String &); + MODBIND1RC(bool, is_valid_letter, char32_t); MODBIND2RC(String, string_to_upper, const String &, const String &); MODBIND2RC(String, string_to_lower, const String &, const String &); diff --git a/modules/text_server_fb/gdextension_build/SConstruct b/modules/text_server_fb/gdextension_build/SConstruct index 29801ede8eaa..07940719eb56 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). +# +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 32dbc59fd4c9..3453c3e8f02f 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/upnp/SCsub b/modules/upnp/SCsub index 4b385b820db8..98c03e9ee90f 100644 --- a/modules/upnp/SCsub +++ b/modules/upnp/SCsub @@ -30,7 +30,8 @@ if env["builtin_miniupnpc"]: env_upnp.Prepend(CPPPATH=[thirdparty_dir + "include"]) env_upnp.Append(CPPDEFINES=["MINIUPNP_STATICLIB"]) - env_upnp.Append(CPPDEFINES=["MINIUPNPC_SET_SOCKET_TIMEOUT"]) + if env["platform"] != "windows": + env_upnp.Append(CPPDEFINES=["MINIUPNPC_SET_SOCKET_TIMEOUT"]) env_thirdparty = env_upnp.Clone() env_thirdparty.disable_warnings() diff --git a/modules/webrtc/webrtc_multiplayer_peer.cpp b/modules/webrtc/webrtc_multiplayer_peer.cpp index 9224760c5bc5..45fa9f46ca46 100644 --- a/modules/webrtc/webrtc_multiplayer_peer.cpp +++ b/modules/webrtc/webrtc_multiplayer_peer.cpp @@ -59,7 +59,7 @@ int WebRTCMultiplayerPeer::get_packet_channel() const { MultiplayerPeer::TransferMode WebRTCMultiplayerPeer::get_packet_mode() const { ERR_FAIL_INDEX_V(next_packet_channel, channels_modes.size(), TRANSFER_MODE_RELIABLE); - return channels_modes[next_packet_channel]; + return channels_modes.get(next_packet_channel); } bool WebRTCMultiplayerPeer::is_server() const { @@ -308,18 +308,18 @@ Error WebRTCMultiplayerPeer::add_peer(Ref p_peer, int p_pe cfg["ordered"] = true; cfg["id"] = 1; - peer->channels[CH_RELIABLE] = p_peer->create_data_channel("reliable", cfg); - ERR_FAIL_COND_V(peer->channels[CH_RELIABLE].is_null(), FAILED); + peer->channels.get(CH_RELIABLE) = p_peer->create_data_channel("reliable", cfg); + ERR_FAIL_COND_V(peer->channels.get(CH_RELIABLE).is_null(), FAILED); cfg["id"] = 2; cfg["maxPacketLifetime"] = p_unreliable_lifetime; - peer->channels[CH_ORDERED] = p_peer->create_data_channel("ordered", cfg); - ERR_FAIL_COND_V(peer->channels[CH_ORDERED].is_null(), FAILED); + peer->channels.get(CH_ORDERED) = p_peer->create_data_channel("ordered", cfg); + ERR_FAIL_COND_V(peer->channels.get(CH_ORDERED).is_null(), FAILED); cfg["id"] = 3; cfg["ordered"] = false; - peer->channels[CH_UNRELIABLE] = p_peer->create_data_channel("unreliable", cfg); - ERR_FAIL_COND_V(peer->channels[CH_UNRELIABLE].is_null(), FAILED); + peer->channels.get(CH_UNRELIABLE) = p_peer->create_data_channel("unreliable", cfg); + ERR_FAIL_COND_V(peer->channels.get(CH_UNRELIABLE).is_null(), FAILED); for (const Dictionary &dict : channels_config) { Ref ch = p_peer->create_data_channel(String::num_int64(dict["id"]), dict); @@ -400,8 +400,8 @@ Error WebRTCMultiplayerPeer::put_packet(const uint8_t *p_buffer, int p_buffer_si ERR_FAIL_COND_V_MSG(!E, ERR_INVALID_PARAMETER, "Invalid target peer: " + itos(target_peer) + "."); ERR_FAIL_COND_V_MSG(E->value->channels.size() <= ch, ERR_INVALID_PARAMETER, vformat("Unable to send packet on channel %d, max channels: %d", ch, E->value->channels.size())); - ERR_FAIL_COND_V(E->value->channels[ch].is_null(), ERR_BUG); - return E->value->channels[ch]->put_packet(p_buffer, p_buffer_size); + ERR_FAIL_COND_V(E->value->channels.get(ch).is_null(), ERR_BUG); + return E->value->channels.get(ch)->put_packet(p_buffer, p_buffer_size); } else { int exclude = -target_peer; @@ -413,8 +413,8 @@ Error WebRTCMultiplayerPeer::put_packet(const uint8_t *p_buffer, int p_buffer_si } ERR_CONTINUE_MSG(F.value->channels.size() <= ch, vformat("Unable to send packet on channel %d, max channels: %d", ch, F.value->channels.size())); - ERR_CONTINUE(F.value->channels[ch].is_null()); - F.value->channels[ch]->put_packet(p_buffer, p_buffer_size); + ERR_CONTINUE(F.value->channels.get(ch).is_null()); + F.value->channels.get(ch)->put_packet(p_buffer, p_buffer_size); } } return OK; diff --git a/modules/websocket/remote_debugger_peer_websocket.cpp b/modules/websocket/remote_debugger_peer_websocket.cpp index dc6833e8c3ba..a40ea0205e68 100644 --- a/modules/websocket/remote_debugger_peer_websocket.cpp +++ b/modules/websocket/remote_debugger_peer_websocket.cpp @@ -74,7 +74,7 @@ void RemoteDebuggerPeerWebSocket::poll() { } while (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN && out_queue.size() > 0) { - Array var = out_queue[0]; + Array var = out_queue.front()->get(); Error err = ws_peer->put_var(var); ERR_BREAK(err != OK); // Peer buffer full? out_queue.pop_front(); @@ -92,7 +92,7 @@ bool RemoteDebuggerPeerWebSocket::has_message() { Array RemoteDebuggerPeerWebSocket::get_message() { ERR_FAIL_COND_V(in_queue.is_empty(), Array()); - Array msg = in_queue[0]; + Array msg = in_queue.front()->get(); in_queue.pop_front(); return msg; } diff --git a/platform/android/SCsub b/platform/android/SCsub index 9266713ea1ba..6ba4fd1231d8 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: @@ -84,10 +86,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", ], @@ -96,8 +109,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 fea8ec3287c7..6a8c4ed86d69 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -2,7 +2,7 @@ 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 c6f2f821178b..9869756be141 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/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 3b1a534daf28..30d57cade58c 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -2974,11 +2974,11 @@ void EditorExportPlatformAndroid::_remove_copied_libs(String p_gdextension_libs_ String EditorExportPlatformAndroid::join_list(const List &p_parts, const String &p_separator) { String ret; - for (int i = 0; i < p_parts.size(); ++i) { - if (i > 0) { + for (List::ConstIterator itr = p_parts.begin(); itr != p_parts.end(); ++itr) { + if (itr != p_parts.begin()) { ret += p_separator; } - ret += p_parts[i]; + ret += *itr; } return ret; } diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index ce53aeebcb7e..fbdf07e6c24a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -894,16 +894,25 @@ class Godot(private val context: Context) : SensorEventListener { */ @SuppressLint("MissingPermission") @Keep - private fun vibrate(durationMs: Int) { + private fun vibrate(durationMs: Int, amplitude: Int) { if (durationMs > 0 && requestPermission("VIBRATE")) { val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibratorService.vibrate( - VibrationEffect.createOneShot( - durationMs.toLong(), - VibrationEffect.DEFAULT_AMPLITUDE + if (amplitude <= -1) { + vibratorService.vibrate( + VibrationEffect.createOneShot( + durationMs.toLong(), + VibrationEffect.DEFAULT_AMPLITUDE + ) ) - ) + } else { + vibratorService.vibrate( + VibrationEffect.createOneShot( + durationMs.toLong(), + amplitude + ) + ) + } } else { // deprecated in API 26 vibratorService.vibrate(durationMs.toLong()) diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 61be6fc5db3d..6e7f5ef5a178 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -72,7 +72,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;"); _get_ca_certificates = p_env->GetMethodID(godot_class, "getCACertificates", "()Ljava/lang/String;"); _init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V"); - _vibrate = p_env->GetMethodID(godot_class, "vibrate", "(I)V"); + _vibrate = p_env->GetMethodID(godot_class, "vibrate", "(II)V"); _get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;"); _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); @@ -331,11 +331,18 @@ void GodotJavaWrapper::init_input_devices() { } } -void GodotJavaWrapper::vibrate(int p_duration_ms) { +void GodotJavaWrapper::vibrate(int p_duration_ms, float p_amplitude) { if (_vibrate) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL(env); - env->CallVoidMethod(godot_instance, _vibrate, p_duration_ms); + + int j_amplitude = -1.0; + + if (p_amplitude != -1.0) { + j_amplitude = CLAMP(int(p_amplitude * 255), 1, 255); + } + + env->CallVoidMethod(godot_instance, _vibrate, p_duration_ms, j_amplitude); } } @@ -344,8 +351,9 @@ int GodotJavaWrapper::create_new_godot_instance(const List &args) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, 0); jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); - for (int i = 0; i < args.size(); i++) { - jstring j_arg = env->NewStringUTF(args[i].utf8().get_data()); + int i = 0; + for (List::ConstIterator itr = args.begin(); itr != args.end(); ++itr, ++i) { + jstring j_arg = env->NewStringUTF(itr->utf8().get_data()); env->SetObjectArrayElement(jargs, i, j_arg); env->DeleteLocalRef(j_arg); } diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 93998021a93f..e86391d4e3e7 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -102,7 +102,7 @@ class GodotJavaWrapper { Vector get_granted_permissions() const; String get_ca_certificates() const; void init_input_devices(); - void vibrate(int p_duration_ms); + void vibrate(int p_duration_ms, float p_amplitude = -1.0); String get_input_fallback_mapping(); int create_new_godot_instance(const List &args); void begin_benchmark_measure(const String &p_context, const String &p_label); diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 463a30785480..c60125c34e32 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -746,8 +746,8 @@ ANativeWindow *OS_Android::get_native_window() const { #endif } -void OS_Android::vibrate_handheld(int p_duration_ms) { - godot_java->vibrate(p_duration_ms); +void OS_Android::vibrate_handheld(int p_duration_ms, float p_amplitude) { + godot_java->vibrate(p_duration_ms, p_amplitude); } String OS_Android::get_config_path() const { diff --git a/platform/android/os_android.h b/platform/android/os_android.h index 7bdbeef77a90..b150ef4f6161 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -153,7 +153,7 @@ class OS_Android : public OS_Unix { virtual Error move_to_trash(const String &p_path) override; - void vibrate_handheld(int p_duration_ms) override; + void vibrate_handheld(int p_duration_ms, float p_amplitude = -1.0) override; virtual String get_config_path() const override; diff --git a/platform/ios/detect.py b/platform/ios/detect.py index 0c9b7b32040a..e3bac4ec5ce8 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 cd6f855d776e..62bc55dce83a 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -218,7 +218,7 @@ } 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 0c0ded5fea3b..20c16478430d 100644 --- a/platform/ios/doc_classes/EditorExportPlatformIOS.xml +++ b/platform/ios/doc_classes/EditorExportPlatformIOS.xml @@ -29,6 +29,9 @@ The "Full Name", "Common Name" or SHA-1 hash of the signing identity used for release export. + + If [code]true[/code], existing "project name" and "project name.xcodeproj" in the export destination directory will be unconditionally deleted during export. + Application distribution target (debug export). @@ -123,12 +126,441 @@ 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]. + + 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]. + A message displayed when requesting access to the device's camera (in English). A message displayed when requesting access to the device's camera (localized). + + Indicates whether your app collects advertising data. + + + 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]. + + + Indicates whether your app links advertising data to the user's identity. + + + Indicates whether your app uses advertising data for tracking. + + + Indicates whether your app collects audio data data. + + + 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]. + + + Indicates whether your app links audio data data to the user's identity. + + + Indicates whether your app uses audio data data for tracking. + + + Indicates whether your app collects browsing history. + + + 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]. + + + Indicates whether your app links browsing history to the user's identity. + + + Indicates whether your app uses browsing history for tracking. + + + Indicates whether your app collects coarse location data. + + + 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]. + + + Indicates whether your app links coarse location data to the user's identity. + + + Indicates whether your app uses coarse location data for tracking. + + + Indicates whether your app collects contacts. + + + 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]. + + + Indicates whether your app links contacts to the user's identity. + + + Indicates whether your app uses contacts for tracking. + + + Indicates whether your app collects crash data. + + + 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]. + + + Indicates whether your app links crash data to the user's identity. + + + Indicates whether your app uses crash data for tracking. + + + Indicates whether your app collects credit information. + + + 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]. + + + Indicates whether your app links credit information to the user's identity. + + + Indicates whether your app uses credit information for tracking. + + + Indicates whether your app collects customer support data. + + + 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]. + + + Indicates whether your app links customer support data to the user's identity. + + + Indicates whether your app uses customer support data for tracking. + + + Indicates whether your app collects device IDs. + + + 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]. + + + Indicates whether your app links device IDs to the user's identity. + + + Indicates whether your app uses device IDs for tracking. + + + Indicates whether your app collects email address. + + + 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]. + + + Indicates whether your app links email address to the user's identity. + + + Indicates whether your app uses email address for tracking. + + + Indicates whether your app collects emails or text messages. + + + 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]. + + + Indicates whether your app links emails or text messages to the user's identity. + + + Indicates whether your app uses emails or text messages for tracking. + + + Indicates whether your app collects environment scanning data. + + + 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]. + + + Indicates whether your app links environment scanning data to the user's identity. + + + Indicates whether your app uses environment scanning data for tracking. + + + Indicates whether your app collects fitness and exercise data. + + + 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]. + + + Indicates whether your app links fitness and exercise data to the user's identity. + + + Indicates whether your app uses fitness and exercise data for tracking. + + + Indicates whether your app collects gameplay content. + + + 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]. + + + Indicates whether your app links gameplay content to the user's identity. + + + Indicates whether your app uses gameplay content for tracking. + + + Indicates whether your app collects user's hand structure and hand movements. + + + 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]. + + + Indicates whether your app links user's hand structure and hand movements to the user's identity. + + + Indicates whether your app uses user's hand structure and hand movements for tracking. + + + Indicates whether your app collects user's head movement. + + + 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]. + + + Indicates whether your app links user's head movement to the user's identity. + + + Indicates whether your app uses user's head movement for tracking. + + + Indicates whether your app collects health and medical data. + + + 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]. + + + Indicates whether your app links health and medical data to the user's identity. + + + Indicates whether your app uses health and medical data for tracking. + + + Indicates whether your app collects user's name. + + + 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]. + + + Indicates whether your app links user's name to the user's identity. + + + Indicates whether your app uses user's name for tracking. + + + Indicates whether your app collects any other contact information. + + + 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]. + + + Indicates whether your app links any other contact information to the user's identity. + + + Indicates whether your app uses any other contact information for tracking. + + + Indicates whether your app collects any other data. + + + 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]. + + + Indicates whether your app links any other data to the user's identity. + + + Indicates whether your app uses any other data for tracking. + + + Indicates whether your app collects any other diagnostic data. + + + 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]. + + + Indicates whether your app links any other diagnostic data to the user's identity. + + + Indicates whether your app uses any other diagnostic data for tracking. + + + Indicates whether your app collects any other financial information. + + + 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]. + + + Indicates whether your app links any other financial information to the user's identity. + + + Indicates whether your app uses any other financial information for tracking. + + + Indicates whether your app collects any other usage data. + + + 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]. + + + Indicates whether your app links any other usage data to the user's identity. + + + Indicates whether your app uses any other usage data for tracking. + + + Indicates whether your app collects any other user generated content. + + + 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]. + + + Indicates whether your app links any other user generated content to the user's identity. + + + Indicates whether your app uses any other user generated content for tracking. + + + Indicates whether your app collects payment information. + + + 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]. + + + Indicates whether your app links payment information to the user's identity. + + + Indicates whether your app uses payment information for tracking. + + + Indicates whether your app collects performance data. + + + 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]. + + + Indicates whether your app links performance data to the user's identity. + + + Indicates whether your app uses performance data for tracking. + + + Indicates whether your app collects phone number. + + + 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]. + + + Indicates whether your app links phone number to the user's identity. + + + Indicates whether your app uses phone number for tracking. + + + Indicates whether your app collects photos or videos. + + + 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]. + + + Indicates whether your app links photos or videos to the user's identity. + + + Indicates whether your app uses photos or videos for tracking. + + + Indicates whether your app collects physical address. + + + 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]. + + + Indicates whether your app links physical address to the user's identity. + + + Indicates whether your app uses physical address for tracking. + + + Indicates whether your app collects precise location data. + + + 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]. + + + Indicates whether your app links precise location data to the user's identity. + + + Indicates whether your app uses precise location data for tracking. + + + Indicates whether your app collects product interaction data. + + + 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]. + + + Indicates whether your app links product interaction data to the user's identity. + + + Indicates whether your app uses product interaction data for tracking. + + + Indicates whether your app collects purchase history. + + + 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]. + + + Indicates whether your app links purchase history to the user's identity. + + + Indicates whether your app uses purchase history for tracking. + + + Indicates whether your app collects search history. + + + 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]. + + + Indicates whether your app links search history to the user's identity. + + + Indicates whether your app uses search history for tracking. + + + Indicates whether your app collects sensitive user information. + + + 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]. + + + Indicates whether your app links sensitive user information to the user's identity. + + + Indicates whether your app uses sensitive user information for tracking. + + + Indicates whether your app collects user IDs. + + + 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]. + + + Indicates whether your app links user IDs to the user's identity. + + + Indicates whether your app uses user IDs for tracking. + + + 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]. + + + 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]. + A message displayed when requesting access to the device's microphone (in English). @@ -141,6 +573,18 @@ A message displayed when requesting access to the user's photo library (localized). + + 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]. + + + 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]. + + + Indicates whether your app uses data for tracking. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files]Privacy manifest files[/url]. + + + 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]. + A custom background color of the storyboard launch screen. diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index 33389129b733..c35c72d09343 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 prop_flag_value; + Vector 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 *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 found_plugins = get_plugins(); for (int i = 0; i < found_plugins.size(); i++) { @@ -220,6 +333,37 @@ void EditorExportPlatformIOS::get_export_options(List *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 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 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 &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 += "\tNSPrivacyCollectedDataTypes\n"; + strnew += "\t\n"; + } + strnew += "\t\t\n"; + strnew += "\t\t\tNSPrivacyCollectedDataType\n"; + strnew += vformat("\t\t\t%s\n", data_collect_type_info[j].type_name); + strnew += "\t\t\t\tNSPrivacyCollectedDataTypeLinked\n"; + if (linked) { + strnew += "\t\t\t\t\n"; + } else { + strnew += "\t\t\t\t\n"; + } + strnew += "\t\t\t\tNSPrivacyCollectedDataTypeTracking\n"; + if (tracking) { + strnew += "\t\t\t\t\n"; + } else { + strnew += "\t\t\t\t\n"; + } + if (purposes != 0) { + strnew += "\t\t\t\tNSPrivacyCollectedDataTypePurposes\n"; + strnew += "\t\t\t\t\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%s\n", data_collect_purpose_info[k].type_name); + } + } + strnew += "\t\t\t\t\n"; + } + strnew += "\t\t\t\n"; + } + } + if (section_opened) { + strnew += "\t\n"; + } + } else if (lines[i].find("$priv_tracking") != -1) { + bool tracking = p_preset->get("privacy/tracking_enabled"); + strnew += "\tNSPrivacyTracking\n"; + if (tracking) { + strnew += "\t\n"; + } else { + strnew += "\t\n"; + } + Vector tracking_domains = p_preset->get("privacy/tracking_domains"); + if (!tracking_domains.is_empty()) { + strnew += "\tNSPrivacyTrackingDomains\n"; + strnew += "\t\n"; + for (const String &E : tracking_domains) { + strnew += "\t\t" + E + "\n"; + } + strnew += "\t\n"; + } + } else if (lines[i].find("$priv_api_types") != -1) { + strnew += "\t\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\n"; + strnew += "\t\t\tNSPrivacyAccessedAPITypeReasons\n"; + strnew += "\t\t\t\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%s\n", api_info[j].prop_flag_value[k]); + } + } + strnew += "\t\t\t\n"; + strnew += "\t\t\tNSPrivacyAccessedAPIType\n"; + strnew += vformat("\t\t\t%s\n", api_info[j].type_name); + strnew += "\t\t\n"; + } + } + strnew += "\t\n"; } else { strnew += lines[i] + "\n"; } @@ -1632,19 +1857,72 @@ Error EditorExportPlatformIOS::_export_project_helper(const Refget("application/delete_old_export_files_unconditionally"); Ref 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 config_file, List keys; config_file->get_section_keys(PluginConfigIOS::PLIST_SECTION, &keys); - for (int i = 0; i < keys.size(); i++) { - Vector key_components = keys[i].split(":"); + for (const String &key : keys) { + Vector key_components = key.split(":"); String key_value = ""; PluginConfigIOS::PlistItemType key_type = PluginConfigIOS::PlistItemType::UNKNOWN; @@ -245,29 +245,29 @@ PluginConfigIOS PluginConfigIOS::load_plugin_config(Ref config_file, switch (key_type) { case PluginConfigIOS::PlistItemType::STRING: { - String raw_value = config_file->get_value(PluginConfigIOS::PLIST_SECTION, keys[i], String()); + String raw_value = config_file->get_value(PluginConfigIOS::PLIST_SECTION, key, String()); value = "" + raw_value + ""; } break; case PluginConfigIOS::PlistItemType::INTEGER: { - int raw_value = config_file->get_value(PluginConfigIOS::PLIST_SECTION, keys[i], 0); + int raw_value = config_file->get_value(PluginConfigIOS::PLIST_SECTION, key, 0); Dictionary value_dictionary; String value_format = "$value"; value_dictionary["value"] = raw_value; value = value_format.format(value_dictionary, "$_"); } break; case PluginConfigIOS::PlistItemType::BOOLEAN: - if (config_file->get_value(PluginConfigIOS::PLIST_SECTION, keys[i], false)) { + if (config_file->get_value(PluginConfigIOS::PLIST_SECTION, key, false)) { value = ""; } else { value = ""; } break; case PluginConfigIOS::PlistItemType::RAW: { - String raw_value = config_file->get_value(PluginConfigIOS::PLIST_SECTION, keys[i], String()); + String raw_value = config_file->get_value(PluginConfigIOS::PLIST_SECTION, key, String()); value = raw_value; } break; case PluginConfigIOS::PlistItemType::STRING_INPUT: { - String raw_value = config_file->get_value(PluginConfigIOS::PLIST_SECTION, keys[i], String()); + String raw_value = config_file->get_value(PluginConfigIOS::PLIST_SECTION, key, String()); value = raw_value; } break; default: diff --git a/platform/ios/ios.h b/platform/ios/ios.h index d488cde257b5..cb5be64cee81 100644 --- a/platform/ios/ios.h +++ b/platform/ios/ios.h @@ -51,7 +51,7 @@ class iOS : public Object { static void alert(const char *p_alert, const char *p_title); bool supports_haptic_engine(); - void vibrate_haptic_engine(float p_duration_seconds); + void vibrate_haptic_engine(float p_duration_seconds, float p_amplitude); String get_model() const; String get_rate_url(int p_app_id) const; diff --git a/platform/ios/ios.mm b/platform/ios/ios.mm index 0a2e1fd5cd41..6943de5ac889 100644 --- a/platform/ios/ios.mm +++ b/platform/ios/ios.mm @@ -69,21 +69,41 @@ return haptic_engine; } -void iOS::vibrate_haptic_engine(float p_duration_seconds) API_AVAILABLE(ios(13)) { +void iOS::vibrate_haptic_engine(float p_duration_seconds, float p_amplitude) API_AVAILABLE(ios(13)) { if (@available(iOS 13, *)) { // We need the @available check every time to make the compiler happy... if (supports_haptic_engine()) { CHHapticEngine *cur_haptic_engine = get_haptic_engine_instance(); if (cur_haptic_engine) { - NSDictionary *hapticDict = @{ - CHHapticPatternKeyPattern : @[ - @{CHHapticPatternKeyEvent : @{ - CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, - CHHapticPatternKeyTime : @(CHHapticTimeImmediate), - CHHapticPatternKeyEventDuration : @(p_duration_seconds) - }, - }, - ], - }; + NSDictionary *hapticDict; + if (p_amplitude < 0) { + hapticDict = @{ + CHHapticPatternKeyPattern : @[ + @{CHHapticPatternKeyEvent : @{ + CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, + CHHapticPatternKeyTime : @(CHHapticTimeImmediate), + CHHapticPatternKeyEventDuration : @(p_duration_seconds), + }, + }, + ], + }; + } else { + hapticDict = @{ + CHHapticPatternKeyPattern : @[ + @{CHHapticPatternKeyEvent : @{ + CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, + CHHapticPatternKeyTime : @(CHHapticTimeImmediate), + CHHapticPatternKeyEventDuration : @(p_duration_seconds), + CHHapticPatternKeyEventParameters : @[ + @{ + CHHapticPatternKeyParameterID : @("HapticIntensity"), + CHHapticPatternKeyParameterValue : @(p_amplitude) + }, + ], + }, + }, + ], + }; + } NSError *error; CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; diff --git a/platform/ios/os_ios.h b/platform/ios/os_ios.h index c4782a47685f..b7c5a7306560 100644 --- a/platform/ios/os_ios.h +++ b/platform/ios/os_ios.h @@ -123,7 +123,7 @@ class OS_IOS : public OS_Unix { virtual String get_unique_id() const override; virtual String get_processor_name() const override; - virtual void vibrate_handheld(int p_duration_ms = 500) override; + virtual void vibrate_handheld(int p_duration_ms = 500, float p_amplitude = -1.0) override; virtual bool _check_internal_feature_support(const String &p_feature) override; diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index 52d496d64172..35b87ea64703 100644 --- a/platform/ios/os_ios.mm +++ b/platform/ios/os_ios.mm @@ -571,9 +571,13 @@ void register_dynamic_symbol(char *name, void *address) { return ret; } -void OS_IOS::vibrate_handheld(int p_duration_ms) { +void OS_IOS::vibrate_handheld(int p_duration_ms, float p_amplitude) { if (ios->supports_haptic_engine()) { - ios->vibrate_haptic_engine((float)p_duration_ms / 1000.f); + if (p_amplitude > 0.0) { + p_amplitude = CLAMP(p_amplitude, 0.0, 1.0); + } + + ios->vibrate_haptic_engine((float)p_duration_ms / 1000.f, p_amplitude); } else { // iOS <13 does not support duration for vibration AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 27dec73b655e..afc9d25a80db 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_=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/SCsub b/platform/linuxbsd/wayland/SCsub index cab45b76723a..add5bdb50417 100644 --- a/platform/linuxbsd/wayland/SCsub +++ b/platform/linuxbsd/wayland/SCsub @@ -199,6 +199,7 @@ if env["vulkan"]: if env["opengl3"]: source_files.append("egl_manager_wayland.cpp") + source_files.append("egl_manager_wayland_gles.cpp") objects = [] diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index da70dae4ff3f..f7995472d087 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -46,6 +46,8 @@ #ifdef GLES3_ENABLED #include "detect_prime_egl.h" #include "drivers/gles3/rasterizer_gles3.h" +#include "wayland/egl_manager_wayland.h" +#include "wayland/egl_manager_wayland_gles.h" #endif String DisplayServerWayland::_get_app_id_from_context(Context p_context) { @@ -548,7 +550,15 @@ float DisplayServerWayland::screen_get_scale(int p_screen) const { MutexLock mutex_lock(wayland_thread.mutex); if (p_screen == SCREEN_OF_MAIN_WINDOW) { - p_screen = window_get_current_screen(); + // Wayland does not expose fractional scale factors at the screen-level, but + // some code relies on it. Since this special screen is the default and a lot + // of code relies on it, we'll return the window's scale, which is what we + // really care about. After all, we have very little use of the actual screen + // enumeration APIs and we're (for now) in single-window mode anyways. + struct wl_surface *wl_surface = wayland_thread.window_get_wl_surface(MAIN_WINDOW_ID); + WaylandThread::WindowState *ws = wayland_thread.wl_surface_get_window_state(wl_surface); + + return wayland_thread.window_state_get_scale_factor(ws); } return wayland_thread.screen_get_data(p_screen).scale; @@ -1208,6 +1218,7 @@ Vector DisplayServerWayland::get_rendering_drivers_func() { #ifdef GLES3_ENABLED drivers.push_back("opengl3"); + drivers.push_back("opengl3_es"); #endif return drivers; @@ -1258,14 +1269,14 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win #ifdef RD_ENABLED #ifdef VULKAN_ENABLED - if (p_rendering_driver == "vulkan") { + if (rendering_driver == "vulkan") { rendering_context = memnew(RenderingContextDriverVulkanWayland); } #endif if (rendering_context) { if (rendering_context->initialize() != OK) { - ERR_PRINT(vformat("Could not initialize %s", p_rendering_driver)); + ERR_PRINT(vformat("Could not initialize %s", rendering_driver)); memdelete(rendering_context); rendering_context = nullptr; r_error = ERR_CANT_CREATE; @@ -1275,7 +1286,14 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win #endif #ifdef GLES3_ENABLED - if (p_rendering_driver == "opengl3") { + if (rendering_driver == "opengl3" || rendering_driver == "opengl3_es") { +#ifdef SOWRAP_ENABLED + if (initialize_wayland_egl(dylibloader_verbose) != 0) { + WARN_PRINT("Can't load the Wayland EGL library."); + return; + } +#endif // SOWRAP_ENABLED + if (getenv("DRI_PRIME") == nullptr) { int prime_idx = -1; @@ -1318,23 +1336,38 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win } } - egl_manager = memnew(EGLManagerWayland); + if (rendering_driver == "opengl3") { + egl_manager = memnew(EGLManagerWayland); -#ifdef SOWRAP_ENABLED - if (initialize_wayland_egl(dylibloader_verbose) != 0) { - WARN_PRINT("Can't load the Wayland EGL library."); - return; - } -#endif // SOWRAP_ENABLED + if (egl_manager->initialize() != OK || egl_manager->open_display(wayland_thread.get_wl_display()) != OK) { + memdelete(egl_manager); + egl_manager = nullptr; - if (egl_manager->initialize() != OK) { - memdelete(egl_manager); - egl_manager = nullptr; - r_error = ERR_CANT_CREATE; - ERR_FAIL_MSG("Could not initialize GLES3."); + bool fallback = GLOBAL_GET("rendering/gl_compatibility/fallback_to_gles"); + if (fallback) { + WARN_PRINT("Your video card drivers seem not to support the required OpenGL version, switching to OpenGLES."); + rendering_driver = "opengl3_es"; + } else { + r_error = ERR_UNAVAILABLE; + ERR_FAIL_MSG("Could not initialize OpenGL."); + } + } else { + RasterizerGLES3::make_current(true); + } } - RasterizerGLES3::make_current(true); + if (rendering_driver == "opengl3_es") { + egl_manager = memnew(EGLManagerWaylandGLES); + + if (egl_manager->initialize() != OK) { + memdelete(egl_manager); + egl_manager = nullptr; + r_error = ERR_CANT_CREATE; + ERR_FAIL_MSG("Could not initialize GLES3."); + } + + RasterizerGLES3::make_current(false); + } } #endif // GLES3_ENABLED diff --git a/platform/linuxbsd/wayland/display_server_wayland.h b/platform/linuxbsd/wayland/display_server_wayland.h index 368f1b402b1d..1bad358462cb 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.h +++ b/platform/linuxbsd/wayland/display_server_wayland.h @@ -45,7 +45,7 @@ #endif //RD_ENABLED #ifdef GLES3_ENABLED -#include "wayland/egl_manager_wayland.h" +#include "drivers/egl/egl_manager.h" #endif #if defined(SPEECHD_ENABLED) @@ -126,7 +126,7 @@ class DisplayServerWayland : public DisplayServer { #endif #ifdef GLES3_ENABLED - EGLManagerWayland *egl_manager = nullptr; + EGLManager *egl_manager = nullptr; #endif #ifdef SPEECHD_ENABLED diff --git a/platform/linuxbsd/wayland/egl_manager_wayland_gles.cpp b/platform/linuxbsd/wayland/egl_manager_wayland_gles.cpp new file mode 100644 index 000000000000..9431b18f0565 --- /dev/null +++ b/platform/linuxbsd/wayland/egl_manager_wayland_gles.cpp @@ -0,0 +1,64 @@ +/**************************************************************************/ +/* egl_manager_wayland_gles.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 "egl_manager_wayland_gles.h" + +#ifdef WAYLAND_ENABLED +#ifdef EGL_ENABLED +#ifdef GLES3_ENABLED + +const char *EGLManagerWaylandGLES::_get_platform_extension_name() const { + return "EGL_KHR_platform_wayland"; +} + +EGLenum EGLManagerWaylandGLES::_get_platform_extension_enum() const { + return EGL_PLATFORM_WAYLAND_KHR; +} + +EGLenum EGLManagerWaylandGLES::_get_platform_api_enum() const { + return EGL_OPENGL_ES_API; +} + +Vector EGLManagerWaylandGLES::_get_platform_display_attributes() const { + return Vector(); +} + +Vector EGLManagerWaylandGLES::_get_platform_context_attribs() const { + Vector ret; + ret.push_back(EGL_CONTEXT_MAJOR_VERSION); + ret.push_back(3); + ret.push_back(EGL_NONE); + + return ret; +} + +#endif // GLES3_ENABLED +#endif // EGL_ENABLED +#endif // WAYLAND_ENABLED diff --git a/platform/linuxbsd/wayland/egl_manager_wayland_gles.h b/platform/linuxbsd/wayland/egl_manager_wayland_gles.h new file mode 100644 index 000000000000..f526f182777b --- /dev/null +++ b/platform/linuxbsd/wayland/egl_manager_wayland_gles.h @@ -0,0 +1,53 @@ +/**************************************************************************/ +/* egl_manager_wayland_gles.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 EGL_MANAGER_WAYLAND_GLES_H +#define EGL_MANAGER_WAYLAND_GLES_H + +#ifdef WAYLAND_ENABLED +#ifdef EGL_ENABLED +#ifdef GLES3_ENABLED + +#include "drivers/egl/egl_manager.h" + +class EGLManagerWaylandGLES : public EGLManager { +public: + virtual const char *_get_platform_extension_name() const override; + virtual EGLenum _get_platform_extension_enum() const override; + virtual EGLenum _get_platform_api_enum() const override; + virtual Vector _get_platform_display_attributes() const override; + virtual Vector _get_platform_context_attribs() const override; +}; + +#endif // GLES3_ENABLED +#endif // EGL_ENABLED +#endif // WAYLAND_ENABLED + +#endif // EGL_MANAGER_WAYLAND_GLES_H diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index 7f9008e9526f..aabf1abdda41 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::Element *E = ss->tablet_tools.find(zwp_tablet_tool_v2); + List::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); } @@ -3406,7 +3427,7 @@ bool WaylandThread::window_get_idle_inhibition(DisplayServer::WindowID p_window_ WaylandThread::ScreenData WaylandThread::screen_get_data(int p_screen) const { ERR_FAIL_INDEX_V(p_screen, registry.wl_outputs.size(), ScreenData()); - return wl_output_get_screen_state(registry.wl_outputs[p_screen])->data; + return wl_output_get_screen_state(registry.wl_outputs.get(p_screen))->data; } int WaylandThread::get_screen_count() const { @@ -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 d49f0c9d34be..d35a5b713989 100644 --- a/platform/linuxbsd/wayland/wayland_thread.h +++ b/platform/linuxbsd/wayland/wayland_thread.h @@ -133,8 +133,8 @@ class WaylandThread { 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 @@ class WaylandThread { // 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 @@ class WaylandThread { 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 0041b4c7f347..b76cbc126fa5 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 3c8b1ebee150..a5ef29e34f3d 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 5d38bf55ea41..db76b7d78a68 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -192,7 +192,7 @@ class DisplayServerMacOS : public DisplayServer { HashMap windows; struct IndicatorData { - id view; + id delegate; id item; }; @@ -431,10 +431,12 @@ class DisplayServerMacOS : public DisplayServer { virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref &p_icon) override; - virtual IndicatorID create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) override; - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref &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 Rect2 status_indicator_get_rect(IndicatorID p_id) const override; virtual void delete_status_indicator(IndicatorID p_id) override; static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Error &r_error); diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index cfe925e79b37..0041848c786d 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -938,7 +938,7 @@ 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 @@ 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 @@ } } } else { - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = false; Variant v_files = Vector(); @@ -1134,7 +1134,7 @@ 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 @@ } } } else { - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = false; Variant v_files = Vector(); @@ -1222,7 +1222,7 @@ 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 @@ 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 @@ } } -DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref &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 img = p_icon->duplicate(); + Ref img = p_icon->get_image(); + img = img->duplicate(); img->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] @@ -3192,13 +3193,18 @@ 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 @@ return iid; } -void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref &p_icon) { +void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref &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 img = p_icon->duplicate(); + Ref img = p_icon->get_image(); + img = img->duplicate(); img->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] @@ -3247,19 +3254,57 @@ } } - [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]; +} + +Rect2 DisplayServerMacOS::status_indicator_get_rect(IndicatorID p_id) const { + ERR_FAIL_COND_V(!indicators.has(p_id), Rect2()); + + NSStatusItem *item = indicators[p_id].item; + NSView *v = item.button; + const NSRect contentRect = [v frame]; + const NSRect nsrect = [v.window convertRectToScreen:contentRect]; + Rect2 rect; + + // Return the position of the top-left corner, for macOS the y starts at the bottom. + const float scale = screen_get_max_scale(); + rect.size.x = nsrect.size.width; + rect.size.y = nsrect.size.height; + rect.size *= scale; + rect.position.x = nsrect.origin.x; + rect.position.y = (nsrect.origin.y + nsrect.size.height); + rect.position *= scale; + rect.position -= _get_screens_origin(); + // macOS native y-coordinate relative to _get_screens_origin() is negative, + // Godot expects a positive value. + rect.position.y *= -1; + return rect; } 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 d75def9b504a..5f52d33318f3 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -909,7 +909,7 @@ Error EditorExportPlatformMacOS::_notarize(const Ref &p_pres return OK; } -Error EditorExportPlatformMacOS::_code_sign(const Ref &p_preset, const String &p_path, const String &p_ent_path, bool p_warn) { +Error EditorExportPlatformMacOS::_code_sign(const Ref &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 &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 &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 &p_pre } Error EditorExportPlatformMacOS::_code_sign_directory(const Ref &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 extensions_to_sign; if (extensions_to_sign.is_empty()) { @@ -1070,7 +1082,8 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref -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 Refcurrent_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 &dir_access, const String &p_src_path, const String &p_in_app_path, bool p_sign_enabled, const Ref &p_preset, const String &p_ent_path, + const String &p_helper_ent_path, bool p_should_error_on_non_code_sign) { static Vector extensions_to_sign; @@ -1186,10 +1200,11 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref &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 &dir_access Error EditorExportPlatformMacOS::_export_macos_plugins_for(Ref p_editor_export_plugin, const String &p_app_path_name, Ref &dir_access, bool p_sign_enabled, const Ref &p_preset, - const String &p_ent_path) { + const String &p_ent_path, const String &p_helper_ent_path) { Error error{ OK }; const Vector &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 &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 &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(""); @@ -1965,7 +1981,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &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 &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 &p Vector> 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 &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 &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 0764b63e8cee..2d615abedee6 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 &p_preset, const Ref &p_icon, Vector &p_data); Error _notarize(const Ref &p_preset, const String &p_path); - Error _code_sign(const Ref &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true); - Error _code_sign_directory(const Ref &p_preset, const String &p_path, const String &p_ent_path, bool p_should_error_on_non_code = true); + Error _code_sign(const Ref &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 &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 &dir_access, const String &p_src_path, const String &p_in_app_path, - bool p_sign_enabled, const Ref &p_preset, const String &p_ent_path, + bool p_sign_enabled, const Ref &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 p_editor_export_plugin, const String &p_app_path_name, Ref &dir_access, bool p_sign_enabled, const Ref &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 &p_preset, const String &p_pkg_path, const String &p_app_path_name); Error _export_debug_script(const Ref &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 c7556c1964e3..a829774a88e3 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 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 37975f08205e..a84de7de60b2 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 93bba84783b7..68a7288ad4a4 100644 --- a/platform/macos/godot_content_view.mm +++ b/platform/macos/godot_content_view.mm @@ -313,7 +313,7 @@ - (BOOL)performDragOperation:(id)sender { } DisplayServerMacOS::WindowData &wd = ds->get_window(window_id); - if (!wd.drop_files_callback.is_null()) { + if (wd.drop_files_callback.is_valid()) { Vector files; NSPasteboard *pboard = [sender draggingPasteboard]; diff --git a/platform/macos/godot_status_item.h b/platform/macos/godot_status_item.h index 1827baa9bdb6..5bc790956efd 100644 --- a/platform/macos/godot_status_item.h +++ b/platform/macos/godot_status_item.h @@ -37,13 +37,12 @@ #import #import -@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 71ed0a0f719d..0990a16b2b3e 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)processMouseEvent:(NSEvent *)event index:(MouseButton)index { } } -- (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 2d83b460078b..7749debfd6d4 100644 --- a/platform/macos/godot_window_delegate.mm +++ b/platform/macos/godot_window_delegate.mm @@ -268,7 +268,7 @@ - (void)windowDidResize:(NSNotification *)notification { 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 @@ - (void)windowDidMove:(NSNotification *)notification { 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 1d9feb64a76c..b5dbb8b9b036 100644 --- a/platform/macos/native_menu_macos.h +++ b/platform/macos/native_menu_macos.h @@ -85,6 +85,8 @@ class NativeMenuMacOS : public NativeMenu { 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 8c2dd9886263..1cf13a2d6941 100644 --- a/platform/macos/native_menu_macos.mm +++ b/platform/macos/native_menu_macos.mm @@ -248,6 +248,13 @@ } } +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 3e0cc9ac4abf..bc5893ab3ad7 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 ac0d5e20eca2..2902f60f2421 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 2d2cc288a149..ccd884b22557 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -10,7 +10,7 @@ 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 06f5eb82f78e..a51c161b9c12 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 &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 41c969b5f4ab..d42303ad254f 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -170,6 +170,7 @@ void EditorExportPlatformWeb::_fix_html(Vector &p_html, const Refget("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 EditorExportPlatformWeb::get_option_icon(int p_index) const { - return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index); + Ref 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 &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 &p_preset, int p_debug_flags) { const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("web"); Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (!da->dir_exists(dest)) { @@ -636,35 +781,40 @@ Error EditorExportPlatformWeb::run(const Ref &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 = 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 952d03cdb4a9..9d3a1a786199 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 logo; Ref run_icon; Ref stop_icon; - int menu_options = 0; + Ref restart_icon; + HTTPServerState server_state = HTTP_SERVER_STATE_OFF; Ref server; @@ -96,6 +102,11 @@ class EditorExportPlatformWeb : public EditorExportPlatform { Error _build_pwa(const Ref &p_preset, const String p_path, const Vector &p_shared_objects); Error _write_or_error(const uint8_t *p_content, int p_len, String p_path); + Error _export_project(const Ref &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 &p_preset, List *r_features) const override; @@ -112,8 +123,8 @@ class EditorExportPlatformWeb : public EditorExportPlatform { 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 get_option_icon(int p_index) const override; virtual Error run(const Ref &p_preset, int p_option, int p_debug_flags) override; virtual Ref get_run_icon() const override; diff --git a/platform/web/javascript_bridge_singleton.cpp b/platform/web/javascript_bridge_singleton.cpp index d72ad8331b38..a2c83d2f2bb5 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 argp; Array arg_arr; diff --git a/platform/web/js/engine/features.js b/platform/web/js/engine/features.js index 81bc82f3c6c4..263ea6ac88d6 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} 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/web/os_web.cpp b/platform/web/os_web.cpp index ab4e7f8470c5..ef8f90421ba0 100644 --- a/platform/web/os_web.cpp +++ b/platform/web/os_web.cpp @@ -174,7 +174,7 @@ void OS_Web::add_frame_delay(bool p_can_draw) { #endif } -void OS_Web::vibrate_handheld(int p_duration_ms) { +void OS_Web::vibrate_handheld(int p_duration_ms, float p_amplitude) { godot_js_input_vibrate_handheld(p_duration_ms); } @@ -276,8 +276,8 @@ OS_Web::OS_Web() { if (AudioDriverWeb::is_available()) { audio_drivers.push_back(memnew(AudioDriverWorklet)); } - for (int i = 0; i < audio_drivers.size(); i++) { - AudioDriverManager::add_driver(audio_drivers[i]); + for (AudioDriverWeb *audio_driver : audio_drivers) { + AudioDriverManager::add_driver(audio_driver); } idb_available = godot_js_os_fs_is_persistent(); diff --git a/platform/web/os_web.h b/platform/web/os_web.h index a825938e9678..55a5fcc6c62f 100644 --- a/platform/web/os_web.h +++ b/platform/web/os_web.h @@ -98,7 +98,7 @@ class OS_Web : public OS_Unix { // Implemented in web_main.cpp loop callback instead. void add_frame_delay(bool p_can_draw) override; - void vibrate_handheld(int p_duration_ms) override; + void vibrate_handheld(int p_duration_ms, float p_amplitude) override; String get_cache_path() const override; String get_config_path() const override; diff --git a/platform/windows/SCsub b/platform/windows/SCsub index 159a273e7001..435c501956c4 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 de751580b784..133711a9eafe 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 3871210977c1..a0a0b610d06f 100644 --- a/platform/windows/crash_handler_windows.h +++ b/platform/windows/crash_handler_windows.h @@ -35,12 +35,15 @@ #include // 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 similarity index 99% rename from platform/windows/crash_handler_windows.cpp rename to platform/windows/crash_handler_windows_seh.cpp index 133d36aa0db1..2abe285d3101 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 000000000000..e11a60bdc7f2 --- /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 +#include +#include +#include +#include +#include + +#include + +#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(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(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(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 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(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(&data)); + if (data.state != nullptr) { + data.index = 1; + backtrace_simple(data.state, 1, &trace_callback, &error_callback, reinterpret_cast(&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 f34d47934595..ccf889f1a377 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -2,6 +2,7 @@ import os import subprocess import sys +from methods import print_warning, print_error from platform_methods import detect_arch from typing import TYPE_CHECKING @@ -17,36 +18,30 @@ def get_name(): return "Windows" -def try_cmd(test, prefix, arch): - if arch: +def get_mingw_tool(tool, prefix="", arch="", test="--version"): + if not prefix: + prefix = os.getenv("MINGW_PREFIX", "") + supported_arches = ["x86_64", "x86_32", "arm64", "arm32"] + if arch in supported_arches: + arches = [arch, ""] + else: + arches = ["x86_64", "x86_32", "arm64", "arm32", ""] + for a in arches: try: + path = f"{get_mingw_bin_prefix(prefix, a)}{tool}" out = subprocess.Popen( - get_mingw_bin_prefix(prefix, arch) + test, + f"{path} {test}", shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, ) out.communicate() if out.returncode == 0: - return True + return path except Exception: pass - else: - for a in ["x86_64", "x86_32", "arm64", "arm32"]: - try: - out = subprocess.Popen( - get_mingw_bin_prefix(prefix, a) + test, - shell=True, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - ) - out.communicate() - if out.returncode == 0: - return True - except Exception: - pass - return False + return "" def can_build(): @@ -64,9 +59,7 @@ def can_build(): if os.name == "posix": # Cross-compiling with MinGW-w64 (old MinGW32 is not supported) - prefix = os.getenv("MINGW_PREFIX", "") - - if try_cmd("gcc --version", prefix, "") or try_cmd("clang --version", prefix, ""): + if get_mingw_tool("gcc") or get_mingw_tool("clang"): return True return False @@ -254,36 +247,26 @@ def get_flags(): def build_res_file(target, source, env: "SConsEnvironment"): + cmdbase = get_mingw_tool("windres", env["mingw_prefix"], env["arch"]) + if not cmdbase: + return -1 + arch_aliases = { "x86_32": "pe-i386", "x86_64": "pe-x86-64", "arm32": "armv7-w64-mingw32", "arm64": "aarch64-w64-mingw32", } - cmdbase = "windres --include-dir . --target=" + arch_aliases[env["arch"]] - - mingw_bin_prefix = get_mingw_bin_prefix(env["mingw_prefix"], env["arch"]) + cmdbase += " --include-dir . --target=" + arch_aliases[env["arch"]] for x in range(len(source)): - ok = True - # Try prefixed executable (MinGW on Linux). - cmd = mingw_bin_prefix + cmdbase + " -i " + str(source[x]) + " -o " + str(target[x]) + cmd = f"{cmdbase} -i {source[x]} -o {target[x]}" try: out = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE).communicate() if len(out[1]): - ok = False - except Exception: - ok = False - - # Try generic executable (MSYS2). - if not ok: - cmd = cmdbase + " -i " + str(source[x]) + " -o " + str(target[x]) - try: - out = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE).communicate() - if len(out[1]): - return -1 - except Exception: return -1 + except Exception: + return -1 return 0 @@ -293,16 +276,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 +319,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 +327,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"] + if not get_mingw_tool("gcc", env["mingw_prefix"], env["arch"]) and not get_mingw_tool( + "clang", 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 +427,10 @@ def spawn_capture(sh, escape, cmd, args, env): 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 +488,10 @@ def spawn_capture(sh, escape, cmd, args, env): 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 +530,16 @@ def spawn_capture(sh, escape, cmd, args, env): 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 +548,7 @@ def spawn_capture(sh, escape, cmd, args, env): 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"]) @@ -606,10 +582,10 @@ def configure_mingw(env: "SConsEnvironment"): ## Build type - if not env["use_llvm"] and not try_cmd("gcc --version", env["mingw_prefix"], env["arch"]): + if not env["use_llvm"] and not get_mingw_tool("gcc", env["mingw_prefix"], env["arch"]): env["use_llvm"] = True - if env["use_llvm"] and not try_cmd("clang --version", env["mingw_prefix"], env["arch"]): + if env["use_llvm"] and not get_mingw_tool("clang", env["mingw_prefix"], env["arch"]): env["use_llvm"] = False # TODO: Re-evaluate the need for this / streamline with common config. @@ -644,27 +620,26 @@ def configure_mingw(env: "SConsEnvironment"): if env["arch"] in ["x86_32", "x86_64"]: env["x86_libtheora_opt_gcc"] = True - mingw_bin_prefix = get_mingw_bin_prefix(env["mingw_prefix"], env["arch"]) - if env["use_llvm"]: - env["CC"] = mingw_bin_prefix + "clang" - env["CXX"] = mingw_bin_prefix + "clang++" - if try_cmd("as --version", env["mingw_prefix"], env["arch"]): - env["AS"] = mingw_bin_prefix + "as" - if try_cmd("ar --version", env["mingw_prefix"], env["arch"]): - env["AR"] = mingw_bin_prefix + "ar" - if try_cmd("ranlib --version", env["mingw_prefix"], env["arch"]): - env["RANLIB"] = mingw_bin_prefix + "ranlib" + env["CC"] = get_mingw_tool("clang", env["mingw_prefix"], env["arch"]) + env["CXX"] = get_mingw_tool("clang++", env["mingw_prefix"], env["arch"]) + tool_as = get_mingw_tool("as", env["mingw_prefix"], env["arch"]) + tool_ar = get_mingw_tool("ar", env["mingw_prefix"], env["arch"]) + tool_ranlib = get_mingw_tool("ranlib", env["mingw_prefix"], env["arch"]) env.extra_suffix = ".llvm" + env.extra_suffix else: - env["CC"] = mingw_bin_prefix + "gcc" - env["CXX"] = mingw_bin_prefix + "g++" - if try_cmd("as --version", env["mingw_prefix"], env["arch"]): - env["AS"] = mingw_bin_prefix + "as" - if try_cmd("gcc-ar --version", env["mingw_prefix"], env["arch"]): - env["AR"] = mingw_bin_prefix + "gcc-ar" - if try_cmd("gcc-ranlib --version", env["mingw_prefix"], env["arch"]): - env["RANLIB"] = mingw_bin_prefix + "gcc-ranlib" + env["CC"] = get_mingw_tool("gcc", env["mingw_prefix"], env["arch"]) + env["CXX"] = get_mingw_tool("g++", env["mingw_prefix"], env["arch"]) + tool_as = get_mingw_tool("as", env["mingw_prefix"], env["arch"]) + tool_ar = get_mingw_tool("gcc-ar", env["mingw_prefix"], env["arch"]) + tool_ranlib = get_mingw_tool("gcc-ranlib", env["mingw_prefix"], env["arch"]) + + if tool_as: + env["AS"] = tool_as + if tool_ar: + env["AR"] = tool_ar + if tool_ranlib: + env["RANLIB"] = tool_ranlib ## LTO @@ -690,7 +665,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 +719,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 +769,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 b6b713687f96..94c368c5045c 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(); @@ -2525,7 +2525,7 @@ Error DisplayServerWindows::dialog_show(String p_title, String p_description, Ve Char16String title = p_title.utf16(); Char16String message = p_description.utf16(); - List buttons; + LocalVector buttons; for (String s : p_buttons) { buttons.push_back(s.utf16()); } @@ -2533,7 +2533,7 @@ Error DisplayServerWindows::dialog_show(String p_title, String p_description, Ve config.pszWindowTitle = (LPCWSTR)(title.get_data()); config.pszContent = (LPCWSTR)(message.get_data()); - const int button_count = MIN(buttons.size(), 8); + const int button_count = MIN((int)buttons.size(), 8); config.cButtons = button_count; // No dynamic stack array size :( @@ -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 &p_icon) { } } -DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref &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 img = p_icon; - if (img != icon) { - img = img->duplicate(); - img->convert(Image::FORMAT_RGBA8); - } + Ref 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 &p_icon) { +void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref &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 img = p_icon; - if (img != icon) { - img = img->duplicate(); - img->convert(Image::FORMAT_RGBA8); - } + Ref img = p_icon->get_image(); + img = img->duplicate(); + img->convert(Image::FORMAT_RGBA8); int w = img->get_width(); int h = img->get_height(); @@ -3317,12 +3313,42 @@ 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)); indicators[p_id].callback = p_callback; } +Rect2 DisplayServerWindows::status_indicator_get_rect(IndicatorID p_id) const { + ERR_FAIL_COND_V(!indicators.has(p_id), Rect2()); + + NOTIFYICONIDENTIFIER nid; + ZeroMemory(&nid, sizeof(NOTIFYICONIDENTIFIER)); + nid.cbSize = sizeof(NOTIFYICONIDENTIFIER); + nid.hWnd = windows[MAIN_WINDOW_ID].hWnd; + nid.uID = p_id; + nid.guidItem = GUID_NULL; + + RECT rect; + if (Shell_NotifyIconGetRect(&nid, &rect) != S_OK) { + return Rect2(); + } + Rect2 ind_rect = Rect2(Point2(rect.left, rect.top) - _get_screens_origin(), Size2(rect.right - rect.left, rect.bottom - rect.top)); + for (int i = 0; i < get_screen_count(); i++) { + Rect2 screen_rect = Rect2(screen_get_position(i), screen_get_size(i)); + if (screen_rect.encloses(ind_rect)) { + return ind_rect; + } + } + return Rect2(); +} + void DisplayServerWindows::delete_status_indicator(IndicatorID p_id) { ERR_FAIL_COND(!indicators.has(p_id)); @@ -3738,8 +3764,13 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } break; case WM_ACTIVATE: { - _process_activate_event(window_id, wParam, lParam); - return 0; // Return to the message loop. + // Activation can happen just after the window has been created, even before the callbacks are set. + // Therefore, it's safer to defer the delivery of the event. + if (!windows[window_id].activate_timer_id) { + windows[window_id].activate_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); + } + windows[window_id].activate_state = GET_WM_ACTIVATE_STATE(wParam, lParam); + return 0; } break; case WM_GETMINMAXINFO: { if (windows[window_id].resizable && !windows[window_id].fullscreen) { @@ -3838,7 +3869,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 }; @@ -3850,11 +3893,13 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA return 0; } } break; - case WM_CLOSE: // Did we receive a close message? - { + case WM_CLOSE: { + if (windows[window_id].activate_timer_id) { + KillTimer(windows[window_id].hWnd, windows[window_id].activate_timer_id); + windows[window_id].activate_timer_id = 0; + } _send_window_event(windows[window_id], WINDOW_EVENT_CLOSE_REQUEST); - - return 0; // Jump back. + return 0; } case WM_MOUSELEAVE: { if (window_mouseover_id == window_id) { @@ -4591,7 +4636,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)); } @@ -4623,6 +4668,10 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA if (!Main::is_iterating()) { Main::iteration(); } + } else if (wParam == windows[window_id].activate_timer_id) { + _process_activate_event(window_id); + KillTimer(windows[window_id].hWnd, windows[window_id].activate_timer_id); + windows[window_id].activate_timer_id = 0; } } break; case WM_SYSKEYUP: @@ -4803,7 +4852,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; @@ -4826,31 +4875,32 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { } } -void DisplayServerWindows::_process_activate_event(WindowID p_window_id, WPARAM wParam, LPARAM lParam) { - if (LOWORD(wParam) == WA_ACTIVE || LOWORD(wParam) == WA_CLICKACTIVE) { +void DisplayServerWindows::_process_activate_event(WindowID p_window_id) { + WindowData &wd = windows[p_window_id]; + if (wd.activate_state == WA_ACTIVE || wd.activate_state == WA_CLICKACTIVE) { last_focused_window = p_window_id; alt_mem = false; control_mem = false; shift_mem = false; gr_mem = false; _set_mouse_mode_impl(mouse_mode); - if (!IsIconic(windows[p_window_id].hWnd)) { - SetFocus(windows[p_window_id].hWnd); + if (!IsIconic(wd.hWnd)) { + SetFocus(wd.hWnd); } - windows[p_window_id].window_focused = true; - _send_window_event(windows[p_window_id], WINDOW_EVENT_FOCUS_IN); + wd.window_focused = true; + _send_window_event(wd, WINDOW_EVENT_FOCUS_IN); } else { // WM_INACTIVE. Input::get_singleton()->release_pressed_events(); - track_mouse_leave_event(windows[p_window_id].hWnd); + track_mouse_leave_event(wd.hWnd); // Release capture unconditionally because it can be set due to dragging, in addition to captured mode. ReleaseCapture(); alt_mem = false; - windows[p_window_id].window_focused = false; - _send_window_event(windows[p_window_id], WINDOW_EVENT_FOCUS_OUT); + wd.window_focused = false; + _send_window_event(wd, WINDOW_EVENT_FOCUS_OUT); } - if ((tablet_get_current_driver() == "wintab") && wintab_available && windows[p_window_id].wtctx) { - wintab_WTEnable(windows[p_window_id].wtctx, GET_WM_ACTIVATE_STATE(wParam, lParam)); + if ((tablet_get_current_driver() == "wintab") && wintab_available && wd.wtctx) { + wintab_WTEnable(wd.wtctx, wd.activate_state); } } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 2fe1b0733d30..618b763bc7bd 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -389,6 +389,7 @@ class DisplayServerWindows : public DisplayServer { bool borderless = false; bool resizable = true; bool window_focused = false; + int activate_state = 0; bool was_maximized = false; bool always_on_top = false; bool no_focus = false; @@ -402,6 +403,7 @@ class DisplayServerWindows : public DisplayServer { // Timers. uint32_t move_timer_id = 0U; + uint32_t activate_timer_id = 0U; HANDLE wtctx; LOGCONTEXTW wtlc; @@ -464,6 +466,7 @@ class DisplayServerWindows : public DisplayServer { WNDPROC user_proc = nullptr; struct IndicatorData { + RID menu_rid; Callable callback; }; @@ -507,7 +510,7 @@ class DisplayServerWindows : public DisplayServer { WindowID _get_focused_window_or_popup() const; void _register_raw_input_devices(WindowID p_target_window); - void _process_activate_event(WindowID p_window_id, WPARAM wParam, LPARAM lParam); + void _process_activate_event(WindowID p_window_id); void _process_key_events(); static void _dispatch_input_events(const Ref &p_event); @@ -684,10 +687,12 @@ class DisplayServerWindows : public DisplayServer { virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref &p_icon) override; - virtual IndicatorID create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) override; - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref &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 Rect2 status_indicator_get_rect(IndicatorID p_id) const override; virtual void delete_status_indicator(IndicatorID p_id) override; virtual void set_context(Context p_context) override; diff --git a/platform/windows/gl_manager_windows_native.cpp b/platform/windows/gl_manager_windows_native.cpp index 80cf818199f4..b45d254fc631 100644 --- a/platform/windows/gl_manager_windows_native.cpp +++ b/platform/windows/gl_manager_windows_native.cpp @@ -88,7 +88,8 @@ typedef int(__cdecl *NvAPI_DRS_CreateApplication_t)(NvDRSSessionHandle, NvDRSPro typedef int(__cdecl *NvAPI_DRS_SaveSettings_t)(NvDRSSessionHandle); typedef int(__cdecl *NvAPI_DRS_SetSetting_t)(NvDRSSessionHandle, NvDRSProfileHandle, NVDRS_SETTING *); typedef int(__cdecl *NvAPI_DRS_FindProfileByName_t)(NvDRSSessionHandle, NvAPI_UnicodeString, NvDRSProfileHandle *); -typedef int(__cdecl *NvAPI_DRS_FindApplicationByName_t)(NvDRSSessionHandle, NvAPI_UnicodeString, NvDRSProfileHandle *, NVDRS_APPLICATION *); +typedef int(__cdecl *NvAPI_DRS_GetApplicationInfo_t)(NvDRSSessionHandle, NvDRSProfileHandle, NvAPI_UnicodeString, NVDRS_APPLICATION *); +typedef int(__cdecl *NvAPI_DRS_DeleteProfile_t)(NvDRSSessionHandle, NvDRSProfileHandle); NvAPI_GetErrorMessage_t NvAPI_GetErrorMessage__; static bool nvapi_err_check(const char *msg, int status) { @@ -139,7 +140,8 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { NvAPI_DRS_SaveSettings_t NvAPI_DRS_SaveSettings = (NvAPI_DRS_SaveSettings_t)NvAPI_QueryInterface(0xFCBC7E14); NvAPI_DRS_SetSetting_t NvAPI_DRS_SetSetting = (NvAPI_DRS_SetSetting_t)NvAPI_QueryInterface(0x577DD202); NvAPI_DRS_FindProfileByName_t NvAPI_DRS_FindProfileByName = (NvAPI_DRS_FindProfileByName_t)NvAPI_QueryInterface(0x7E4A9A0B); - NvAPI_DRS_FindApplicationByName_t NvAPI_DRS_FindApplicationByName = (NvAPI_DRS_FindApplicationByName_t)NvAPI_QueryInterface(0xEEE566B2); + NvAPI_DRS_GetApplicationInfo_t NvAPI_DRS_GetApplicationInfo = (NvAPI_DRS_GetApplicationInfo_t)NvAPI_QueryInterface(0xED1F8C69); + NvAPI_DRS_DeleteProfile_t NvAPI_DRS_DeleteProfile = (NvAPI_DRS_DeleteProfile_t)NvAPI_QueryInterface(0x17093206); if (!nvapi_err_check("NVAPI: Init failed", NvAPI_Initialize())) { return; @@ -165,23 +167,45 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { } String app_executable_name = OS::get_singleton()->get_executable_path().get_file(); - String app_friendly_name = GLOBAL_GET("application/config/name"); + String app_profile_name = GLOBAL_GET("application/config/name"); // We need a name anyways, so let's use the engine name if an application name is not available // (this is used mostly by the Project Manager) - if (app_friendly_name.is_empty()) { - app_friendly_name = VERSION_NAME; + if (app_profile_name.is_empty()) { + app_profile_name = VERSION_NAME; } - String app_profile_name = app_friendly_name + " Nvidia Profile"; + String old_profile_name = app_profile_name + " Nvidia Profile"; Char16String app_profile_name_u16 = app_profile_name.utf16(); + Char16String old_profile_name_u16 = old_profile_name.utf16(); Char16String app_executable_name_u16 = app_executable_name.utf16(); - Char16String app_friendly_name_u16 = app_friendly_name.utf16(); + + // A previous error in app creation logic could result in invalid profiles, + // clean these if they exist before proceeding. + NvDRSProfileHandle old_profile_handle; + + int old_status = NvAPI_DRS_FindProfileByName(session_handle, (NvU16 *)(old_profile_name_u16.ptrw()), &old_profile_handle); + + if (old_status == 0) { + print_verbose("NVAPI: Deleting old profile..."); + + if (!nvapi_err_check("NVAPI: Error deleting old profile", NvAPI_DRS_DeleteProfile(session_handle, old_profile_handle))) { + NvAPI_DRS_DestroySession(session_handle); + NvAPI_Unload(); + return; + } + + if (!nvapi_err_check("NVAPI: Error deleting old profile", NvAPI_DRS_SaveSettings(session_handle))) { + NvAPI_DRS_DestroySession(session_handle); + NvAPI_Unload(); + return; + } + } NvDRSProfileHandle profile_handle = nullptr; int profile_status = NvAPI_DRS_FindProfileByName(session_handle, (NvU16 *)(app_profile_name_u16.ptrw()), &profile_handle); if (profile_status != 0) { - print_verbose("NVAPI: Profile not found, creating...."); + print_verbose("NVAPI: Profile not found, creating..."); NVDRS_PROFILE profile_info; profile_info.version = NVDRS_PROFILE_VER; @@ -195,22 +219,18 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { } } - NvDRSProfileHandle app_profile_handle = nullptr; NVDRS_APPLICATION_V4 app; app.version = NVDRS_APPLICATION_VER_V4; - int app_status = NvAPI_DRS_FindApplicationByName(session_handle, (NvU16 *)(app_executable_name_u16.ptrw()), &app_profile_handle, &app); + int app_status = NvAPI_DRS_GetApplicationInfo(session_handle, profile_handle, (NvU16 *)(app_executable_name_u16.ptrw()), &app); if (app_status != 0) { - print_verbose("NVAPI: Application not found, adding to profile..."); + print_verbose("NVAPI: Application not found in profile, creating..."); app.isPredefined = 0; - app.isMetro = 1; - app.isCommandLine = 1; memcpy(app.appName, app_executable_name_u16.get_data(), sizeof(char16_t) * app_executable_name_u16.size()); - memcpy(app.userFriendlyName, app_friendly_name_u16.get_data(), sizeof(char16_t) * app_friendly_name_u16.size()); - memcpy(app.launcher, L"", 1); - memcpy(app.fileInFolder, L"", 1); + memcpy(app.launcher, L"", sizeof(wchar_t)); + memcpy(app.fileInFolder, L"", sizeof(wchar_t)); if (!nvapi_err_check("NVAPI: Error creating application", NvAPI_DRS_CreateApplication(session_handle, profile_handle, &app))) { NvAPI_DRS_DestroySession(session_handle); @@ -244,11 +264,13 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { NvAPI_Unload(); return; } + if (thread_control_val == OGL_THREAD_CONTROL_DISABLE) { print_verbose("NVAPI: Disabled OpenGL threaded optimization successfully"); } else { print_verbose("NVAPI: Enabled OpenGL threaded optimization successfully"); } + NvAPI_DRS_DestroySession(session_handle); } diff --git a/platform/windows/godot.natvis b/platform/windows/godot.natvis index d049ed9a3df6..e0d3cfe13a1e 100644 --- a/platform/windows/godot.natvis +++ b/platform/windows/godot.natvis @@ -29,7 +29,7 @@ - + _p && _p->variant_map.head_element ? _p->variant_map.num_elements : 0 @@ -38,7 +38,7 @@ _p ? _p->variant_map.head_element : nullptr next (*this),view(MapHelper) - + @@ -186,6 +186,7 @@ {reinterpret_cast<const Variant::PackedArrayRef<Vector2>*>(_data.packed_array)->array} {reinterpret_cast<const Variant::PackedArrayRef<Vector3>*>(_data.packed_array)->array} {reinterpret_cast<const Variant::PackedArrayRef<Color>*>(_data.packed_array)->array} + {reinterpret_cast<const Variant::PackedArrayRef<Vector4>*>(_data.packed_array)->array} [INVALID] ((String *)(_data._mem))->_cowdata._ptr,s32 @@ -214,12 +215,13 @@ reinterpret_cast<const Variant::PackedArrayRef<unsigned char>*>(_data.packed_array)->array reinterpret_cast<const Variant::PackedArrayRef<int>*>(_data.packed_array)->array *reinterpret_cast<PackedInt64Array *>(&_data.packed_array[1]) - reinterpret_cast<const Variant::PackedArrayRef<float>*>(_data.packed_array)->array + reinterpret_cast<const Variant::PackedArrayRef<float>*>(_data.packed_array)->array reinterpret_cast<const Variant::PackedArrayRef<double>*>(_data.packed_array)->array reinterpret_cast<const Variant::PackedArrayRef<String>*>(_data.packed_array)->array reinterpret_cast<const Variant::PackedArrayRef<Vector2>*>(_data.packed_array)->array reinterpret_cast<const Variant::PackedArrayRef<Vector3>*>(_data.packed_array)->array reinterpret_cast<const Variant::PackedArrayRef<Color>*>(_data.packed_array)->array + reinterpret_cast<const Variant::PackedArrayRef<Vector4>*>(_data.packed_array)->array diff --git a/platform/windows/godot_windows.cpp b/platform/windows/godot_windows.cpp index 5f41b4e568dc..486c3120fcad 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/joypad_windows.cpp b/platform/windows/joypad_windows.cpp index 60edb00dd284..a5f1629cf038 100644 --- a/platform/windows/joypad_windows.cpp +++ b/platform/windows/joypad_windows.cpp @@ -427,7 +427,7 @@ void JoypadWindows::process_joypads() { const LONG axes[] = { DIJOFS_X, DIJOFS_Y, DIJOFS_Z, DIJOFS_RX, DIJOFS_RY, DIJOFS_RZ, (LONG)DIJOFS_SLIDER(0), (LONG)DIJOFS_SLIDER(1) }; int values[] = { js.lX, js.lY, js.lZ, js.lRx, js.lRy, js.lRz, js.rglSlider[0], js.rglSlider[1] }; - for (int j = 0; j < joy->joy_axis.size(); j++) { + for (uint32_t j = 0; j < joy->joy_axis.size(); j++) { for (int k = 0; k < count; k++) { if (joy->joy_axis[j] == axes[k]) { input->joy_axis(joy->id, (JoyAxis)j, axis_correct(values[k])); diff --git a/platform/windows/joypad_windows.h b/platform/windows/joypad_windows.h index cfddbcc8dc0d..87c7af76577d 100644 --- a/platform/windows/joypad_windows.h +++ b/platform/windows/joypad_windows.h @@ -77,7 +77,7 @@ class JoypadWindows { DWORD last_pad; LPDIRECTINPUTDEVICE8 di_joy; - List joy_axis; + LocalVector joy_axis; GUID guid; dinput_gamepad() { diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index abed93d414b4..157702655eae 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(FileAccess::ACCESS_RESOURCES); FileAccess::make_default(FileAccess::ACCESS_USERDATA); FileAccess::make_default(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/platform_windows_builders.py b/platform/windows/platform_windows_builders.py index 729d55cea612..fc52e3945601 100644 --- a/platform/windows/platform_windows_builders.py +++ b/platform/windows/platform_windows_builders.py @@ -1,24 +1,17 @@ """Functions used to generate source files during build time""" import os -from detect import get_mingw_bin_prefix -from detect import try_cmd +from detect import get_mingw_tool def make_debug_mingw(target, source, env): - dst = str(target[0]) - # Force separate debug symbols if executable size is larger than 1.9 GB. - if env["separate_debug_symbols"] or os.stat(dst).st_size >= 2040109465: - mingw_bin_prefix = get_mingw_bin_prefix(env["mingw_prefix"], env["arch"]) - if try_cmd("objcopy --version", env["mingw_prefix"], env["arch"]): - os.system(mingw_bin_prefix + "objcopy --only-keep-debug {0} {0}.debugsymbols".format(dst)) - else: - os.system("objcopy --only-keep-debug {0} {0}.debugsymbols".format(dst)) - if try_cmd("strip --version", env["mingw_prefix"], env["arch"]): - os.system(mingw_bin_prefix + "strip --strip-debug --strip-unneeded {0}".format(dst)) - else: - os.system("strip --strip-debug --strip-unneeded {0}".format(dst)) - if try_cmd("objcopy --version", env["mingw_prefix"], env["arch"]): - os.system(mingw_bin_prefix + "objcopy --add-gnu-debuglink={0}.debugsymbols {0}".format(dst)) - else: - os.system("objcopy --add-gnu-debuglink={0}.debugsymbols {0}".format(dst)) + objcopy = get_mingw_tool("objcopy", env["mingw_prefix"], env["arch"]) + strip = get_mingw_tool("strip", env["mingw_prefix"], env["arch"]) + + if not objcopy or not strip: + print('`separate_debug_symbols` requires both "objcopy" and "strip" to function.') + return + + os.system("{0} --only-keep-debug {1} {1}.debugsymbols".format(objcopy, target[0])) + os.system("{0} --strip-debug --strip-unneeded {1}".format(strip, target[0])) + os.system("{0} --add-gnu-debuglink={1}.debugsymbols {1}".format(objcopy, target[0])) diff --git a/platform/windows/windows_terminal_logger.cpp b/platform/windows/windows_terminal_logger.cpp index 6881a7559697..6fc33afe9967 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 56115db4a49a..57b11d1a4778 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/animated_sprite_2d.cpp b/scene/2d/animated_sprite_2d.cpp index 6eaf31d701e3..37e9d1f8c19d 100644 --- a/scene/2d/animated_sprite_2d.cpp +++ b/scene/2d/animated_sprite_2d.cpp @@ -305,7 +305,7 @@ void AnimatedSprite2D::set_sprite_frames(const Ref &p_frames) { autoplay = String(); } else { if (!frames->has_animation(animation)) { - set_animation(al[0]); + set_animation(al.front()->get()); } if (!frames->has_animation(autoplay)) { autoplay = String(); diff --git a/scene/2d/parallax_2d.cpp b/scene/2d/parallax_2d.cpp index 555f3b031c9a..aacab3213d99 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/2d/physics/collision_polygon_2d.cpp b/scene/2d/physics/collision_polygon_2d.cpp index 72ee4d52c5f0..a9b47ef4d453 100644 --- a/scene/2d/physics/collision_polygon_2d.cpp +++ b/scene/2d/physics/collision_polygon_2d.cpp @@ -144,7 +144,7 @@ void CollisionPolygon2D::_notification(int p_what) { } #endif - const Color stroke_color = Color(0.9, 0.2, 0.0); + const Color stroke_color = get_tree()->get_debug_collisions_color(); draw_polyline(polygon, stroke_color); // Draw the last segment. draw_line(polygon[polygon.size() - 1], polygon[0], stroke_color); diff --git a/scene/2d/tile_map_layer.cpp b/scene/2d/tile_map_layer.cpp index ab3c48562c7c..fd1a638b589d 100644 --- a/scene/2d/tile_map_layer.cpp +++ b/scene/2d/tile_map_layer.cpp @@ -212,8 +212,7 @@ void TileMapLayer::_rendering_update(bool p_force_cleanup) { // Free all quadrants. if (forced_cleanup || quandrant_shape_changed) { for (const KeyValue> &kv : rendering_quadrant_map) { - for (int i = 0; i < kv.value->canvas_items.size(); i++) { - const RID &ci = kv.value->canvas_items[i]; + for (const RID &ci : kv.value->canvas_items) { if (ci.is_valid()) { rs->free(ci); } @@ -354,8 +353,7 @@ void TileMapLayer::_rendering_update(bool p_force_cleanup) { } else { // Free the quadrant. - for (int i = 0; i < rendering_quadrant->canvas_items.size(); i++) { - const RID &ci = rendering_quadrant->canvas_items[i]; + for (const RID &ci : rendering_quadrant->canvas_items) { if (ci.is_valid()) { rs->free(ci); } diff --git a/scene/3d/cpu_particles_3d.cpp b/scene/3d/cpu_particles_3d.cpp index 0dc9834539de..5aa50a4a21f9 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)) { @@ -1125,7 +1125,7 @@ void CPUParticles3D::_particles_process(double p_delta) { //turn particle by rotation in Y if (particle_flags[PARTICLE_FLAG_ROTATE_Y]) { Basis rot_y(Vector3(0, 1, 0), p.custom[0]); - p.transform.basis = p.transform.basis * rot_y; + p.transform.basis = rot_y; } } diff --git a/scene/3d/decal.cpp b/scene/3d/decal.cpp index 8415fb38cbba..485599d0fb55 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 8af386f282b8..54631a8dff58 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 8fd5f25749cc..3a05ec9c9ea9 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 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 9f71c881a9f2..54370f42dad2 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/occluder_instance_3d.cpp b/scene/3d/occluder_instance_3d.cpp index 2f77185d0dae..150771545b43 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/skeleton_3d.cpp b/scene/3d/skeleton_3d.cpp index db36a0a63013..2ddccb025335 100644 --- a/scene/3d/skeleton_3d.cpp +++ b/scene/3d/skeleton_3d.cpp @@ -264,6 +264,8 @@ void Skeleton3D::_update_process_order() { } } + bones_backup.resize(bones.size()); + process_order_dirty = false; emit_signal("bone_list_changed"); @@ -271,16 +273,15 @@ void Skeleton3D::_update_process_order() { #ifndef DISABLE_DEPRECATED void Skeleton3D::setup_simulator() { + if (simulator && simulator->get_parent() == this) { + remove_child(simulator); + simulator->queue_free(); + } PhysicalBoneSimulator3D *sim = memnew(PhysicalBoneSimulator3D); simulator = sim; sim->is_compat = true; sim->set_active(false); // Don't run unneeded process. - add_child(sim); -} - -void Skeleton3D::remove_simulator() { - remove_child(simulator); - memdelete(simulator); + add_child(simulator); } #endif // _DISABLE_DEPRECATED @@ -294,11 +295,6 @@ void Skeleton3D::_notification(int p_what) { setup_simulator(); #endif // _DISABLE_DEPRECATED } break; -#ifndef DISABLE_DEPRECATED - case NOTIFICATION_EXIT_TREE: { - remove_simulator(); - } break; -#endif // _DISABLE_DEPRECATED case NOTIFICATION_UPDATE_SKELETON: { // Update bone transforms to apply unprocessed poses. force_update_all_dirty_bones(); @@ -310,19 +306,10 @@ void Skeleton3D::_notification(int p_what) { // Process modifiers. _find_modifiers(); - LocalVector current_bone_poses; - LocalVector current_pose_positions; - LocalVector current_pose_rotations; - LocalVector current_pose_scales; - LocalVector current_bone_global_poses; if (!modifiers.is_empty()) { // Store unmodified bone poses. - for (int i = 0; i < len; i++) { - current_bone_poses.push_back(bones[i].pose_cache); - current_pose_positions.push_back(bones[i].pose_position); - current_pose_rotations.push_back(bones[i].pose_rotation); - current_pose_scales.push_back(bones[i].pose_scale); - current_bone_global_poses.push_back(bones[i].global_pose); + for (int i = 0; i < bones.size(); i++) { + bones_backup[i].save(bones[i]); } _process_modifiers(); } @@ -388,12 +375,8 @@ void Skeleton3D::_notification(int p_what) { if (!modifiers.is_empty()) { // Restore unmodified bone poses. - for (int i = 0; i < len; i++) { - bonesptr[i].pose_cache = current_bone_poses[i]; - bonesptr[i].pose_position = current_pose_positions[i]; - bonesptr[i].pose_rotation = current_pose_rotations[i]; - bonesptr[i].pose_scale = current_pose_scales[i]; - bonesptr[i].global_pose = current_bone_global_poses[i]; + for (int i = 0; i < bones.size(); i++) { + bones_backup[i].restore(bones.write[i]); } } @@ -869,7 +852,7 @@ void Skeleton3D::force_update_bone_children_transforms(int p_bone_idx) { bones_to_process.push_back(p_bone_idx); while (bones_to_process.size() > 0) { - int current_bone_idx = bones_to_process[0]; + int current_bone_idx = bones_to_process.front()->get(); bones_to_process.erase(current_bone_idx); Bone &b = bonesptr[current_bone_idx]; diff --git a/scene/3d/skeleton_3d.h b/scene/3d/skeleton_3d.h index 5b6f60dbd463..23b94239936a 100644 --- a/scene/3d/skeleton_3d.h +++ b/scene/3d/skeleton_3d.h @@ -69,7 +69,6 @@ class Skeleton3D : public Node3D { #ifndef DISABLE_DEPRECATED Node *simulator = nullptr; void setup_simulator(); - void remove_simulator(); #endif // _DISABLE_DEPRECATED public: @@ -88,15 +87,15 @@ class Skeleton3D : public Node3D { struct Bone { String name; - int parent; + int parent = -1; Vector child_bones; Transform3D rest; Transform3D global_rest; - bool enabled; - Transform3D pose_cache; + bool enabled = true; bool pose_cache_dirty = true; + Transform3D pose_cache; Vector3 pose_position; Quaternion pose_rotation; Vector3 pose_scale = Vector3(1, 1, 1); @@ -116,15 +115,29 @@ class Skeleton3D : public Node3D { bool global_pose_override_reset = false; Transform3D global_pose_override; #endif // _DISABLE_DEPRECATED + }; - Bone() { - parent = -1; - child_bones = Vector(); - enabled = true; -#ifndef DISABLE_DEPRECATED - global_pose_override_amount = 0; - global_pose_override_reset = false; -#endif // _DISABLE_DEPRECATED + struct BonePoseBackup { + Transform3D pose_cache; + Vector3 pose_position; + Quaternion pose_rotation; + Vector3 pose_scale = Vector3(1, 1, 1); + Transform3D global_pose; + + void save(const Bone &p_bone) { + pose_cache = p_bone.pose_cache; + pose_position = p_bone.pose_position; + pose_rotation = p_bone.pose_rotation; + pose_scale = p_bone.pose_scale; + global_pose = p_bone.global_pose; + } + + void restore(Bone &r_bone) { + r_bone.pose_cache = pose_cache; + r_bone.pose_position = pose_position; + r_bone.pose_rotation = pose_rotation; + r_bone.pose_scale = pose_scale; + r_bone.global_pose = global_pose; } }; @@ -156,6 +169,7 @@ class Skeleton3D : public Node3D { void _process_modifiers(); void _process_changed(); void _make_modifiers_dirty(); + LocalVector bones_backup; #ifndef DISABLE_DEPRECATED void _add_bone_bind_compat_88791(const String &p_name); diff --git a/scene/3d/sprite_3d.cpp b/scene/3d/sprite_3d.cpp index a7ac278bc19b..7d2a821d169f 100644 --- a/scene/3d/sprite_3d.cpp +++ b/scene/3d/sprite_3d.cpp @@ -1191,7 +1191,7 @@ void AnimatedSprite3D::set_sprite_frames(const Ref &p_frames) { autoplay = String(); } else { if (!frames->has_animation(animation)) { - set_animation(al[0]); + set_animation(al.front()->get()); } if (!frames->has_animation(autoplay)) { autoplay = String(); diff --git a/scene/3d/visual_instance_3d.cpp b/scene/3d/visual_instance_3d.cpp index 89f7ab239181..e0dc300a6bab 100644 --- a/scene/3d/visual_instance_3d.cpp +++ b/scene/3d/visual_instance_3d.cpp @@ -194,6 +194,7 @@ Ref GeometryInstance3D::get_material_overlay() const { void GeometryInstance3D::set_transparency(float p_transparency) { transparency = CLAMP(p_transparency, 0.0f, 1.0f); RS::get_singleton()->instance_geometry_set_transparency(get_instance(), transparency); + update_configuration_warnings(); } float GeometryInstance3D::get_transparency() const { @@ -441,6 +442,14 @@ PackedStringArray GeometryInstance3D::get_configuration_warnings() const { warnings.push_back(RTR("The GeometryInstance3D is configured to fade out smoothly over distance, but the fade transition distance is set to 0.\nTo resolve this, increase Visibility Range End Margin above 0.")); } + if (!Math::is_zero_approx(transparency) && OS::get_singleton()->get_current_rendering_method() != "forward_plus") { + warnings.push_back(RTR("GeometryInstance3D transparency is only available when using the Forward+ rendering method.")); + } + + if ((visibility_range_fade_mode == VISIBILITY_RANGE_FADE_SELF || visibility_range_fade_mode == VISIBILITY_RANGE_FADE_DEPENDENCIES) && OS::get_singleton()->get_current_rendering_method() != "forward_plus") { + warnings.push_back(RTR("GeometryInstance3D visibility range transparency fade is only available when using the Forward+ rendering method.")); + } + return warnings; } diff --git a/scene/3d/voxel_gi.cpp b/scene/3d/voxel_gi.cpp index 938d6e569903..fbdda67526e3 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_hand_modifier_3d.cpp b/scene/3d/xr_hand_modifier_3d.cpp index 1e78a4630f5c..baaa9eee48d6 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 67d1694d41a2..3d78f32b64f0 100644 --- a/scene/3d/xr_hand_modifier_3d.h +++ b/scene/3d/xr_hand_modifier_3d.h @@ -73,6 +73,9 @@ class XRHandModifier3D : public SkeletonModifier3D { BoneUpdate bone_update = BONE_UPDATE_FULL; JointData joints[XRHandTracker::HAND_JOINT_MAX]; + bool has_stored_previous_transforms = false; + Vector 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_blend_tree.cpp b/scene/animation/animation_blend_tree.cpp index 71f9c45eea51..f3385b4cdc38 100644 --- a/scene/animation/animation_blend_tree.cpp +++ b/scene/animation/animation_blend_tree.cpp @@ -215,7 +215,7 @@ AnimationNode::NodeTimeInfo AnimationNodeAnimation::_process(const AnimationMixe } else if (cur_loop_mode == Animation::LOOP_PINGPONG) { if (!Math::is_zero_approx(anim_size)) { if (Math::fposmod(cur_playback_time, anim_size * 2.0) >= anim_size) { - cur_delta = -cur_delta; // Needed for retrieveing discrete keys correctly. + cur_delta = -cur_delta; // Needed for retrieving discrete keys correctly. } prev_playback_time = Math::pingpong(prev_playback_time, anim_size); cur_playback_time = Math::pingpong(cur_playback_time, anim_size); diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index 5a3a5f9bc0af..d22b58346f03 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; @@ -2090,7 +2093,7 @@ Ref AnimationMixer::apply_reset(bool p_user_initiated) { void AnimationMixer::capture(const StringName &p_name, double p_duration, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) { ERR_FAIL_COND(!active); ERR_FAIL_COND(!has_animation(p_name)); - ERR_FAIL_COND(Math::is_zero_approx(p_duration)); + ERR_FAIL_COND(p_duration <= 0); Ref reference_animation = get_animation(p_name); if (!cache_valid) { diff --git a/scene/animation/animation_node_state_machine.cpp b/scene/animation/animation_node_state_machine.cpp index f5bef899da56..87574a66ed4f 100644 --- a/scene/animation/animation_node_state_machine.cpp +++ b/scene/animation/animation_node_state_machine.cpp @@ -350,18 +350,18 @@ float AnimationNodeStateMachinePlayback::get_fading_pos() const { void AnimationNodeStateMachinePlayback::_clear_path_children(AnimationTree *p_tree, AnimationNodeStateMachine *p_state_machine, bool p_test_only) { List child_nodes; p_state_machine->get_child_nodes(&child_nodes); - for (int i = 0; i < child_nodes.size(); i++) { - Ref anodesm = child_nodes[i].node; + for (const AnimationNode::ChildNode &child_node : child_nodes) { + Ref anodesm = child_node.node; if (anodesm.is_valid() && anodesm->get_state_machine_type() == AnimationNodeStateMachine::STATE_MACHINE_TYPE_GROUPED) { - Ref playback = p_tree->get(base_path + child_nodes[i].name + "/playback"); + Ref playback = p_tree->get(base_path + child_node.name + "/playback"); ERR_FAIL_COND(!playback.is_valid()); - playback->_set_base_path(base_path + child_nodes[i].name + "/"); + playback->_set_base_path(base_path + child_node.name + "/"); if (p_test_only) { playback = playback->duplicate(); } playback->path.clear(); playback->_clear_path_children(p_tree, anodesm.ptr(), p_test_only); - if (current != child_nodes[i].name) { + if (current != child_node.name) { playback->_start(anodesm.ptr()); // Can restart. } } diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp index d46470282f12..22138004769d 100644 --- a/scene/animation/animation_player.cpp +++ b/scene/animation/animation_player.cpp @@ -125,6 +125,8 @@ void AnimationPlayer::_validate_property(PropertyInfo &p_property) const { } p_property.hint_string = hint; + } else if (!auto_capture && p_property.name.begins_with("playback_auto_capture_")) { + p_property.usage = PROPERTY_USAGE_NONE; } } @@ -149,7 +151,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(); } @@ -370,73 +372,21 @@ void AnimationPlayer::play_backwards(const StringName &p_name, double p_custom_b play(p_name, p_custom_blend, -1, true); } -void AnimationPlayer::play_with_capture(const StringName &p_name, double p_duration, double p_custom_blend, float p_custom_scale, bool p_from_end, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) { - StringName name = p_name; - if (name == StringName()) { - name = playback.assigned; - } - - if (signbit(p_duration)) { - double max_dur = 0; - Ref anim = get_animation(name); - if (anim.is_valid()) { - double current_pos = playback.current.pos; - if (playback.assigned != name) { - current_pos = p_from_end ? anim->get_length() : 0; - } - for (int i = 0; i < anim->get_track_count(); i++) { - if (anim->track_get_type(i) != Animation::TYPE_VALUE) { - continue; - } - if (anim->value_track_get_update_mode(i) != Animation::UPDATE_CAPTURE) { - continue; - } - if (anim->track_get_key_count(i) == 0) { - continue; - } - max_dur = MAX(max_dur, p_from_end ? current_pos - anim->track_get_key_time(i, anim->track_get_key_count(i) - 1) : anim->track_get_key_time(i, 0) - current_pos); - } - } - p_duration = max_dur; +void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { + if (auto_capture) { + play_with_capture(p_name, auto_capture_duration, p_custom_blend, p_custom_scale, p_from_end, auto_capture_transition_type, auto_capture_ease_type); + } else { + _play(p_name, p_custom_blend, p_custom_scale, p_from_end); } - - capture(name, p_duration, p_trans_type, p_ease_type); - play(name, p_custom_blend, p_custom_scale, p_from_end); } -void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { +void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { StringName name = p_name; if (name == StringName()) { name = playback.assigned; } -#ifdef TOOLS_ENABLED - if (!Engine::get_singleton()->is_editor_hint()) { - bool warn_enabled = false; - if (capture_cache.animation.is_null()) { - Ref anim = get_animation(name); - if (anim.is_valid()) { - for (int i = 0; i < anim->get_track_count(); i++) { - if (anim->track_get_type(i) != Animation::TYPE_VALUE) { - continue; - } - if (anim->value_track_get_update_mode(i) != Animation::UPDATE_CAPTURE) { - continue; - } - if (anim->track_get_key_count(i) == 0) { - continue; - } - warn_enabled = true; - } - } - } - if (warn_enabled) { - WARN_PRINT_ONCE_ED("Capture track found. If you want to interpolate animation with captured frame, you can use play_with_capture() instead of play()."); - } - } -#endif - ERR_FAIL_COND_MSG(!animation_set.has(name), vformat("Animation not found: %s.", name)); Playback &c = playback; @@ -525,6 +475,47 @@ void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, floa } } +void AnimationPlayer::_capture(const StringName &p_name, bool p_from_end, double p_duration, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) { + StringName name = p_name; + if (name == StringName()) { + name = playback.assigned; + } + + Ref anim = get_animation(name); + if (anim.is_null() || !anim->is_capture_included()) { + return; + } + if (signbit(p_duration)) { + double max_dur = 0; + double current_pos = playback.current.pos; + if (playback.assigned != name) { + current_pos = p_from_end ? anim->get_length() : 0; + } + for (int i = 0; i < anim->get_track_count(); i++) { + if (anim->track_get_type(i) != Animation::TYPE_VALUE) { + continue; + } + if (anim->value_track_get_update_mode(i) != Animation::UPDATE_CAPTURE) { + continue; + } + if (anim->track_get_key_count(i) == 0) { + continue; + } + max_dur = MAX(max_dur, p_from_end ? current_pos - anim->track_get_key_time(i, anim->track_get_key_count(i) - 1) : anim->track_get_key_time(i, 0) - current_pos); + } + p_duration = max_dur; + } + if (Math::is_zero_approx(p_duration)) { + return; + } + capture(name, p_duration, p_trans_type, p_ease_type); +} + +void AnimationPlayer::play_with_capture(const StringName &p_name, double p_duration, double p_custom_blend, float p_custom_scale, bool p_from_end, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) { + _capture(p_name, p_from_end, p_duration, p_trans_type, p_ease_type); + _play(p_name, p_custom_blend, p_custom_scale, p_from_end); +} + bool AnimationPlayer::is_playing() const { return playing; } @@ -725,6 +716,39 @@ double AnimationPlayer::get_blend_time(const StringName &p_animation1, const Str } } +void AnimationPlayer::set_auto_capture(bool p_auto_capture) { + auto_capture = p_auto_capture; + notify_property_list_changed(); +} + +bool AnimationPlayer::is_auto_capture() const { + return auto_capture; +} + +void AnimationPlayer::set_auto_capture_duration(double p_auto_capture_duration) { + auto_capture_duration = p_auto_capture_duration; +} + +double AnimationPlayer::get_auto_capture_duration() const { + return auto_capture_duration; +} + +void AnimationPlayer::set_auto_capture_transition_type(Tween::TransitionType p_auto_capture_transition_type) { + auto_capture_transition_type = p_auto_capture_transition_type; +} + +Tween::TransitionType AnimationPlayer::get_auto_capture_transition_type() const { + return auto_capture_transition_type; +} + +void AnimationPlayer::set_auto_capture_ease_type(Tween::EaseType p_auto_capture_ease_type) { + auto_capture_ease_type = p_auto_capture_ease_type; +} + +Tween::EaseType AnimationPlayer::get_auto_capture_ease_type() const { + return auto_capture_ease_type; +} + #ifdef TOOLS_ENABLED void AnimationPlayer::get_argument_options(const StringName &p_function, int p_idx, List *r_options) const { const String pf = p_function; @@ -815,9 +839,18 @@ void AnimationPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_default_blend_time", "sec"), &AnimationPlayer::set_default_blend_time); ClassDB::bind_method(D_METHOD("get_default_blend_time"), &AnimationPlayer::get_default_blend_time); + ClassDB::bind_method(D_METHOD("set_auto_capture", "auto_capture"), &AnimationPlayer::set_auto_capture); + ClassDB::bind_method(D_METHOD("is_auto_capture"), &AnimationPlayer::is_auto_capture); + ClassDB::bind_method(D_METHOD("set_auto_capture_duration", "auto_capture_duration"), &AnimationPlayer::set_auto_capture_duration); + ClassDB::bind_method(D_METHOD("get_auto_capture_duration"), &AnimationPlayer::get_auto_capture_duration); + ClassDB::bind_method(D_METHOD("set_auto_capture_transition_type", "auto_capture_transition_type"), &AnimationPlayer::set_auto_capture_transition_type); + ClassDB::bind_method(D_METHOD("get_auto_capture_transition_type"), &AnimationPlayer::get_auto_capture_transition_type); + ClassDB::bind_method(D_METHOD("set_auto_capture_ease_type", "auto_capture_ease_type"), &AnimationPlayer::set_auto_capture_ease_type); + ClassDB::bind_method(D_METHOD("get_auto_capture_ease_type"), &AnimationPlayer::get_auto_capture_ease_type); + ClassDB::bind_method(D_METHOD("play", "name", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false)); ClassDB::bind_method(D_METHOD("play_backwards", "name", "custom_blend"), &AnimationPlayer::play_backwards, DEFVAL(StringName()), DEFVAL(-1)); - ClassDB::bind_method(D_METHOD("play_with_capture", "name", "duration", "custom_blend", "custom_speed", "from_end", "trans_type", "ease_type"), &AnimationPlayer::play_with_capture, DEFVAL(-1.0), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false), DEFVAL(Tween::TRANS_LINEAR), DEFVAL(Tween::EASE_IN)); + ClassDB::bind_method(D_METHOD("play_with_capture", "name", "duration", "custom_blend", "custom_speed", "from_end", "trans_type", "ease_type"), &AnimationPlayer::play_with_capture, DEFVAL(StringName()), DEFVAL(-1.0), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false), DEFVAL(Tween::TRANS_LINEAR), DEFVAL(Tween::EASE_IN)); ClassDB::bind_method(D_METHOD("pause"), &AnimationPlayer::pause); ClassDB::bind_method(D_METHOD("stop", "keep_state"), &AnimationPlayer::stop, DEFVAL(false)); ClassDB::bind_method(D_METHOD("is_playing"), &AnimationPlayer::is_playing); @@ -855,6 +888,10 @@ void AnimationPlayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "current_animation_position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "", "get_current_animation_position"); ADD_GROUP("Playback Options", "playback_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playback_auto_capture"), "set_auto_capture", "is_auto_capture"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "playback_auto_capture_duration", PROPERTY_HINT_NONE, "suffix:s"), "set_auto_capture_duration", "get_auto_capture_duration"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "playback_auto_capture_transition_type", PROPERTY_HINT_ENUM, "Linear,Sine,Quint,Quart,Expo,Elastic,Cubic,Circ,Bounce,Back,Spring"), "set_auto_capture_transition_type", "get_auto_capture_transition_type"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "playback_auto_capture_ease_type", PROPERTY_HINT_ENUM, "In,Out,InOut,OutIn"), "set_auto_capture_ease_type", "get_auto_capture_ease_type"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "playback_default_blend_time", PROPERTY_HINT_RANGE, "0,4096,0.01,suffix:s"), "set_default_blend_time", "get_default_blend_time"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed_scale", PROPERTY_HINT_RANGE, "-4,4,0.001,or_less,or_greater"), "set_speed_scale", "get_speed_scale"); diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h index 13e1e37ca9cc..f270f32193f4 100644 --- a/scene/animation/animation_player.h +++ b/scene/animation/animation_player.h @@ -56,6 +56,12 @@ class AnimationPlayer : public AnimationMixer { float speed_scale = 1.0; double default_blend_time = 0.0; + + bool auto_capture = true; + double auto_capture_duration = -1.0; + Tween::TransitionType auto_capture_transition_type = Tween::TRANS_LINEAR; + Tween::EaseType auto_capture_ease_type = Tween::EASE_IN; + bool is_stopping = false; struct PlaybackData { @@ -108,6 +114,8 @@ class AnimationPlayer : public AnimationMixer { bool reset_on_save = true; bool movie_quit_on_finish = false; + void _play(const StringName &p_name, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); + void _capture(const StringName &p_name, bool p_from_end = false, double p_duration = -1.0, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN); void _process_playback_data(PlaybackData &cd, double p_delta, float p_blend, bool p_seeked, bool p_started, bool p_is_current = false); void _blend_playback_data(double p_delta, bool p_started); void _stop_internal(bool p_reset, bool p_keep_state); @@ -158,9 +166,18 @@ class AnimationPlayer : public AnimationMixer { void set_default_blend_time(double p_default); double get_default_blend_time() const; + void set_auto_capture(bool p_auto_capture); + bool is_auto_capture() const; + void set_auto_capture_duration(double p_auto_capture_duration); + double get_auto_capture_duration() const; + void set_auto_capture_transition_type(Tween::TransitionType p_auto_capture_transition_type); + Tween::TransitionType get_auto_capture_transition_type() const; + void set_auto_capture_ease_type(Tween::EaseType p_auto_capture_ease_type); + Tween::EaseType get_auto_capture_ease_type() const; + void play(const StringName &p_name = StringName(), double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); void play_backwards(const StringName &p_name = StringName(), double p_custom_blend = -1); - void play_with_capture(const StringName &p_name, double p_duration = -1.0, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN); + void play_with_capture(const StringName &p_name = StringName(), double p_duration = -1.0, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN); void queue(const StringName &p_name); Vector get_queue(); void clear_queue(); diff --git a/scene/animation/animation_tree.cpp b/scene/animation/animation_tree.cpp index 4880a0f6ed7a..28a163768f1c 100644 --- a/scene/animation/animation_tree.cpp +++ b/scene/animation/animation_tree.cpp @@ -854,10 +854,10 @@ void AnimationTree::_setup_animation_player() { } List list; player->get_animation_library_list(&list); - for (int i = 0; i < list.size(); i++) { - Ref lib = player->get_animation_library(list[i]); + for (const StringName &E : list) { + Ref lib = player->get_animation_library(E); if (lib.is_valid()) { - add_animation_library(list[i], lib); + add_animation_library(E, lib); } } } diff --git a/scene/debugger/scene_debugger.cpp b/scene/debugger/scene_debugger.cpp index 19e5693736d7..07c32eef1360 100644 --- a/scene/debugger/scene_debugger.cpp +++ b/scene/debugger/scene_debugger.cpp @@ -421,9 +421,9 @@ void SceneDebuggerObject::_parse_script_properties(Script *p_script, ScriptInsta void SceneDebuggerObject::serialize(Array &r_arr, int p_max_size) { Array send_props; - for (int i = 0; i < properties.size(); i++) { - const PropertyInfo &pi = properties[i].first; - Variant &var = properties[i].second; + for (SceneDebuggerObject::SceneDebuggerProperty &property : properties) { + const PropertyInfo &pi = property.first; + Variant &var = property.second; Ref res = var; @@ -510,7 +510,7 @@ SceneDebuggerTree::SceneDebuggerTree(Node *p_root) { const StringName &is_visible_sn = SNAME("is_visible"); const StringName &is_visible_in_tree_sn = SNAME("is_visible_in_tree"); while (stack.size()) { - Node *n = stack[0]; + Node *n = stack.front()->get(); stack.pop_front(); int count = n->get_child_count(); diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 4f90504e35f0..8131fe7aaa52 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 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 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 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 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 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 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 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 caret_columns; - Vector> from_selections; - Vector> 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 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 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 caret_edit_order = get_caret_index_edit_order(); - Vector 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 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 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 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 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 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 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 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 1770d4f4d897..56f8cce548c3 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -309,11 +309,14 @@ class CodeEdit : public TextEdit { 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 @@ class CodeEdit : public TextEdit { 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 get_folded_lines() const; @@ -489,6 +493,10 @@ class CodeEdit : public TextEdit { 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/color_picker.cpp b/scene/gui/color_picker.cpp index 848a598ebb82..aeaaac1efcb3 100644 --- a/scene/gui/color_picker.cpp +++ b/scene/gui/color_picker.cpp @@ -458,8 +458,8 @@ void ColorPicker::set_editor_settings(Object *p_editor_settings) { } } - for (int i = 0; i < preset_cache.size(); i++) { - presets.push_back(preset_cache[i]); + for (const Color &preset : preset_cache) { + presets.push_back(preset); } if (recent_preset_cache.is_empty()) { @@ -469,8 +469,8 @@ void ColorPicker::set_editor_settings(Object *p_editor_settings) { } } - for (int i = 0; i < recent_preset_cache.size(); i++) { - recent_presets.push_back(recent_preset_cache[i]); + for (const Color &preset : recent_preset_cache) { + recent_presets.push_back(preset); } _update_presets(); @@ -660,8 +660,8 @@ void ColorPicker::_update_presets() { for (int i = 1; i < preset_container->get_child_count(); i++) { preset_container->get_child(i)->queue_free(); } - for (int i = 0; i < preset_cache.size(); i++) { - _add_preset_button(preset_size, preset_cache[i]); + for (const Color &preset : preset_cache) { + _add_preset_button(preset_size, preset); } _notification(NOTIFICATION_VISIBILITY_CHANGED); } @@ -677,13 +677,13 @@ void ColorPicker::_update_recent_presets() { } recent_presets.clear(); - for (int i = 0; i < recent_preset_cache.size(); i++) { - recent_presets.push_back(recent_preset_cache[i]); + for (const Color &preset : recent_preset_cache) { + recent_presets.push_back(preset); } int preset_size = _get_preset_size(); - for (int i = 0; i < recent_presets.size(); i++) { - _add_recent_preset_button(preset_size, recent_presets[i]); + for (const Color &preset : recent_presets) { + _add_recent_preset_button(preset_size, preset); } _notification(NOTIFICATION_VISIBILITY_CHANGED); @@ -937,8 +937,9 @@ void ColorPicker::erase_recent_preset(const Color &p_color) { PackedColorArray ColorPicker::get_presets() const { PackedColorArray arr; arr.resize(presets.size()); - for (int i = 0; i < presets.size(); i++) { - arr.set(i, presets[i]); + int i = 0; + for (List::ConstIterator itr = presets.begin(); itr != presets.end(); ++itr, ++i) { + arr.set(i, *itr); } return arr; } @@ -946,8 +947,9 @@ PackedColorArray ColorPicker::get_presets() const { PackedColorArray ColorPicker::get_recent_presets() const { PackedColorArray arr; arr.resize(recent_presets.size()); - for (int i = 0; i < recent_presets.size(); i++) { - arr.set(i, recent_presets[i]); + int i = 0; + for (List::ConstIterator itr = recent_presets.begin(); itr != recent_presets.end(); ++itr, ++i) { + arr.set(i, *itr); } return arr; } diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index d430fe9bfcfa..2dd12b92f3d0 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 { @@ -208,28 +208,42 @@ void Control::set_root_layout_direction(int p_root_dir) { #ifdef TOOLS_ENABLED void Control::get_argument_options(const StringName &p_function, int p_idx, List *r_options) const { ERR_READ_THREAD_GUARD; - CanvasItem::get_argument_options(p_function, p_idx, r_options); - if (p_idx == 0) { - List sn; const String pf = p_function; + Theme::DataType type = Theme::DATA_TYPE_MAX; + if (pf == "add_theme_color_override" || pf == "has_theme_color" || pf == "has_theme_color_override" || pf == "get_theme_color") { - ThemeDB::get_singleton()->get_default_theme()->get_color_list(get_class(), &sn); - } else if (pf == "add_theme_style_override" || pf == "has_theme_style" || pf == "has_theme_style_override" || pf == "get_theme_style") { - ThemeDB::get_singleton()->get_default_theme()->get_stylebox_list(get_class(), &sn); + type = Theme::DATA_TYPE_COLOR; + } else if (pf == "add_theme_constant_override" || pf == "has_theme_constant" || pf == "has_theme_constant_override" || pf == "get_theme_constant") { + type = Theme::DATA_TYPE_CONSTANT; } else if (pf == "add_theme_font_override" || pf == "has_theme_font" || pf == "has_theme_font_override" || pf == "get_theme_font") { - ThemeDB::get_singleton()->get_default_theme()->get_font_list(get_class(), &sn); + type = Theme::DATA_TYPE_FONT; } else if (pf == "add_theme_font_size_override" || pf == "has_theme_font_size" || pf == "has_theme_font_size_override" || pf == "get_theme_font_size") { - ThemeDB::get_singleton()->get_default_theme()->get_font_size_list(get_class(), &sn); - } else if (pf == "add_theme_constant_override" || pf == "has_theme_constant" || pf == "has_theme_constant_override" || pf == "get_theme_constant") { - ThemeDB::get_singleton()->get_default_theme()->get_constant_list(get_class(), &sn); + type = Theme::DATA_TYPE_FONT_SIZE; + } else if (pf == "add_theme_icon_override" || pf == "has_theme_icon" || pf == "has_theme_icon_override" || pf == "get_theme_icon") { + type = Theme::DATA_TYPE_ICON; + } else if (pf == "add_theme_style_override" || pf == "has_theme_style" || pf == "has_theme_style_override" || pf == "get_theme_style") { + type = Theme::DATA_TYPE_STYLEBOX; } - sn.sort_custom(); - for (const StringName &name : sn) { - r_options->push_back(String(name).quote()); + if (type != Theme::DATA_TYPE_MAX) { + List theme_items; + ThemeDB::get_singleton()->get_class_items(get_class_name(), &theme_items, true, type); + + List sn; + for (const ThemeDB::ThemeItemBind &E : theme_items) { + if (E.data_type == type) { + sn.push_back(E.item_name); + } + } + + sn.sort_custom(); + for (const StringName &name : sn) { + r_options->push_back(String(name).quote()); + } } } + CanvasItem::get_argument_options(p_function, p_idx, r_options); } #endif @@ -1383,6 +1397,15 @@ void Control::_set_position(const Point2 &p_point) { void Control::set_position(const Point2 &p_point, bool p_keep_offsets) { ERR_MAIN_THREAD_GUARD; + +#ifdef TOOLS_ENABLED + // Can't compute anchors, set position directly and return immediately. + if (saving && !is_inside_tree()) { + data.pos_cache = p_point; + return; + } +#endif + if (p_keep_offsets) { _compute_anchors(Rect2(p_point, data.size_cache), data.offset, data.anchor); } else { @@ -1441,6 +1464,14 @@ void Control::set_size(const Size2 &p_size, bool p_keep_offsets) { new_size.y = min.y; } +#ifdef TOOLS_ENABLED + // Can't compute anchors, set size directly and return immediately. + if (saving && !is_inside_tree()) { + data.size_cache = new_size; + return; + } +#endif + if (p_keep_offsets) { _compute_anchors(Rect2(data.pos_cache, new_size), data.offset, data.anchor); } else { @@ -3140,6 +3171,14 @@ Control *Control::make_custom_tooltip(const String &p_text) const { void Control::_notification(int p_notification) { ERR_MAIN_THREAD_GUARD; switch (p_notification) { +#ifdef TOOLS_ENABLED + case NOTIFICATION_EDITOR_PRE_SAVE: { + saving = true; + } break; + case NOTIFICATION_EDITOR_POST_SAVE: { + saving = false; + } break; +#endif case NOTIFICATION_POSTINITIALIZE: { data.initialized = true; diff --git a/scene/gui/control.h b/scene/gui/control.h index bdb908e089ce..c784d4330dac 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -47,6 +47,10 @@ class ThemeContext; class Control : public CanvasItem { GDCLASS(Control, CanvasItem); +#ifdef TOOLS_ENABLED + bool saving = false; +#endif + public: enum Anchor { ANCHOR_BEGIN = 0, diff --git a/scene/gui/flow_container.cpp b/scene/gui/flow_container.cpp index 9f79da2905b1..f8128a9dc679 100644 --- a/scene/gui/flow_container.cpp +++ b/scene/gui/flow_container.cpp @@ -38,6 +38,7 @@ struct _LineData { int min_line_length = 0; int stretch_avail = 0; float stretch_ratio_total = 0; + bool is_filled = false; }; void FlowContainer::_resort() { @@ -58,6 +59,7 @@ void FlowContainer::_resort() { float line_stretch_ratio_total = 0; int current_container_size = vertical ? get_rect().size.y : get_rect().size.x; int children_in_current_line = 0; + Control *last_child = nullptr; // First pass for line wrapping and minimum size calculation. for (int i = 0; i < get_child_count(); i++) { @@ -77,7 +79,7 @@ void FlowContainer::_resort() { } if (ofs.y + child_msc.y > current_container_size) { line_length = ofs.y - theme_cache.v_separation; - lines_data.push_back(_LineData{ children_in_current_line, line_height, line_length, current_container_size - line_length, line_stretch_ratio_total }); + lines_data.push_back(_LineData{ children_in_current_line, line_height, line_length, current_container_size - line_length, line_stretch_ratio_total, true }); // Move in new column (vertical line). ofs.x += line_height + theme_cache.h_separation; @@ -99,7 +101,7 @@ void FlowContainer::_resort() { } if (ofs.x + child_msc.x > current_container_size) { line_length = ofs.x - theme_cache.h_separation; - lines_data.push_back(_LineData{ children_in_current_line, line_height, line_length, current_container_size - line_length, line_stretch_ratio_total }); + lines_data.push_back(_LineData{ children_in_current_line, line_height, line_length, current_container_size - line_length, line_stretch_ratio_total, true }); // Move in new line. ofs.y += line_height + theme_cache.v_separation; @@ -116,11 +118,16 @@ void FlowContainer::_resort() { ofs.x += child_msc.x; } + last_child = child; children_minsize_cache[child] = child_msc; children_in_current_line++; } line_length = vertical ? (ofs.y) : (ofs.x); - lines_data.push_back(_LineData{ children_in_current_line, line_height, line_length, current_container_size - line_length, line_stretch_ratio_total }); + bool is_filled = false; + if (last_child != nullptr) { + is_filled = vertical ? (ofs.y + last_child->get_combined_minimum_size().y > current_container_size ? true : false) : (ofs.x + last_child->get_combined_minimum_size().x > current_container_size ? true : false); + } + lines_data.push_back(_LineData{ children_in_current_line, line_height, line_length, current_container_size - line_length, line_stretch_ratio_total, is_filled }); // Second pass for in-line expansion and alignment. @@ -158,17 +165,43 @@ void FlowContainer::_resort() { // but only if the line doesn't contain a child that expands. if (child_idx_in_line == 0 && Math::is_equal_approx(line_data.stretch_ratio_total, 0)) { int alignment_ofs = 0; + bool is_not_first_line_and_not_filled = current_line_idx != 0 && !line_data.is_filled; + float prior_stretch_avail = is_not_first_line_and_not_filled ? lines_data[current_line_idx - 1].stretch_avail : 0.0; switch (alignment) { - case ALIGNMENT_CENTER: - alignment_ofs = line_data.stretch_avail / 2; - break; - case ALIGNMENT_END: - alignment_ofs = line_data.stretch_avail; - break; + case ALIGNMENT_BEGIN: { + if (last_wrap_alignment != LAST_WRAP_ALIGNMENT_INHERIT && is_not_first_line_and_not_filled) { + if (last_wrap_alignment == LAST_WRAP_ALIGNMENT_END) { + alignment_ofs = line_data.stretch_avail - prior_stretch_avail; + } else if (last_wrap_alignment == LAST_WRAP_ALIGNMENT_CENTER) { + alignment_ofs = (line_data.stretch_avail - prior_stretch_avail) * 0.5; + } + } + } break; + case ALIGNMENT_CENTER: { + if (last_wrap_alignment != LAST_WRAP_ALIGNMENT_INHERIT && last_wrap_alignment != LAST_WRAP_ALIGNMENT_CENTER && is_not_first_line_and_not_filled) { + if (last_wrap_alignment == LAST_WRAP_ALIGNMENT_END) { + alignment_ofs = line_data.stretch_avail - (prior_stretch_avail * 0.5); + } else { // Is LAST_WRAP_ALIGNMENT_BEGIN + alignment_ofs = prior_stretch_avail * 0.5; + } + } else { + alignment_ofs = line_data.stretch_avail * 0.5; + } + } break; + case ALIGNMENT_END: { + if (last_wrap_alignment != LAST_WRAP_ALIGNMENT_INHERIT && last_wrap_alignment != LAST_WRAP_ALIGNMENT_END && is_not_first_line_and_not_filled) { + if (last_wrap_alignment == LAST_WRAP_ALIGNMENT_BEGIN) { + alignment_ofs = prior_stretch_avail; + } else { // Is LAST_WRAP_ALIGNMENT_CENTER + alignment_ofs = prior_stretch_avail + (line_data.stretch_avail - prior_stretch_avail) * 0.5; + } + } else { + alignment_ofs = line_data.stretch_avail; + } + } break; default: break; } - if (vertical) { /* VERTICAL */ ofs.y += alignment_ofs; } else { /* HORIZONTAL */ @@ -314,6 +347,18 @@ FlowContainer::AlignmentMode FlowContainer::get_alignment() const { return alignment; } +void FlowContainer::set_last_wrap_alignment(LastWrapAlignmentMode p_last_wrap_alignment) { + if (last_wrap_alignment == p_last_wrap_alignment) { + return; + } + last_wrap_alignment = p_last_wrap_alignment; + _resort(); +} + +FlowContainer::LastWrapAlignmentMode FlowContainer::get_last_wrap_alignment() const { + return last_wrap_alignment; +} + void FlowContainer::set_vertical(bool p_vertical) { ERR_FAIL_COND_MSG(is_fixed, "Can't change orientation of " + get_class() + "."); vertical = p_vertical; @@ -346,6 +391,8 @@ void FlowContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_alignment", "alignment"), &FlowContainer::set_alignment); ClassDB::bind_method(D_METHOD("get_alignment"), &FlowContainer::get_alignment); + ClassDB::bind_method(D_METHOD("set_last_wrap_alignment", "last_wrap_alignment"), &FlowContainer::set_last_wrap_alignment); + ClassDB::bind_method(D_METHOD("get_last_wrap_alignment"), &FlowContainer::get_last_wrap_alignment); ClassDB::bind_method(D_METHOD("set_vertical", "vertical"), &FlowContainer::set_vertical); ClassDB::bind_method(D_METHOD("is_vertical"), &FlowContainer::is_vertical); ClassDB::bind_method(D_METHOD("set_reverse_fill", "reverse_fill"), &FlowContainer::set_reverse_fill); @@ -354,8 +401,13 @@ void FlowContainer::_bind_methods() { BIND_ENUM_CONSTANT(ALIGNMENT_BEGIN); BIND_ENUM_CONSTANT(ALIGNMENT_CENTER); BIND_ENUM_CONSTANT(ALIGNMENT_END); + BIND_ENUM_CONSTANT(LAST_WRAP_ALIGNMENT_INHERIT); + BIND_ENUM_CONSTANT(LAST_WRAP_ALIGNMENT_BEGIN); + BIND_ENUM_CONSTANT(LAST_WRAP_ALIGNMENT_CENTER); + BIND_ENUM_CONSTANT(LAST_WRAP_ALIGNMENT_END); ADD_PROPERTY(PropertyInfo(Variant::INT, "alignment", PROPERTY_HINT_ENUM, "Begin,Center,End"), "set_alignment", "get_alignment"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "last_wrap_alignment", PROPERTY_HINT_ENUM, "Inherit,Begin,Center,End"), "set_last_wrap_alignment", "get_last_wrap_alignment"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "vertical"), "set_vertical", "is_vertical"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "reverse_fill"), "set_reverse_fill", "is_reverse_fill"); diff --git a/scene/gui/flow_container.h b/scene/gui/flow_container.h index 90da73aaab89..65ebc89c7844 100644 --- a/scene/gui/flow_container.h +++ b/scene/gui/flow_container.h @@ -42,6 +42,12 @@ class FlowContainer : public Container { ALIGNMENT_CENTER, ALIGNMENT_END }; + enum LastWrapAlignmentMode { + LAST_WRAP_ALIGNMENT_INHERIT, + LAST_WRAP_ALIGNMENT_BEGIN, + LAST_WRAP_ALIGNMENT_CENTER, + LAST_WRAP_ALIGNMENT_END + }; private: int cached_size = 0; @@ -50,6 +56,7 @@ class FlowContainer : public Container { bool vertical = false; bool reverse_fill = false; AlignmentMode alignment = ALIGNMENT_BEGIN; + LastWrapAlignmentMode last_wrap_alignment = LAST_WRAP_ALIGNMENT_INHERIT; struct ThemeCache { int h_separation = 0; @@ -71,6 +78,9 @@ class FlowContainer : public Container { void set_alignment(AlignmentMode p_alignment); AlignmentMode get_alignment() const; + void set_last_wrap_alignment(LastWrapAlignmentMode p_last_wrap_alignment); + LastWrapAlignmentMode get_last_wrap_alignment() const; + void set_vertical(bool p_vertical); bool is_vertical() const; @@ -102,5 +112,6 @@ class VFlowContainer : public FlowContainer { }; VARIANT_ENUM_CAST(FlowContainer::AlignmentMode); +VARIANT_ENUM_CAST(FlowContainer::LastWrapAlignmentMode); #endif // FLOW_CONTAINER_H diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index ef9c7e35edc8..646e45b27aec 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 &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 49998beb4244..fa1059c66760 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/line_edit.cpp b/scene/gui/line_edit.cpp index ddfe202c13cc..c263e14b8a27 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -1805,7 +1805,6 @@ void LineEdit::clear_internal() { } Size2 LineEdit::get_minimum_size() const { - Ref style = theme_cache.normal; Ref font = theme_cache.font; int font_size = theme_cache.font_size; @@ -1834,7 +1833,8 @@ Size2 LineEdit::get_minimum_size() const { } min_size.width += icon_max_width; - return style->get_minimum_size() + min_size; + Size2 style_min_size = theme_cache.normal->get_minimum_size().max(theme_cache.read_only->get_minimum_size()); + return style_min_size + min_size; } void LineEdit::deselect() { diff --git a/scene/gui/popup.cpp b/scene/gui/popup.cpp index 87383283fd2e..97b33c4f748f 100644 --- a/scene/gui/popup.cpp +++ b/scene/gui/popup.cpp @@ -77,6 +77,9 @@ void Popup::_notification(int p_what) { _initialize_visible_parents(); } else { _deinitialize_visible_parents(); + if (hide_reason == HIDE_REASON_NONE) { + hide_reason = HIDE_REASON_CANCELED; + } emit_signal(SNAME("popup_hide")); popped_up = false; } @@ -87,6 +90,7 @@ void Popup::_notification(int p_what) { if (!is_in_edited_scene_root()) { if (has_focus()) { popped_up = true; + hide_reason = HIDE_REASON_NONE; } } } break; @@ -100,6 +104,7 @@ void Popup::_notification(int p_what) { case NOTIFICATION_WM_CLOSE_REQUEST: { if (!is_in_edited_scene_root()) { + hide_reason = HIDE_REASON_UNFOCUSED; _close_pressed(); } } break; @@ -114,6 +119,7 @@ void Popup::_notification(int p_what) { void Popup::_parent_focused() { if (popped_up && get_flag(FLAG_POPUP)) { + hide_reason = HIDE_REASON_UNFOCUSED; _close_pressed(); } } diff --git a/scene/gui/popup.h b/scene/gui/popup.h index 48818686f74f..69a81ad98c7e 100644 --- a/scene/gui/popup.h +++ b/scene/gui/popup.h @@ -43,6 +43,16 @@ class Popup : public Window { LocalVector visible_parents; bool popped_up = false; +public: + enum HideReason { + HIDE_REASON_NONE, + HIDE_REASON_CANCELED, // E.g., because of rupture of UI flow (app unfocused). Includes closed programmatically. + HIDE_REASON_UNFOCUSED, // E.g., user clicked outside. + }; + +private: + HideReason hide_reason = HIDE_REASON_NONE; + void _initialize_visible_parents(); void _deinitialize_visible_parents(); @@ -60,6 +70,8 @@ class Popup : public Window { virtual void _post_popup() override; public: + HideReason get_hide_reason() const { return hide_reason; } + Popup(); ~Popup(); }; diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index b2617e6fc746..90ce01e38377 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.compat.inc b/scene/gui/rich_text_label.compat.inc index 626278a4052d..97739c4b7900 100644 --- a/scene/gui/rich_text_label.compat.inc +++ b/scene/gui/rich_text_label.compat.inc @@ -38,9 +38,14 @@ void RichTextLabel::_add_image_bind_compat_80410(const Ref &p_image, add_image(p_image, p_width, p_height, p_color, p_alignment, p_region, Variant(), false, String(), false); } +bool RichTextLabel::_remove_paragraph_bind_compat_91098(int p_paragraph) { + return remove_paragraph(p_paragraph, false); +} + void RichTextLabel::_bind_compatibility_methods() { ClassDB::bind_compatibility_method(D_METHOD("push_meta", "data"), &RichTextLabel::_push_meta_bind_compat_89024); ClassDB::bind_compatibility_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align", "region"), &RichTextLabel::_add_image_bind_compat_80410, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2())); + ClassDB::bind_compatibility_method(D_METHOD("remove_paragraph", "paragraph"), &RichTextLabel::_remove_paragraph_bind_compat_91098); } #endif // DISABLE_DEPRECATED diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index e177bed20a83..19b02f33c6af 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(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(fade_item); - break; - } - fade_item = fade_item->parent; - } - - Vector 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(item_fx); - - Ref charfx = item_custom->char_fx_transform; - Ref 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(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(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(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(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(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(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 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(item_fx); + + Ref charfx = item_custom->char_fx_transform; + Ref 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(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(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(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(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(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 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(fade_item); - break; - } - fade_item = fade_item->parent; - } - - Vector 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(item_fx); - - Ref charfx = item_custom->char_fx_transform; - Ref 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(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(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(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(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(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 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; @@ -3438,7 +3342,7 @@ void RichTextLabel::_remove_frame(HashSet &r_erase_list, ItemFrame *p_fr } } -bool RichTextLabel::remove_paragraph(const int p_paragraph) { +bool RichTextLabel::remove_paragraph(int p_paragraph, bool p_no_invalidate) { _stop_thread(); MutexLock data_lock(data_mutex); @@ -3487,8 +3391,44 @@ bool RichTextLabel::remove_paragraph(const int p_paragraph) { selection.click_frame = nullptr; selection.click_item = nullptr; - deselect(); + selection.active = false; + if (p_no_invalidate) { + // Do not invalidate cache, only update vertical offsets of the paragraphs after deleted one and scrollbar. + int to_line = main->first_invalid_line.load() - 1; + float total_height = (p_paragraph == 0) ? 0 : _calculate_line_vertical_offset(main->lines[p_paragraph - 1]); + for (int i = p_paragraph; i < to_line; i++) { + MutexLock lock(main->lines[to_line - 1].text_buf->get_mutex()); + main->lines[i].offset.y = total_height; + total_height = _calculate_line_vertical_offset(main->lines[i]); + } + updating_scroll = true; + vscroll->set_max(total_height); + updating_scroll = false; + + main->first_invalid_line.store(MAX(main->first_invalid_line.load() - 1, 0)); + main->first_resized_line.store(MAX(main->first_resized_line.load() - 1, 0)); + main->first_invalid_font_line.store(MAX(main->first_invalid_font_line.load() - 1, 0)); + } else { + // Invalidate cache after the deleted paragraph. + main->first_invalid_line.store(MIN(main->first_invalid_line.load(), p_paragraph)); + main->first_resized_line.store(MIN(main->first_resized_line.load(), p_paragraph)); + main->first_invalid_font_line.store(MIN(main->first_invalid_font_line.load(), p_paragraph)); + } + queue_redraw(); + + return true; +} + +bool RichTextLabel::invalidate_paragraph(int p_paragraph) { + _stop_thread(); + MutexLock data_lock(data_mutex); + + if (p_paragraph >= (int)main->lines.size() || p_paragraph < 0) { + return false; + } + + // Invalidate cache. main->first_invalid_line.store(MIN(main->first_invalid_line.load(), p_paragraph)); main->first_resized_line.store(MIN(main->first_resized_line.load(), p_paragraph)); main->first_invalid_font_line.store(MIN(main->first_invalid_font_line.load(), p_paragraph)); @@ -4121,7 +4061,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; } } @@ -5947,7 +5887,8 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align", "region", "key", "pad", "tooltip", "size_in_percent"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2()), DEFVAL(Variant()), DEFVAL(false), DEFVAL(String()), DEFVAL(false)); ClassDB::bind_method(D_METHOD("update_image", "key", "mask", "image", "width", "height", "color", "inline_align", "region", "pad", "tooltip", "size_in_percent"), &RichTextLabel::update_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2()), DEFVAL(false), DEFVAL(String()), DEFVAL(false)); ClassDB::bind_method(D_METHOD("newline"), &RichTextLabel::add_newline); - ClassDB::bind_method(D_METHOD("remove_paragraph", "paragraph"), &RichTextLabel::remove_paragraph); + ClassDB::bind_method(D_METHOD("remove_paragraph", "paragraph", "no_invalidate"), &RichTextLabel::remove_paragraph, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("invalidate_paragraph", "paragraph"), &RichTextLabel::invalidate_paragraph); ClassDB::bind_method(D_METHOD("push_font", "font", "font_size"), &RichTextLabel::push_font, DEFVAL(0)); ClassDB::bind_method(D_METHOD("push_font_size", "font_size"), &RichTextLabel::push_font_size); ClassDB::bind_method(D_METHOD("push_normal"), &RichTextLabel::push_normal); @@ -6404,65 +6345,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 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 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 RichTextLabel::_get_custom_effect_by_code(String p_bbcode_identifier) { for (int i = 0; i < custom_effects.size(); i++) { Ref effect = custom_effects[i]; diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index a993d922d280..189ee1da6e6b 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, @@ -125,6 +134,7 @@ class RichTextLabel : public Control { #ifndef DISABLE_DEPRECATED void _push_meta_bind_compat_89024(const Variant &p_meta); void _add_image_bind_compat_80410(const Ref &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region); + bool _remove_paragraph_bind_compat_91098(int p_paragraph); static void _bind_compatibility_methods(); #endif @@ -596,7 +606,6 @@ class RichTextLabel : public Control { Size2 _get_image_size(const Ref &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); @@ -656,7 +665,8 @@ class RichTextLabel : public Control { void add_image(const Ref &p_image, int p_width = 0, int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlignment p_alignment = INLINE_ALIGNMENT_CENTER, const Rect2 &p_region = Rect2(), const Variant &p_key = Variant(), bool p_pad = false, const String &p_tooltip = String(), bool p_size_in_percent = false); void update_image(const Variant &p_key, BitField p_mask, const Ref &p_image, int p_width = 0, int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlignment p_alignment = INLINE_ALIGNMENT_CENTER, const Rect2 &p_region = Rect2(), bool p_pad = false, const String &p_tooltip = String(), bool p_size_in_percent = false); void add_newline(); - bool remove_paragraph(const int p_paragraph); + bool remove_paragraph(int p_paragraph, bool p_no_invalidate = false); + bool invalidate_paragraph(int p_paragraph); void push_dropcap(const String &p_string, const Ref &p_font, int p_size, const Rect2 &p_dropcap_margins = Rect2(), const Color &p_color = Color(1, 1, 1), int p_ol_size = 0, const Color &p_ol_color = Color(0, 0, 0, 0)); void _push_def_font(DefaultFont p_def_font); void _push_def_font_var(DefaultFont p_def_font, const Ref &p_font, int p_size = -1); diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index e2feb59a8c7a..dc53cf82e6d0 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -147,9 +147,7 @@ void TabContainer::_notification(int p_what) { if (get_tab_count() > 0) { _refresh_tab_names(); } - } break; - case NOTIFICATION_POST_ENTER_TREE: { if (setup_current_tab >= -1) { set_current_tab(setup_current_tab); setup_current_tab = -2; @@ -191,6 +189,25 @@ void TabContainer::_notification(int p_what) { } } break; + case NOTIFICATION_VISIBILITY_CHANGED: { + if (!is_visible() || setup_current_tab > -2) { + return; + } + + updating_visibility = true; + + // As the visibility change notification will be triggered for all children soon after, + // beat it to the punch and make sure that the correct node is the only one visible first. + // Otherwise, it can prevent a tab change done right before this container was made visible. + Vector controls = _get_tab_controls(); + int current = get_current_tab(); + for (int i = 0; i < controls.size(); i++) { + controls[i]->set_visible(i == current); + } + + updating_visibility = false; + } break; + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { @@ -611,6 +628,7 @@ void TabContainer::set_current_tab(int p_current) { setup_current_tab = p_current; return; } + tab_bar->set_current_tab(p_current); } diff --git a/scene/gui/text_edit.compat.inc b/scene/gui/text_edit.compat.inc new file mode 100644 index 000000000000..bf73229868ef --- /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 38b4ffc8ae9e..49cfa8a0300f 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 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 &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 &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 &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 &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 &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 &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 &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 &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 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); - } - } + begin_multicaret_edit(); - insert_text_at_caret("\n", i); - - 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 carets_to_remove; + begin_multicaret_edit(); - Vector 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 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 "" 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 carets_to_remove; + begin_multicaret_edit(); + + Vector 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; + } - Vector 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); + if (has_selection(caret_index)) { + delete_selection(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) { + 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 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); + } + } - 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(); + _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); + + // 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,82 +3400,180 @@ 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()); + + // Use a complex operation so subsequent calls aren't merged together. + begin_complex_operation(); - _insert_text(p_at, 0, p_text + "\n"); + 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); - 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); + 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 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(); - queue_redraw(); } -void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { - ERR_FAIL_INDEX(p_from_line, text.size()); +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(); +} + +void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { + ERR_FAIL_INDEX(p_from_line, text.size()); ERR_FAIL_INDEX(p_from_column, text[p_from_line].length() + 1); ERR_FAIL_INDEX(p_to_line, text.size()); ERR_FAIL_INDEX(p_to_column, text[p_to_line].length() + 1); 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; } @@ -4612,6 +4494,9 @@ void TextEdit::set_multiple_carets_enabled(bool p_enabled) { multi_carets_enabled = p_enabled; if (!multi_carets_enabled) { remove_secondary_carets(); + multicaret_edit_count = 0; + multicaret_edit_ignore_carets.clear(); + multicaret_edit_merge_queued = false; } } @@ -4619,270 +4504,411 @@ 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 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) { + if (!multi_carets_enabled) { + return; + } + 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); + ERR_FAIL_COND_MSG(new_caret_index < 0, "Failed to add a caret."); - 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 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 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 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() && get_caret_count() > 1) { + 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() && get_caret_count() > 1) { + 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; + } + } + 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; } - i--; - caret_edit_order = get_caret_index_edit_order(); - continue; } } + 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 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 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(); } -Vector TextEdit::get_caret_index_edit_order() { - if (!caret_index_edit_dirty) { - return caret_index_edit_order; +// Starts a multicaret edit operation. Call this before iterating over the carets and call [end_multicaret_edit] afterwards. +void TextEdit::begin_multicaret_edit() { + if (!multi_carets_enabled) { + return; } + multicaret_edit_count++; +} - 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; +void TextEdit::end_multicaret_edit() { + if (!multi_carets_enabled) { + return; + } + if (multicaret_edit_count > 0) { + multicaret_edit_count--; + } + if (multicaret_edit_count != 0) { + return; + } - 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 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 +4928,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 +4940,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 +4948,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 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 +4986,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 +5026,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 +5087,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 +5106,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 +5152,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(); } @@ -5213,176 +5223,328 @@ void TextEdit::skip_selection_for_next_occurrence() { return; } - int to_column = (has_selection(caret) ? get_selection_to_column(caret) : get_caret_column(caret)) + 1; - int end = next_occurrence.x + (to_column - column); - int new_caret = add_caret(next_occurrence.y, end); + int to_column = (has_selection(caret) ? get_selection_to_column(caret) : get_caret_column(caret)) + 1; + int end = next_occurrence.x + (to_column - column); + int new_caret = add_caret(next_occurrence.y, end); + + if (new_caret != -1) { + select(next_occurrence.y, next_occurrence.x, next_occurrence.y, end, new_caret); + adjust_viewport_to_caret(new_caret); + merge_overlapping_carets(); + } + + // Deselect word under previous caret. + if (has_selection(caret)) { + select_word_under_caret(caret); + } + + // Remove previous caret. + if (get_caret_count() > 1) { + remove_caret(caret); + } +} + +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_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); + + 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); + } +} + +bool TextEdit::has_selection(int p_caret) const { + 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 (carets[i].selection.active) { + return true; + } + } + return false; +} + +String TextEdit::get_selected_text(int p_caret) { + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); + + 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 sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + + if (!has_selection(caret_index)) { + continue; + } + 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_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 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 ret; + int last_to_line = INT_MIN; + + Vector 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 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 ret; + Vector 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; - if (new_caret != -1) { - select(next_occurrence.y, next_occurrence.x, next_occurrence.y, end, new_caret); - adjust_viewport_to_caret(new_caret); - merge_overlapping_carets(); + 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; - // Deselect word under previous caret. - if (has_selection(caret)) { - select_word_under_caret(caret); + // 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); } - // Remove previous caret. - if (get_caret_count() > 1) { - remove_caret(caret); + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); } } -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::set_selection_origin_column(int p_column, int p_caret) { if (!selecting_enabled) { return; } + ERR_FAIL_INDEX(p_caret, carets.size()); - 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; - - 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; + p_column = CLAMP(p_column, 0, get_line(get_selection_origin_line(p_caret)).length()); - } 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 selection_moved = get_selection_origin_column(p_caret) != p_column; - caret_index_edit_dirty = true; - queue_redraw(); -} + carets.write[p_caret].selection.origin_column = p_column; -bool TextEdit::has_selection(int p_caret) const { - ERR_FAIL_COND_V(p_caret > carets.size(), false); - for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; - } + 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)); - if (carets[i].selection.active) { - return true; - } + // 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); } - return false; -} - -String TextEdit::get_selected_text(int p_caret) { - ERR_FAIL_COND_V(p_caret > carets.size(), ""); - - StringBuilder selected_text; - Vector 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 (!has_selection(caret_idx)) { - 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) { - selected_text += "\n"; - } + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); } - - return selected_text.as_string(); } -int TextEdit::get_selection_line(int p_caret) const { +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 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 +6386,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 +6475,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 +6519,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 +6563,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 +6725,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 +6800,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 +6873,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 +6904,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 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 +6932,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 +6948,163 @@ void TextEdit::_backspace_internal(int p_caret) { } begin_complex_operation(); - Vector 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 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 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 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 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 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 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 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 sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); 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 +7363,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 +7427,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 +7584,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 +7644,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 */ @@ -7780,9 +8035,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 TextEdit::get_caret_index_edit_order() { + Vector 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 +8207,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 +8248,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 1099295d3b5d..efade3987673 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -389,18 +389,12 @@ class TextEdit : public Control { /* 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 @@ class TextEdit : public Control { // Vector containing all the carets, index '0' is the "main caret" and should never be removed. Vector carets; - Vector 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 multicaret_edit_ignore_carets; CaretType caret_type = CaretType::CARET_TYPE_LINE; @@ -438,12 +434,18 @@ class TextEdit : public Control { 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 @@ class TextEdit : public Control { 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 @@ class TextEdit : public Control { /*** 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 @@ class TextEdit : public Control { 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 @@ class TextEdit : public Control { 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 @@ class TextEdit : public Control { 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 @@ class TextEdit : public Control { 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 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 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 @@ class TextEdit : public Control { 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 @@ class TextEdit : public Control { 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 get_line_ranges_from_carets(bool p_only_selections = false, bool p_merge_adjacent = true) const; + TypedArray 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 @@ class TextEdit : public Control { Color get_font_color() const; + /* Deprecated. */ +#ifndef DISABLE_DEPRECATED + Vector 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 0b197c8c0236..df90257e0333 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 5261cbe3eb56..bbe5ddf1c3e0 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 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 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 uvs; Vector 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 b17d345f1fe4..df0bb365e583 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -3152,9 +3152,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int } void Tree::_text_editor_popup_modal_close() { - if (Input::get_singleton()->is_key_pressed(Key::ESCAPE) || - Input::get_singleton()->is_key_pressed(Key::KP_ENTER) || - Input::get_singleton()->is_key_pressed(Key::ENTER)) { + if (popup_editor->get_hide_reason() == Popup::HIDE_REASON_CANCELED) { return; } @@ -3429,7 +3427,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 000000000000..7136fded151e --- /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 56aa453407d1..cabba0f2ed25 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 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 colors = { p_color }; + + RenderingServer::get_singleton()->canvas_item_add_polyline(canvas_item, points, colors, p_width, p_antialiased); + } } void CanvasItem::draw_texture(const Ref &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 8cec086ca6d9..ae7b195eade8 100644 --- a/scene/main/canvas_item.h +++ b/scene/main/canvas_item.h @@ -166,6 +166,12 @@ class CanvasItem : public Node { 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 @@ class CanvasItem : public Node { void draw_multiline(const Vector &p_points, const Color &p_color, real_t p_width = -1.0); void draw_multiline_colors(const Vector &p_points, const Vector &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 &p_texture, const Point2 &p_pos, const Color &p_modulate = Color(1, 1, 1, 1)); void draw_texture_rect(const Ref &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 &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 305b80ffe5cf..e54209c1dd4f 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -2930,8 +2930,10 @@ void Node::_duplicate_properties_node(const Node *p_root, const Node *p_original } } - for (int i = 0; i < p_copy->get_child_count(); i++) { - _duplicate_properties_node(p_root, p_original->get_child(i), p_copy->get_child(i)); + for (int i = 0; i < p_original->get_child_count(); i++) { + Node *copy_child = p_copy->get_child(i); + ERR_FAIL_NULL_MSG(copy_child, "Child node disappeared while duplicating."); + _duplicate_properties_node(p_root, p_original->get_child(i), copy_child); } } @@ -3231,7 +3233,7 @@ void Node::print_orphan_nodes() { ObjectDB::debug_objects(_print_orphan_nodes_routine); for (const KeyValue> &E : _print_orphan_nodes_map) { - print_line(itos(E.key) + " - Stray Node: " + E.value[0] + " (Type: " + E.value[1] + ")"); + print_line(itos(E.key) + " - Stray Node: " + E.value.get(0) + " (Type: " + E.value.get(1) + ")"); } // Flush it after use. diff --git a/scene/main/status_indicator.cpp b/scene/main/status_indicator.cpp index 54b2ff75ca69..891974f68f1e 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(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(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,15 @@ 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); + ClassDB::bind_method(D_METHOD("get_rect"), &StatusIndicator::get_rect); 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 +94,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 &p_icon) { +void StatusIndicator::set_icon(const Ref &p_icon) { ERR_MAIN_THREAD_GUARD; icon = p_icon; if (iid != DisplayServer::INVALID_INDICATOR_ID) { @@ -86,7 +102,7 @@ void StatusIndicator::set_icon(const Ref &p_icon) { } } -Ref StatusIndicator::get_icon() const { +Ref StatusIndicator::get_icon() const { return icon; } @@ -102,6 +118,30 @@ String StatusIndicator::get_tooltip() const { return tooltip; } +void StatusIndicator::set_menu(const NodePath &p_menu) { + PopupMenu *pm = Object::cast_to(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(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 +162,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(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(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; } @@ -133,3 +183,10 @@ void StatusIndicator::set_visible(bool p_visible) { bool StatusIndicator::is_visible() const { return visible; } + +Rect2 StatusIndicator::get_rect() const { + if (iid == DisplayServer::INVALID_INDICATOR_ID) { + return Rect2(); + } + return DisplayServer::get_singleton()->status_indicator_get_rect(iid); +} diff --git a/scene/main/status_indicator.h b/scene/main/status_indicator.h index aa3aa68d782f..cd38da6e6c1e 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 icon; + Ref icon; String tooltip; bool visible = true; DisplayServer::IndicatorID iid = DisplayServer::INVALID_INDICATOR_ID; + NodePath menu; protected: void _notification(int p_what); @@ -49,14 +50,19 @@ class StatusIndicator : public Node { void _callback(MouseButton p_index, const Point2i &p_pos); public: - void set_icon(const Ref &p_icon); - Ref get_icon() const; + void set_icon(const Ref &p_icon); + Ref 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; + + Rect2 get_rect() const; }; #endif // STATUS_INDICATOR_H diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 46713f942c70..2536ebfcc400 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 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 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; @@ -2306,6 +2318,7 @@ void Viewport::_gui_force_drag(Control *p_base, const Variant &p_data, Control * gui.dragging = true; gui.drag_data = p_data; gui.mouse_focus = nullptr; + gui.mouse_focus_mask.clear(); if (p_control) { _gui_set_drag_preview(p_base, p_control); @@ -2361,7 +2374,7 @@ void Viewport::_gui_hide_control(Control *p_control) { if (gui.key_focus == p_control) { gui_release_focus(); } - if (gui.mouse_over == p_control || gui.mouse_over_hierarchy.find(p_control) >= 0) { + if (gui.mouse_over == p_control || gui.mouse_over_hierarchy.has(p_control)) { _drop_mouse_over(p_control->get_parent_control()); } if (gui.drag_mouse_over == p_control) { @@ -2378,13 +2391,10 @@ 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; } - if (gui.mouse_over == p_control || gui.mouse_over_hierarchy.find(p_control) >= 0) { + if (gui.mouse_over == p_control || gui.mouse_over_hierarchy.has(p_control)) { _drop_mouse_over(p_control->get_parent_control()); } if (gui.drag_mouse_over == p_control) { @@ -2758,7 +2768,7 @@ bool Viewport::_sub_windows_forward_input(const Ref &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 +2829,7 @@ bool Viewport::_sub_windows_forward_input(const Ref &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 +3588,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) { @@ -3718,6 +3735,28 @@ Viewport::VRSMode Viewport::get_vrs_mode() const { return vrs_mode; } +void Viewport::set_vrs_update_mode(VRSUpdateMode p_vrs_update_mode) { + ERR_MAIN_THREAD_GUARD; + + vrs_update_mode = p_vrs_update_mode; + switch (p_vrs_update_mode) { + case VRS_UPDATE_ONCE: { + RS::get_singleton()->viewport_set_vrs_update_mode(viewport, RS::VIEWPORT_VRS_UPDATE_ONCE); + } break; + case VRS_UPDATE_ALWAYS: { + RS::get_singleton()->viewport_set_vrs_update_mode(viewport, RS::VIEWPORT_VRS_UPDATE_ALWAYS); + } break; + default: { + RS::get_singleton()->viewport_set_vrs_update_mode(viewport, RS::VIEWPORT_VRS_UPDATE_DISABLED); + } break; + } +} + +Viewport::VRSUpdateMode Viewport::get_vrs_update_mode() const { + ERR_READ_THREAD_GUARD_V(VRS_UPDATE_DISABLED); + return vrs_update_mode; +} + void Viewport::set_vrs_texture(Ref p_texture) { ERR_MAIN_THREAD_GUARD; vrs_texture = p_texture; @@ -4758,6 +4797,9 @@ void Viewport::_bind_methods() { ClassDB::bind_method(D_METHOD("set_vrs_mode", "mode"), &Viewport::set_vrs_mode); ClassDB::bind_method(D_METHOD("get_vrs_mode"), &Viewport::get_vrs_mode); + ClassDB::bind_method(D_METHOD("set_vrs_update_mode", "mode"), &Viewport::set_vrs_update_mode); + ClassDB::bind_method(D_METHOD("get_vrs_update_mode"), &Viewport::get_vrs_update_mode); + ClassDB::bind_method(D_METHOD("set_vrs_texture", "texture"), &Viewport::set_vrs_texture); ClassDB::bind_method(D_METHOD("get_vrs_texture"), &Viewport::get_vrs_texture); @@ -4790,6 +4832,7 @@ void Viewport::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fsr_sharpness", PROPERTY_HINT_RANGE, "0,2,0.1"), "set_fsr_sharpness", "get_fsr_sharpness"); ADD_GROUP("Variable Rate Shading", "vrs_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "vrs_mode", PROPERTY_HINT_ENUM, "Disabled,Texture,Depth buffer,XR"), "set_vrs_mode", "get_vrs_mode"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "vrs_update_mode", PROPERTY_HINT_ENUM, "Disabled,Once,Always"), "set_vrs_update_mode", "get_vrs_update_mode"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "vrs_texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_vrs_texture", "get_vrs_texture"); #endif ADD_GROUP("Canvas Items", "canvas_item_"); @@ -4913,12 +4956,21 @@ void Viewport::_bind_methods() { BIND_ENUM_CONSTANT(VRS_TEXTURE); BIND_ENUM_CONSTANT(VRS_XR); BIND_ENUM_CONSTANT(VRS_MAX); + + BIND_ENUM_CONSTANT(VRS_UPDATE_DISABLED); + BIND_ENUM_CONSTANT(VRS_UPDATE_ONCE); + BIND_ENUM_CONSTANT(VRS_UPDATE_ALWAYS); + BIND_ENUM_CONSTANT(VRS_UPDATE_MAX); } void Viewport::_validate_property(PropertyInfo &p_property) const { if (vrs_mode != VRS_TEXTURE && (p_property.name == "vrs_texture")) { p_property.usage = PROPERTY_USAGE_NO_EDITOR; } + + if (vrs_mode == VRS_DISABLED && (p_property.name == "vrs_update_mode")) { + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } } Viewport::Viewport() { diff --git a/scene/main/viewport.h b/scene/main/viewport.h index 21832a454c4f..2474b890b068 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -213,6 +213,13 @@ class Viewport : public Node { VRS_MAX }; + enum VRSUpdateMode { + VRS_UPDATE_DISABLED, + VRS_UPDATE_ONCE, + VRS_UPDATE_ALWAYS, + VRS_UPDATE_MAX + }; + private: friend class ViewportTexture; @@ -337,6 +344,7 @@ class Viewport : public Node { // VRS VRSMode vrs_mode = VRS_DISABLED; + VRSUpdateMode vrs_update_mode = VRS_UPDATE_ONCE; Ref vrs_texture; struct GUI { @@ -345,7 +353,6 @@ class Viewport : public Node { bool key_event_accepted = false; HashMap touch_focus; Control *mouse_focus = nullptr; - Control *last_mouse_focus = nullptr; Control *mouse_click_grabber = nullptr; BitField mouse_focus_mask; Control *key_focus = nullptr; @@ -616,6 +623,7 @@ class Viewport : public Node { bool gui_is_dragging() const; bool gui_is_drag_successful() const; + void gui_cancel_drag(); Control *gui_find_control(const Point2 &p_global); @@ -636,6 +644,9 @@ class Viewport : public Node { void set_vrs_mode(VRSMode p_vrs_mode); VRSMode get_vrs_mode() const; + void set_vrs_update_mode(VRSUpdateMode p_vrs_update_mode); + VRSUpdateMode get_vrs_update_mode() const; + void set_vrs_texture(Ref p_texture); Ref get_vrs_texture() const; @@ -844,6 +855,7 @@ VARIANT_ENUM_CAST(Viewport::DebugDraw); VARIANT_ENUM_CAST(Viewport::SDFScale); VARIANT_ENUM_CAST(Viewport::SDFOversize); VARIANT_ENUM_CAST(Viewport::VRSMode); +VARIANT_ENUM_CAST(Viewport::VRSUpdateMode); VARIANT_ENUM_CAST(SubViewport::ClearMode); VARIANT_ENUM_CAST(Viewport::RenderInfo); VARIANT_ENUM_CAST(Viewport::RenderInfoType); diff --git a/scene/main/window.cpp b/scene/main/window.cpp index 0ccc056a8dc1..929720fcf4c6 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/skeleton/skeleton_modification_2d_physicalbones.cpp b/scene/resources/2d/skeleton/skeleton_modification_2d_physicalbones.cpp index aa8d7d0b3beb..001000fa17ba 100644 --- a/scene/resources/2d/skeleton/skeleton_modification_2d_physicalbones.cpp +++ b/scene/resources/2d/skeleton/skeleton_modification_2d_physicalbones.cpp @@ -194,7 +194,7 @@ void SkeletonModification2DPhysicalBones::fetch_physical_bones() { node_queue.push_back(stack->skeleton); while (node_queue.size() > 0) { - Node *node_to_process = node_queue[0]; + Node *node_to_process = node_queue.front()->get(); node_queue.pop_front(); if (node_to_process != nullptr) { diff --git a/scene/resources/2d/tile_set.cpp b/scene/resources/2d/tile_set.cpp index 57cc4ad602ee..6649cb9b8285 100644 --- a/scene/resources/2d/tile_set.cpp +++ b/scene/resources/2d/tile_set.cpp @@ -4650,7 +4650,7 @@ Ref 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/animation.cpp b/scene/resources/animation.cpp index cd530f100efc..d7b952824847 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -247,6 +247,7 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) { } vt->update_mode = UpdateMode(um); } + capture_included = capture_included || (vt->update_mode == UPDATE_CAPTURE); Vector times = d["times"]; Array values = d["values"]; @@ -966,6 +967,28 @@ void Animation::remove_track(int p_track) { memdelete(t); tracks.remove_at(p_track); emit_changed(); + _check_capture_included(); +} + +void Animation::set_capture_included(bool p_capture_included) { + capture_included = p_capture_included; +} + +bool Animation::is_capture_included() const { + return capture_included; +} + +void Animation::_check_capture_included() { + capture_included = false; + for (int i = 0; i < tracks.size(); i++) { + if (tracks[i]->type == TYPE_VALUE) { + ValueTrack *vt = static_cast(tracks[i]); + if (vt->update_mode == UPDATE_CAPTURE) { + capture_included = true; + break; + } + } + } } int Animation::get_track_count() const { @@ -2681,6 +2704,8 @@ void Animation::value_track_set_update_mode(int p_track, UpdateMode p_mode) { ValueTrack *vt = static_cast(t); vt->update_mode = p_mode; + + _check_capture_included(); emit_changed(); } @@ -3870,9 +3895,13 @@ void Animation::_bind_methods() { ClassDB::bind_method(D_METHOD("compress", "page_size", "fps", "split_tolerance"), &Animation::compress, DEFVAL(8192), DEFVAL(120), DEFVAL(4.0)); + ClassDB::bind_method(D_METHOD("_set_capture_included", "capture_included"), &Animation::set_capture_included); + ClassDB::bind_method(D_METHOD("is_capture_included"), &Animation::is_capture_included); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "length", PROPERTY_HINT_RANGE, "0.001,99999,0.001,suffix:s"), "set_length", "get_length"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_mode", PROPERTY_HINT_ENUM, "None,Linear,Ping-Pong"), "set_loop_mode", "get_loop_mode"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "step", PROPERTY_HINT_RANGE, "0,4096,0.001,suffix:s"), "set_step", "get_step"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "capture_included", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_NO_EDITOR), "_set_capture_included", "is_capture_included"); BIND_ENUM_CONSTANT(TYPE_VALUE); BIND_ENUM_CONSTANT(TYPE_POSITION_3D); diff --git a/scene/resources/animation.h b/scene/resources/animation.h index 6005172c11fc..cc7bbae8a398 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -268,6 +268,8 @@ class Animation : public Resource { double length = 1.0; real_t step = 1.0 / 30; LoopMode loop_mode = LOOP_NONE; + bool capture_included = false; + void _check_capture_included(); void _track_update_hash(int p_track); @@ -392,6 +394,9 @@ class Animation : public Resource { int add_track(TrackType p_type, int p_at_pos = -1); void remove_track(int p_track); + void set_capture_included(bool p_capture_included); + bool is_capture_included() const; + int get_track_count() const; TrackType track_get_type(int p_track) const; diff --git a/scene/resources/audio_stream_wav.cpp b/scene/resources/audio_stream_wav.cpp index 0185c6ef8558..ba5dad088f81 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 -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 +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 *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } else { - do_resample((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } } break; case AudioStreamWAV::FORMAT_16_BITS: { if (is_stereo) { - do_resample((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } else { - do_resample((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } } break; case AudioStreamWAV::FORMAT_IMA_ADPCM: { if (is_stereo) { - do_resample((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } else { - do_resample((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } } break; + case AudioStreamWAV::FORMAT_QOA: { + if (is_stereo) { + do_resample((uint8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); + } else { + do_resample((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 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 AudioStreamWAV::instantiate_playback() { Ref sample; sample.instantiate(); sample->base = Ref(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 959d1ceca0bb..146142d8a429 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 base; - template - 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 + 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 @@ class AudioStreamPlaybackWAV : public AudioStreamPlayback { virtual void tag_used_streams() override; AudioStreamPlaybackWAV(); + ~AudioStreamPlaybackWAV(); }; class AudioStreamWAV : public AudioStream { @@ -88,7 +104,8 @@ class AudioStreamWAV : public AudioStream { 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 b381096df806..15b40e776c6d 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 073403f71e38..ecf79c581b9e 100644 --- a/scene/resources/material.h +++ b/scene/resources/material.h @@ -219,6 +219,7 @@ class BaseMaterial3D : public Material { BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, + BLEND_MODE_PREMULT_ALPHA, BLEND_MODE_MAX }; diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp index 94a031947abc..407b12d72f06 100644 --- a/scene/resources/packed_scene.cpp +++ b/scene/resources/packed_scene.cpp @@ -191,6 +191,7 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const { Node *node = nullptr; MissingNode *missing_node = nullptr; + bool is_inherited_scene = false; if (i == 0 && base_scene_idx >= 0) { // Scene inheritance on root node. @@ -201,7 +202,7 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const { if (p_edit_state != GEN_EDIT_STATE_DISABLED) { node->set_scene_inherited_state(sdata->get_state()); } - + is_inherited_scene = true; } else if (n.instance >= 0) { // Instance a scene into this node. if (n.instance & FLAG_INSTANCE_IS_PLACEHOLDER) { @@ -346,6 +347,12 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const { } else { Variant value = props[nprops[j].value]; + // Making sure that instances of inherited scenes don't share the same + // reference between them. + if (is_inherited_scene) { + value = value.duplicate(true); + } + if (value.get_type() == Variant::OBJECT) { //handle resources that are local to scene by duplicating them if needed Ref res = value; diff --git a/scene/resources/particle_process_material.cpp b/scene/resources/particle_process_material.cpp index 685625ab72ed..0b65b3324022 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/resource_format_text.cpp b/scene/resources/resource_format_text.cpp index f70a00a9f6ac..2e27ac9198e3 100644 --- a/scene/resources/resource_format_text.cpp +++ b/scene/resources/resource_format_text.cpp @@ -37,11 +37,12 @@ #include "core/object/script_language.h" #include "core/version.h" -// Version 2: changed names for Basis, AABB, Vectors, etc. -// Version 3: new string ID for ext/subresources, breaks forward compat. -// Version 4: PackedByteArray is now stored as base64 encoded. -#define FORMAT_VERSION_COMPAT 3 +// Version 2: Changed names for Basis, AABB, Vectors, etc. +// Version 3: New string ID for ext/subresources, breaks forward compat. +// Version 4: PackedByteArray can be base64 encoded, and PackedVector4Array was added. #define FORMAT_VERSION 4 +// For compat, save as version 3 if not using PackedVector4Array or no big PackedByteArray. +#define FORMAT_VERSION_COMPAT 3 #define BINARY_FORMAT_VERSION 4 @@ -1979,6 +1980,9 @@ void ResourceFormatSaverTextInstance::_find_resources(const Variant &p_variant, use_compat = false; } } break; + case Variant::PACKED_VECTOR4_ARRAY: { + use_compat = false; + } break; default: { } } diff --git a/scene/resources/shader.cpp b/scene/resources/shader.cpp index 5b375905cc05..0087a5e7f2f5 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 *p_params, bool p_get_gr List 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 *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 pattern_ref = pattern.search(code); + if (pattern_ref != nullptr) { + RegExMatch *match = pattern_ref.ptr(); + const RegEx pattern_tip("\\/\\*\\*([\\s\\S]*?)\\*/"); + Ref 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/syntax_highlighter.cpp b/scene/resources/syntax_highlighter.cpp index c73539582984..6da4215031ee 100644 --- a/scene/resources/syntax_highlighter.cpp +++ b/scene/resources/syntax_highlighter.cpp @@ -303,7 +303,7 @@ Dictionary CodeHighlighter::_get_line_syntax_highlighting_impl(int p_line) { } // Check for dot or underscore or 'x' for hex notation in floating point number or 'e' for scientific notation. - if ((str[j] == '.' || str[j] == 'x' || str[j] == '_' || str[j] == 'f' || str[j] == 'e') && !in_word && prev_is_number && !is_number) { + if ((str[j] == '.' || str[j] == 'x' || str[j] == '_' || str[j] == 'f' || str[j] == 'e' || (uint_suffix_enabled && str[j] == 'u')) && !in_word && prev_is_number && !is_number) { is_number = true; is_a_symbol = false; is_char = false; @@ -617,6 +617,10 @@ void CodeHighlighter::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "color_regions"), "set_color_regions", "get_color_regions"); } +void CodeHighlighter::set_uint_suffix_enabled(bool p_enabled) { + uint_suffix_enabled = p_enabled; +} + void CodeHighlighter::set_number_color(Color p_color) { number_color = p_color; clear_highlighting_cache(); diff --git a/scene/resources/syntax_highlighter.h b/scene/resources/syntax_highlighter.h index cac2807ee20f..02afd9045e7c 100644 --- a/scene/resources/syntax_highlighter.h +++ b/scene/resources/syntax_highlighter.h @@ -93,6 +93,8 @@ class CodeHighlighter : public SyntaxHighlighter { Color symbol_color; Color number_color; + bool uint_suffix_enabled = false; + protected: static void _bind_methods(); @@ -139,6 +141,8 @@ class CodeHighlighter : public SyntaxHighlighter { void set_member_variable_color(Color p_color); Color get_member_variable_color() const; + + void set_uint_suffix_enabled(bool p_enabled); }; #endif // SYNTAX_HIGHLIGHTER_H diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp index 6f1aa5c8502b..a57af6886d18 100644 --- a/scene/resources/visual_shader.cpp +++ b/scene/resources/visual_shader.cpp @@ -585,12 +585,12 @@ int VisualShaderNodeCustom::get_input_port_count() const { VisualShaderNodeCustom::PortType VisualShaderNodeCustom::get_input_port_type(int p_port) const { ERR_FAIL_INDEX_V(p_port, input_ports.size(), PORT_TYPE_SCALAR); - return (PortType)input_ports[p_port].type; + return (PortType)input_ports.get(p_port).type; } String VisualShaderNodeCustom::get_input_port_name(int p_port) const { ERR_FAIL_INDEX_V(p_port, input_ports.size(), ""); - return input_ports[p_port].name; + return input_ports.get(p_port).name; } int VisualShaderNodeCustom::get_default_input_port(PortType p_type) const { @@ -605,12 +605,12 @@ int VisualShaderNodeCustom::get_output_port_count() const { VisualShaderNodeCustom::PortType VisualShaderNodeCustom::get_output_port_type(int p_port) const { ERR_FAIL_INDEX_V(p_port, output_ports.size(), PORT_TYPE_SCALAR); - return (PortType)output_ports[p_port].type; + return (PortType)output_ports.get(p_port).type; } String VisualShaderNodeCustom::get_output_port_name(int p_port) const { ERR_FAIL_INDEX_V(p_port, output_ports.size(), ""); - return output_ports[p_port].name; + return output_ports.get(p_port).name; } String VisualShaderNodeCustom::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) const { @@ -865,7 +865,7 @@ int VisualShader::get_varyings_count() const { const VisualShader::Varying *VisualShader::get_varying_by_index(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, varyings_list.size(), nullptr); - return &varyings_list[p_idx]; + return &varyings_list.get(p_idx); } void VisualShader::set_varying_mode(const String &p_name, VaryingMode p_mode) { @@ -2478,10 +2478,11 @@ void VisualShader::_update_shader() const { } } - for (int i = 0; i < parameters.size(); i++) { - VisualShaderNodeParameter *parameter = parameters[i]; + int idx = 0; + for (List::Iterator itr = parameters.begin(); itr != parameters.end(); ++itr, ++idx) { + VisualShaderNodeParameter *parameter = *itr; if (used_parameter_names.has(parameter->get_parameter_name())) { - global_code += parameter->generate_global(get_mode(), Type(i), -1); + global_code += parameter->generate_global(get_mode(), Type(idx), -1); const_cast(parameter)->set_global_code_generated(true); } else { const_cast(parameter)->set_global_code_generated(false); @@ -2791,8 +2792,9 @@ void VisualShader::_update_shader() const { const_cast(this)->set_code(final_code); for (int i = 0; i < default_tex_params.size(); i++) { - for (int j = 0; j < default_tex_params[i].params.size(); j++) { - const_cast(this)->set_default_texture_parameter(default_tex_params[i].name, default_tex_params[i].params[j], j); + int j = 0; + for (List>::ConstIterator itr = default_tex_params[i].params.begin(); itr != default_tex_params[i].params.end(); ++itr, ++j) { + const_cast(this)->set_default_texture_parameter(default_tex_params[i].name, *itr, j); } } if (previous_code != final_code) { @@ -3684,7 +3686,7 @@ String VisualShaderNodeParameterRef::get_parameter_name_by_index(int p_idx) cons ERR_FAIL_COND_V(!shader_rid.is_valid(), String()); if (p_idx >= 0 && p_idx < parameters[shader_rid].size()) { - return parameters[shader_rid][p_idx].name; + return parameters[shader_rid].get(p_idx).name; } return ""; } @@ -3692,9 +3694,9 @@ String VisualShaderNodeParameterRef::get_parameter_name_by_index(int p_idx) cons VisualShaderNodeParameterRef::ParameterType VisualShaderNodeParameterRef::get_parameter_type_by_name(const String &p_name) const { ERR_FAIL_COND_V(!shader_rid.is_valid(), PARAMETER_TYPE_FLOAT); - for (int i = 0; i < parameters[shader_rid].size(); i++) { - if (parameters[shader_rid][i].name == p_name) { - return parameters[shader_rid][i].type; + for (const VisualShaderNodeParameterRef::Parameter ¶meter : parameters[shader_rid]) { + if (parameter.name == p_name) { + return parameter.type; } } return PARAMETER_TYPE_FLOAT; @@ -3704,7 +3706,7 @@ VisualShaderNodeParameterRef::ParameterType VisualShaderNodeParameterRef::get_pa ERR_FAIL_COND_V(!shader_rid.is_valid(), PARAMETER_TYPE_FLOAT); if (p_idx >= 0 && p_idx < parameters[shader_rid].size()) { - return parameters[shader_rid][p_idx].type; + return parameters[shader_rid].get(p_idx).type; } return PARAMETER_TYPE_FLOAT; } @@ -3713,7 +3715,7 @@ VisualShaderNodeParameterRef::PortType VisualShaderNodeParameterRef::get_port_ty ERR_FAIL_COND_V(!shader_rid.is_valid(), PORT_TYPE_SCALAR); if (p_idx >= 0 && p_idx < parameters[shader_rid].size()) { - switch (parameters[shader_rid][p_idx].type) { + switch (parameters[shader_rid].get(p_idx).type) { case PARAMETER_TYPE_FLOAT: return PORT_TYPE_SCALAR; case PARAMETER_TYPE_INT: @@ -4928,6 +4930,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); @@ -4980,15 +4986,15 @@ int VisualShaderNodeVarying::get_varyings_count() const { String VisualShaderNodeVarying::get_varying_name_by_index(int p_idx) const { if (p_idx >= 0 && p_idx < varyings.size()) { - return varyings[p_idx].name; + return varyings.get(p_idx).name; } return ""; } VisualShader::VaryingType VisualShaderNodeVarying::get_varying_type_by_name(const String &p_name) const { - for (int i = 0; i < varyings.size(); i++) { - if (varyings[i].name == p_name) { - return varyings[i].type; + for (const VisualShaderNodeVarying::Varying &varying : varyings) { + if (varying.name == p_name) { + return varying.type; } } return VisualShader::VARYING_TYPE_FLOAT; @@ -4996,15 +5002,15 @@ VisualShader::VaryingType VisualShaderNodeVarying::get_varying_type_by_name(cons VisualShader::VaryingType VisualShaderNodeVarying::get_varying_type_by_index(int p_idx) const { if (p_idx >= 0 && p_idx < varyings.size()) { - return varyings[p_idx].type; + return varyings.get(p_idx).type; } return VisualShader::VARYING_TYPE_FLOAT; } VisualShader::VaryingMode VisualShaderNodeVarying::get_varying_mode_by_name(const String &p_name) const { - for (int i = 0; i < varyings.size(); i++) { - if (varyings[i].name == p_name) { - return varyings[i].mode; + for (const VisualShaderNodeVarying::Varying &varying : varyings) { + if (varying.name == p_name) { + return varying.mode; } } return VisualShader::VARYING_MODE_VERTEX_TO_FRAG_LIGHT; @@ -5012,14 +5018,14 @@ VisualShader::VaryingMode VisualShaderNodeVarying::get_varying_mode_by_name(cons VisualShader::VaryingMode VisualShaderNodeVarying::get_varying_mode_by_index(int p_idx) const { if (p_idx >= 0 && p_idx < varyings.size()) { - return varyings[p_idx].mode; + return varyings.get(p_idx).mode; } return VisualShader::VARYING_MODE_VERTEX_TO_FRAG_LIGHT; } VisualShaderNodeVarying::PortType VisualShaderNodeVarying::get_port_type_by_index(int p_idx) const { if (p_idx >= 0 && p_idx < varyings.size()) { - return get_port_type(varyings[p_idx].type, 0); + return get_port_type(varyings.get(p_idx).type, 0); } return PORT_TYPE_SCALAR; } diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h index d7270f3ac661..d32e2465b9f7 100644 --- a/scene/resources/visual_shader.h +++ b/scene/resources/visual_shader.h @@ -878,6 +878,7 @@ class VisualShaderNodeExpression : public VisualShaderNodeGroupBase { 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/scene/resources/visual_shader_particle_nodes.cpp b/scene/resources/visual_shader_particle_nodes.cpp index 5ff1f79a2251..cc8887623257 100644 --- a/scene/resources/visual_shader_particle_nodes.cpp +++ b/scene/resources/visual_shader_particle_nodes.cpp @@ -1641,11 +1641,11 @@ String VisualShaderNodeParticleEmit::generate_code(Shader::Mode p_mode, VisualSh String flags_str; - for (int i = 0; i < flags_arr.size(); i++) { - if (i > 0) { + for (List::ConstIterator itr = flags_arr.begin(); itr != flags_arr.end(); ++itr) { + if (itr != flags_arr.begin()) { flags_str += "|"; } - flags_str += flags_arr[i]; + flags_str += *itr; } if (flags_str.is_empty()) { diff --git a/scu_builders.py b/scu_builders.py index e6adf6543c23..a9ae42822280 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/debugger/servers_debugger.cpp b/servers/debugger/servers_debugger.cpp index 06be73acc514..eded0ecfe57e 100644 --- a/servers/debugger/servers_debugger.cpp +++ b/servers/debugger/servers_debugger.cpp @@ -97,12 +97,10 @@ Array ServersDebugger::ServersProfilerFrame::serialize() { arr.push_back(script_time); arr.push_back(servers.size()); - for (int i = 0; i < servers.size(); i++) { - ServerInfo &s = servers[i]; + for (const ServerInfo &s : servers) { arr.push_back(s.name); arr.push_back(s.functions.size() * 2); - for (int j = 0; j < s.functions.size(); j++) { - ServerFunctionInfo &f = s.functions[j]; + for (const ServerFunctionInfo &f : s.functions) { arr.push_back(f.name); arr.push_back(f.time); } diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 9600caa21457..fcbedbc1f836 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -709,12 +709,12 @@ void DisplayServer::set_icon(const Ref &p_icon) { WARN_PRINT("Icon not supported by this display server."); } -DisplayServer::IndicatorID DisplayServer::create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServer::create_status_indicator(const Ref &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 &p_icon) { +void DisplayServer::status_indicator_set_icon(IndicatorID p_id, const Ref &p_icon) { WARN_PRINT("Status indicator not supported by this display server."); } @@ -722,10 +722,19 @@ 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."); } +Rect2 DisplayServer::status_indicator_get_rect(IndicatorID p_id) const { + WARN_PRINT("Status indicator not supported by this display server."); + return Rect2(); +} + void DisplayServer::delete_status_indicator(IndicatorID p_id) { WARN_PRINT("Status indicator not supported by this display server."); } @@ -977,7 +986,9 @@ 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("status_indicator_get_rect", "id"), &DisplayServer::status_indicator_get_rect); ClassDB::bind_method(D_METHOD("delete_status_indicator", "id"), &DisplayServer::delete_status_indicator); ClassDB::bind_method(D_METHOD("tablet_get_driver_count"), &DisplayServer::tablet_get_driver_count); diff --git a/servers/display_server.h b/servers/display_server.h index aab51644c061..30f6ee5ccfd6 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -564,10 +564,12 @@ class DisplayServer : public Object { virtual void set_native_icon(const String &p_filename); virtual void set_icon(const Ref &p_icon); - virtual IndicatorID create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback); - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref &p_icon); + virtual IndicatorID create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback); + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref &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 Rect2 status_indicator_get_rect(IndicatorID p_id) const; virtual void delete_status_indicator(IndicatorID p_id); enum Context { diff --git a/servers/physics_2d/godot_area_2d.h b/servers/physics_2d/godot_area_2d.h index d14ddb6a2eca..e6c3b45d6c35 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 c3c9e494a43c..701dc7391773 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/physics_3d/godot_shape_3d.cpp b/servers/physics_3d/godot_shape_3d.cpp index 6eb983d5e0c8..70b6bcf19ed9 100644 --- a/servers/physics_3d/godot_shape_3d.cpp +++ b/servers/physics_3d/godot_shape_3d.cpp @@ -1133,7 +1133,7 @@ void GodotConvexPolygonShape3D::_setup(const Vector &p_vertices) { max_support = s; } } - if (extreme_vertices.find(best_vertex) == -1) + if (!extreme_vertices.has(best_vertex)) extreme_vertices.push_back(best_vertex); } } diff --git a/servers/physics_3d/godot_soft_body_3d.cpp b/servers/physics_3d/godot_soft_body_3d.cpp index e62197732679..fd9141f46e9d 100644 --- a/servers/physics_3d/godot_soft_body_3d.cpp +++ b/servers/physics_3d/godot_soft_body_3d.cpp @@ -626,11 +626,11 @@ void GodotSoftBody3D::generate_bending_constraints(int p_distance) { for (Link &link : links) { const int ia = (int)(link.n[0] - &nodes[0]); const int ib = (int)(link.n[1] - &nodes[0]); - if (node_links[ia].find(ib) == -1) { + if (!node_links[ia].has(ib)) { node_links[ia].push_back(ib); } - if (node_links[ib].find(ia) == -1) { + if (!node_links[ib].has(ia)) { node_links[ib].push_back(ia); } } diff --git a/servers/register_server_types.cpp b/servers/register_server_types.cpp index 1a75614a4c0a..7a15d202a330 100644 --- a/servers/register_server_types.cpp +++ b/servers/register_server_types.cpp @@ -325,6 +325,7 @@ void register_server_types() { PhysicsServer3DManager::get_singleton()->set_default_server("GodotPhysics3D"); GDREGISTER_ABSTRACT_CLASS(XRInterface); + GDREGISTER_CLASS(XRVRS); GDREGISTER_CLASS(XRBodyTracker); GDREGISTER_CLASS(XRControllerTracker); GDREGISTER_CLASS(XRFaceTracker); diff --git a/servers/rendering/dummy/storage/texture_storage.h b/servers/rendering/dummy/storage/texture_storage.h index 1331ca72c251..43572b65e097 100644 --- a/servers/rendering/dummy/storage/texture_storage.h +++ b/servers/rendering/dummy/storage/texture_storage.h @@ -193,6 +193,8 @@ class TextureStorage : public RendererTextureStorage { virtual void render_target_set_vrs_mode(RID p_render_target, RS::ViewportVRSMode p_mode) override {} virtual RS::ViewportVRSMode render_target_get_vrs_mode(RID p_render_target) const override { return RS::VIEWPORT_VRS_DISABLED; } + virtual void render_target_set_vrs_update_mode(RID p_render_target, RS::ViewportVRSUpdateMode p_mode) override {} + virtual RS::ViewportVRSUpdateMode render_target_get_vrs_update_mode(RID p_render_target) const override { return RS::VIEWPORT_VRS_UPDATE_DISABLED; } virtual void render_target_set_vrs_texture(RID p_render_target, RID p_texture) override {} virtual RID render_target_get_vrs_texture(RID p_render_target) const override { return RID(); } diff --git a/servers/rendering/renderer_canvas_cull.cpp b/servers/rendering/renderer_canvas_cull.cpp index 34f9069649ce..e48c72cec72a 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/copy_effects.cpp b/servers/rendering/renderer_rd/effects/copy_effects.cpp index a363b03dd869..c7a7532d7694 100644 --- a/servers/rendering/renderer_rd/effects/copy_effects.cpp +++ b/servers/rendering/renderer_rd/effects/copy_effects.cpp @@ -661,10 +661,9 @@ void CopyEffects::copy_raster(RID p_source_texture, RID p_dest_framebuffer) { RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin(p_dest_framebuffer, RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_STORE, RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_DISCARD); RD::get_singleton()->draw_list_bind_render_pipeline(draw_list, blur_raster.pipelines[BLUR_MODE_COPY].get_render_pipeline(RD::INVALID_ID, RD::get_singleton()->framebuffer_get_format(p_dest_framebuffer))); RD::get_singleton()->draw_list_bind_uniform_set(draw_list, uniform_set_cache->get_cache(shader, 0, u_source_texture), 0); - RD::get_singleton()->draw_list_bind_index_array(draw_list, material_storage->get_quad_index_array()); RD::get_singleton()->draw_list_set_push_constant(draw_list, &blur_raster.push_constant, sizeof(BlurRasterPushConstant)); - RD::get_singleton()->draw_list_draw(draw_list, true); + RD::get_singleton()->draw_list_draw(draw_list, false, 1u, 3u); RD::get_singleton()->draw_list_end(); } @@ -927,10 +926,9 @@ void CopyEffects::make_mipmap_raster(RID p_source_rd_texture, RID p_dest_texture RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin(dest_framebuffer, RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_STORE, RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_DISCARD); RD::get_singleton()->draw_list_bind_render_pipeline(draw_list, blur_raster.pipelines[mode].get_render_pipeline(RD::INVALID_ID, RD::get_singleton()->framebuffer_get_format(dest_framebuffer))); RD::get_singleton()->draw_list_bind_uniform_set(draw_list, uniform_set_cache->get_cache(shader, 0, u_source_rd_texture), 0); - RD::get_singleton()->draw_list_bind_index_array(draw_list, material_storage->get_quad_index_array()); RD::get_singleton()->draw_list_set_push_constant(draw_list, &blur_raster.push_constant, sizeof(BlurRasterPushConstant)); - RD::get_singleton()->draw_list_draw(draw_list, true); + RD::get_singleton()->draw_list_draw(draw_list, false, 1u, 3u); RD::get_singleton()->draw_list_end(); } diff --git a/servers/rendering/renderer_rd/effects/ss_effects.cpp b/servers/rendering/renderer_rd/effects/ss_effects.cpp index 3db82c8fbdc0..36a2470c7b7b 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 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/effects/vrs.cpp b/servers/rendering/renderer_rd/effects/vrs.cpp index 30c318fb9ab9..94453bf95fb5 100644 --- a/servers/rendering/renderer_rd/effects/vrs.cpp +++ b/servers/rendering/renderer_rd/effects/vrs.cpp @@ -80,7 +80,16 @@ void VRS::copy_vrs(RID p_source_rd_texture, RID p_dest_framebuffer, bool p_multi RD::Uniform u_source_rd_texture(RD::UNIFORM_TYPE_SAMPLER_WITH_TEXTURE, 0, Vector({ default_sampler, p_source_rd_texture })); - VRSMode mode = p_multiview ? VRS_MULTIVIEW : VRS_DEFAULT; + VRSPushConstant push_constant = {}; + + int mode = p_multiview ? VRS_MULTIVIEW : VRS_DEFAULT; + + // Set maximum texel factor based on maximum fragment size, some GPUs do not support 8x8 (fragment shading rate approach). + if (MIN(RD::get_singleton()->limit_get(RD::LIMIT_VRS_MAX_FRAGMENT_WIDTH), RD::get_singleton()->limit_get(RD::LIMIT_VRS_MAX_FRAGMENT_HEIGHT)) > 4) { + push_constant.max_texel_factor = 3.0; + } else { + push_constant.max_texel_factor = 2.0; + } RID shader = vrs_shader.shader.version_get_shader(vrs_shader.shader_version, mode); ERR_FAIL_COND(shader.is_null()); @@ -88,7 +97,7 @@ void VRS::copy_vrs(RID p_source_rd_texture, RID p_dest_framebuffer, bool p_multi RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin(p_dest_framebuffer, RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_STORE, RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_DISCARD, Vector()); RD::get_singleton()->draw_list_bind_render_pipeline(draw_list, vrs_shader.pipelines[mode].get_render_pipeline(RD::INVALID_ID, RD::get_singleton()->framebuffer_get_format(p_dest_framebuffer))); RD::get_singleton()->draw_list_bind_uniform_set(draw_list, uniform_set_cache->get_cache(shader, 0, u_source_rd_texture), 0); - // RD::get_singleton()->draw_list_set_push_constant(draw_list, &vrs_shader.push_constant, sizeof(VRSPushConstant)); + RD::get_singleton()->draw_list_set_push_constant(draw_list, &push_constant, sizeof(VRSPushConstant)); RD::get_singleton()->draw_list_draw(draw_list, false, 1u, 3u); RD::get_singleton()->draw_list_end(); } @@ -111,12 +120,11 @@ Size2i VRS::get_vrs_texture_size(const Size2i p_base_size) const { void VRS::update_vrs_texture(RID p_vrs_fb, RID p_render_target) { TextureStorage *texture_storage = TextureStorage::get_singleton(); RS::ViewportVRSMode vrs_mode = texture_storage->render_target_get_vrs_mode(p_render_target); + RS::ViewportVRSUpdateMode vrs_update_mode = texture_storage->render_target_get_vrs_update_mode(p_render_target); - if (vrs_mode != RS::VIEWPORT_VRS_DISABLED) { + if (vrs_mode != RS::VIEWPORT_VRS_DISABLED && vrs_update_mode != RS::VIEWPORT_VRS_UPDATE_DISABLED) { RD::get_singleton()->draw_command_begin_label("VRS Setup"); - // TODO figure out if image has changed since it was last copied so we can save some resources.. - if (vrs_mode == RS::VIEWPORT_VRS_TEXTURE) { RID vrs_texture = texture_storage->render_target_get_vrs_texture(p_render_target); if (vrs_texture.is_valid()) { @@ -145,6 +153,10 @@ void VRS::update_vrs_texture(RID p_vrs_fb, RID p_render_target) { #endif // _3D_DISABLED } + if (vrs_update_mode == RS::VIEWPORT_VRS_UPDATE_ONCE) { + texture_storage->render_target_set_vrs_update_mode(p_render_target, RS::VIEWPORT_VRS_UPDATE_DISABLED); + } + RD::get_singleton()->draw_command_end_label(); } } diff --git a/servers/rendering/renderer_rd/effects/vrs.h b/servers/rendering/renderer_rd/effects/vrs.h index b18cf5579143..94878e46615f 100644 --- a/servers/rendering/renderer_rd/effects/vrs.h +++ b/servers/rendering/renderer_rd/effects/vrs.h @@ -47,11 +47,12 @@ class VRS { VRS_MAX, }; - /* we have no push constant here (yet) struct VRSPushConstant { - + float max_texel_factor; // 4x8, 8x4 and 8x8 are only available on some GPUs. + float res1; + float res2; + float res3; }; - */ struct VRSShader { // VRSPushConstant push_constant; diff --git a/servers/rendering/renderer_rd/environment/fog.cpp b/servers/rendering/renderer_rd/environment/fog.cpp index 48537a97d9fe..2dfcd6741168 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 c7ab7ea4626d..2df03316888d 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 8d865ba4405c..9e0dacc1f29d 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(&blend_mode, BLEND_MODE_MIX); actions.render_mode_values["blend_sub"] = Pair(&blend_mode, BLEND_MODE_SUB); actions.render_mode_values["blend_mul"] = Pair(&blend_mode, BLEND_MODE_MUL); + actions.render_mode_values["blend_premul_alpha"] = Pair(&blend_mode, BLEND_MODE_PREMULT_ALPHA); actions.render_mode_values["alpha_to_coverage"] = Pair(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); actions.render_mode_values["alpha_to_coverage_and_one"] = Pair(&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 3b83b2b58221..d5332032f9f0 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 @@ class SceneShaderForwardClustered { 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 0810f567cbdc..dd722cc2ddde 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(&blend_mode, BLEND_MODE_MIX); actions.render_mode_values["blend_sub"] = Pair(&blend_mode, BLEND_MODE_SUB); actions.render_mode_values["blend_mul"] = Pair(&blend_mode, BLEND_MODE_MUL); + actions.render_mode_values["blend_premul_alpha"] = Pair(&blend_mode, BLEND_MODE_PREMULT_ALPHA); actions.render_mode_values["alpha_to_coverage"] = Pair(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); actions.render_mode_values["alpha_to_coverage_and_one"] = Pair(&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"; @@ -575,12 +587,13 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.usage_defines["COLOR"] = "#define COLOR_USED\n"; 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\n"; + actions.usage_defines["LIGHT_VERTEX"] = "#define LIGHT_VERTEX_USED\n"; actions.usage_defines["ALPHA_SCISSOR_THRESHOLD"] = "#define ALPHA_SCISSOR_USED\n"; 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 da189c6f678a..833b06c1e31b 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 @@ class SceneShaderForwardMobile { 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/shader_rd.cpp b/servers/rendering/renderer_rd/shader_rd.cpp index 789c6c284558..1c1b8366e5f6 100644 --- a/servers/rendering/renderer_rd/shader_rd.cpp +++ b/servers/rendering/renderer_rd/shader_rd.cpp @@ -687,8 +687,8 @@ void ShaderRD::enable_group(int p_group) { // Compile all versions again to include the new group. List all_versions; version_owner.get_owned_list(&all_versions); - for (int i = 0; i < all_versions.size(); i++) { - Version *version = version_owner.get_or_null(all_versions[i]); + for (const RID &E : all_versions) { + Version *version = version_owner.get_or_null(E); _compile_version(version, p_group); } } diff --git a/servers/rendering/renderer_rd/shaders/effects/vrs.glsl b/servers/rendering/renderer_rd/shaders/effects/vrs.glsl index 23b0373eefe8..b4fcaa467349 100644 --- a/servers/rendering/renderer_rd/shaders/effects/vrs.glsl +++ b/servers/rendering/renderer_rd/shaders/effects/vrs.glsl @@ -19,6 +19,14 @@ layout(location = 0) out vec3 uv_interp; layout(location = 0) out vec2 uv_interp; #endif +layout(push_constant, std430) uniform Params { + float max_texel_factor; + float res1; + float res2; + float res3; +} +params; + void main() { vec2 base_arr[3] = vec2[](vec2(-1.0, -1.0), vec2(-1.0, 3.0), vec2(3.0, -1.0)); gl_Position = vec4(base_arr[gl_VertexIndex], 0.0, 1.0); @@ -53,6 +61,14 @@ layout(set = 0, binding = 0) uniform sampler2D source_color; layout(location = 0) out uint frag_color; +layout(push_constant, std430) uniform Params { + float max_texel_factor; + float res1; + float res2; + float res3; +} +params; + void main() { #ifdef USE_MULTIVIEW vec3 uv = uv_interp; @@ -60,20 +76,22 @@ void main() { vec2 uv = uv_interp; #endif -#ifdef USE_MULTIVIEW + // Input is standardized. R for X, G for Y, 0.0 (0) = 1, 0.33 (85) = 2, 0.66 (170) = 3, 1.0 (255) = 8 vec4 color = textureLod(source_color, uv, 0.0); - frag_color = uint(color.r * 255.0); -#else /* USE_MULTIVIEW */ - vec4 color = textureLod(source_color, uv, 0.0); - - // for user supplied VRS map we do a color mapping - color.r *= 3.0; - frag_color = int(color.r) << 2; - color.g *= 3.0; - frag_color += int(color.g); - - // note 1x4, 4x1, 1x8, 8x1, 2x8 and 8x2 are not supported - // 4x8, 8x4 and 8x8 are only available on some GPUs -#endif /* USE_MULTIVIEW */ + // Output image shading rate image for VRS according to VK_KHR_fragment_shading_rate. + color.r = clamp(floor(color.r * params.max_texel_factor + 0.1), 0.0, params.max_texel_factor); + color.g = clamp(floor(color.g * params.max_texel_factor + 0.1), 0.0, params.max_texel_factor); + + // Note 1x4, 4x1, 1x8, 8x1, 2x8 and 8x2 are not supported: + if (color.r < (color.g - 1.0)) { + color.r = color.g - 1.0; + } + if (color.g < (color.r - 1.0)) { + color.g = color.r - 1.0; + } + + // Encode to frag_color; + frag_color = int(color.r + 0.1) << 2; + frag_color += int(color.g + 0.1); } 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 cb07579c4b1d..20b080da4d8b 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 @@ -2458,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 b98ea5a27faf..1637326b48c4 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 @@ -1846,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 5fa415472759..efdf1c2278f4 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/storage_rd/light_storage.cpp b/servers/rendering/renderer_rd/storage_rd/light_storage.cpp index d1ff9fc3628c..3d294ca8cb10 100644 --- a/servers/rendering/renderer_rd/storage_rd/light_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/light_storage.cpp @@ -2025,7 +2025,7 @@ void LightStorage::shadow_atlas_set_size(RID p_atlas, int p_size, bool p_16_bits for (int i = 0; i < 4; i++) { //clear subdivisions shadow_atlas->quadrants[i].shadows.clear(); - shadow_atlas->quadrants[i].shadows.resize(int64_t(1) << int64_t(shadow_atlas->quadrants[i].subdivision)); + shadow_atlas->quadrants[i].shadows.resize(int64_t(shadow_atlas->quadrants[i].subdivision * shadow_atlas->quadrants[i].subdivision)); } //erase shadow atlas reference from lights diff --git a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp index 1c3076b128f6..a10c672379b6 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 = 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 b7934cb3dea7..b5fdf8bebb7b 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 af30a3286660..f844919df1c4 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]; @@ -3965,6 +3960,20 @@ RS::ViewportVRSMode TextureStorage::render_target_get_vrs_mode(RID p_render_targ return rt->vrs_mode; } +void TextureStorage::render_target_set_vrs_update_mode(RID p_render_target, RS::ViewportVRSUpdateMode p_mode) { + RenderTarget *rt = render_target_owner.get_or_null(p_render_target); + ERR_FAIL_NULL(rt); + + rt->vrs_update_mode = p_mode; +} + +RS::ViewportVRSUpdateMode TextureStorage::render_target_get_vrs_update_mode(RID p_render_target) const { + RenderTarget *rt = render_target_owner.get_or_null(p_render_target); + ERR_FAIL_NULL_V(rt, RS::VIEWPORT_VRS_UPDATE_DISABLED); + + return rt->vrs_update_mode; +} + void TextureStorage::render_target_set_vrs_texture(RID p_render_target, RID p_texture) { RenderTarget *rt = render_target_owner.get_or_null(p_render_target); ERR_FAIL_NULL(rt); diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.h b/servers/rendering/renderer_rd/storage_rd/texture_storage.h index 1304b284d507..704f5fb1bdad 100644 --- a/servers/rendering/renderer_rd/storage_rd/texture_storage.h +++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.h @@ -366,6 +366,7 @@ class TextureStorage : public RendererTextureStorage { // VRS RS::ViewportVRSMode vrs_mode = RS::VIEWPORT_VRS_DISABLED; + RS::ViewportVRSUpdateMode vrs_update_mode = RS::VIEWPORT_VRS_UPDATE_ONCE; RID vrs_texture; // overridden textures @@ -746,6 +747,8 @@ class TextureStorage : public RendererTextureStorage { virtual void render_target_set_vrs_mode(RID p_render_target, RS::ViewportVRSMode p_mode) override; virtual RS::ViewportVRSMode render_target_get_vrs_mode(RID p_render_target) const override; + virtual void render_target_set_vrs_update_mode(RID p_render_target, RS::ViewportVRSUpdateMode p_mode) override; + virtual RS::ViewportVRSUpdateMode render_target_get_vrs_update_mode(RID p_render_target) const override; virtual void render_target_set_vrs_texture(RID p_render_target, RID p_texture) override; virtual RID render_target_get_vrs_texture(RID p_render_target) const override; diff --git a/servers/rendering/renderer_rd/storage_rd/utilities.cpp b/servers/rendering/renderer_rd/storage_rd/utilities.cpp index 8b780a6f7b13..8ff1d2bc4682 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 5adba5dc6a7c..a848c86bd2a5 100644 --- a/servers/rendering/renderer_scene_occlusion_cull.h +++ b/servers/rendering/renderer_scene_occlusion_cull.h @@ -98,8 +98,8 @@ class RendererSceneOcclusionCull { 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/renderer_viewport.cpp b/servers/rendering/renderer_viewport.cpp index 31d5a9074cfa..e739ffa535ee 100644 --- a/servers/rendering/renderer_viewport.cpp +++ b/servers/rendering/renderer_viewport.cpp @@ -85,7 +85,7 @@ Vector RendererViewport::_sort_active_viewports() } while (!nodes.is_empty()) { - const Viewport *node = nodes[0]; + const Viewport *node = nodes.front()->get(); nodes.pop_front(); for (int i = active_viewports.size() - 1; i >= 0; --i) { @@ -1450,6 +1450,13 @@ void RendererViewport::viewport_set_vrs_mode(RID p_viewport, RS::ViewportVRSMode _configure_3d_render_buffers(viewport); } +void RendererViewport::viewport_set_vrs_update_mode(RID p_viewport, RS::ViewportVRSUpdateMode p_mode) { + Viewport *viewport = viewport_owner.get_or_null(p_viewport); + ERR_FAIL_NULL(viewport); + + RSG::texture_storage->render_target_set_vrs_update_mode(viewport->render_target, p_mode); +} + void RendererViewport::viewport_set_vrs_texture(RID p_viewport, RID p_texture) { Viewport *viewport = viewport_owner.get_or_null(p_viewport); ERR_FAIL_NULL(viewport); diff --git a/servers/rendering/renderer_viewport.h b/servers/rendering/renderer_viewport.h index 5107398c54b3..8bdce04c506a 100644 --- a/servers/rendering/renderer_viewport.h +++ b/servers/rendering/renderer_viewport.h @@ -298,6 +298,7 @@ class RendererViewport { virtual RID viewport_find_from_screen_attachment(DisplayServer::WindowID p_id = DisplayServer::MAIN_WINDOW_ID) const; void viewport_set_vrs_mode(RID p_viewport, RS::ViewportVRSMode p_mode); + void viewport_set_vrs_update_mode(RID p_viewport, RS::ViewportVRSUpdateMode p_mode); void viewport_set_vrs_texture(RID p_viewport, RID p_texture); void handle_timestamp(String p_timestamp, uint64_t p_cpu_time, uint64_t p_gpu_time); diff --git a/servers/rendering/rendering_device.cpp b/servers/rendering/rendering_device.cpp index 962531c866b3..1ee5362f2ce0 100644 --- a/servers/rendering/rendering_device.cpp +++ b/servers/rendering/rendering_device.cpp @@ -3451,9 +3451,9 @@ RenderingDevice::DrawListID RenderingDevice::draw_list_begin(RID p_framebuffer, if (p_region != Rect2() && p_region != Rect2(Vector2(), viewport_size)) { // Check custom region. Rect2i viewport(viewport_offset, viewport_size); Rect2i regioni = p_region; - if (!(regioni.position.x >= viewport.position.x) && (regioni.position.y >= viewport.position.y) && - ((regioni.position.x + regioni.size.x) <= (viewport.position.x + viewport.size.x)) && - ((regioni.position.y + regioni.size.y) <= (viewport.position.y + viewport.size.y))) { + if (!((regioni.position.x >= viewport.position.x) && (regioni.position.y >= viewport.position.y) && + ((regioni.position.x + regioni.size.x) <= (viewport.position.x + viewport.size.x)) && + ((regioni.position.y + regioni.size.y) <= (viewport.position.y + viewport.size.y)))) { ERR_FAIL_V_MSG(INVALID_ID, "When supplying a custom region, it must be contained within the framebuffer rectangle"); } @@ -3858,6 +3858,8 @@ void RenderingDevice::draw_list_draw(DrawListID p_list, bool p_use_indices, uint draw_graph.add_draw_list_draw(to_draw, p_instances); } + + dl->state.draw_count++; } void RenderingDevice::draw_list_enable_scissor(DrawListID p_list, const Rect2 &p_rect) { @@ -4201,6 +4203,7 @@ void RenderingDevice::compute_list_dispatch(ComputeListID p_list, uint32_t p_x_g } draw_graph.add_compute_list_dispatch(p_x_groups, p_y_groups, p_z_groups); + cl->state.dispatch_count++; } void RenderingDevice::compute_list_dispatch_threads(ComputeListID p_list, uint32_t p_x_threads, uint32_t p_y_threads, uint32_t p_z_threads) { @@ -4294,6 +4297,7 @@ void RenderingDevice::compute_list_dispatch_indirect(ComputeListID p_list, RID p } draw_graph.add_compute_list_dispatch_indirect(buffer->driver_id, p_offset); + cl->state.dispatch_count++; if (buffer->draw_tracker != nullptr) { draw_graph.add_compute_list_usage(buffer->draw_tracker, RDG::RESOURCE_USAGE_INDIRECT_BUFFER_READ); @@ -5206,8 +5210,8 @@ void RenderingDevice::_free_rids(T &p_owner, const char *p_type) { } void RenderingDevice::capture_timestamp(const String &p_name) { - ERR_FAIL_COND_MSG(draw_list != nullptr, "Capturing timestamps during draw list creation is not allowed. Offending timestamp was: " + p_name); - ERR_FAIL_COND_MSG(compute_list != nullptr, "Capturing timestamps during compute list creation is not allowed. Offending timestamp was: " + p_name); + ERR_FAIL_COND_MSG(draw_list != nullptr && draw_list->state.draw_count > 0, "Capturing timestamps during draw list creation is not allowed. Offending timestamp was: " + p_name); + ERR_FAIL_COND_MSG(compute_list != nullptr && compute_list->state.dispatch_count > 0, "Capturing timestamps during compute list creation is not allowed. Offending timestamp was: " + p_name); ERR_FAIL_COND(frames[frame].timestamp_count >= max_timestamp_query_elements); draw_graph.add_capture_timestamp(frames[frame].timestamp_pool, frames[frame].timestamp_count); diff --git a/servers/rendering/rendering_device.h b/servers/rendering/rendering_device.h index 9a898a2fcaeb..9db2fdfbf4d2 100644 --- a/servers/rendering/rendering_device.h +++ b/servers/rendering/rendering_device.h @@ -1046,6 +1046,7 @@ class RenderingDevice : public RenderingDeviceCommons { uint32_t pipeline_shader_layout_hash = 0; RID vertex_array; RID index_array; + uint32_t draw_count = 0; } state; #ifdef DEBUG_ENABLED @@ -1147,6 +1148,7 @@ class RenderingDevice : public RenderingDeviceCommons { uint32_t local_group_size[3] = { 0, 0, 0 }; uint8_t push_constant_data[MAX_PUSH_CONSTANT_SIZE] = {}; uint32_t push_constant_size = 0; + uint32_t dispatch_count = 0; } state; #ifdef DEBUG_ENABLED diff --git a/servers/rendering/rendering_device_commons.h b/servers/rendering/rendering_device_commons.h index 28d641c879a6..918bf9b834ad 100644 --- a/servers/rendering/rendering_device_commons.h +++ b/servers/rendering/rendering_device_commons.h @@ -810,6 +810,8 @@ class RenderingDeviceCommons : public Object { LIMIT_SUBGROUP_OPERATIONS, LIMIT_VRS_TEXEL_WIDTH, LIMIT_VRS_TEXEL_HEIGHT, + LIMIT_VRS_MAX_FRAGMENT_WIDTH, + LIMIT_VRS_MAX_FRAGMENT_HEIGHT, }; enum Features { diff --git a/servers/rendering/rendering_device_graph.cpp b/servers/rendering/rendering_device_graph.cpp index adac7ee3eb11..b04f2ebbaa89 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 0bcbe15814b2..d98dba6e3ca1 100644 --- a/servers/rendering/rendering_server_default.h +++ b/servers/rendering/rendering_server_default.h @@ -105,10 +105,6 @@ class RenderingServerDefault : public RenderingServer { _changes_changed(); } -#define DISPLAY_CHANGED \ - changes++; \ - _changes_changed(); - #else _FORCE_INLINE_ static void redraw_request() { changes++; @@ -677,6 +673,7 @@ class RenderingServerDefault : public RenderingServer { FUNC2(call_set_vsync_mode, DisplayServer::VSyncMode, DisplayServer::WindowID) FUNC2(viewport_set_vrs_mode, RID, ViewportVRSMode) + FUNC2(viewport_set_vrs_update_mode, RID, ViewportVRSUpdateMode) FUNC2(viewport_set_vrs_texture, RID, RID) /* COMPOSITOR EFFECT */ @@ -1053,6 +1050,10 @@ class RenderingServerDefault : public RenderingServer { 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_compiler.cpp b/servers/rendering/shader_compiler.cpp index 2b49d42f9e52..87f608bfe6d8 100644 --- a/servers/rendering/shader_compiler.cpp +++ b/servers/rendering/shader_compiler.cpp @@ -479,8 +479,7 @@ String ShaderCompiler::_dump_node_code(const SL::Node *p_node, int p_level, Gene struct_code += _mkid(pnode->vstructs[i].name); struct_code += " "; struct_code += "{\n"; - for (int j = 0; j < st->members.size(); j++) { - SL::MemberNode *m = st->members[j]; + for (SL::MemberNode *m : st->members) { if (m->datatype == SL::TYPE_STRUCT) { struct_code += _mkid(m->struct_name); } else { @@ -807,10 +806,11 @@ String ShaderCompiler::_dump_node_code(const SL::Node *p_node, int p_level, Gene code += _mktab(p_level - 1) + "{\n"; } - for (int i = 0; i < bnode->statements.size(); i++) { - String scode = _dump_node_code(bnode->statements[i], p_level, r_gen_code, p_actions, p_default_actions, p_assigning); + int i = 0; + for (List::ConstIterator itr = bnode->statements.begin(); itr != bnode->statements.end(); ++itr, ++i) { + String scode = _dump_node_code(*itr, p_level, r_gen_code, p_actions, p_default_actions, p_assigning); - if (bnode->statements[i]->type == SL::Node::NODE_TYPE_CONTROL_FLOW || bnode->single_statement) { + if ((*itr)->type == SL::Node::NODE_TYPE_CONTROL_FLOW || bnode->single_statement) { code += scode; //use directly if (bnode->use_comma_between_statements && i + 1 < bnode->statements.size()) { code += ","; diff --git a/servers/rendering/shader_language.cpp b/servers/rendering/shader_language.cpp index 99b3f54379e7..5a02980929df 100644 --- a/servers/rendering/shader_language.cpp +++ b/servers/rendering/shader_language.cpp @@ -4076,6 +4076,8 @@ PropertyInfo ShaderLanguage::uniform_to_property_info(const ShaderNode::Uniform case ShaderLanguage::TYPE_BOOL: if (p_uniform.array_size > 0) { pi.type = Variant::PACKED_INT32_ARRAY; + pi.hint = PROPERTY_HINT_TYPE_STRING; + pi.hint_string = itos(Variant::INT) + "/" + itos(PROPERTY_HINT_FLAGS) + ":" + RTR("On"); } else { pi.type = Variant::BOOL; } @@ -4083,6 +4085,8 @@ PropertyInfo ShaderLanguage::uniform_to_property_info(const ShaderNode::Uniform case ShaderLanguage::TYPE_BVEC2: if (p_uniform.array_size > 0) { pi.type = Variant::PACKED_INT32_ARRAY; + pi.hint = PROPERTY_HINT_TYPE_STRING; + pi.hint_string = itos(Variant::INT) + "/" + itos(PROPERTY_HINT_FLAGS) + ":x,y"; } else { pi.type = Variant::INT; pi.hint = PROPERTY_HINT_FLAGS; @@ -4092,6 +4096,8 @@ PropertyInfo ShaderLanguage::uniform_to_property_info(const ShaderNode::Uniform case ShaderLanguage::TYPE_BVEC3: if (p_uniform.array_size > 0) { pi.type = Variant::PACKED_INT32_ARRAY; + pi.hint = PROPERTY_HINT_TYPE_STRING; + pi.hint_string = itos(Variant::INT) + "/" + itos(PROPERTY_HINT_FLAGS) + ":x,y,z"; } else { pi.type = Variant::INT; pi.hint = PROPERTY_HINT_FLAGS; @@ -4101,6 +4107,8 @@ PropertyInfo ShaderLanguage::uniform_to_property_info(const ShaderNode::Uniform case ShaderLanguage::TYPE_BVEC4: if (p_uniform.array_size > 0) { pi.type = Variant::PACKED_INT32_ARRAY; + pi.hint = PROPERTY_HINT_TYPE_STRING; + pi.hint_string = itos(Variant::INT) + "/" + itos(PROPERTY_HINT_FLAGS) + ":x,y,z,w"; } else { pi.type = Variant::INT; pi.hint = PROPERTY_HINT_FLAGS; @@ -4111,11 +4119,16 @@ PropertyInfo ShaderLanguage::uniform_to_property_info(const ShaderNode::Uniform case ShaderLanguage::TYPE_INT: { if (p_uniform.array_size > 0) { pi.type = Variant::PACKED_INT32_ARRAY; + // TODO: Handle range and encoding for for unsigned values. } else { pi.type = Variant::INT; + pi.hint = PROPERTY_HINT_RANGE; if (p_uniform.hint == ShaderLanguage::ShaderNode::Uniform::HINT_RANGE) { - pi.hint = PROPERTY_HINT_RANGE; pi.hint_string = rtos(p_uniform.hint_range[0]) + "," + rtos(p_uniform.hint_range[1]) + "," + rtos(p_uniform.hint_range[2]); + } else if (p_uniform.type == ShaderLanguage::TYPE_UINT) { + pi.hint_string = "0," + itos(UINT32_MAX); + } else { + pi.hint_string = itos(INT32_MIN) + "," + itos(INT32_MAX); } } } break; @@ -4123,6 +4136,7 @@ PropertyInfo ShaderLanguage::uniform_to_property_info(const ShaderNode::Uniform case ShaderLanguage::TYPE_IVEC2: { if (p_uniform.array_size > 0) { pi.type = Variant::PACKED_INT32_ARRAY; + // TODO: Handle vector pairs? } else { pi.type = Variant::VECTOR2I; } @@ -4131,6 +4145,7 @@ PropertyInfo ShaderLanguage::uniform_to_property_info(const ShaderNode::Uniform case ShaderLanguage::TYPE_IVEC3: { if (p_uniform.array_size > 0) { pi.type = Variant::PACKED_INT32_ARRAY; + // TODO: Handle vector pairs? } else { pi.type = Variant::VECTOR3I; } @@ -4139,6 +4154,7 @@ PropertyInfo ShaderLanguage::uniform_to_property_info(const ShaderNode::Uniform case ShaderLanguage::TYPE_IVEC4: { if (p_uniform.array_size > 0) { pi.type = Variant::PACKED_INT32_ARRAY; + // TODO: Handle vector pairs? } else { pi.type = Variant::VECTOR4I; } @@ -5201,11 +5217,11 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons funcname->name = name; func->arguments.push_back(funcname); - for (int i = 0; i < pstruct->members.size(); i++) { + for (List::Element *E = pstruct->members.front(); E; E = E->next()) { Node *nexpr; - if (pstruct->members[i]->array_size != 0) { - nexpr = _parse_array_constructor(p_block, p_function_info, pstruct->members[i]->get_datatype(), pstruct->members[i]->struct_name, pstruct->members[i]->array_size); + if (E->get()->array_size != 0) { + nexpr = _parse_array_constructor(p_block, p_function_info, E->get()->get_datatype(), E->get()->struct_name, E->get()->array_size); if (!nexpr) { return nullptr; } @@ -5214,12 +5230,12 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons if (!nexpr) { return nullptr; } - if (!_compare_datatypes_in_nodes(pstruct->members[i], nexpr)) { + if (!_compare_datatypes_in_nodes(E->get(), nexpr)) { return nullptr; } } - if (i + 1 < pstruct->members.size()) { + if (E->next()) { tk = _get_token(); if (tk.type != TK_COMMA) { _set_expected_error(","); @@ -7469,8 +7485,8 @@ Error ShaderLanguage::_parse_block(BlockNode *p_block, const FunctionInfo &p_fun continue; } else { HashSet constants; - for (int i = 0; i < switch_block->statements.size(); i++) { // Checks for duplicates. - ControlFlowNode *flow = static_cast(switch_block->statements[i]); + for (ShaderLanguage::Node *statement : switch_block->statements) { // Checks for duplicates. + ControlFlowNode *flow = static_cast(statement); if (flow) { if (flow->flow_op == FLOW_OP_CASE) { if (flow->expressions[0]->type == Node::NODE_TYPE_CONSTANT) { @@ -9846,9 +9862,9 @@ Error ShaderLanguage::_find_last_flow_op_in_op(ControlFlowNode *p_flow, FlowOper Error ShaderLanguage::_find_last_flow_op_in_block(BlockNode *p_block, FlowOperation p_op) { bool found = false; - for (int i = p_block->statements.size() - 1; i >= 0; i--) { - if (p_block->statements[i]->type == Node::NODE_TYPE_CONTROL_FLOW) { - ControlFlowNode *flow = static_cast(p_block->statements[i]); + for (List::Element *E = p_block->statements.back(); E; E = E->prev()) { + if (E->get()->type == Node::NODE_TYPE_CONTROL_FLOW) { + ControlFlowNode *flow = static_cast(E->get()); if (flow->flow_op == p_op) { found = true; break; @@ -9858,8 +9874,8 @@ Error ShaderLanguage::_find_last_flow_op_in_block(BlockNode *p_block, FlowOperat break; } } - } else if (p_block->statements[i]->type == Node::NODE_TYPE_BLOCK) { - BlockNode *block = static_cast(p_block->statements[i]); + } else if (E->get()->type == Node::NODE_TYPE_BLOCK) { + BlockNode *block = static_cast(E->get()); if (_find_last_flow_op_in_block(block, p_op) == OK) { found = true; break; @@ -10151,8 +10167,8 @@ Error ShaderLanguage::complete(const String &p_code, const ShaderCompileInfo &p_ case COMPLETION_STRUCT: { if (shader->structs.has(completion_struct)) { StructNode *node = shader->structs[completion_struct].shader_struct; - for (int i = 0; i < node->members.size(); i++) { - ScriptLanguage::CodeCompletionOption option(node->members[i]->name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER); + for (ShaderLanguage::MemberNode *member : node->members) { + ScriptLanguage::CodeCompletionOption option(member->name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER); r_options->push_back(option); } } diff --git a/servers/rendering/shader_types.cpp b/servers/rendering/shader_types.cpp index af51083dc337..ab9cf666ec3a 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") }); @@ -495,7 +496,7 @@ ShaderTypes::ShaderTypes() { shader_types_list.push_back("sky"); shader_types_list.push_back("fog"); - for (int i = 0; i < shader_types_list.size(); i++) { - shader_types.insert(shader_types_list[i]); + for (const String &type : shader_types_list) { + shader_types.insert(type); } } diff --git a/servers/rendering/storage/texture_storage.h b/servers/rendering/storage/texture_storage.h index cf37fbfb4a05..72c4dd305bde 100644 --- a/servers/rendering/storage/texture_storage.h +++ b/servers/rendering/storage/texture_storage.h @@ -169,6 +169,8 @@ class RendererTextureStorage { virtual void render_target_set_vrs_mode(RID p_render_target, RS::ViewportVRSMode p_mode) = 0; virtual RS::ViewportVRSMode render_target_get_vrs_mode(RID p_render_target) const = 0; + virtual void render_target_set_vrs_update_mode(RID p_render_target, RS::ViewportVRSUpdateMode p_mode) = 0; + virtual RS::ViewportVRSUpdateMode render_target_get_vrs_update_mode(RID p_render_target) const = 0; virtual void render_target_set_vrs_texture(RID p_render_target, RID p_texture) = 0; virtual RID render_target_get_vrs_texture(RID p_render_target) const = 0; diff --git a/servers/rendering/storage/variant_converters.h b/servers/rendering/storage/variant_converters.h index 7dbdb0f5176a..cee663e878f3 100644 --- a/servers/rendering/storage/variant_converters.h +++ b/servers/rendering/storage/variant_converters.h @@ -239,7 +239,8 @@ inline bool is_convertible_array(Variant::Type type) { return type == Variant::ARRAY || type == Variant::PACKED_VECTOR2_ARRAY || type == Variant::PACKED_VECTOR3_ARRAY || - type == Variant::PACKED_COLOR_ARRAY; + type == Variant::PACKED_COLOR_ARRAY || + type == Variant::PACKED_VECTOR4_ARRAY; } template diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index 0dd8dee0741b..c2dffe6770de 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."); } @@ -2821,6 +2825,7 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("viewport_get_measured_render_time_gpu", "viewport"), &RenderingServer::viewport_get_measured_render_time_gpu); ClassDB::bind_method(D_METHOD("viewport_set_vrs_mode", "viewport", "mode"), &RenderingServer::viewport_set_vrs_mode); + ClassDB::bind_method(D_METHOD("viewport_set_vrs_update_mode", "viewport", "mode"), &RenderingServer::viewport_set_vrs_update_mode); ClassDB::bind_method(D_METHOD("viewport_set_vrs_texture", "viewport", "texture"), &RenderingServer::viewport_set_vrs_texture); BIND_ENUM_CONSTANT(VIEWPORT_SCALING_3D_MODE_BILINEAR); @@ -2829,7 +2834,7 @@ void RenderingServer::_bind_methods() { BIND_ENUM_CONSTANT(VIEWPORT_SCALING_3D_MODE_MAX); BIND_ENUM_CONSTANT(VIEWPORT_UPDATE_DISABLED); - BIND_ENUM_CONSTANT(VIEWPORT_UPDATE_ONCE); // Then goes to disabled); must be manually updated. + BIND_ENUM_CONSTANT(VIEWPORT_UPDATE_ONCE); // Then goes to disabled, must be manually updated. BIND_ENUM_CONSTANT(VIEWPORT_UPDATE_WHEN_VISIBLE); // Default BIND_ENUM_CONSTANT(VIEWPORT_UPDATE_WHEN_PARENT_VISIBLE); BIND_ENUM_CONSTANT(VIEWPORT_UPDATE_ALWAYS); @@ -2911,6 +2916,11 @@ void RenderingServer::_bind_methods() { BIND_ENUM_CONSTANT(VIEWPORT_VRS_XR); BIND_ENUM_CONSTANT(VIEWPORT_VRS_MAX); + BIND_ENUM_CONSTANT(VIEWPORT_VRS_UPDATE_DISABLED); + BIND_ENUM_CONSTANT(VIEWPORT_VRS_UPDATE_ONCE); // Then goes to disabled, must be manually updated. + BIND_ENUM_CONSTANT(VIEWPORT_VRS_UPDATE_ALWAYS); + BIND_ENUM_CONSTANT(VIEWPORT_VRS_UPDATE_MAX); + /* SKY API */ ClassDB::bind_method(D_METHOD("sky_create"), &RenderingServer::sky_create); @@ -3427,6 +3437,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 f3218b98276f..3dea6b2c630d 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 class TypedArray; @@ -1031,7 +1057,15 @@ class RenderingServer : public Object { VIEWPORT_VRS_MAX, }; + enum ViewportVRSUpdateMode { + VIEWPORT_VRS_UPDATE_DISABLED, + VIEWPORT_VRS_UPDATE_ONCE, + VIEWPORT_VRS_UPDATE_ALWAYS, + VIEWPORT_VRS_UPDATE_MAX, + }; + virtual void viewport_set_vrs_mode(RID p_viewport, ViewportVRSMode p_mode) = 0; + virtual void viewport_set_vrs_update_mode(RID p_viewport, ViewportVRSUpdateMode p_mode) = 0; virtual void viewport_set_vrs_texture(RID p_viewport, RID p_texture) = 0; /* SKY API */ @@ -1685,7 +1719,7 @@ class RenderingServer : public Object { #ifndef DISABLE_DEPRECATED // Never actually used, should be removed when we can break compatibility. - enum Features { + enum Features{ FEATURE_SHADERS, FEATURE_MULTITHREADED, }; @@ -1709,6 +1743,7 @@ class RenderingServer : public Object { 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 @@ -1789,6 +1824,7 @@ VARIANT_ENUM_CAST(RenderingServer::ViewportOcclusionCullingBuildQuality); VARIANT_ENUM_CAST(RenderingServer::ViewportSDFOversize); VARIANT_ENUM_CAST(RenderingServer::ViewportSDFScale); VARIANT_ENUM_CAST(RenderingServer::ViewportVRSMode); +VARIANT_ENUM_CAST(RenderingServer::ViewportVRSUpdateMode); VARIANT_ENUM_CAST(RenderingServer::SkyMode); VARIANT_ENUM_CAST(RenderingServer::CompositorEffectCallbackType); VARIANT_ENUM_CAST(RenderingServer::CompositorEffectFlags); diff --git a/servers/text/text_server_extension.cpp b/servers/text/text_server_extension.cpp index 8f9797805f89..c13f5db5e944 100644 --- a/servers/text/text_server_extension.cpp +++ b/servers/text/text_server_extension.cpp @@ -332,6 +332,7 @@ void TextServerExtension::_bind_methods() { GDVIRTUAL_BIND(_strip_diacritics, "string"); GDVIRTUAL_BIND(_is_valid_identifier, "string"); + GDVIRTUAL_BIND(_is_valid_letter, "unicode"); GDVIRTUAL_BIND(_string_get_word_breaks, "string", "language", "chars_per_line"); GDVIRTUAL_BIND(_string_get_character_breaks, "string", "language"); @@ -1492,6 +1493,14 @@ bool TextServerExtension::is_valid_identifier(const String &p_string) const { return TextServer::is_valid_identifier(p_string); } +bool TextServerExtension::is_valid_letter(char32_t p_unicode) const { + bool ret; + if (GDVIRTUAL_CALL(_is_valid_letter, p_unicode, ret)) { + return ret; + } + return TextServer::is_valid_letter(p_unicode); +} + String TextServerExtension::strip_diacritics(const String &p_string) const { String ret; if (GDVIRTUAL_CALL(_strip_diacritics, p_string, ret)) { diff --git a/servers/text/text_server_extension.h b/servers/text/text_server_extension.h index f447f0f5bdd2..d5cd1f753e84 100644 --- a/servers/text/text_server_extension.h +++ b/servers/text/text_server_extension.h @@ -563,6 +563,8 @@ class TextServerExtension : public TextServer { virtual bool is_valid_identifier(const String &p_string) const override; GDVIRTUAL1RC(bool, _is_valid_identifier, const String &); + virtual bool is_valid_letter(char32_t p_unicode) const override; + GDVIRTUAL1RC(bool, _is_valid_letter, char32_t); virtual String string_to_upper(const String &p_string, const String &p_language = "") const override; virtual String string_to_lower(const String &p_string, const String &p_language = "") const override; diff --git a/servers/text_server.cpp b/servers/text_server.cpp index 562f2df411cd..127c674f0c35 100644 --- a/servers/text_server.cpp +++ b/servers/text_server.cpp @@ -490,6 +490,7 @@ void TextServer::_bind_methods() { ClassDB::bind_method(D_METHOD("strip_diacritics", "string"), &TextServer::strip_diacritics); ClassDB::bind_method(D_METHOD("is_valid_identifier", "string"), &TextServer::is_valid_identifier); + ClassDB::bind_method(D_METHOD("is_valid_letter", "unicode"), &TextServer::is_valid_letter); ClassDB::bind_method(D_METHOD("string_to_upper", "string", "language"), &TextServer::string_to_upper, DEFVAL("")); ClassDB::bind_method(D_METHOD("string_to_lower", "string", "language"), &TextServer::string_to_lower, DEFVAL("")); @@ -2182,6 +2183,10 @@ bool TextServer::is_valid_identifier(const String &p_string) const { return true; } +bool TextServer::is_valid_letter(char32_t p_unicode) const { + return is_unicode_letter(p_unicode); +} + TextServer::TextServer() { _init_diacritics_map(); } diff --git a/servers/text_server.h b/servers/text_server.h index 775dbb55086e..5835b36ec4e1 100644 --- a/servers/text_server.h +++ b/servers/text_server.h @@ -547,6 +547,7 @@ class TextServer : public RefCounted { virtual String strip_diacritics(const String &p_string) const; virtual bool is_valid_identifier(const String &p_string) const; + virtual bool is_valid_letter(char32_t p_unicode) const; // Other string operations. virtual String string_to_upper(const String &p_string, const String &p_language = "") const = 0; diff --git a/servers/xr/xr_interface.cpp b/servers/xr/xr_interface.cpp index 9ced28fd52da..26f315a45426 100644 --- a/servers/xr/xr_interface.cpp +++ b/servers/xr/xr_interface.cpp @@ -132,13 +132,7 @@ void XRInterface::set_primary(bool p_primary) { XRInterface::XRInterface() {} -XRInterface::~XRInterface() { - if (vrs.vrs_texture.is_valid()) { - ERR_FAIL_NULL(RenderingServer::get_singleton()); - RS::get_singleton()->free(vrs.vrs_texture); - vrs.vrs_texture = RID(); - } -} +XRInterface::~XRInterface() {} // query if this interface supports this play area mode bool XRInterface::supports_play_area_mode(XRInterface::PlayAreaMode p_mode) { @@ -176,80 +170,7 @@ int XRInterface::get_camera_feed_id() { } RID XRInterface::get_vrs_texture() { - // Default logic will return a standard VRS image based on our target size and default projections. - // Note that this only gets called if VRS is supported on the hardware. - - int32_t texel_width = RD::get_singleton()->limit_get(RD::LIMIT_VRS_TEXEL_WIDTH); - int32_t texel_height = RD::get_singleton()->limit_get(RD::LIMIT_VRS_TEXEL_HEIGHT); - int view_count = get_view_count(); - Size2 target_size = get_render_target_size(); - real_t aspect = target_size.x / target_size.y; // is this y/x ? - Size2 vrs_size = Size2(round(0.5 + target_size.x / texel_width), round(0.5 + target_size.y / texel_height)); - real_t radius = vrs_size.length() * 0.5; - Size2 vrs_sizei = vrs_size; - - if (vrs.size != vrs_sizei) { - const uint8_t densities[] = { - 0, // 1x1 - 1, // 1x2 - // 2, // 1x4 - not supported - // 3, // 1x8 - not supported - // 4, // 2x1 - 5, // 2x2 - 6, // 2x4 - // 9, // 4x2 - 10, // 4x4 - }; - - // out with the old - if (vrs.vrs_texture.is_valid()) { - RS::get_singleton()->free(vrs.vrs_texture); - vrs.vrs_texture = RID(); - } - - // in with the new - Vector> images; - vrs.size = vrs_sizei; - - for (int i = 0; i < view_count && i < 2; i++) { - PackedByteArray data; - data.resize(vrs_sizei.x * vrs_sizei.y); - uint8_t *data_ptr = data.ptrw(); - - // Our near and far don't matter much for what we're doing here, but there are some interfaces that will remember this as the near and far and may fail as a result... - Projection cm = get_projection_for_view(i, aspect, 0.1, 1000.0); - Vector3 center = cm.xform(Vector3(0.0, 0.0, 999.0)); - - Vector2i view_center; - view_center.x = int(vrs_size.x * (center.x + 1.0) * 0.5); - view_center.y = int(vrs_size.y * (center.y + 1.0) * 0.5); - - int d = 0; - for (int y = 0; y < vrs_sizei.y; y++) { - for (int x = 0; x < vrs_sizei.x; x++) { - Vector2 offset = Vector2(x - view_center.x, y - view_center.y); - offset.y *= aspect; - real_t distance = offset.length(); - int idx = round(5.0 * distance / radius); - if (idx > 4) { - idx = 4; - } - uint8_t density = densities[idx]; - - data_ptr[d++] = density; - } - } - images.push_back(Image::create_from_data(vrs_sizei.x, vrs_sizei.y, false, Image::FORMAT_R8, data)); - } - - if (images.size() == 1) { - vrs.vrs_texture = RS::get_singleton()->texture_2d_create(images[0]); - } else { - vrs.vrs_texture = RS::get_singleton()->texture_2d_layered_create(images, RS::TEXTURE_LAYERED_2D_ARRAY); - } - } - - return vrs.vrs_texture; + return RID(); } /** these are optional, so we want dummies **/ diff --git a/servers/xr/xr_interface.h b/servers/xr/xr_interface.h index d7bd21244992..7beec219bb55 100644 --- a/servers/xr/xr_interface.h +++ b/servers/xr/xr_interface.h @@ -34,6 +34,7 @@ #include "core/math/projection.h" #include "core/os/thread_safe.h" #include "servers/xr_server.h" +#include "xr_vrs.h" // forward declaration struct BlitToScreen; @@ -122,17 +123,20 @@ class XRInterface : public RefCounted { /** 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 post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) = 0; /* inform XR interface we finished our viewport draw process */ @@ -145,19 +149,16 @@ class XRInterface : public RefCounted { virtual bool start_passthrough() { return false; } virtual void stop_passthrough() {} - /** environment blend mode. */ + /** environment blend mode **/ virtual Array get_supported_environment_blend_modes(); virtual XRInterface::EnvironmentBlendMode get_environment_blend_mode() const { return XR_ENV_BLEND_MODE_OPAQUE; } virtual bool set_environment_blend_mode(EnvironmentBlendMode mode) { return false; } + /** VRS **/ + virtual RID get_vrs_texture(); /* obtain VRS texture */ + XRInterface(); ~XRInterface(); - -private: - struct VRSData { - RID vrs_texture; - Size2i size; - } vrs; }; VARIANT_ENUM_CAST(XRInterface::Capabilities); diff --git a/servers/xr/xr_vrs.cpp b/servers/xr/xr_vrs.cpp new file mode 100644 index 000000000000..9d1e2f2068dc --- /dev/null +++ b/servers/xr/xr_vrs.cpp @@ -0,0 +1,151 @@ +/**************************************************************************/ +/* xr_vrs.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 "xr_vrs.h" + +#include "servers/rendering/renderer_scene_render.h" +#include "servers/rendering_server.h" + +void XRVRS::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_vrs_min_radius"), &XRVRS::get_vrs_min_radius); + ClassDB::bind_method(D_METHOD("set_vrs_min_radius", "radius"), &XRVRS::set_vrs_min_radius); + + ClassDB::bind_method(D_METHOD("get_vrs_strength"), &XRVRS::get_vrs_strength); + ClassDB::bind_method(D_METHOD("set_vrs_strength", "strength"), &XRVRS::set_vrs_strength); + + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "vrs_min_radius", PROPERTY_HINT_RANGE, "1.0,100.0,1.0"), "set_vrs_min_radius", "get_vrs_min_radius"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "vrs_strength", PROPERTY_HINT_RANGE, "0.1,10.0,0.1"), "set_vrs_strength", "get_vrs_strength"); + + ClassDB::bind_method(D_METHOD("make_vrs_texture", "target_size", "eye_foci"), &XRVRS::make_vrs_texture); +} + +XRVRS::~XRVRS() { + if (vrs_texture.is_valid()) { + ERR_FAIL_NULL(RS::get_singleton()); + RS::get_singleton()->free(vrs_texture); + vrs_texture = RID(); + } +} + +float XRVRS::get_vrs_min_radius() const { + return vrs_min_radius; +} + +void XRVRS::set_vrs_min_radius(float p_vrs_min_radius) { + if (p_vrs_min_radius < 1.0) { + WARN_PRINT_ONCE("VRS minimum radius can not be set below 1.0"); + vrs_min_radius = 1.0; + } else if (p_vrs_min_radius > 100.0) { + WARN_PRINT_ONCE("VRS minimum radius can not be set above 100.0"); + vrs_min_radius = 100.0; + } else { + vrs_min_radius = p_vrs_min_radius; + vrs_dirty = true; + } +} + +float XRVRS::get_vrs_strength() const { + return vrs_strength; +} + +void XRVRS::set_vrs_strength(float p_vrs_strength) { + if (p_vrs_strength < 0.1) { + WARN_PRINT_ONCE("VRS strength can not be set below 0.1"); + vrs_strength = 0.1; + } else if (p_vrs_strength > 10.0) { + WARN_PRINT_ONCE("VRS strength can not be set above 10.0"); + vrs_strength = 10.0; + } else { + vrs_strength = p_vrs_strength; + vrs_dirty = true; + } +} + +RID XRVRS::make_vrs_texture(const Size2 &p_target_size, const PackedVector2Array &p_eye_foci) { + ERR_FAIL_COND_V(p_eye_foci.is_empty(), RID()); + + int32_t texel_width = RD::get_singleton()->limit_get(RD::LIMIT_VRS_TEXEL_WIDTH); + int32_t texel_height = RD::get_singleton()->limit_get(RD::LIMIT_VRS_TEXEL_HEIGHT); + + Size2 vrs_size = Size2(0.5 + p_target_size.x / texel_width, 0.5 + p_target_size.y / texel_height).round(); + float max_radius = 0.5 * MIN(vrs_size.x, vrs_size.y); // Maximum radius that fits inside of our image + float min_radius = vrs_min_radius * max_radius / 100.0; // Minimum radius as a percentage of our size + real_t outer_radius = MAX(1.0, (max_radius - min_radius) / vrs_strength); + Size2 vrs_sizei = vrs_size; + + // Our density map is now unified, with a value of (0.0, 0.0) meaning a 1x1 texel size and (1.0, 1.0) an max texel size. + // For our standard VRS extension on Vulkan this means a maximum of 8x8. + // For the density map extension this scales depending on the max texel size. + + if (target_size != vrs_sizei || eye_foci != p_eye_foci || vrs_dirty) { + // Out with the old. + if (vrs_texture.is_valid()) { + RS::get_singleton()->free(vrs_texture); + vrs_texture = RID(); + } + + // In with the new. + Vector> images; + target_size = vrs_sizei; + eye_foci = p_eye_foci; + + for (int i = 0; i < eye_foci.size() && i < RendererSceneRender::MAX_RENDER_VIEWS; i++) { + PackedByteArray data; + data.resize(vrs_sizei.x * vrs_sizei.y * 2); + uint8_t *data_ptr = data.ptrw(); + + Vector2i view_center; + view_center.x = int(vrs_size.x * (eye_foci[i].x + 1.0) * 0.5); + view_center.y = int(vrs_size.y * (eye_foci[i].y + 1.0) * 0.5); + + int d = 0; + for (int y = 0; y < vrs_sizei.y; y++) { + for (int x = 0; x < vrs_sizei.x; x++) { + Vector2 offset = Vector2(x - view_center.x, y - view_center.y); + real_t density = 255.0 * MAX(0.0, (Math::abs(offset.x) - min_radius) / outer_radius); + data_ptr[d++] = MIN(255, density); + density = 255.0 * MAX(0.0, (Math::abs(offset.y) - min_radius) / outer_radius); + data_ptr[d++] = MIN(255, density); + } + } + images.push_back(Image::create_from_data(vrs_sizei.x, vrs_sizei.y, false, Image::FORMAT_RG8, data)); + } + + if (images.size() == 1) { + vrs_texture = RS::get_singleton()->texture_2d_create(images[0]); + } else { + vrs_texture = RS::get_singleton()->texture_2d_layered_create(images, RS::TEXTURE_LAYERED_2D_ARRAY); + } + + vrs_dirty = false; + } + + return vrs_texture; +} diff --git a/servers/xr/xr_vrs.h b/servers/xr/xr_vrs.h new file mode 100644 index 000000000000..35dfe55620d0 --- /dev/null +++ b/servers/xr/xr_vrs.h @@ -0,0 +1,67 @@ +/**************************************************************************/ +/* xr_vrs.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 XR_VRS_H +#define XR_VRS_H + +#include "core/object/class_db.h" +#include "core/object/object.h" +#include "core/templates/rid.h" +#include "core/variant/variant.h" + +/* This is a helper class for generating stereoscopic VRS images */ + +class XRVRS : public Object { + GDCLASS(XRVRS, Object); + +private: + float vrs_min_radius = 20.0; + float vrs_strength = 1.0; + bool vrs_dirty = true; + + RID vrs_texture; + Size2i target_size; + PackedVector2Array eye_foci; + +protected: + static void _bind_methods(); + +public: + ~XRVRS(); + + float get_vrs_min_radius() const; + void set_vrs_min_radius(float p_vrs_min_radius); + float get_vrs_strength() const; + void set_vrs_strength(float p_vrs_strength); + + RID make_vrs_texture(const Size2 &p_target_size, const PackedVector2Array &p_eye_foci); +}; + +#endif // XR_VRS_H diff --git a/servers/xr_server.cpp b/servers/xr_server.cpp index f1105a650d02..2cfe98ea1e1f 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 &p_interface) { ERR_FAIL_COND(p_interface.is_null()); @@ -185,12 +244,12 @@ void XRServer::add_interface(const Ref &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 &p_interface) { ERR_FAIL_COND(p_interface.is_null()); @@ -200,33 +259,33 @@ void XRServer::remove_interface(const Ref &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 XRServer::get_interface(int p_index) const { ERR_FAIL_INDEX_V(p_index, interfaces.size(), nullptr); return interfaces[p_index]; -}; +} Ref 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(); -}; +} TypedArray XRServer::get_interfaces() const { Array ret; @@ -238,14 +297,14 @@ TypedArray XRServer::get_interfaces() const { iface_info["name"] = interfaces[i]->get_name(); ret.push_back(iface_info); - }; + } return ret; -}; +} Ref XRServer::get_primary_interface() const { return primary_interface; -}; +} void XRServer::set_primary_interface(const Ref &p_primary_interface) { if (p_primary_interface.is_null()) { @@ -256,7 +315,7 @@ void XRServer::set_primary_interface(const Ref &p_primary_interface print_verbose("XR: Primary interface set to: " + primary_interface->get_name()); } -}; +} void XRServer::add_tracker(const Ref &p_tracker) { ERR_FAIL_COND(p_tracker.is_null()); @@ -272,7 +331,7 @@ void XRServer::add_tracker(const Ref &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 &p_tracker) { ERR_FAIL_COND(p_tracker.is_null()); @@ -285,7 +344,7 @@ void XRServer::remove_tracker(const Ref &p_tracker) { // and remove it trackers.erase(tracker_name); } -}; +} Dictionary XRServer::get_trackers(int p_tracker_types) { Dictionary res; @@ -307,7 +366,7 @@ Ref 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(); } -}; +} 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 717728171aac..cd9c241bb0aa 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 @@ class XRServer : public Object { Ref 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/core/object/test_class_db.h b/tests/core/object/test_class_db.h index 5f7de11c7179..fb62d0f05646 100644 --- a/tests/core/object/test_class_db.h +++ b/tests/core/object/test_class_db.h @@ -141,7 +141,7 @@ struct NamesCache { StringName vector3_type = StaticCString::create("Vector3"); // Object not included as it must be checked for all derived classes - static constexpr int nullable_types_count = 17; + static constexpr int nullable_types_count = 18; StringName nullable_types[nullable_types_count] = { string_type, string_name_type, @@ -161,6 +161,7 @@ struct NamesCache { StaticCString::create(_STR(PackedVector2Array)), StaticCString::create(_STR(PackedVector3Array)), StaticCString::create(_STR(PackedColorArray)), + StaticCString::create(_STR(PackedVector4Array)), }; bool is_nullable_type(const StringName &p_type) const { @@ -258,6 +259,7 @@ bool arg_default_value_is_assignable_to_type(const Context &p_context, const Var case Variant::PACKED_VECTOR2_ARRAY: case Variant::PACKED_VECTOR3_ARRAY: case Variant::PACKED_COLOR_ARRAY: + case Variant::PACKED_VECTOR4_ARRAY: case Variant::CALLABLE: case Variant::SIGNAL: return p_arg_type.name == Variant::get_type_name(p_val.get_type()); @@ -548,8 +550,6 @@ void add_exposed_classes(Context &r_context) { for (const MethodInfo &E : method_list) { const MethodInfo &method_info = E; - int argc = method_info.arguments.size(); - if (method_info.name.is_empty()) { continue; } @@ -611,8 +611,9 @@ void add_exposed_classes(Context &r_context) { method.return_type.name = Variant::get_type_name(return_info.type); } - for (int i = 0; i < argc; i++) { - PropertyInfo arg_info = method_info.arguments[i]; + int i = 0; + for (List::ConstIterator itr = method_info.arguments.begin(); itr != method_info.arguments.end(); ++itr, ++i) { + const PropertyInfo &arg_info = *itr; String orig_arg_name = arg_info.name; @@ -684,10 +685,9 @@ void add_exposed_classes(Context &r_context) { TEST_FAIL_COND(!String(signal.name).is_valid_identifier(), "Signal name is not a valid identifier: '", exposed_class.name, ".", signal.name, "'."); - int argc = method_info.arguments.size(); - - for (int i = 0; i < argc; i++) { - PropertyInfo arg_info = method_info.arguments[i]; + int i = 0; + for (List::ConstIterator itr = method_info.arguments.begin(); itr != method_info.arguments.end(); ++itr, ++i) { + const PropertyInfo &arg_info = *itr; String orig_arg_name = arg_info.name; diff --git a/tests/core/object/test_object.h b/tests/core/object/test_object.h index 3a3013a102f0..d714d71416a5 100644 --- a/tests/core/object/test_object.h +++ b/tests/core/object/test_object.h @@ -142,7 +142,7 @@ TEST_CASE("[Object] Core getters") { inheritance_list.size() == 1, "The inheritance list should consist of Object only"); CHECK_MESSAGE( - inheritance_list[0] == "Object", + inheritance_list.front()->get() == "Object", "The inheritance list should consist of Object only"); } diff --git a/tests/core/os/test_os.h b/tests/core/os/test_os.h index 63f8b1823801..6ee0ff82e728 100644 --- a/tests/core/os/test_os.h +++ b/tests/core/os/test_os.h @@ -79,8 +79,8 @@ TEST_CASE("[OS] Non-UTF-8 environment variables") { TEST_CASE("[OS] Command line arguments") { List arguments = OS::get_singleton()->get_cmdline_args(); bool found = false; - for (int i = 0; i < arguments.size(); i++) { - if (arguments[i] == "--test") { + for (const String &arg : arguments) { + if (arg == "--test") { found = true; break; } diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h index 64f03e587997..18828c3b70bf 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -360,18 +360,37 @@ TEST_CASE("[String] Substr") { TEST_CASE("[String] Find") { String s = "Pretty Woman Woman"; - CHECK(s.find("tty") == 3); - CHECK(s.find("Wo", 9) == 13); - CHECK(s.find("Revenge of the Monster Truck") == -1); - CHECK(s.rfind("man") == 15); + MULTICHECK_STRING_EQ(s, find, "tty", 3); + MULTICHECK_STRING_EQ(s, find, "Revenge of the Monster Truck", -1); + MULTICHECK_STRING_INT_EQ(s, find, "Wo", 9, 13); + MULTICHECK_STRING_EQ(s, find, "", -1); + MULTICHECK_STRING_EQ(s, find, "Pretty Woman Woman", 0); + MULTICHECK_STRING_EQ(s, find, "WOMAN", -1); + MULTICHECK_STRING_INT_EQ(s, find, "", 9, -1); + + MULTICHECK_STRING_EQ(s, rfind, "", -1); + MULTICHECK_STRING_EQ(s, rfind, "foo", -1); + MULTICHECK_STRING_EQ(s, rfind, "Pretty Woman Woman", 0); + MULTICHECK_STRING_EQ(s, rfind, "man", 15); + MULTICHECK_STRING_EQ(s, rfind, "WOMAN", -1); + MULTICHECK_STRING_INT_EQ(s, rfind, "", 15, -1); } TEST_CASE("[String] Find no case") { String s = "Pretty Whale Whale"; - CHECK(s.findn("WHA") == 7); - CHECK(s.findn("WHA", 9) == 13); - CHECK(s.findn("Revenge of the Monster SawFish") == -1); - CHECK(s.rfindn("WHA") == 13); + MULTICHECK_STRING_EQ(s, findn, "WHA", 7); + MULTICHECK_STRING_INT_EQ(s, findn, "WHA", 9, 13); + MULTICHECK_STRING_EQ(s, findn, "Revenge of the Monster SawFish", -1); + MULTICHECK_STRING_EQ(s, findn, "", -1); + MULTICHECK_STRING_EQ(s, findn, "wha", 7); + MULTICHECK_STRING_EQ(s, findn, "Wha", 7); + MULTICHECK_STRING_INT_EQ(s, findn, "", 3, -1); + + MULTICHECK_STRING_EQ(s, rfindn, "WHA", 13); + MULTICHECK_STRING_EQ(s, rfindn, "", -1); + MULTICHECK_STRING_EQ(s, rfindn, "wha", 13); + MULTICHECK_STRING_EQ(s, rfindn, "Wha", 13); + MULTICHECK_STRING_INT_EQ(s, rfindn, "", 13, -1); } TEST_CASE("[String] Find MK") { @@ -392,11 +411,9 @@ TEST_CASE("[String] Find MK") { TEST_CASE("[String] Find and replace") { String s = "Happy Birthday, Anna!"; - s = s.replace("Birthday", "Halloween"); - CHECK(s == "Happy Halloween, Anna!"); - - s = s.replace_first("H", "W"); - CHECK(s == "Wappy Halloween, Anna!"); + MULTICHECK_STRING_STRING_EQ(s, replace, "Birthday", "Halloween", "Happy Halloween, Anna!"); + MULTICHECK_STRING_STRING_EQ(s, replace_first, "y", "Y", "HappY Birthday, Anna!"); + MULTICHECK_STRING_STRING_EQ(s, replacen, "Y", "Y", "HappY BirthdaY, Anna!"); } TEST_CASE("[String] Insertion") { @@ -557,51 +574,76 @@ TEST_CASE("[String] String to float") { TEST_CASE("[String] Slicing") { String s = "Mars,Jupiter,Saturn,Uranus"; - const char *slices[4] = { "Mars", "Jupiter", "Saturn", "Uranus" }; - for (int i = 0; i < s.get_slice_count(","); i++) { - CHECK(s.get_slice(",", i) == slices[i]); - } + MULTICHECK_GET_SLICE(s, ",", slices); +} + +TEST_CASE("[String] Begins with") { + // Test cases for true: + MULTICHECK_STRING_EQ(String("res://foobar"), begins_with, "res://", true); + MULTICHECK_STRING_EQ(String("abc"), begins_with, "abc", true); + MULTICHECK_STRING_EQ(String("abc"), begins_with, "", true); + MULTICHECK_STRING_EQ(String(""), begins_with, "", true); + + // Test cases for false: + MULTICHECK_STRING_EQ(String("res"), begins_with, "res://", false); + MULTICHECK_STRING_EQ(String("abcdef"), begins_with, "foo", false); + MULTICHECK_STRING_EQ(String("abc"), begins_with, "ax", false); + MULTICHECK_STRING_EQ(String(""), begins_with, "abc", false); + + // Test "const char *" version also with nullptr. + String s("foo"); + bool state = s.begins_with(nullptr) == false; + CHECK_MESSAGE(state, "nullptr check failed"); + + String empty(""); + state = empty.begins_with(nullptr) == false; + CHECK_MESSAGE(state, "nullptr check with empty string failed"); +} + +TEST_CASE("[String] Ends with") { + // Test cases for true: + MULTICHECK_STRING_EQ(String("res://foobar"), ends_with, "foobar", true); + MULTICHECK_STRING_EQ(String("abc"), ends_with, "abc", true); + MULTICHECK_STRING_EQ(String("abc"), ends_with, "", true); + MULTICHECK_STRING_EQ(String(""), ends_with, "", true); + + // Test cases for false: + MULTICHECK_STRING_EQ(String("res"), ends_with, "res://", false); + MULTICHECK_STRING_EQ(String("abcdef"), ends_with, "foo", false); + MULTICHECK_STRING_EQ(String("abc"), ends_with, "ax", false); + MULTICHECK_STRING_EQ(String(""), ends_with, "abc", false); + + // Test "const char *" version also with nullptr. + String s("foo"); + bool state = s.ends_with(nullptr) == false; + CHECK_MESSAGE(state, "nullptr check failed"); + + String empty(""); + state = empty.ends_with(nullptr) == false; + CHECK_MESSAGE(state, "nullptr check with empty string failed"); } TEST_CASE("[String] Splitting") { String s = "Mars,Jupiter,Saturn,Uranus"; - Vector l; - const char *slices_l[3] = { "Mars", "Jupiter", "Saturn,Uranus" }; - const char *slices_r[3] = { "Mars,Jupiter", "Saturn", "Uranus" }; - const char *slices_3[4] = { "t", "e", "s", "t" }; - - l = s.split(",", true, 2); - CHECK(l.size() == 3); - for (int i = 0; i < l.size(); i++) { - CHECK(l[i] == slices_l[i]); - } + MULTICHECK_SPLIT(s, split, ",", true, 2, slices_l, 3); - l = s.rsplit(",", true, 2); - CHECK(l.size() == 3); - for (int i = 0; i < l.size(); i++) { - CHECK(l[i] == slices_r[i]); - } + const char *slices_r[3] = { "Mars,Jupiter", "Saturn", "Uranus" }; + MULTICHECK_SPLIT(s, rsplit, ",", true, 2, slices_r, 3); s = "test"; - l = s.split(); - CHECK(l.size() == 4); - for (int i = 0; i < l.size(); i++) { - CHECK(l[i] == slices_3[i]); - } + const char *slices_3[4] = { "t", "e", "s", "t" }; + MULTICHECK_SPLIT(s, split, "", true, 0, slices_3, 4); s = ""; - l = s.split(); - CHECK(l.size() == 1); - CHECK(l[0] == ""); - - l = s.split("", false); - CHECK(l.size() == 0); + const char *slices_4[1] = { "" }; + MULTICHECK_SPLIT(s, split, "", true, 0, slices_4, 1); + MULTICHECK_SPLIT(s, split, "", false, 0, slices_4, 0); s = "Mars Jupiter Saturn Uranus"; const char *slices_s[4] = { "Mars", "Jupiter", "Saturn", "Uranus" }; - l = s.split_spaces(); + Vector l = s.split_spaces(); for (int i = 0; i < l.size(); i++) { CHECK(l[i] == slices_s[i]); } @@ -644,69 +686,6 @@ TEST_CASE("[String] Splitting") { } } -struct test_27_data { - char const *data; - char const *part; - bool expected; -}; - -TEST_CASE("[String] Begins with") { - test_27_data tc[] = { - // Test cases for true: - { "res://foobar", "res://", true }, - { "abc", "abc", true }, - { "abc", "", true }, - { "", "", true }, - // Test cases for false: - { "res", "res://", false }, - { "abcdef", "foo", false }, - { "abc", "ax", false }, - { "", "abc", false } - }; - size_t count = sizeof(tc) / sizeof(tc[0]); - bool state = true; - for (size_t i = 0; i < count; ++i) { - String s = tc[i].data; - state = s.begins_with(tc[i].part) == tc[i].expected; - CHECK_MESSAGE(state, "first check failed at: ", i); - - String sb = tc[i].part; - state = s.begins_with(sb) == tc[i].expected; - CHECK_MESSAGE(state, "second check failed at: ", i); - } - - // Test "const char *" version also with nullptr. - String s("foo"); - state = s.begins_with(nullptr) == false; - CHECK_MESSAGE(state, "nullptr check failed"); - - String empty(""); - state = empty.begins_with(nullptr) == false; - CHECK_MESSAGE(state, "nullptr check with empty string failed"); -} - -TEST_CASE("[String] Ends with") { - test_27_data tc[] = { - // test cases for true: - { "res://foobar", "foobar", true }, - { "abc", "abc", true }, - { "abc", "", true }, - { "", "", true }, - // test cases for false: - { "res", "res://", false }, - { "", "abc", false }, - { "abcdef", "foo", false }, - { "abc", "xc", false } - }; - size_t count = sizeof(tc) / sizeof(tc[0]); - for (size_t i = 0; i < count; ++i) { - String s = tc[i].data; - String sb = tc[i].part; - bool state = s.ends_with(sb) == tc[i].expected; - CHECK_MESSAGE(state, "check failed at: ", i); - } -} - TEST_CASE("[String] format") { const String value_format = "red=\"$red\" green=\"$green\" blue=\"$blue\" alpha=\"$alpha\""; @@ -1498,39 +1477,62 @@ TEST_CASE("[String] Cyrillic to_lower()") { } TEST_CASE("[String] Count and countn functionality") { -#define COUNT_TEST(x) \ - { \ - bool success = x; \ - state = state && success; \ - } + String s = String(""); + MULTICHECK_STRING_EQ(s, count, "Test", 0); - bool state = true; + s = "Test"; + MULTICHECK_STRING_EQ(s, count, "", 0); - COUNT_TEST(String("").count("Test") == 0); - COUNT_TEST(String("Test").count("") == 0); - COUNT_TEST(String("Test").count("test") == 0); - COUNT_TEST(String("Test").count("TEST") == 0); - COUNT_TEST(String("TEST").count("TEST") == 1); - COUNT_TEST(String("Test").count("Test") == 1); - COUNT_TEST(String("aTest").count("Test") == 1); - COUNT_TEST(String("Testa").count("Test") == 1); - COUNT_TEST(String("TestTestTest").count("Test") == 3); - COUNT_TEST(String("TestTestTest").count("TestTest") == 1); - COUNT_TEST(String("TestGodotTestGodotTestGodot").count("Test") == 3); - - COUNT_TEST(String("TestTestTestTest").count("Test", 4, 8) == 1); - COUNT_TEST(String("TestTestTestTest").count("Test", 4, 12) == 2); - COUNT_TEST(String("TestTestTestTest").count("Test", 4, 16) == 3); - COUNT_TEST(String("TestTestTestTest").count("Test", 4) == 3); - - COUNT_TEST(String("Test").countn("test") == 1); - COUNT_TEST(String("Test").countn("TEST") == 1); - COUNT_TEST(String("testTest-Testatest").countn("tEst") == 4); - COUNT_TEST(String("testTest-TeStatest").countn("tEsT", 4, 16) == 2); + s = "Test"; + MULTICHECK_STRING_EQ(s, count, "test", 0); - CHECK(state); + s = "Test"; + MULTICHECK_STRING_EQ(s, count, "TEST", 0); + + s = "TEST"; + MULTICHECK_STRING_EQ(s, count, "TEST", 1); + + s = "Test"; + MULTICHECK_STRING_EQ(s, count, "Test", 1); + + s = "aTest"; + MULTICHECK_STRING_EQ(s, count, "Test", 1); + + s = "Testa"; + MULTICHECK_STRING_EQ(s, count, "Test", 1); + + s = "TestTestTest"; + MULTICHECK_STRING_EQ(s, count, "Test", 3); + + s = "TestTestTest"; + MULTICHECK_STRING_EQ(s, count, "TestTest", 1); + + s = "TestGodotTestGodotTestGodot"; + MULTICHECK_STRING_EQ(s, count, "Test", 3); + + s = "TestTestTestTest"; + MULTICHECK_STRING_INT_INT_EQ(s, count, "Test", 4, 8, 1); + + s = "TestTestTestTest"; + MULTICHECK_STRING_INT_INT_EQ(s, count, "Test", 4, 12, 2); + + s = "TestTestTestTest"; + MULTICHECK_STRING_INT_INT_EQ(s, count, "Test", 4, 16, 3); + + s = "TestTestTestTest"; + MULTICHECK_STRING_INT_EQ(s, count, "Test", 4, 3); + + s = "Test"; + MULTICHECK_STRING_EQ(s, countn, "test", 1); + + s = "Test"; + MULTICHECK_STRING_EQ(s, countn, "TEST", 1); + + s = "testTest-Testatest"; + MULTICHECK_STRING_EQ(s, countn, "tEst", 4); -#undef COUNT_TEST + s = "testTest-TeStatest"; + MULTICHECK_STRING_INT_INT_EQ(s, countn, "tEsT", 4, 16, 2); } TEST_CASE("[String] Bigrams") { @@ -1703,9 +1705,19 @@ TEST_CASE("[String] Strip edges") { TEST_CASE("[String] Trim") { String s = "aaaTestbbb"; - CHECK(s.trim_prefix("aaa") == "Testbbb"); - CHECK(s.trim_suffix("bbb") == "aaaTest"); - CHECK(s.trim_suffix("Test") == s); + MULTICHECK_STRING_EQ(s, trim_prefix, "aaa", "Testbbb"); + MULTICHECK_STRING_EQ(s, trim_prefix, "Test", s); + MULTICHECK_STRING_EQ(s, trim_prefix, "", s); + MULTICHECK_STRING_EQ(s, trim_prefix, "aaaTestbbb", ""); + MULTICHECK_STRING_EQ(s, trim_prefix, "bbb", s); + MULTICHECK_STRING_EQ(s, trim_prefix, "AAA", s); + + MULTICHECK_STRING_EQ(s, trim_suffix, "bbb", "aaaTest"); + MULTICHECK_STRING_EQ(s, trim_suffix, "Test", s); + MULTICHECK_STRING_EQ(s, trim_suffix, "", s); + MULTICHECK_STRING_EQ(s, trim_suffix, "aaaTestbbb", ""); + MULTICHECK_STRING_EQ(s, trim_suffix, "aaa", s); + MULTICHECK_STRING_EQ(s, trim_suffix, "BBB", s); } TEST_CASE("[String] Right/Left") { diff --git a/tests/core/templates/test_local_vector.h b/tests/core/templates/test_local_vector.h index 2873a9a028ee..c9544c625bea 100644 --- a/tests/core/templates/test_local_vector.h +++ b/tests/core/templates/test_local_vector.h @@ -63,7 +63,7 @@ TEST_CASE("[LocalVector] Push Back.") { CHECK(vector[4] == 4); } -TEST_CASE("[LocalVector] Find.") { +TEST_CASE("[LocalVector] Find, has.") { LocalVector vector; vector.push_back(3); vector.push_back(1); @@ -85,6 +85,15 @@ TEST_CASE("[LocalVector] Find.") { CHECK(vector.find(-1) == -1); CHECK(vector.find(5) == -1); + + CHECK(vector.has(0)); + CHECK(vector.has(1)); + CHECK(vector.has(2)); + CHECK(vector.has(3)); + CHECK(vector.has(4)); + + CHECK(!vector.has(-1)); + CHECK(!vector.has(5)); } TEST_CASE("[LocalVector] Remove.") { diff --git a/tests/core/variant/test_dictionary.h b/tests/core/variant/test_dictionary.h index 5bc56075da1f..aba20972d9b3 100644 --- a/tests/core/variant/test_dictionary.h +++ b/tests/core/variant/test_dictionary.h @@ -105,7 +105,7 @@ TEST_CASE("[Dictionary] get_key_lists()") { map[1] = 3; map.get_key_list(ptr); CHECK(keys.size() == 1); - CHECK(int(keys[0]) == 1); + CHECK(int(keys.front()->get()) == 1); map[2] = 4; map.get_key_list(ptr); CHECK(keys.size() == 3); diff --git a/tests/core/variant/test_variant.h b/tests/core/variant/test_variant.h index 54ca06c6c426..be615975f889 100644 --- a/tests/core/variant/test_variant.h +++ b/tests/core/variant/test_variant.h @@ -2034,6 +2034,10 @@ TEST_CASE("[Variant] Identity comparison") { CHECK(packed_color_array.identity_compare(packed_color_array)); CHECK_FALSE(packed_color_array.identity_compare(PackedColorArray())); + Variant packed_vector4_array = PackedVector4Array(); + CHECK(packed_vector4_array.identity_compare(packed_vector4_array)); + CHECK_FALSE(packed_vector4_array.identity_compare(PackedVector4Array())); + Variant packed_float32_array = PackedFloat32Array(); CHECK(packed_float32_array.identity_compare(packed_float32_array)); CHECK_FALSE(packed_float32_array.identity_compare(PackedFloat32Array())); diff --git a/tests/display_server_mock.h b/tests/display_server_mock.h index 8d8a678e2009..fd79a46c5ce2 100644 --- a/tests/display_server_mock.h +++ b/tests/display_server_mock.h @@ -47,6 +47,9 @@ class DisplayServerMock : public DisplayServerHeadless { Callable event_callback; Callable input_event_callback; + String clipboard_text; + String primary_clipboard_text; + static Vector get_rendering_drivers_func() { Vector drivers; drivers.push_back("dummy"); @@ -86,7 +89,7 @@ class DisplayServerMock : public DisplayServerHeadless { } 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 @@ class DisplayServerMock : public DisplayServerHeadless { switch (p_feature) { case FEATURE_MOUSE: case FEATURE_CURSOR_SHAPE: + case FEATURE_CLIPBOARD: + case FEATURE_CLIPBOARD_PRIMARY: return true; default: { } @@ -131,6 +136,11 @@ class DisplayServerMock : public DisplayServerHeadless { 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 b0a46b8107aa..c02830b6df9e 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 +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_sprite_frames.h b/tests/scene/test_sprite_frames.h index bf127cd42c02..55854b90e474 100644 --- a/tests/scene/test_sprite_frames.h +++ b/tests/scene/test_sprite_frames.h @@ -74,9 +74,10 @@ TEST_CASE("[SpriteFrames] Animation addition, list getter, renaming, removal, an sname_list.size() == test_names.size(), "StringName List getter returned list of expected size"); - for (int i = 0; i < test_names.size(); i++) { + int idx = 0; + for (List::ConstIterator itr = sname_list.begin(); itr != sname_list.end(); ++itr, ++idx) { CHECK_MESSAGE( - sname_list[i] == StringName(test_names[i]), + *itr == StringName(test_names[idx]), "StringName List getter returned expected values"); } diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 8577dd714834..b2d9f5100e81 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 +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,55 +625,118 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("text_set"); - ERR_PRINT_ON; - } - SUBCASE("[TextEdit] swap lines") { - ((Array)lines_edited_args[1])[1] = 1; + // 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_text("testing\nswap"); + text_edit->set_line(0, "test"); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "testing\nswap"); - SIGNAL_CHECK("text_set", empty_signal_args); + 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_changed"); + SIGNAL_CHECK_FALSE("text_set"); - text_edit->set_caret_column(text_edit->get_line(0).length()); + // 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_CHECK("caret_changed", empty_signal_args); - - ((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); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); - // Order does not matter. Should also work if not editable. - text_edit->set_editable(false); - text_edit->swap_lines(1, 0); + text_edit->set_line(0, "\tset line"); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "swap\ntesting"); + 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("caret_changed", empty_signal_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); + text_edit->remove_secondary_carets(); - lines_edited_args.reverse(); + // 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)); - // Single undo/redo action - text_edit->undo(); + text_edit->set_line(0, "multiple\nlines"); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "testing\nswap"); - SIGNAL_CHECK("lines_edited_from", lines_edited_args); + 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") { + lines_edited_args = build_array(build_array(0, 0), build_array(0, 1)); + + text_edit->set_text("testing\nswap"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nswap"); + 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"); + + 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)); + + // Order does not matter. Works when not editable. + text_edit->set_editable(false); + text_edit->swap_lines(1, 0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "swap\ntesting"); + 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); - lines_edited_args.reverse(); + // Single undo/redo action. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nswap"); + 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"); text_edit->redo(); MessageQueue::get_singleton()->flush(); @@ -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,58 +1661,134 @@ 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()); + + // 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()); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // 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); - } - - SUBCASE("[TextEdit] mouse word select") { - /* Set size for mouse input. */ - text_edit->set_size(Size2(200, 200)); - text_edit->set_text("this is some text\nfor selection"); + // 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(); - SIGNAL_DISCARD("caret_changed"); - SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE); - CHECK(text_edit->has_selection()); - CHECK(text_edit->get_selected_text() == "for"); - CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); - 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() == 3); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 3); - SIGNAL_CHECK("caret_changed", empty_signal_args); + 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); - SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE); + 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. + text_edit->set_size(Size2(200, 200)); + + text_edit->set_text("this is some text\nfor selection\n"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + // 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); + 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() == 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); + + // 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()); + + // 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"); - 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); + // 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()); + + // 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()); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // 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,53 +2284,457 @@ 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"); - } - // Add readonly test? - SUBCASE("[TextEdit] text drag") { - TextEdit *target_text_edit = memnew(TextEdit); - SceneTree::get_singleton()->get_root()->add_child(target_text_edit); + // 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); - target_text_edit->set_size(Size2(200, 200)); - target_text_edit->set_position(Point2(400, 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); + } + SUBCASE("[TextEdit] text drag") { text_edit->set_size(Size2(200, 200)); - - CHECK_FALSE(text_edit->is_mouse_over_selection()); - text_edit->set_text("drag me"); - text_edit->select_all(); + text_edit->set_text("drag test\ndrop here ''"); text_edit->grab_click_focus(); 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 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_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() + 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 me"); + 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); - 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); - CHECK(text_edit->get_viewport()->gui_is_dragging()); + // 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); - SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + // 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() == ""); - CHECK(target_text_edit->get_text() == "drag me"); - + 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); + + target_text_edit->set_size(Size2(200, 200)); + target_text_edit->set_position(Point2(400, 0)); + + text_edit->set_size(Size2(200, 200)); + + CHECK_FALSE(text_edit->is_mouse_over_selection()); + 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(); + + // 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_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); + + 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); + + // 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); + + 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,193 +2831,874 @@ 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"); - } - - SUBCASE("[TextEdit] copy") { - // TODO: Cannot test need display server support. - } - - SUBCASE("[TextEdit] paste") { - // TODO: Cannot test need display server support. - } - - SUBCASE("[TextEdit] paste primary") { - // TODO: Cannot test need display server support. - } - - SIGNAL_UNWATCH(text_edit, "text_set"); - SIGNAL_UNWATCH(text_edit, "text_changed"); - SIGNAL_UNWATCH(text_edit, "lines_edited_from"); - 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); - - 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); + // 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)); - // For the second caret. - Array args2; - args2.push_back(0); - args2.push_back(1); - lines_edited_args.push_front(args2); - - ((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); + 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(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) == 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(); - 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_column(4, false, 1); + // 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->set_editable(false); - 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)); + 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); - CHECK(text_edit->get_caret_line(1) == 3); + // 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") { + 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") { + // 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") { + // 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"); + SIGNAL_UNWATCH(text_edit, "text_changed"); + SIGNAL_UNWATCH(text_edit, "lines_edited_from"); + SIGNAL_UNWATCH(text_edit, "caret_changed"); + } + + 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 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."); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // 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)); + + 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_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, -1, 1); + text_edit->set_caret_column(4, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + 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_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); 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 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(1) == 1); - CHECK(text_edit->get_caret_column(1) == 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); - text_edit->set_caret_column(text_edit->get_line(0).length()); - text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + // 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)); - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); + 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); + 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("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); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + // Remove selected text when there are selections. + text_edit->select(1, 0, 1, 4); text_edit->add_caret(3, 4); - text_edit->select(3, 0, 3, 4, 1); + 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\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) == 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); + // 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)); - 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 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(15, false, 1); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(3); - args2.push_back(3); - lines_edited_args.push_front(args2); + 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.\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) == 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::ACTION_NONE); - // With selection should be a normal backspace. - ((Array)lines_edited_args[1])[0] = 1; - ((Array)lines_edited_args[1])[1] = 1; + // 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() == "\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() == " 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(1) == 3); - CHECK(text_edit->get_caret_column(1) == 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); - ((Array)lines_edited_args[0])[1] = 2; - ((Array)lines_edited_args[1])[1] = 0; - - // Start of line should also be a normal backspace. + // 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 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 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) == 0); + 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); - text_edit->set_caret_column(text_edit->get_line(0).length()); - text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + // Backspace with multiple carets that will overlap. + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(8); + text_edit->add_caret(0, 7); + text_edit->add_caret(0, 9); 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(0, 0), build_array(0, 0)); - 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_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() == text_edit->get_line(0).length()); - 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_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); + 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); - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; - ((Array)lines_edited_args[1])[0] = 0; + // 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_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() == "\n"); + 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) == 18); + 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) == 0); + SIGNAL_CHECK_FALSE("caret_changed"); 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 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); - - 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); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); 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); - 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); + 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,108 +4693,250 @@ 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->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"); + + // 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)); + + SEND_GUI_ACTION("ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + 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(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); + + // 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("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() == "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()); + 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(10, false, 1); + MessageQueue::get_singleton()->flush(); + 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() == "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) == 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() == " 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) == 10); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); - text_edit->add_caret(2, 4); - text_edit->select(2, 0, 2, 4, 1); + // 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); + // 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"); - // For the second caret. - Array args2; - args2.push_back(2); - args2.push_back(2); - lines_edited_args.push_front(args2); - - // 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() == "one test"); + CHECK(text_edit->get_caret_count() == 1); CHECK_FALSE(text_edit->has_selection(0)); - - 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_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); - // With selection should be a normal delete. - ((Array)lines_edited_args[0])[0] = 3; - ((Array)lines_edited_args[1])[0] = 1; - 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"); + // 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(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() == "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(); - 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); + // 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); - ((Array)lines_edited_args[1])[0] = 0; - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; - text_edit->set_caret_column(0); - text_edit->set_caret_column(0, false, 1); - MessageQueue::get_singleton()->flush(); + 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"); - text_edit->set_editable(false); + // 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)); + 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)); - SIGNAL_CHECK_FALSE("caret_changed"); - SIGNAL_CHECK_FALSE("text_changed"); - SIGNAL_CHECK_FALSE("lines_edited_from"); - text_edit->set_editable(true); + 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); - 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); + 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)); - CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + 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)); - SIGNAL_CHECK_FALSE("caret_changed"); + 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); } @@ -2320,192 +4945,337 @@ 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.\nthis ffi some test text."); - 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); - + 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(1); - args2.push_back(1); - lines_edited_args.push_front(args2); + // 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)); - // With selection should be a normal delete. SEND_GUI_ACTION("ui_text_delete"); 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() == " 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); - CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + // 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)); + + // 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); - // 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); + // 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("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"); 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.\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); - // 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 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)); - ((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 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); - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); + // 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() == " 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(1) == 0); - CHECK(text_edit->get_caret_column(1) == 20); + 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); + + // 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)); + 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); + // Undo both deletes. + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1), build_array(0, 0), build_array(1, 1)); + + 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)); - 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", reverse_nested(lines_edited_args)); + + // 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)); + 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); + // 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(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"); + + // 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_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)); - 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); - CHECK_FALSE(text_edit->has_selection(0)); + 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)); - CHECK(text_edit->get_caret_line(1) == 0); - CHECK(text_edit->get_caret_column(1) == 18); - 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); - } + 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); + // Select to start of word with shift. + text_edit->deselect(); text_edit->set_caret_column(7); - - text_edit->add_caret(2, 7); - CHECK(text_edit->get_caret_count() == 2); + text_edit->set_caret_column(16, false, 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. #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) == "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->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) == "tes"); + CHECK(text_edit->get_caret_line(1) == 2); + 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"); - // Should still move caret with selection. + // 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() == 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) == 2); - 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) == 8); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Normal word left. + // 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_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"); @@ -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 caret_index_get_order; - caret_index_get_order.push_back(1); - caret_index_get_order.push_back(0); + SUBCASE("[TextEdit] sort carets") { + Vector 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(); + + // 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(); - 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); + // 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,325 @@ 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_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_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) == 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); + + // Does nothing if multiple carets are disabled. + text_edit->set_multiple_carets_enabled(false); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 1); } memdelete(text_edit); @@ -3992,7 +7493,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 +7743,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/scene/test_timer.h b/tests/scene/test_timer.h new file mode 100644 index 000000000000..913ed92de505 --- /dev/null +++ b/tests/scene/test_timer.h @@ -0,0 +1,217 @@ +/**************************************************************************/ +/* test_timer.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_TIMER_H +#define TEST_TIMER_H + +#include "scene/main/timer.h" + +#include "tests/test_macros.h" + +namespace TestTimer { + +TEST_CASE("[SceneTree][Timer] Check Timer Setters and Getters") { + Timer *test_timer = memnew(Timer); + + SUBCASE("[Timer] Timer set and get wait time") { + // check default + CHECK(Math::is_equal_approx(test_timer->get_wait_time(), 1.0)); + + test_timer->set_wait_time(50.0); + CHECK(Math::is_equal_approx(test_timer->get_wait_time(), 50.0)); + + test_timer->set_wait_time(42.0); + CHECK(Math::is_equal_approx(test_timer->get_wait_time(), 42.0)); + + // wait time remains unchanged if we attempt to set it negative or zero + ERR_PRINT_OFF; + test_timer->set_wait_time(-22.0); + ERR_PRINT_ON; + CHECK(Math::is_equal_approx(test_timer->get_wait_time(), 42.0)); + + ERR_PRINT_OFF; + test_timer->set_wait_time(0.0); + ERR_PRINT_ON; + CHECK(Math::is_equal_approx(test_timer->get_wait_time(), 42.0)); + } + + SUBCASE("[Timer] Timer set and get one shot") { + // check default + CHECK(test_timer->is_one_shot() == false); + + test_timer->set_one_shot(true); + CHECK(test_timer->is_one_shot() == true); + + test_timer->set_one_shot(false); + CHECK(test_timer->is_one_shot() == false); + } + + SUBCASE("[Timer] Timer set and get autostart") { + // check default + CHECK(test_timer->has_autostart() == false); + + test_timer->set_autostart(true); + CHECK(test_timer->has_autostart() == true); + + test_timer->set_autostart(false); + CHECK(test_timer->has_autostart() == false); + } + + SUBCASE("[Timer] Timer start and stop") { + test_timer->set_autostart(false); + } + + SUBCASE("[Timer] Timer set and get paused") { + // check default + CHECK(test_timer->is_paused() == false); + + test_timer->set_paused(true); + CHECK(test_timer->is_paused() == true); + + test_timer->set_paused(false); + CHECK(test_timer->is_paused() == false); + } + + memdelete(test_timer); +} + +TEST_CASE("[SceneTree][Timer] Check Timer Start and Stop") { + Timer *test_timer = memnew(Timer); + + SUBCASE("[Timer] Timer start and stop") { + SceneTree::get_singleton()->get_root()->add_child(test_timer); + + test_timer->start(5.0); + + CHECK(Math::is_equal_approx(test_timer->get_wait_time(), 5.0)); + CHECK(Math::is_equal_approx(test_timer->get_time_left(), 5.0)); + + test_timer->start(-2.0); + + // the wait time and time left remains unchanged when started with a negative start time + CHECK(Math::is_equal_approx(test_timer->get_wait_time(), 5.0)); + CHECK(Math::is_equal_approx(test_timer->get_time_left(), 5.0)); + + test_timer->stop(); + CHECK(test_timer->is_processing() == false); + CHECK(test_timer->has_autostart() == false); + } + + memdelete(test_timer); +} + +TEST_CASE("[SceneTree][Timer] Check Timer process callback") { + Timer *test_timer = memnew(Timer); + + SUBCASE("[Timer] Timer process callback") { + // check default + CHECK(test_timer->get_timer_process_callback() == Timer::TimerProcessCallback::TIMER_PROCESS_IDLE); + + test_timer->set_timer_process_callback(Timer::TimerProcessCallback::TIMER_PROCESS_PHYSICS); + CHECK(test_timer->get_timer_process_callback() == Timer::TimerProcessCallback::TIMER_PROCESS_PHYSICS); + + test_timer->set_timer_process_callback(Timer::TimerProcessCallback::TIMER_PROCESS_IDLE); + CHECK(test_timer->get_timer_process_callback() == Timer::TimerProcessCallback::TIMER_PROCESS_IDLE); + } + + memdelete(test_timer); +} + +TEST_CASE("[SceneTree][Timer] Check Timer timeout signal") { + Timer *test_timer = memnew(Timer); + SceneTree::get_singleton()->get_root()->add_child(test_timer); + + test_timer->set_process(true); + test_timer->set_physics_process(true); + + SUBCASE("[Timer] Timer process timeout signal must be emitted") { + SIGNAL_WATCH(test_timer, SNAME("timeout")); + test_timer->start(0.1); + + SceneTree::get_singleton()->process(0.2); + + Array signal_args; + signal_args.push_back(Array()); + + SIGNAL_CHECK(SNAME("timeout"), signal_args); + + SIGNAL_UNWATCH(test_timer, SNAME("timeout")); + } + + SUBCASE("[Timer] Timer process timeout signal must not be emitted") { + SIGNAL_WATCH(test_timer, SNAME("timeout")); + test_timer->start(0.1); + + SceneTree::get_singleton()->process(0.05); + + Array signal_args; + signal_args.push_back(Array()); + + SIGNAL_CHECK_FALSE(SNAME("timeout")); + + SIGNAL_UNWATCH(test_timer, SNAME("timeout")); + } + + test_timer->set_timer_process_callback(Timer::TimerProcessCallback::TIMER_PROCESS_PHYSICS); + + SUBCASE("[Timer] Timer physics process timeout signal must be emitted") { + SIGNAL_WATCH(test_timer, SNAME("timeout")); + test_timer->start(0.1); + + SceneTree::get_singleton()->physics_process(0.2); + + Array signal_args; + signal_args.push_back(Array()); + + SIGNAL_CHECK(SNAME("timeout"), signal_args); + + SIGNAL_UNWATCH(test_timer, SNAME("timeout")); + } + + SUBCASE("[Timer] Timer physics process timeout signal must not be emitted") { + SIGNAL_WATCH(test_timer, SNAME("timeout")); + test_timer->start(0.1); + + SceneTree::get_singleton()->physics_process(0.05); + + Array signal_args; + signal_args.push_back(Array()); + + SIGNAL_CHECK_FALSE(SNAME("timeout")); + + SIGNAL_UNWATCH(test_timer, SNAME("timeout")); + } + + memdelete(test_timer); +} + +} // namespace TestTimer + +#endif // TEST_TIMER_H diff --git a/tests/servers/test_text_server.h b/tests/servers/test_text_server.h index 334c642d26d9..d982102a0383 100644 --- a/tests/servers/test_text_server.h +++ b/tests/servers/test_text_server.h @@ -637,6 +637,97 @@ TEST_SUITE("[TextServer]") { } } + SUBCASE("[TextServer] Unicode letters") { + for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) { + Ref ts = TextServerManager::get_singleton()->get_interface(i); + CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface."); + + struct ul_testcase { + int fail_index = -1; // Expecting failure at given index. + char32_t text[10]; // Using 0 as the terminator. + }; + ul_testcase cases[14] = { + { + 0, + { 0x2D, 0x33, 0x30, 0, 0, 0, 0, 0, 0, 0 }, // "-30" + }, + { + 1, + { 0x61, 0x2E, 0x31, 0, 0, 0, 0, 0, 0, 0 }, // "a.1" + }, + { + 1, + { 0x61, 0x2C, 0x31, 0, 0, 0, 0, 0, 0, 0 }, // "a,1" + }, + { + 0, + { 0x31, 0x65, 0x2D, 0x32, 0, 0, 0, 0, 0, 0 }, // "1e-2" + }, + { + 0, + { 0xAB, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // "Left-Pointing Double Angle Quotation Mark" + }, + { + -1, + { 0x41, 0x42, 0, 0, 0, 0, 0, 0, 0, 0 }, // "AB" + }, + { + 4, + { 0x54, 0x65, 0x73, 0x74, 0x31, 0, 0, 0, 0, 0 }, // "Test1" + }, + { + 2, + { 0x54, 0x65, 0x2A, 0x73, 0x74, 0, 0, 0, 0, 0 }, // "Te*st" + }, + { + 4, + { 0x74, 0x65, 0x73, 0x74, 0x5F, 0x74, 0x65, 0x73, 0x74, 0x65 }, // "test_teste" + }, + { + 4, + { 0x74, 0x65, 0x73, 0x74, 0x20, 0x74, 0x65, 0x73, 0x74, 0 }, // "test test" + }, + { + -1, + { 0x643, 0x402, 0x716, 0xB05, 0, 0, 0, 0, 0, 0 }, // "كЂܖଅ" (arabic letters), + }, + { + -1, + { 0x643, 0x402, 0x716, 0xB05, 0x54, 0x65, 0x73, 0x74, 0x30AA, 0x4E21 }, // 0-3 arabic letters, 4-7 latin letters, 8-9 CJK letters + }, + { + -1, + { 0x4D2, 0x4D6, 0x4DA, 0x4DC, 0, 0, 0, 0, 0, 0 }, // "ӒӖӚӜ" cyrillic letters + }, + { + -1, + { 0xC2, 0xC3, 0xC4, 0xC5, 0x100, 0x102, 0x104, 0xC7, 0x106, 0x108 }, // "ÂÃÄÅĀĂĄÇĆĈ" rarer latin letters + }, + }; + + for (int j = 0; j < 14; j++) { + ul_testcase test = cases[j]; + int failed_on_index = -1; + for (int k = 0; k < 10; k++) { + char32_t character = test.text[k]; + if (character == 0) { + break; + } + if (!ts->is_valid_letter(character)) { + failed_on_index = k; + break; + } + } + + if (test.fail_index == -1) { + CHECK_MESSAGE(test.fail_index == failed_on_index, "In interface ", ts->get_name() + ": In test case ", j, ", the character at index ", failed_on_index, " should have been a letter."); + } else { + CHECK_MESSAGE(test.fail_index == failed_on_index, "In interface ", ts->get_name() + ": In test case ", j, ", expected first non-letter at index ", test.fail_index, ", but found at index ", failed_on_index); + } + } + } + } + SUBCASE("[TextServer] Strip Diacritics") { for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) { Ref ts = TextServerManager::get_singleton()->get_interface(i); diff --git a/tests/test_macros.h b/tests/test_macros.h index a173b37a2de2..10f4c59a9085 100644 --- a/tests/test_macros.h +++ b/tests/test_macros.h @@ -119,6 +119,7 @@ DOCTEST_STRINGIFY_VARIANT(PackedStringArray); DOCTEST_STRINGIFY_VARIANT(PackedVector2Array); DOCTEST_STRINGIFY_VARIANT(PackedVector3Array); DOCTEST_STRINGIFY_VARIANT(PackedColorArray); +DOCTEST_STRINGIFY_VARIANT(PackedVector4Array); // Register test commands to be launched from the command-line. // For instance: REGISTER_TEST_COMMAND("gdscript-parser" &test_parser_func). @@ -136,6 +137,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 +163,14 @@ int register_test_command(String p_command, TestFunc p_function); MessageQueue::get_singleton()->flush(); \ } +#define SEND_GUI_KEY_UP_EVENT(m_input) \ + { \ + Ref 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); \ @@ -396,4 +406,71 @@ class SignalWatcher : public Object { #define SIGNAL_CHECK_FALSE(m_signal) CHECK(SignalWatcher::get_singleton()->check_false(m_signal)); #define SIGNAL_DISCARD(m_signal) SignalWatcher::get_singleton()->discard_signal(m_signal); +#define MULTICHECK_STRING_EQ(m_obj, m_func, m_param1, m_eq) \ + CHECK(m_obj.m_func(m_param1) == m_eq); \ + CHECK(m_obj.m_func(U##m_param1) == m_eq); \ + CHECK(m_obj.m_func(L##m_param1) == m_eq); \ + CHECK(m_obj.m_func(String(m_param1)) == m_eq); + +#define MULTICHECK_STRING_INT_EQ(m_obj, m_func, m_param1, m_param2, m_eq) \ + CHECK(m_obj.m_func(m_param1, m_param2) == m_eq); \ + CHECK(m_obj.m_func(U##m_param1, m_param2) == m_eq); \ + CHECK(m_obj.m_func(L##m_param1, m_param2) == m_eq); \ + CHECK(m_obj.m_func(String(m_param1), m_param2) == m_eq); + +#define MULTICHECK_STRING_INT_INT_EQ(m_obj, m_func, m_param1, m_param2, m_param3, m_eq) \ + CHECK(m_obj.m_func(m_param1, m_param2, m_param3) == m_eq); \ + CHECK(m_obj.m_func(U##m_param1, m_param2, m_param3) == m_eq); \ + CHECK(m_obj.m_func(L##m_param1, m_param2, m_param3) == m_eq); \ + CHECK(m_obj.m_func(String(m_param1), m_param2, m_param3) == m_eq); + +#define MULTICHECK_STRING_STRING_EQ(m_obj, m_func, m_param1, m_param2, m_eq) \ + CHECK(m_obj.m_func(m_param1, m_param2) == m_eq); \ + CHECK(m_obj.m_func(U##m_param1, U##m_param2) == m_eq); \ + CHECK(m_obj.m_func(L##m_param1, L##m_param2) == m_eq); \ + CHECK(m_obj.m_func(String(m_param1), String(m_param2)) == m_eq); + +#define MULTICHECK_GET_SLICE(m_obj, m_param1, m_slices) \ + for (int i = 0; i < m_obj.get_slice_count(m_param1); ++i) { \ + CHECK(m_obj.get_slice(m_param1, i) == m_slices[i]); \ + } \ + for (int i = 0; i < m_obj.get_slice_count(U##m_param1); ++i) { \ + CHECK(m_obj.get_slice(U##m_param1, i) == m_slices[i]); \ + } \ + for (int i = 0; i < m_obj.get_slice_count(L##m_param1); ++i) { \ + CHECK(m_obj.get_slice(L##m_param1, i) == m_slices[i]); \ + } \ + for (int i = 0; i < m_obj.get_slice_count(String(m_param1)); ++i) { \ + CHECK(m_obj.get_slice(String(m_param1), i) == m_slices[i]); \ + } + +#define MULTICHECK_SPLIT(m_obj, m_func, m_param1, m_param2, m_param3, m_slices, m_expected_size) \ + do { \ + Vector string_list; \ + \ + string_list = m_obj.m_func(m_param1, m_param2, m_param3); \ + CHECK(m_expected_size == string_list.size()); \ + for (int i = 0; i < string_list.size(); ++i) { \ + CHECK(string_list[i] == m_slices[i]); \ + } \ + \ + string_list = m_obj.m_func(U##m_param1, m_param2, m_param3); \ + CHECK(m_expected_size == string_list.size()); \ + for (int i = 0; i < string_list.size(); ++i) { \ + CHECK(string_list[i] == m_slices[i]); \ + } \ + \ + string_list = m_obj.m_func(L##m_param1, m_param2, m_param3); \ + CHECK(m_expected_size == string_list.size()); \ + for (int i = 0; i < string_list.size(); ++i) { \ + CHECK(string_list[i] == m_slices[i]); \ + } \ + \ + string_list = m_obj.m_func(String(m_param1), m_param2, m_param3); \ + CHECK(m_expected_size == string_list.size()); \ + for (int i = 0; i < string_list.size(); ++i) { \ + CHECK(string_list[i] == m_slices[i]); \ + } \ + } while (0) + #endif // TEST_MACROS_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 56bd8739c6c6..69d8113e6476 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -118,6 +118,7 @@ #include "tests/scene/test_sprite_frames.h" #include "tests/scene/test_text_edit.h" #include "tests/scene/test_theme.h" +#include "tests/scene/test_timer.h" #include "tests/scene/test_viewport.h" #include "tests/scene/test_visual_shader.h" #include "tests/scene/test_window.h" @@ -187,7 +188,7 @@ int test_main(int argc, char *argv[]) { } // Doctest runner. doctest::Context test_context; - List test_args; + LocalVector test_args; // Clean arguments of "--test" from the args. for (int x = 0; x < argc; x++) { @@ -200,7 +201,7 @@ int test_main(int argc, char *argv[]) { if (test_args.size() > 0) { // Convert Godot command line arguments back to standard arguments. char **doctest_args = new char *[test_args.size()]; - for (int x = 0; x < test_args.size(); x++) { + for (uint32_t x = 0; x < test_args.size(); x++) { // Operation to convert Godot string to non wchar string. CharString cs = test_args[x].utf8(); const char *str = cs.get_data(); @@ -212,7 +213,7 @@ int test_main(int argc, char *argv[]) { test_context.applyCommandLine(test_args.size(), doctest_args); - for (int x = 0; x < test_args.size(); x++) { + for (uint32_t x = 0; x < test_args.size(); x++) { delete[] doctest_args[x]; } delete[] doctest_args; diff --git a/tests/test_validate_testing.h b/tests/test_validate_testing.h index 4673d42b5a27..f2e3bf3bb451 100644 --- a/tests/test_validate_testing.h +++ b/tests/test_validate_testing.h @@ -181,6 +181,13 @@ TEST_SUITE("Validate tests") { color_arr.push_back(Color(2, 2, 2)); INFO(color_arr); + PackedVector4Array vec4_arr; + vec4_arr.push_back(Vector4(0, 0, 0, 0)); + vec4_arr.push_back(Vector4(1, 1, 1, 1)); + vec4_arr.push_back(Vector4(2, 2, 2, 2)); + vec4_arr.push_back(Vector4(3, 3, 3, 3)); + INFO(vec4_arr); + // doctest string concatenation. CHECK_MESSAGE(true, var, " ", vec2, " ", rect2, " ", color); } diff --git a/thirdparty/README.md b/thirdparty/README.md index 4c44a9b6f61d..255915b655f8 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) @@ -877,7 +894,7 @@ number and run the script. ## ufbx - Upstream: https://github.com/ufbx/ufbx -- Version: git (v0.11.1, 2024) +- Version: 0.14.0 (80ff790ab36507b99ec7e4ef55b9cfb076ce821b, 2024) - License: MIT Files extracted from upstream source: diff --git a/thirdparty/clipper2/include/clipper2/clipper.core.h b/thirdparty/clipper2/include/clipper2/clipper.core.h index a77cdad5f40f..0de7c3720e05 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.core.h +++ b/thirdparty/clipper2/include/clipper2/clipper.core.h @@ -138,7 +138,7 @@ namespace Clipper2Lib } template - explicit Point(const Point& p) + explicit Point(const Point& 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 - explicit Point(const Point& p) { Init(p.x, p.y); } + explicit Point(const Point& 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 000000000000..a4f06ef37ead --- /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 +- explicit Point(const Point& p) ++ explicit Point(const Point& 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 +- explicit Point(const Point& p) { Init(p.x, p.y); } ++ explicit Point(const Point& 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 6786afd90b68..99362a23deea 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 38ecb514bd82..ee0cc188fc80 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 +#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 #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 053c5853a709..1bf35c1404a5 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 1301d10b6500..307ea4dbb8e3 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 a2fa0dadeeb1..cf7663a3afba 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 88c55101669d..a98efe51d8e0 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 000000000000..097d2774e5df --- /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 000000000000..ff2c8677c068 --- /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 +#include +#include + +#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 000000000000..fcac485b237c --- /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 + +#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 000000000000..f597195f1301 --- /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 000000000000..7b629008525e --- /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 + +#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 000000000000..69cea4ca1e4f --- /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 +#include +#include + +#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 000000000000..0c745c191b6b --- /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 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 header file. */ +#define HAVE_INTTYPES_H 1 + +/* Define to 1 if you have KERN_PROC and KERN_PROC_PATHNAME in . + */ +/* #undef HAVE_KERN_PROC */ + +/* Define to 1 if you have KERN_PROCARGS and KERN_PROC_PATHNAME in + . */ +/* #undef HAVE_KERN_PROC_ARGS */ + +/* Define if -llzma is available. */ +#define HAVE_LIBLZMA 1 + +/* Define to 1 if you have the 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 header file. */ +/* #undef HAVE_MACH_O_DYLD_H */ + +/* Define to 1 if you have the 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 header file. */ +#define HAVE_STDINT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDLIB_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRINGS_H 1 + +/* Define to 1 if you have the 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 header file. */ +/* #undef HAVE_SYS_LDR_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_MMAN_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_STAT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TYPES_H 1 + +/* Define to 1 if you have the 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 000000000000..5b2724e6a720 --- /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 +#include +#include +#include + +#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 000000000000..0472f4721ab6 --- /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 +#include +#include +#include +#include +#include + +#if defined (HAVE_KERN_PROC_ARGS) || defined (HAVE_KERN_PROC) +#include +#endif + +#ifdef HAVE_MACH_O_DYLD_H +#include +#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 000000000000..aa7bd7adff56 --- /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 000000000000..bb481f373bf7 --- /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 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 000000000000..720251900b4a --- /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 +#include +#include + +#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 000000000000..924631d2e61d --- /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 +#include +#include +#include +#include + +#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 000000000000..93d0d3abb493 --- /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 +#include +#include + +#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 000000000000..1811c8d2e086 --- /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 +#include +#include +#include + +#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 000000000000..785e726e6be3 --- /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 000000000000..a60a980e65ed --- /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 +#include + +#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 000000000000..0f368a239074 --- /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 +#include + +#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 aed276766fd6..2011827b7a22 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 000000000000..1043d8bbe78f --- /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 000000000000..2dde8df09853 --- /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 + +#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 + +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 */ diff --git a/thirdparty/ufbx/ufbx.c b/thirdparty/ufbx/ufbx.c index e6b2c91c650b..6d584369a7b2 100644 --- a/thirdparty/ufbx/ufbx.c +++ b/thirdparty/ufbx/ufbx.c @@ -43,6 +43,7 @@ #define UFBXI_FACE_GROUP_HASH_BITS 8 #define UFBXI_MIN_THREADED_DEFLATE_BYTES 256 #define UFBXI_MIN_THREADED_ASCII_VALUES 64 +#define UFBXI_GEOMETRY_CACHE_BUFFER_SIZE 512 #ifndef UFBXI_MAX_NURBS_ORDER #define UFBXI_MAX_NURBS_ORDER 128 @@ -210,6 +211,7 @@ #define ufbx_fmin ufbxi_math_fn(fmin) #define ufbx_fmax ufbxi_math_fn(fmax) #define ufbx_nextafter ufbxi_math_fn(nextafter) + #define ufbx_rint ufbxi_math_fn(rint) #define ufbx_ceil ufbxi_math_fn(ceil) #define ufbx_isnan ufbxi_math_fn(isnan) #endif @@ -229,6 +231,7 @@ double ufbx_fabs(double x); double ufbx_copysign(double x, double y); double ufbx_nextafter(double x, double y); + double ufbx_rint(double x); double ufbx_ceil(double x); int ufbx_isnan(double x); #endif @@ -439,6 +442,22 @@ #endif #endif +#if !defined(UFBX_STANDARD_C) && (defined(_MSC_VER) && defined(_M_X64)) || ((defined(__GNUC__) || defined(__clang__)) && defined(__x86_64__)) || defined(UFBX_USE_SSE) + #define UFBXI_HAS_SSE 1 + #include + #include +#else + #define UFBXI_HAS_SSE 0 +#endif + +#if !defined(UFBX_LITTLE_ENDIAN) + #if !defined(UFBX_STANDARD_C) && (defined(_M_IX86) || defined(__i386__) || defined(_M_X64) || defined(__x86_64__) || defined(_M_ARM64) || defined(__aarch64__) || defined(__wasm__) || defined(__EMSCRIPTEN__)) + #define UFBX_LITTLE_ENDIAN 1 + #else + #define UFBX_LITTLE_ENDIAN 0 + #endif +#endif + // Unaligned little-endian load functions // On platforms that support unaligned access natively (x86, x64, ARM64) just use normal loads, // with unaligned attributes, otherwise do manual byte-wise load. @@ -551,29 +570,11 @@ ufbx_static_assert(sizeof_f64, sizeof(double) == 8); // -- Version -#define UFBX_SOURCE_VERSION ufbx_pack_version(0, 11, 1) -const uint32_t ufbx_source_version = UFBX_SOURCE_VERSION; +#define UFBX_SOURCE_VERSION ufbx_pack_version(0, 14, 0) +ufbx_abi_data_def const uint32_t ufbx_source_version = UFBX_SOURCE_VERSION; ufbx_static_assert(source_header_version, UFBX_SOURCE_VERSION/1000u == UFBX_HEADER_VERSION/1000u); -// -- Architecture - -#if !defined(UFBX_STANDARD_C) && (defined(_MSC_VER) && defined(_M_X64)) || ((defined(__GNUC__) || defined(__clang__)) && defined(__x86_64__)) || defined(UFBX_USE_SSE) - #define UFBXI_HAS_SSE 1 - #include - #include -#else - #define UFBXI_HAS_SSE 0 -#endif - -#if !defined(UFBX_LITTLE_ENDIAN) - #if !defined(UFBX_STANDARD_C) && (defined(_M_IX86) || defined(__i386__) || defined(_M_X64) || defined(__x86_64__) || defined(_M_ARM64) || defined(__aarch64__) || defined(__wasm__) || defined(__EMSCRIPTEN__)) - #define UFBX_LITTLE_ENDIAN 1 - #else - #define UFBX_LITTLE_ENDIAN 0 - #endif -#endif - // -- Fast copy #if UFBXI_HAS_SSE @@ -1032,6 +1033,53 @@ static ufbxi_noinline void ufbxi_stable_sort(size_t stride, size_t linear_size, if (dst != data) memcpy((void*)data, dst, size * stride); } +static ufbxi_forceinline void ufbxi_swap(void *a, void *b, size_t size) +{ + char *ca = (char*)a, *cb = (char*)b; +#if defined(UFBXI_HAS_UNALIGNED) + ufbxi_nounroll while (size >= 4) { + uint32_t t = *(ufbxi_unaligned ufbxi_unaligned_u32*)ca; + *(ufbxi_unaligned ufbxi_unaligned_u32*)ca = *(ufbxi_unaligned ufbxi_unaligned_u32*)cb; + *(ufbxi_unaligned ufbxi_unaligned_u32*)cb = t; + ca += 4; cb += 4; size -= 4; + } +#endif + ufbxi_nounroll while (size > 0) { + char t = *ca; *ca = *cb; *cb = t; + ca++; cb++; size--; + } +} + +static ufbxi_noinline void ufbxi_unstable_sort(void *in_data, size_t size, size_t stride, ufbxi_less_fn *less_fn, void *less_user) +{ + if (size <= 1) return; + char *data = (char*)in_data; + size_t start = (size - 1) >> 1; + size_t end = size - 1; + for (;;) { + size_t root = start; + size_t child; + while ((child = root*2 + 1) <= end) { + size_t next = less_fn(less_user, data + child * stride, data + root * stride) ? root : child; + if (child + 1 <= end && less_fn(less_user, data + next * stride, data + (child + 1) * stride)) { + next = child + 1; + } + if (next == root) break; + ufbxi_swap(data + root * stride, data + next * stride, stride); + root = next; + } + + if (start > 0) { + start--; + } else if (end > 0) { + ufbxi_swap(data + end * stride, data, stride); + end--; + } else { + break; + } + } +} + // -- Float parsing // // Custom float parsing that handles floats up to (-)ddddddddddddddddddd.ddddddddddddddddddd @@ -1158,7 +1206,7 @@ typedef enum { UFBXI_PARSE_DOUBLE_VERIFY_LENGTH = 0x2, } ufbxi_parse_double_flag; -static uint64_t ufbxi_pow5_tab[] = { +static const uint64_t ufbxi_pow5_tab[] = { UINT64_C(0x8000000000000000), // 5^0 * 2^63 UINT64_C(0xa000000000000000), // 5^1 * 2^61 UINT64_C(0xc800000000000000), // 5^2 * 2^59 @@ -1188,10 +1236,10 @@ static uint64_t ufbxi_pow5_tab[] = { UINT64_C(0xa56fa5b99019a5c8), // 5^26 * 2^3 UINT64_C(0xcecb8f27f4200f3a), // 5^27 * 2^1 }; -static int8_t ufbxi_pow2_tab[] = { +static const int8_t ufbxi_pow2_tab[] = { 62, 59, 56, 53, 49, 46, 43, 39, 36, 33, 29, 26, 23, 19, 16, 13, 9, 6, 3, -1, -4, -7, -11, -14, -17, -21, -24, -27, }; -const double ufbxi_pow10_tab_f64[] = { +static const double ufbxi_pow10_tab_f64[] = { 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, }; @@ -1201,8 +1249,8 @@ static ufbxi_noinline uint32_t ufbxi_parse_double_init_flags() // and rounding to nearest, which we can check for with `1 + eps == 1 - eps`. #if defined(FLT_EVAL_METHOD) #if FLT_EVAL_METHOD == 0 || FLT_EVAL_METHOD == 1 - static volatile double eps = 2.2250738585072014e-308; - if (1.0 + eps == 1.0 - eps) return UFBXI_PARSE_DOUBLE_ALLOW_FAST_PATH; + static volatile double ufbxi_volatile_eps = 2.2250738585072014e-308; + if (1.0 + ufbxi_volatile_eps == 1.0 - ufbxi_volatile_eps) return UFBXI_PARSE_DOUBLE_ALLOW_FAST_PATH; #endif #endif @@ -2834,7 +2882,7 @@ static ufbxi_noinline void ufbxi_panicf_imp(ufbx_panic *panic, const char *fmt, { if (panic && panic->did_panic) return; - va_list args; + va_list args; // ufbxi_uninit va_start(args, fmt); if (panic) { @@ -2960,7 +3008,7 @@ static ufbxi_noinline void ufbxi_fmt_err_info(ufbx_error *err, const char *fmt, { if (!err) return; - va_list args; + va_list args; // ufbxi_uninit va_start(args, fmt); err->info_length = (size_t)ufbxi_vsnprintf(err->info, sizeof(err->info), fmt, args); va_end(args); @@ -3544,14 +3592,14 @@ static ufbxi_forceinline void *ufbxi_push_size_fast(ufbxi_buf *b, size_t size, s } } -static ufbxi_forceinline void *ufbxi_push_size_zero(ufbxi_buf *b, size_t size, size_t n) +static ufbxi_noinline void *ufbxi_push_size_zero(ufbxi_buf *b, size_t size, size_t n) { void *ptr = ufbxi_push_size(b, size, n); if (ptr) memset(ptr, 0, size * n); return ptr; } -ufbxi_nodiscard static ufbxi_forceinline void *ufbxi_push_size_copy(ufbxi_buf *b, size_t size, size_t n, const void *data) +ufbxi_nodiscard static ufbxi_noinline void *ufbxi_push_size_copy(ufbxi_buf *b, size_t size, size_t n, const void *data) { // Always succeed with an empty non-NULL buffer for empty allocations, even if `data == NULL` ufbx_assert(size > 0); @@ -3563,6 +3611,18 @@ ufbxi_nodiscard static ufbxi_forceinline void *ufbxi_push_size_copy(ufbxi_buf *b return ptr; } +ufbxi_nodiscard static ufbxi_forceinline void *ufbxi_push_size_copy_fast(ufbxi_buf *b, size_t size, size_t n, const void *data) +{ + // Always succeed with an empty non-NULL buffer for empty allocations, even if `data == NULL` + ufbx_assert(size > 0); + if (n == 0) return (void*)ufbxi_zero_size_buffer; + + ufbx_assert(data); + void *ptr = ufbxi_push_size_fast(b, size, n); + if (ptr) memcpy(ptr, data, size * n); + return ptr; +} + static ufbxi_noinline void ufbxi_buf_free_unused(ufbxi_buf *b) { ufbx_assert(!b->unordered); @@ -3774,6 +3834,7 @@ static ufbxi_noinline void ufbxi_buf_clear(ufbxi_buf *buf) #define ufbxi_push(b, type, n) ufbxi_maybe_null((type*)ufbxi_push_size((b), sizeof(type), (n))) #define ufbxi_push_zero(b, type, n) ufbxi_maybe_null((type*)ufbxi_push_size_zero((b), sizeof(type), (n))) #define ufbxi_push_copy(b, type, n, data) ufbxi_maybe_null((type*)ufbxi_push_size_copy((b), sizeof(type), (n), (data))) +#define ufbxi_push_copy_fast(b, type, n, data) ufbxi_maybe_null((type*)ufbxi_push_size_copy_fast((b), sizeof(type), (n), (data))) #define ufbxi_push_fast(b, type, n) ufbxi_maybe_null((type*)ufbxi_push_size_fast((b), sizeof(type), (n))) #define ufbxi_pop(b, type, n, dst) ufbxi_pop_size((b), sizeof(type), (n), (dst), false) #define ufbxi_peek(b, type, n, dst) ufbxi_pop_size((b), sizeof(type), (n), (dst), true) @@ -4273,7 +4334,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_vwarnf_imp(ufbxi_warnings *ws, u ufbxi_nodiscard static ufbxi_noinline int ufbxi_warnf_imp(ufbxi_warnings *ws, ufbx_warning_type type, uint32_t element_id, const char *fmt, ...) { // NOTE: `ws` may be `NULL` here, handled by `ufbxi_vwarnf()` - va_list args; + va_list args; // ufbxi_uninit va_start(args, fmt); int ok = ufbxi_vwarnf_imp(ws, type, element_id, fmt, args); va_end(args); @@ -4705,6 +4766,8 @@ static const char ufbxi_AspectHeight[] = "AspectHeight"; static const char ufbxi_AspectRatioMode[] = "AspectRatioMode"; static const char ufbxi_AspectW[] = "AspectW"; static const char ufbxi_AspectWidth[] = "AspectWidth"; +static const char ufbxi_Audio[] = "Audio"; +static const char ufbxi_AudioLayer[] = "AudioLayer"; static const char ufbxi_BaseLayer[] = "BaseLayer"; static const char ufbxi_BinaryData[] = "BinaryData"; static const char ufbxi_BindPose[] = "BindPose"; @@ -4982,7 +5045,7 @@ static const char ufbxi_d_X[] = "d|X"; static const char ufbxi_d_Y[] = "d|Y"; static const char ufbxi_d_Z[] = "d|Z"; -static ufbx_string ufbxi_strings[] = { +static const ufbx_string ufbxi_strings[] = { { ufbxi_AllSame, 7 }, { ufbxi_Alphas, 6 }, { ufbxi_AmbientColor, 12 }, @@ -4998,6 +5061,8 @@ static ufbx_string ufbxi_strings[] = { { ufbxi_AspectRatioMode, 15 }, { ufbxi_AspectW, 7 }, { ufbxi_AspectWidth, 11 }, + { ufbxi_Audio, 5 }, + { ufbxi_AudioLayer, 10 }, { ufbxi_BaseLayer, 9 }, { ufbxi_BinaryData, 10 }, { ufbxi_BindPose, 8 }, @@ -5336,6 +5401,11 @@ ufbx_inline ufbx_vec3 ufbxi_normalize3(ufbx_vec3 a) { } } +ufbx_inline ufbx_real ufbxi_distsq2(ufbx_vec2 a, ufbx_vec2 b) { + ufbx_real dx = a.x - b.x, dy = a.y - b.y; + return dx*dx + dy*dy; +} + static ufbxi_noinline ufbx_vec3 ufbxi_slow_normalize3(const ufbx_vec3 *a) { return ufbxi_normalize3(*a); } @@ -5384,8 +5454,6 @@ struct ufbxi_thread_pool { ufbxi_task_group groups[UFBX_THREAD_GROUP_COUNT]; uint32_t group; - double accumulated_cost; - uint32_t num_tasks; ufbxi_task_imp *tasks; }; @@ -5496,22 +5564,7 @@ ufbxi_nodiscard ufbxi_noinline static uint32_t ufbxi_thread_pool_available_tasks return pool->num_tasks - (pool->start_index - pool->wait_index); } -static void ufbxi_thread_pool_flush(ufbxi_thread_pool *pool) -{ - uint32_t start_index = pool->execute_index; - uint32_t count = pool->start_index - start_index; - if (count > 0) { -#if 0 - if (pool->opts.pool.run_fn) { - uint32_t ran_count = pool->opts.pool.run_fn(pool->opts.pool.user, (ufbx_thread_pool_context)pool, pool->group, start_index, count); - pool->execute_index = start_index + ran_count; - } -#endif - } - pool->accumulated_cost = 0.0; -} - -static void ufbxi_thread_pool_flush_group(ufbxi_thread_pool *pool) +ufbxi_noinline static void ufbxi_thread_pool_flush_group(ufbxi_thread_pool *pool) { uint32_t group = pool->group; uint32_t start_index = pool->execute_index; @@ -5523,7 +5576,6 @@ static void ufbxi_thread_pool_flush_group(ufbxi_thread_pool *pool) pool->groups[group].max_index = start_index + count; pool->execute_index = start_index + count; } - pool->accumulated_cost = 0.0; pool->group = (group + 1) % UFBX_THREAD_GROUP_COUNT; } @@ -5550,17 +5602,12 @@ ufbxi_nodiscard ufbxi_noinline static ufbxi_task *ufbxi_thread_pool_create_task( return &imp->task; } -static void ufbxi_thread_pool_run_task(ufbxi_thread_pool *pool, ufbxi_task *task, double cost) +static void ufbxi_thread_pool_run_task(ufbxi_thread_pool *pool, ufbxi_task *task) { (void)task; uint32_t index = pool->start_index; ufbx_assert(task == &pool->tasks[index % pool->num_tasks].task); pool->start_index = index + 1; - pool->accumulated_cost += cost; - - if (pool->accumulated_cost >= 256*1024) { - ufbxi_thread_pool_flush(pool); - } } // -- Type definitions @@ -5791,6 +5838,11 @@ typedef struct { ufbx_anim_stack *stack; } ufbxi_tmp_anim_stack; +typedef struct { + ufbx_string absolute_filename; + ufbx_blob content; +} ufbxi_file_content; + typedef struct { // Current line and tokens. @@ -5978,6 +6030,7 @@ typedef struct { bool has_geometry_transform_nodes; bool has_scale_helper_nodes; + bool retain_vertex_w; ufbx_mirror_axis mirror_axis; @@ -5997,6 +6050,9 @@ typedef struct { ufbxi_node legacy_node; uint64_t legacy_implicit_anim_layer_id; + ufbxi_file_content *file_content; + size_t num_file_content; + int64_t ktime_sec; double ktime_sec_double; @@ -7177,6 +7233,7 @@ typedef enum { UFBXI_PARSE_LAYERED_TEXTURE, UFBXI_PARSE_SELECTION_NODE, UFBXI_PARSE_COLLECTION, + UFBXI_PARSE_AUDIO, UFBXI_PARSE_UNKNOWN_OBJECT, UFBXI_PARSE_LAYER_ELEMENT_NORMAL, UFBXI_PARSE_LAYER_ELEMENT_BINORMAL, @@ -7250,6 +7307,7 @@ static ufbxi_noinline ufbxi_parse_state ufbxi_update_parse_state(ufbxi_parse_sta if (name == ufbxi_LayeredTexture) return UFBXI_PARSE_LAYERED_TEXTURE; if (name == ufbxi_SelectionNode) return UFBXI_PARSE_SELECTION_NODE; if (name == ufbxi_Collection) return UFBXI_PARSE_COLLECTION; + if (name == ufbxi_Audio) return UFBXI_PARSE_AUDIO; return UFBXI_PARSE_UNKNOWN_OBJECT; case UFBXI_PARSE_MODEL: @@ -7483,7 +7541,7 @@ static bool ufbxi_is_array_node(ufbxi_context *uc, ufbxi_parse_state parent, con info->flags = UFBXI_ARRAY_FLAG_RESULT; return true; } else if (name == ufbxi_NormalsW) { - info->type = uc->opts.retain_dom ? 'r' : '-'; + info->type = uc->retain_vertex_w ? 'r' : '-'; info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; return true; } @@ -7499,7 +7557,7 @@ static bool ufbxi_is_array_node(ufbxi_context *uc, ufbxi_parse_state parent, con info->flags = UFBXI_ARRAY_FLAG_RESULT; return true; } else if (name == ufbxi_BinormalsW) { - info->type = uc->opts.retain_dom ? 'r' : '-'; + info->type = uc->retain_vertex_w ? 'r' : '-'; info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; return true; } @@ -7515,7 +7573,7 @@ static bool ufbxi_is_array_node(ufbxi_context *uc, ufbxi_parse_state parent, con info->flags = UFBXI_ARRAY_FLAG_RESULT; return true; } else if (name == ufbxi_TangentsW) { - info->type = uc->opts.retain_dom ? 'r' : '-'; + info->type = uc->retain_vertex_w ? 'r' : '-'; info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; return true; } @@ -7723,6 +7781,13 @@ static bool ufbxi_is_array_node(ufbxi_context *uc, ufbxi_parse_state parent, con } break; + case UFBXI_PARSE_AUDIO: + if (name == ufbxi_Content) { + info->type = uc->opts.ignore_embedded ? '-' : 'C'; + return true; + } + break; + default: if (name == ufbxi_BinaryData) { info->type = uc->opts.ignore_embedded ? '-' : 'C'; @@ -7800,6 +7865,10 @@ static ufbxi_noinline bool ufbxi_is_raw_string(ufbxi_context *uc, ufbxi_parse_st if (!strcmp(name, "Member")) return true; break; + case UFBXI_PARSE_AUDIO: + if (name == ufbxi_Content) return true; + break; + case UFBXI_PARSE_LEGACY_MODEL: if (name == ufbxi_Material) return true; if (name == ufbxi_Link) return true; @@ -7834,6 +7903,7 @@ static ufbxi_noinline bool ufbxi_is_raw_string(ufbxi_context *uc, ufbxi_parse_st ufbxi_nodiscard static ufbxi_noinline char *ufbxi_swap_endian(ufbxi_context *uc, const void *src, size_t count, size_t elem_size) { + ufbxi_dev_assert(elem_size > 1); size_t total_size = count * elem_size; ufbxi_check_return(!ufbxi_does_overflow(total_size, count, elem_size), NULL); if (uc->swap_arr_size < total_size) { @@ -7843,26 +7913,20 @@ ufbxi_nodiscard static ufbxi_noinline char *ufbxi_swap_endian(ufbxi_context *uc, const char *s = (const char*)src; switch (elem_size) { - case 1: - for (size_t i = 0; i < count; i++) { - d[0] = s[0]; - d += 1; s += 1; - } - break; case 2: - for (size_t i = 0; i < count; i++) { + ufbxi_nounroll for (size_t i = 0; i < count; i++) { d[0] = s[1]; d[1] = s[0]; d += 2; s += 2; } break; case 4: - for (size_t i = 0; i < count; i++) { + ufbxi_nounroll for (size_t i = 0; i < count; i++) { d[0] = s[3]; d[1] = s[2]; d[2] = s[1]; d[3] = s[0]; d += 4; s += 4; } break; case 8: - for (size_t i = 0; i < count; i++) { + ufbxi_nounroll for (size_t i = 0; i < count; i++) { d[0] = s[7]; d[1] = s[6]; d[2] = s[5]; d[3] = s[4]; d[4] = s[3]; d[5] = s[2]; d[6] = s[1]; d[7] = s[0]; d += 8; s += 8; @@ -8338,6 +8402,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_binary_parse_node(ufbxi_context t->array_size = size; t->src_type = src_type; t->dst_type = dst_type; + t->arr_type = arr->type; t->dst_data = arr_data; t->inflate_retain = uc->inflate_retain; @@ -8359,7 +8424,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_binary_parse_node(ufbxi_context } task->data = t; - ufbxi_thread_pool_run_task(&uc->thread_pool, task, (double)encoded_size); + ufbxi_thread_pool_run_task(&uc->thread_pool, task); deferred = true; } } @@ -8507,9 +8572,9 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_binary_parse_node(ufbxi_context switch (type) { - case 'C': case 'B': + case 'C': case 'B': case 'Z': type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2); - vals[i].f = (double)(vals[i].i = (int64_t)value[0]); + vals[i].f = (double)(vals[i].i = (int64_t)(uint8_t)value[0]); ufbxi_consume_bytes(uc, 2); break; @@ -9201,22 +9266,17 @@ typedef struct { void *arr_data; char arr_type; size_t arr_size; - uint32_t parse_flags; - const ufbxi_ascii_span *spans; size_t num_spans; - size_t offset; - } ufbxi_ascii_array_task; -ufbxi_noinline static const char *ufbxi_ascii_array_task_parse_floats(ufbxi_ascii_array_task *t, const char *src, const char *src_end) +ufbxi_noinline static const char *ufbxi_ascii_array_task_parse_floats(ufbxi_ascii_array_task *t, const char *src, const char *src_end, uint32_t parse_flags) { size_t offset = t->offset; float *dst_float = t->arr_type == 'f' ? (float*)t->arr_data + offset : NULL; double *dst_double = t->arr_type == 'd' ? (double*)t->arr_data + offset : NULL; ufbx_assert(dst_float || dst_double); - uint32_t parse_flags = t->parse_flags; const char *src_begin = src; while (src != src_end) { @@ -9281,7 +9341,8 @@ ufbxi_noinline static const char *ufbxi_ascii_array_task_parse_ints(ufbxi_ascii_ ufbxi_noinline static const char *ufbxi_ascii_array_task_parse(ufbxi_ascii_array_task *t, const char *src, const char *src_end) { if (t->arr_type == 'f' || t->arr_type == 'd') { - return ufbxi_ascii_array_task_parse_floats(t, src, src_end); + uint32_t flags = ufbxi_parse_double_init_flags(); + return ufbxi_ascii_array_task_parse_floats(t, src, src_end, flags); } else { return ufbxi_ascii_array_task_parse_ints(t, src, src_end); } @@ -9804,7 +9865,6 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_parse_node(ufbxi_context * t.arr_size = deferred_size; t.num_spans = num_spans; t.spans = spans; - t.parse_flags = uc->double_parse_flags; t.offset = 0; // TODO: Split these further @@ -9812,7 +9872,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_parse_node(ufbxi_context * if (task) { task->data = ufbxi_push_copy(tmp_buf, ufbxi_ascii_array_task, 1, &t); ufbxi_check(task->data); - ufbxi_thread_pool_run_task(&uc->thread_pool, task, deferred_size * 10.0); + ufbxi_thread_pool_run_task(&uc->thread_pool, task); } else { ufbxi_check_msg(ufbxi_ascii_array_task_imp(&t), "Threaded ASCII parse error"); } @@ -10574,7 +10634,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_strings(ufbxi_context *uc) // Push all the global 'ufbxi_*' strings into the pool without copying them // This allows us to compare name pointers to the global values - ufbxi_for(ufbx_string, str, ufbxi_strings, ufbxi_arraycount(ufbxi_strings)) { + ufbxi_for(const ufbx_string, str, ufbxi_strings, ufbxi_arraycount(ufbxi_strings)) { #if defined(UFBX_REGRESSION) ufbx_assert(strlen(str->data) == str->length); ufbx_assert(ufbxi_str_less(reg_prev, *str)); @@ -10748,9 +10808,9 @@ static ufbxi_forceinline bool ufbxi_is_quat_identity(ufbx_quat v) return (v.x == 0.0) & (v.y == 0.0) & (v.z == 0.0) & (v.w == 1.0); } -static ufbxi_forceinline bool ufbxi_is_transform_identity(ufbx_transform t) +static ufbxi_noinline bool ufbxi_is_transform_identity(const ufbx_transform *t) { - return (bool)((int)ufbxi_is_vec3_zero(t.translation) & (int)ufbxi_is_quat_identity(t.rotation) & (int)ufbxi_is_vec3_one(t.scale)); + return (bool)((int)ufbxi_is_vec3_zero(t->translation) & (int)ufbxi_is_quat_identity(t->rotation) & (int)ufbxi_is_vec3_one(t->scale)); } static ufbxi_forceinline uint32_t ufbxi_get_name_key(const char *name, size_t len) @@ -10789,7 +10849,7 @@ static ufbxi_forceinline bool ufbxi_name_key_less(ufbx_prop *prop, const char *d return prop_len < name_len; } -static const char *ufbxi_node_prop_names[] = { +static const char *const ufbxi_node_prop_names[] = { "AxisLen", "DefaultAttributeIndex", "Freeze", @@ -10867,8 +10927,8 @@ static const char *ufbxi_node_prop_names[] = { ufbxi_nodiscard static ufbxi_noinline int ufbxi_init_node_prop_names(ufbxi_context *uc) { ufbxi_check(ufbxi_map_grow(&uc->node_prop_set, const char*, ufbxi_arraycount(ufbxi_node_prop_names))); - ufbxi_for_ptr(const char, p_name, ufbxi_node_prop_names, ufbxi_arraycount(ufbxi_node_prop_names)) { - const char *name = *p_name; + for (size_t i = 0; i < ufbxi_arraycount(ufbxi_node_prop_names); i++) { + const char *name = ufbxi_node_prop_names[i]; const char *pooled = ufbxi_push_string_imp(&uc->string_pool, name, strlen(name), NULL, false, true); ufbxi_check(pooled); uint32_t hash = ufbxi_hash_ptr(pooled); @@ -11266,16 +11326,6 @@ ufbxi_nodiscard static int ufbxi_match_exporter(ufbxi_context *uc) } else if (ufbxi_match_version_string("motionbuilder/mocap/online version ?.?", creator, version)) { uc->exporter = UFBX_EXPORTER_MOTION_BUILDER; uc->exporter_version = ufbx_pack_version(version[0], version[1], 0); - } else if (ufbxi_match_version_string("fbx unity export version ?.?", creator, version)) { - uc->exporter = UFBX_EXPORTER_BC_UNITY_EXPORTER; - uc->exporter_version = ufbx_pack_version(version[0], version[1], 0); - } else if (ufbxi_match_version_string("fbx unity export version ?.?.?", creator, version)) { - uc->exporter = UFBX_EXPORTER_BC_UNITY_EXPORTER; - uc->exporter_version = ufbx_pack_version(version[0], version[1], version[2]); - } else if (ufbxi_match_version_string("made using asset forge", creator, version)) { - uc->exporter = UFBX_EXPORTER_BC_UNITY_EXPORTER; - } else if (ufbxi_match_version_string("model created by kenney", creator, version)) { - uc->exporter = UFBX_EXPORTER_BC_UNITY_EXPORTER; } uc->scene.metadata.exporter = uc->exporter; @@ -11407,7 +11457,7 @@ static ufbxi_noinline int ufbxi_push_synthetic_id(ufbxi_context *uc, uint64_t *p return 1; } -ufbxi_nodiscard static int ufbxi_split_type_and_name(ufbxi_context *uc, ufbx_string type_and_name, ufbx_string *type, ufbx_string *name) +ufbxi_nodiscard ufbxi_noinline static int ufbxi_split_type_and_name(ufbxi_context *uc, ufbx_string type_and_name, ufbx_string *type, ufbx_string *name) { // Name and type are packed in a single property as Type::Name (in ASCII) // or Name\x00\x01Type (in binary) @@ -11495,9 +11545,9 @@ ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_push_element_size(ufbx uint32_t typed_id = (uint32_t)uc->tmp_typed_element_offsets[type].num_items; uint32_t element_id = uc->num_elements++; - ufbxi_check_return(ufbxi_push_copy(&uc->tmp_typed_element_offsets[type], size_t, 1, &uc->tmp_element_byte_offset), NULL); - ufbxi_check_return(ufbxi_push_copy(&uc->tmp_element_offsets, size_t, 1, &uc->tmp_element_byte_offset), NULL); - ufbxi_check_return(ufbxi_push_copy(&uc->tmp_element_fbx_ids, uint64_t, 1, &info->fbx_id), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_typed_element_offsets[type], size_t, 1, &uc->tmp_element_byte_offset), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_offsets, size_t, 1, &uc->tmp_element_byte_offset), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_fbx_ids, uint64_t, 1, &info->fbx_id), NULL); uc->tmp_element_byte_offset += aligned_size; ufbx_element *elem = (ufbx_element*)ufbxi_push_zero(&uc->tmp_elements, uint64_t, aligned_size/8); @@ -11513,7 +11563,7 @@ ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_push_element_size(ufbx *uc->p_element_id = element_id; } - ufbxi_check_return(ufbxi_push_copy(&uc->tmp_element_ptrs, ufbx_element*, 1, &elem), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_ptrs, ufbx_element*, 1, &elem), NULL); ufbxi_check_return(ufbxi_insert_fbx_id(uc, info->fbx_id, element_id), NULL); @@ -11527,8 +11577,8 @@ ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_push_synthetic_element uint32_t typed_id = (uint32_t)uc->tmp_typed_element_offsets[type].num_items; uint32_t element_id = uc->num_elements++; - ufbxi_check_return(ufbxi_push_copy(&uc->tmp_typed_element_offsets[type], size_t, 1, &uc->tmp_element_byte_offset), NULL); - ufbxi_check_return(ufbxi_push_copy(&uc->tmp_element_offsets, size_t, 1, &uc->tmp_element_byte_offset), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_typed_element_offsets[type], size_t, 1, &uc->tmp_element_byte_offset), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_offsets, size_t, 1, &uc->tmp_element_byte_offset), NULL); uc->tmp_element_byte_offset += aligned_size; ufbx_element *elem = (ufbx_element*)ufbxi_push_zero(&uc->tmp_elements, uint64_t, aligned_size/8); @@ -11542,12 +11592,12 @@ ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_push_synthetic_element elem->name.length = strlen(name); } - ufbxi_check_return(ufbxi_push_copy(&uc->tmp_element_ptrs, ufbx_element*, 1, &elem), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_ptrs, ufbx_element*, 1, &elem), NULL); uint64_t fbx_id = ufbxi_synthetic_id_from_pointer(elem); *p_fbx_id = fbx_id; - ufbxi_check_return(ufbxi_push_copy(&uc->tmp_element_fbx_ids, uint64_t, 1, &fbx_id), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_fbx_ids, uint64_t, 1, &fbx_id), NULL); ufbxi_check_return(ufbxi_insert_fbx_id(uc, fbx_id, element_id), NULL); return elem; @@ -11867,8 +11917,19 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_check_indices(ufbxi_context *uc, return 1; } +ufbx_static_assert(vertex_real_size, sizeof(ufbx_vertex_real) == sizeof(ufbx_vertex_attrib)); +ufbx_static_assert(vertex_vec2_size, sizeof(ufbx_vertex_vec2) == sizeof(ufbx_vertex_attrib)); +ufbx_static_assert(vertex_vec3_size, sizeof(ufbx_vertex_vec3) == sizeof(ufbx_vertex_attrib)); +ufbx_static_assert(vertex_vec4_size, sizeof(ufbx_vertex_vec4) == sizeof(ufbx_vertex_attrib)); + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_warn_polygon_mapping(ufbxi_context *uc, const char *data_name, const char *mapping) +{ + ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_POLYGON_MAPPING, "Ignoring geometry '%s' with bad mapping mode '%s'", data_name, mapping)); + return 1; +} + ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_vertex_element(ufbxi_context *uc, ufbx_mesh *mesh, ufbxi_node *node, - ufbx_vertex_attrib *attrib, const char *data_name, const char *index_name, char data_type, size_t num_components) + ufbx_vertex_attrib *attrib, const char *data_name, const char *index_name, const char *w_name, char data_type, size_t num_components) { ufbx_real **p_dst_data = (ufbx_real**)&attrib->values.data; @@ -11895,8 +11956,8 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_vertex_element(ufbxi_contex attrib->exists = true; attrib->indices.count = mesh->num_indices; - const char *mapping = NULL; - ufbxi_check(ufbxi_find_val1(node, ufbxi_MappingInformationType, "C", (char**)&mapping)); + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_MappingInformationType, "C", (char**)&mapping)); attrib->values.count = num_elems ? num_elems : 1; @@ -11979,7 +12040,9 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_vertex_element(ufbxi_contex attrib->unique_per_vertex = true; } else { - ufbxi_fail("Invalid mapping"); + memset(attrib, 0, sizeof(ufbx_vertex_attrib)); + ufbxi_check(ufbxi_warn_polygon_mapping(uc, data_name, mapping)); + return 1; } } else { @@ -12030,7 +12093,22 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_vertex_element(ufbxi_contex attrib->unique_per_vertex = true; } else { - ufbxi_fail("Invalid mapping"); + memset(attrib, 0, sizeof(ufbx_vertex_attrib)); + ufbxi_check(ufbxi_warn_polygon_mapping(uc, data_name, mapping)); + return 1; + } + } + + if (uc->opts.retain_vertex_attrib_w && w_name) { + ufbxi_value_array *w_data = ufbxi_find_array(node, w_name, 'r'); + if (w_data) { + if (w_data->size == num_elems) { + attrib->values_w.count = w_data->size; + attrib->values_w.data = (ufbx_real*)w_data->data; + } else { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_BAD_VERTEX_W_ATTRIBUTE, "Bad W array size %s=%zu, %s=%zu", + w_name, w_data->size, data_name, num_elems)); + } } } @@ -12355,11 +12433,11 @@ typedef struct { uint32_t id, index; } ufbxi_id_group; -static int ufbxi_cmp_int32(const void *va, const void *vb) +static bool ufbxi_less_int32(void *user, const void *va, const void *vb) { + (void)user; const int32_t a = *(const int32_t*)va, b = *(const int32_t*)vb; - if (a != b) return a < b ? -1 : +1; - return 0; + return a < b; } ufbx_static_assert(mesh_mat_point_faces, offsetof(ufbx_mesh_part, num_point_faces) - offsetof(ufbx_mesh_part, num_empty_faces) == 1 * sizeof(size_t)); @@ -12409,7 +12487,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_assign_face_groups(ufbxi_buf *bu } // Sort and deduplicate remaining IDs - qsort(ids, num_ids, sizeof(uint32_t), &ufbxi_cmp_int32); + ufbxi_unstable_sort(ids, num_ids, sizeof(uint32_t), &ufbxi_less_int32, NULL); size_t num_groups = 0; for (size_t i = 0; i < num_ids; ) { @@ -12668,13 +12746,13 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_mesh(ufbxi_context *uc, ufb if (n->name == ufbxi_LayerElementNormal) { if (mesh->vertex_normal.exists) continue; ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&mesh->vertex_normal, - ufbxi_Normals, ufbxi_NormalsIndex, 'r', 3)); + ufbxi_Normals, ufbxi_NormalsIndex, ufbxi_NormalsW, 'r', 3)); } else if (n->name == ufbxi_LayerElementBinormal) { ufbxi_tangent_layer *layer = &bitangents[num_bitangents_read++]; ufbxi_ignore(ufbxi_get_val1(n, "I", &layer->index)); ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&layer->elem, - ufbxi_Binormals, ufbxi_BinormalsIndex, 'r', 3)); + ufbxi_Binormals, ufbxi_BinormalsIndex, ufbxi_BinormalsW, 'r', 3)); if (!layer->elem.exists) num_bitangents_read--; } else if (n->name == ufbxi_LayerElementTangent) { @@ -12682,7 +12760,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_mesh(ufbxi_context *uc, ufb ufbxi_ignore(ufbxi_get_val1(n, "I", &layer->index)); ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&layer->elem, - ufbxi_Tangents, ufbxi_TangentsIndex, 'r', 3)); + ufbxi_Tangents, ufbxi_TangentsIndex, ufbxi_TangentsW, 'r', 3)); if (!layer->elem.exists) num_tangents_read--; } else if (n->name == ufbxi_LayerElementUV) { @@ -12694,7 +12772,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_mesh(ufbxi_context *uc, ufb } ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&set->vertex_uv, - ufbxi_UV, ufbxi_UVIndex, 'r', 2)); + ufbxi_UV, ufbxi_UVIndex, NULL, 'r', 2)); if (!set->vertex_uv.exists) mesh->uv_sets.count--; } else if (n->name == ufbxi_LayerElementColor) { @@ -12706,40 +12784,46 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_mesh(ufbxi_context *uc, ufb } ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&set->vertex_color, - ufbxi_Colors, ufbxi_ColorIndex, 'r', 4)); + ufbxi_Colors, ufbxi_ColorIndex, NULL, 'r', 4)); if (!set->vertex_color.exists) mesh->color_sets.count--; } else if (n->name == ufbxi_LayerElementVertexCrease) { ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&mesh->vertex_crease, - ufbxi_VertexCrease, ufbxi_VertexCreaseIndex, 'r', 1)); + ufbxi_VertexCrease, ufbxi_VertexCreaseIndex, NULL, 'r', 1)); } else if (n->name == ufbxi_LayerElementEdgeCrease) { - const char *mapping = NULL; - ufbxi_check(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); if (mapping == ufbxi_ByEdge) { if (mesh->edge_crease.count) continue; ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->edge_crease.data, &mesh->edge_crease.count, n, ufbxi_EdgeCrease, 'r', mesh->num_edges)); + } else { + ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_EdgeCrease, mapping)); } } else if (n->name == ufbxi_LayerElementSmoothing) { - const char *mapping = NULL; - ufbxi_check(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); if (mapping == ufbxi_ByEdge) { if (mesh->edge_smoothing.count) continue; ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->edge_smoothing.data, &mesh->edge_smoothing.count, n, ufbxi_Smoothing, 'b', mesh->num_edges)); } else if (mapping == ufbxi_ByPolygon) { if (mesh->face_smoothing.count) continue; ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_smoothing.data, &mesh->face_smoothing.count, n, ufbxi_Smoothing, 'b', mesh->num_faces)); + } else { + ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_Smoothing, mapping)); } } else if (n->name == ufbxi_LayerElementVisibility) { - const char *mapping = NULL; - ufbxi_check(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); if (mapping == ufbxi_ByEdge) { if (mesh->edge_visibility.count) continue; ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->edge_visibility.data, &mesh->edge_visibility.count, n, ufbxi_Visibility, 'b', mesh->num_edges)); + } else { + ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_Visibility, mapping)); } } else if (n->name == ufbxi_LayerElementMaterial) { if (mesh->face_material.count) continue; - const char *mapping = NULL; - ufbxi_check(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); if (mapping == ufbxi_ByPolygon) { ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_material.data, &mesh->face_material.count, n, ufbxi_Materials, 'i', mesh->num_faces)); } else if (mapping == ufbxi_AllSame) { @@ -12756,6 +12840,8 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_mesh(ufbxi_context *uc, ufb *p_mat = material; } } + } else { + ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_Materials, mapping)); } } else if (n->name == ufbxi_LayerElementPolygonGroup) { if (mesh->face_group.count) continue; @@ -13168,6 +13254,24 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_blend_channel(ufbxi_context } ufbxi_check(ufbxi_push_copy(&uc->tmp_full_weights, ufbx_real_list, 1, &list)); + // Blender saves blend shapes with DeformPercent as a field, not a property. + // However, the animations are mapped to the DeformPercent property. + ufbxi_node *deform_percent = ufbxi_find_child(node, ufbxi_DeformPercent); + if (channel->props.props.count == 0 && deform_percent) { + size_t num_shape_props = 1; + ufbx_prop *shape_props = ufbxi_push_zero(&uc->result, ufbx_prop, num_shape_props); + ufbxi_check(shape_props); + shape_props[0].name.data = ufbxi_DeformPercent; + shape_props[0].name.length = sizeof(ufbxi_DeformPercent) - 1; + shape_props[0]._internal_key = ufbxi_get_name_key_c(ufbxi_DeformPercent); + shape_props[0].type = UFBX_PROP_NUMBER; + shape_props[0].value_str = ufbx_empty_string; + shape_props[0].value_real = 100.0f; + ufbxi_ignore(ufbxi_get_val1(deform_percent, "R", &shape_props[0].value_real)); + channel->props.props.data = shape_props; + channel->props.props.count = num_shape_props; + } + return 1; } @@ -13833,6 +13937,21 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_character(ufbxi_context *uc return 1; } +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_audio_clip(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_audio_clip *audio = ufbxi_push_element(uc, info, ufbx_audio_clip, UFBX_ELEMENT_AUDIO_CLIP); + ufbxi_check(audio); + + audio->filename = ufbx_empty_string; + audio->absolute_filename = ufbx_empty_string; + audio->relative_filename = ufbx_empty_string; + + ufbxi_node *content_node = ufbxi_find_child(node, ufbxi_Content); + ufbxi_check(ufbxi_read_embedded_blob(uc, &audio->content, content_node)); + + return 1; +} + typedef struct { ufbx_constraint_type type; const char *name; @@ -14112,6 +14231,10 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_object(ufbxi_context *uc, u ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_cache_file), UFBX_ELEMENT_CACHE_FILE)); } else if (name == ufbxi_ObjectMetaData) { ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_metadata_object), UFBX_ELEMENT_METADATA_OBJECT)); + } else if (name == ufbxi_AudioLayer) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_audio_layer), UFBX_ELEMENT_AUDIO_LAYER)); + } else if (name == ufbxi_Audio) { + ufbxi_check(ufbxi_read_audio_clip(uc, node, &info)); } else { ufbxi_check(ufbxi_read_unknown(uc, node, &info, type_str, sub_type_str, name)); } @@ -14178,8 +14301,6 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_objects_threaded(ufbxi_cont uc->p_element_id = NULL; } batch->num_nodes = 0; - - ufbxi_thread_pool_flush(&uc->thread_pool); } ufbxi_buf *tmp_buf = &uc->tmp_thread_parse[batch_index]; @@ -15213,7 +15334,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_mesh(ufbxi_context * set->index = 0; set->name.data = ufbxi_empty_char; ufbxi_check(ufbxi_read_vertex_element(uc, mesh, uv_info, (ufbx_vertex_attrib*)&set->vertex_uv, - ufbxi_TextureUV, ufbxi_TextureUVVerticeIndex, 'r', 2)); + ufbxi_TextureUV, ufbxi_TextureUVVerticeIndex, NULL, 'r', 2)); mesh->uv_sets.data = set; mesh->uv_sets.count = 1; @@ -15909,11 +16030,7 @@ static ufbxi_noinline void ufbxi_obj_free(ufbxi_context *uc) ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_read_line(ufbxi_context *uc) { - if (uc->obj.eof) { - uc->obj.line.data = "\n"; - uc->obj.line.length = 1; - return 1; - } + ufbxi_dev_assert(!uc->obj.eof); size_t offset = 0; @@ -17078,7 +17195,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_pre_finalize_scene(ufbxi_context { bool required = false; if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES || uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY) required = true; - if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_HELPER_NODES || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE) required = true; + if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_HELPER_NODES || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK) required = true; if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT) required = true; #if defined(UFBX_REGRESSION) required = true; @@ -17255,6 +17372,8 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_pre_finalize_scene(ufbxi_context pre_nodes[dst->typed_id].has_skin_deformer = true; } } + } else if (src->type == UFBX_ELEMENT_SKIN_DEFORMER) { + pre_nodes[dst->typed_id].has_skin_deformer = true; } } } else if (tmp->src_prop.length == 0 && tmp->dst_prop.length != 0) { @@ -17376,7 +17495,8 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_pre_finalize_scene(ufbxi_context ufbx_real dx = (ufbx_real)ufbx_fabs(scale.x - ref); ufbx_real dy = (ufbx_real)ufbx_fabs(scale.y - ref); ufbx_real dz = (ufbx_real)ufbx_fabs(scale.z - ref); - if (dx + dy + dz >= scale_epsilon || !pre_node->has_constant_scale || (ufbx_real)ufbx_fabs(scale.x) <= compensate_epsilon) { + if ((dx + dy + dz >= scale_epsilon || !pre_node->has_constant_scale || (ufbx_real)ufbx_fabs(scale.x) <= compensate_epsilon) + && uc->opts.inherit_mode_handling != UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK) { ufbxi_check(ufbxi_setup_scale_helper(uc, node, fbx_id)); // If we added a geometry transform helper that may scale further helpers @@ -17415,7 +17535,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_pre_finalize_scene(ufbxi_context } } - } else if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE) { + } else if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK) { if ((ufbx_real)ufbx_fabs(scale.x - 1.0f) >= scale_epsilon) { node->is_scale_compensate_parent = true; } @@ -17939,7 +18059,9 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_dst_elements(ufbxi_context } uc->tmp_element_flag[element_id] = 1; } - ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_element*, 1, &conn->src)); + ufbx_element **p_elem = ufbxi_push(&uc->tmp_stack, ufbx_element*, 1); + ufbxi_check(p_elem); + *p_elem = conn->src; num_elements++; } } @@ -17975,7 +18097,9 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_src_elements(ufbxi_context } uc->tmp_element_flag[element_id] = 1; } - ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_element*, 1, &conn->dst)); + ufbx_element **p_elem = ufbxi_push(&uc->tmp_stack, ufbx_element*, 1); + ufbxi_check(p_elem); + *p_elem = conn->dst; num_elements++; } } @@ -18194,13 +18318,6 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_material_textures(ufbxi_con return 1; } -ufbxi_noinline static bool ufbxi_video_ptr_less(void *user, const void *va, const void *vb) -{ - (void)user; - const ufbx_video *a = *(const ufbx_video**)va, *b = *(const ufbx_video**)vb; - return ufbxi_str_less(a->absolute_filename, b->absolute_filename); -} - static ufbxi_noinline bool ufbxi_bone_pose_less(void *user, const void *va, const void *vb) { (void)user; @@ -18208,13 +18325,6 @@ static ufbxi_noinline bool ufbxi_bone_pose_less(void *user, const void *va, cons return a->bone_node->typed_id < b->bone_node->typed_id; } -ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_videos_by_filename(ufbxi_context *uc, ufbx_video **videos, size_t count) -{ - ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_video*))); - ufbxi_stable_sort(sizeof(ufbx_video*), 32, videos, uc->tmp_arr, count, &ufbxi_video_ptr_less, NULL); - return 1; -} - ufbxi_nodiscard ufbxi_noinline static ufbx_anim_prop *ufbxi_find_anim_prop_start(ufbx_anim_layer *layer, const ufbx_element *element) { size_t index = SIZE_MAX; @@ -19380,11 +19490,11 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_shader_texture(ufbxi_co shader->type = type; - static const char *name_props[] = { + static const char *const name_props[] = { "3dsMax|params|OSLShaderName", }; - static const char *source_props[] = { + static const char *const source_props[] = { "3dsMax|params|OSLCode", }; @@ -19996,7 +20106,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_modify_geometry(ufbxi_context *u if (node->is_root) continue; node->geometry_transform = ufbxi_get_geometry_transform(&node->props, node); - if (!ufbxi_is_transform_identity(node->geometry_transform)) { + if (!ufbxi_is_transform_identity(&node->geometry_transform)) { node->geometry_to_node = ufbx_transform_to_matrix(&node->geometry_transform); node->has_geometry_transform = true; } else { @@ -20167,6 +20277,12 @@ ufbxi_noinline static void ufbxi_postprocess_scene(ufbxi_context *uc) } } } + + if (uc->exporter == UFBX_EXPORTER_BLENDER_BINARY) { + uc->scene.metadata.ortho_size_unit = 1.0f / uc->scene.metadata.geometry_scale; + } else { + uc->scene.metadata.ortho_size_unit = 30.0f; + } } ufbxi_noinline static size_t ufbxi_next_path_segment(const char *data, size_t begin, size_t length) @@ -20263,6 +20379,84 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_filenames(ufbxi_context return 1; } +ufbxi_noinline static bool ufbxi_file_content_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbxi_file_content *a = (const ufbxi_file_content*)va, *b = (const ufbxi_file_content*)vb; + return ufbxi_str_less(a->absolute_filename, b->absolute_filename); +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_file_contents(ufbxi_context *uc, ufbxi_file_content *content, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbxi_file_content))); + ufbxi_stable_sort(sizeof(ufbxi_file_content), 32, content, uc->tmp_arr, count, &ufbxi_file_content_less, NULL); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_push_file_content(ufbxi_context *uc, ufbx_string *p_filename, ufbx_blob *p_data) +{ + if (p_data->size == 0 || p_filename->length == 0) return 1; + ufbxi_file_content *content = ufbxi_push(&uc->tmp_stack, ufbxi_file_content, 1); + ufbxi_check(content); + + content->absolute_filename = *p_filename; + content->content = *p_data; + return 1; +} + +ufbxi_noinline static void ufbxi_fetch_file_content(ufbxi_context *uc, ufbx_string *p_filename, ufbx_blob *p_data) +{ + if (p_data->size > 0) return; + ufbx_string filename = *p_filename; + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbxi_file_content, 8, &index, uc->file_content, 0, uc->num_file_content, + ( ufbxi_str_less(a->absolute_filename, filename) ), + ( a->absolute_filename.data == filename.data )); + if (index != SIZE_MAX) { + *p_data = uc->file_content[index].content; + } +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_file_content(ufbxi_context *uc) +{ + size_t initial_stack = uc->tmp_stack.num_items; + + ufbxi_for_ptr_list(ufbx_video, p_video, uc->scene.videos) { + ufbx_video *video = *p_video; + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&video->filename, (ufbxi_strblob*)&video->absolute_filename, (ufbxi_strblob*)&video->relative_filename, false)); + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&video->raw_filename, (ufbxi_strblob*)&video->raw_absolute_filename, (ufbxi_strblob*)&video->raw_relative_filename, true)); + ufbxi_check(ufbxi_push_file_content(uc, &video->absolute_filename, &video->content)); + } + + ufbxi_for_ptr_list(ufbx_audio_clip, p_clip, uc->scene.audio_clips) { + ufbx_audio_clip *clip = *p_clip; + clip->absolute_filename = ufbx_find_string(&clip->props, "Path", ufbx_empty_string); + clip->relative_filename = ufbx_find_string(&clip->props, "RelPath", ufbx_empty_string); + clip->raw_absolute_filename = ufbx_find_blob(&clip->props, "Path", ufbx_empty_blob); + clip->raw_relative_filename = ufbx_find_blob(&clip->props, "RelPath", ufbx_empty_blob); + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&clip->filename, (ufbxi_strblob*)&clip->absolute_filename, (ufbxi_strblob*)&clip->relative_filename, false)); + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&clip->raw_filename, (ufbxi_strblob*)&clip->raw_absolute_filename, (ufbxi_strblob*)&clip->raw_relative_filename, true)); + ufbxi_check(ufbxi_push_file_content(uc, &clip->absolute_filename, &clip->content)); + } + + uc->num_file_content = uc->tmp_stack.num_items - initial_stack; + uc->file_content = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_file_content, uc->num_file_content); + ufbxi_check(uc->file_content); + ufbxi_check(ufbxi_sort_file_contents(uc, uc->file_content, uc->num_file_content)); + + ufbxi_for_ptr_list(ufbx_video, p_video, uc->scene.videos) { + ufbx_video *video = *p_video; + ufbxi_fetch_file_content(uc, &video->absolute_filename, &video->content); + } + + ufbxi_for_ptr_list(ufbx_audio_clip, p_clip, uc->scene.audio_clips) { + ufbx_audio_clip *clip = *p_clip; + ufbxi_fetch_file_content(uc, &clip->absolute_filename, &clip->content); + } + + return 1; +} + ufbxi_nodiscard ufbxi_noinline static int ufbxi_validate_indices(ufbxi_context *uc, ufbx_uint32_list *indices, size_t max_index) { if (max_index == 0 && uc->opts.index_error_handling == UFBX_INDEX_ERROR_HANDLING_CLAMP) { @@ -20281,6 +20475,19 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_validate_indices(ufbxi_context * return 1; } +static bool ufbxi_material_part_usage_less(void *user, const void *va, const void *vb) +{ + ufbx_mesh_part *parts = (ufbx_mesh_part*)user; + uint32_t a = *(const uint32_t*)va, b = *(const uint32_t*)vb; + ufbx_mesh_part *pa = &parts[a]; + ufbx_mesh_part *pb = &parts[b]; + if (pa->face_indices.count == 0 || pb->face_indices.count == 0) { + if (pa->face_indices.count == pb->face_indices.count) return a < b; + return pa->face_indices.count > pb->face_indices.count; + } + return pa->face_indices.data[0] < pb->face_indices.data[0]; +} + ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_mesh_material(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh) { size_t num_materials = mesh->materials.count; @@ -20330,6 +20537,14 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_mesh_material(ufbxi_buf part->face_indices.data[part->num_faces++] = (uint32_t)i; } } + + mesh->material_part_usage_order.count = num_parts; + mesh->material_part_usage_order.data = ufbxi_push(buf, uint32_t, num_parts); + ufbxi_check_err(error, mesh->material_part_usage_order.data); + for (size_t i = 0; i < num_parts; i++) { + mesh->material_part_usage_order.data[i] = (uint32_t)i; + } + ufbxi_unstable_sort(mesh->material_part_usage_order.data, num_parts, sizeof(uint32_t), &ufbxi_material_part_usage_less, parts); } return 1; @@ -20484,6 +20699,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc case UFBX_ELEMENT_MESH: node->mesh = (ufbx_mesh*)elem; break; case UFBX_ELEMENT_LIGHT: node->light = (ufbx_light*)elem; break; case UFBX_ELEMENT_CAMERA: node->camera = (ufbx_camera*)elem; break; + case UFBX_ELEMENT_BONE: node->bone = (ufbx_bone*)elem; break; default: /* No shorthand */ break; } } @@ -20716,6 +20932,10 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc ufbxi_check(ufbxi_sort_blend_keyframes(uc, channel->keyframes.data, channel->keyframes.count)); full_weights++; + + if (channel->keyframes.count > 0) { + channel->target_shape = channel->keyframes.data[channel->keyframes.count - 1].shape; + } } ufbxi_buf_free(&uc->tmp_full_weights); @@ -20815,6 +21035,8 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc part->num_line_faces = mesh->num_line_faces; part->face_indices.data = uc->consecutive_indices; part->face_indices.count = mesh->num_faces; + mesh->material_part_usage_order.data = uc->zero_indices; + mesh->material_part_usage_order.count = 1; } if (mesh->materials.count == 1) { @@ -21014,7 +21236,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc if (material->shader) { material->shader_type = material->shader->type; } else { - if (uc->exporter == UFBX_EXPORTER_BLENDER_BINARY && uc->exporter_version >= ufbx_pack_version(4,12,0)) { + if (uc->opts.use_blender_pbr_material && uc->exporter == UFBX_EXPORTER_BLENDER_BINARY && uc->exporter_version >= ufbx_pack_version(4,12,0)) { material->shader_type = UFBX_SHADER_BLENDER_PHONG; } @@ -21149,39 +21371,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc } } - // HACK: If there are multiple textures in an FBX file that use the same embedded - // texture they get duplicated Video elements instead of a shared one _and only one - // of them has the content?!_ So let's gather all Video instances with content and - // sort them by filename so we can patch the other ones.. - ufbx_video **content_videos = ufbxi_push(&uc->tmp, ufbx_video*, uc->scene.videos.count); - ufbxi_check(content_videos); - - size_t num_content_videos = 0; - ufbxi_for_ptr_list(ufbx_video, p_video, uc->scene.videos) { - ufbx_video *video = *p_video; - ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&video->filename, (ufbxi_strblob*)&video->absolute_filename, (ufbxi_strblob*)&video->relative_filename, false)); - ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&video->raw_filename, (ufbxi_strblob*)&video->raw_absolute_filename, (ufbxi_strblob*)&video->raw_relative_filename, true)); - if (video->content.size > 0) { - content_videos[num_content_videos++] = video; - } - } - - if (num_content_videos > 0) { - ufbxi_check(ufbxi_sort_videos_by_filename(uc, content_videos, num_content_videos)); - - ufbxi_for_ptr_list(ufbx_video, p_video, uc->scene.videos) { - ufbx_video *video = *p_video; - if (video->content.size > 0) continue; - - size_t index = SIZE_MAX; - ufbxi_macro_lower_bound_eq(ufbx_video*, 16, &index, content_videos, 0, num_content_videos, - ( ufbxi_str_less((*a)->absolute_filename, video->absolute_filename) ), - ( (*a)->absolute_filename.data == video->absolute_filename.data )); - if (index != SIZE_MAX) { - video->content = content_videos[index]->content; - } - } - } + ufbxi_check(ufbxi_resolve_file_content(uc)); ufbxi_for_ptr_list(ufbx_texture, p_texture, uc->scene.textures) { ufbx_texture *texture = *p_texture; @@ -21302,6 +21492,11 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc ufbxi_check(constraint->targets.data); } + ufbxi_for_ptr_list(ufbx_audio_layer, p_layer, uc->scene.audio_layers) { + ufbx_audio_layer *layer = *p_layer; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &layer->clips, &layer->element, false, true, NULL, UFBX_ELEMENT_AUDIO_CLIP)); + } + ufbxi_for_ptr_list(ufbx_lod_group, p_lod, uc->scene.lod_groups) { ufbxi_check(ufbxi_finalize_lod_group(uc, *p_lod)); } @@ -21370,7 +21565,7 @@ static ufbxi_forceinline void ufbxi_mul_scale_real(ufbx_transform *t, ufbx_real t->scale.z *= v; } -static ufbxi_forceinline ufbx_quat ufbxi_mul_quat(ufbx_quat a, ufbx_quat b) +static ufbxi_noinline ufbx_quat ufbxi_mul_quat(ufbx_quat a, ufbx_quat b) { ufbx_quat r; r.x = a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y; @@ -21395,7 +21590,7 @@ static ufbxi_forceinline void ufbxi_add_weighted_quat(ufbx_quat *r, ufbx_quat b, r->w += b.w * w; } -static ufbxi_forceinline void ufbxi_add_weighted_mat(ufbx_matrix *r, const ufbx_matrix *b, ufbx_real w) +static ufbxi_noinline void ufbxi_add_weighted_mat(ufbx_matrix *r, const ufbx_matrix *b, ufbx_real w) { ufbxi_add_weighted_vec3(&r->cols[0], b->cols[0], w); ufbxi_add_weighted_vec3(&r->cols[1], b->cols[1], w); @@ -21719,7 +21914,7 @@ ufbxi_noinline static void ufbxi_update_node(ufbx_node *node, const ufbx_transfo node->unscaled_node_to_world = unscaled_node_to_parent; } - if (!ufbxi_is_transform_identity(node->geometry_transform)) { + if (!ufbxi_is_transform_identity(&node->geometry_transform)) { node->geometry_to_node = ufbx_transform_to_matrix(&node->geometry_transform); node->geometry_to_world = ufbx_matrix_mul(&node->node_to_world, &node->geometry_to_node); node->has_geometry_transform = true; @@ -21794,7 +21989,7 @@ ufbxi_noinline static void ufbxi_update_camera(ufbx_scene *scene, ufbx_camera *c ufbx_real fov_y = ufbxi_find_real(&camera->props, ufbxi_FieldOfViewY, 0.0f); ufbx_real focal_length = ufbxi_find_real(&camera->props, ufbxi_FocalLength, 0.0f); - ufbx_real ortho_extent = (ufbx_real)30.0 * ufbxi_find_real(&camera->props, ufbxi_OrthoZoom, 1.0f); + ufbx_real ortho_extent = scene->metadata.ortho_size_unit * ufbxi_find_real(&camera->props, ufbxi_OrthoZoom, 1.0f); ufbxi_aperture_format format = ufbxi_aperture_formats[camera->aperture_format]; ufbx_vec2 film_size = { (ufbx_real)format.film_size_x * (ufbx_real)0.001, (ufbx_real)format.film_size_y * (ufbx_real)0.001 }; @@ -22042,7 +22237,7 @@ ufbxi_noinline static void ufbxi_update_material(ufbx_scene *scene, ufbx_materia ufbxi_noinline static void ufbxi_update_texture(ufbx_texture *texture) { texture->uv_transform = ufbxi_get_texture_transform(&texture->props); - if (!ufbxi_is_transform_identity(texture->uv_transform)) { + if (!ufbxi_is_transform_identity(&texture->uv_transform)) { texture->has_uv_transform = true; texture->texture_to_uv = ufbx_transform_to_matrix(&texture->uv_transform); texture->uv_to_texture = ufbx_matrix_invert(&texture->texture_to_uv); @@ -22204,7 +22399,7 @@ static ufbxi_forceinline void ufbxi_mirror_matrix_src(ufbx_matrix *m, ufbx_mirro m->cols[ax].z = -m->cols[ax].z; } -static ufbxi_forceinline void ufbxi_mirror_matrix(ufbx_matrix *m, ufbx_mirror_axis axis) +static ufbxi_noinline void ufbxi_mirror_matrix(ufbx_matrix *m, ufbx_mirror_axis axis) { if (axis == 0) return; ufbxi_mirror_matrix_src(m, axis); @@ -22466,7 +22661,10 @@ ufbxi_noinline static void ufbxi_update_adjust_transforms(ufbxi_context *uc, ufb } if (parent->is_scale_compensate_parent && node->original_inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) { ufbx_vec3 scale = ufbxi_find_vec3(&parent->props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f); - node->adjust_post_scale *= 1.0f / scale.x; + ufbx_real size = scale.x; + if (ufbx_fabs(scale.y - 1.0f) < ufbx_fabs(size - 1.0f)) size = scale.y; + if (ufbx_fabs(scale.z - 1.0f) < ufbx_fabs(size - 1.0f)) size = scale.z; + node->adjust_post_scale *= 1.0f / size; node->has_adjust_transform = true; } } @@ -22613,6 +22811,19 @@ static ufbxi_noinline void ufbxi_update_scene_settings(ufbx_scene_settings *sett } } +static ufbxi_noinline void ufbxi_update_scene_settings_obj(ufbxi_context *uc) +{ + ufbx_scene_settings *settings = &uc->scene.settings; + settings->original_unit_meters = settings->unit_meters = uc->opts.obj_unit_meters; + if (ufbx_coordinate_axes_valid(uc->opts.obj_axes)) { + settings->axes = uc->opts.obj_axes; + } else { + settings->axes.right = UFBX_COORDINATE_AXIS_UNKNOWN; + settings->axes.up = UFBX_COORDINATE_AXIS_UNKNOWN; + settings->axes.front = UFBX_COORDINATE_AXIS_UNKNOWN; + } +} + // -- Geometry caches #if UFBXI_FEATURE_GEOMETRY_CACHE @@ -23306,10 +23517,6 @@ static ufbxi_noinline int ufbxi_cache_setup_channels(ufbxi_cache_context *cc) static ufbxi_noinline int ufbxi_cache_load_imp(ufbxi_cache_context *cc, ufbx_string filename) { - // `ufbx_geometry_cache_opts` must be cleared to zero first! - ufbx_assert(cc->opts._begin_zero == 0 && cc->opts._end_zero == 0); - ufbxi_check_err_msg(&cc->error, cc->opts._begin_zero == 0 && cc->opts._end_zero == 0, "Uninitialized options"); - cc->tmp.ator = cc->ator_tmp; cc->tmp_stack.ator = cc->ator_tmp; @@ -23473,14 +23680,15 @@ typedef struct { size_t data_size; } ufbxi_external_file; -static int ufbxi_cmp_external_file(const void *va, const void *vb) +static bool ufbxi_less_external_file(void *user, const void *va, const void *vb) { + (void)user; const ufbxi_external_file *a = (const ufbxi_external_file*)va, *b = (const ufbxi_external_file*)vb; - if (a->type != b->type) return a->type < b->type ? -1 : 1; + if (a->type != b->type) return a->type < b->type; int cmp = ufbxi_str_cmp(a->filename, b->filename); - if (cmp != 0) return cmp; - if (a->index != b->index) return a->index < b->index ? -1 : 1; - return 0; + if (cmp != 0) return cmp < 0; + if (a->index != b->index) return a->index < b->index; + return false; } ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_external_cache(ufbxi_context *uc, ufbxi_external_file *file) @@ -23568,7 +23776,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_external_files(ufbxi_contex // Sort and load the external files ufbxi_external_file *files = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_external_file, num_files); ufbxi_check(files); - qsort(files, num_files, sizeof(ufbxi_external_file), &ufbxi_cmp_external_file); + ufbxi_unstable_sort(files, num_files, sizeof(ufbxi_external_file), &ufbxi_less_external_file, NULL); ufbxi_external_file_type prev_type = UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE; const char *prev_name = NULL; @@ -23641,7 +23849,7 @@ static ufbxi_noinline void ufbxi_transform_to_axes(ufbxi_context *uc, ufbx_coord if (uc->opts.space_conversion == UFBX_SPACE_CONVERSION_TRANSFORM_ROOT) { ufbx_matrix axis_mat = uc->axis_matrix; - if (!ufbxi_is_transform_identity(uc->scene.root_node->local_transform)) { + if (!ufbxi_is_transform_identity(&uc->scene.root_node->local_transform)) { ufbx_matrix root_mat = ufbx_transform_to_matrix(&uc->scene.root_node->local_transform); axis_mat = ufbx_matrix_mul(&root_mat, &axis_mat); } @@ -23878,10 +24086,6 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_imp(ufbxi_context *uc) { // Check for deferred failure if (uc->deferred_failure) return 0; - - // `ufbx_load_opts` must be cleared to zero first! - ufbx_assert(uc->opts._begin_zero == 0 && uc->opts._end_zero == 0); - ufbxi_check_msg(uc->opts._begin_zero == 0 && uc->opts._end_zero == 0, "Uninitialized options"); ufbxi_check(uc->opts.path_separator >= 0x20 && uc->opts.path_separator <= 0x7e); ufbxi_check(ufbxi_fixup_opts_string(uc, &uc->opts.filename, false)); @@ -23914,6 +24118,8 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_imp(ufbxi_context *uc) uc->data_begin = uc->data = ufbxi_zero_size_buffer; } + uc->retain_vertex_w = (uc->opts.retain_dom || uc->opts.retain_vertex_attrib_w) && !uc->opts.ignore_geometry; + ufbxi_check(ufbxi_load_strings(uc)); ufbxi_check(ufbxi_load_maps(uc)); ufbxi_check(ufbxi_determine_format(uc)); @@ -23953,6 +24159,9 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_imp(ufbxi_context *uc) ufbxi_check(ufbxi_finalize_scene(uc)); ufbxi_update_scene_settings(&uc->scene.settings); + if (uc->scene.metadata.file_format == UFBX_FILE_FORMAT_OBJ) { + ufbxi_update_scene_settings_obj(uc); + } // Axis conversion if (ufbx_coordinate_axes_valid(uc->opts.target_axes)) { @@ -24308,7 +24517,7 @@ typedef struct ufbxi_anim_layer_combine_ctx { bool has_rotation_order; } ufbxi_anim_layer_combine_ctx; -static double ufbxi_pow_abs(double v, double e) +static ufbxi_noinline double ufbxi_pow_abs(double v, double e) { if (e <= 0.0) return 1.0; if (e >= 1.0) return v; @@ -24640,6 +24849,13 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_translate_element_list(ufbxi_eva return 1; } +static ufbxi_noinline void ufbxi_translate_maps(ufbxi_eval_context *ec, ufbx_material_map *maps, size_t count) +{ + ufbxi_nounroll ufbxi_for(ufbx_material_map, map, maps, count) { + map->texture = (ufbx_texture*)ufbxi_translate_element(ec, map->texture); + } +} + ufbxi_nodiscard static ufbxi_noinline int ufbxi_translate_anim(ufbxi_eval_context *ec, ufbx_anim **p_anim) { ufbx_anim *anim = ufbxi_push_copy(&ec->result, ufbx_anim, 1, *p_anim); @@ -24651,10 +24867,6 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_translate_anim(ufbxi_eval_contex ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_imp(ufbxi_eval_context *ec) { - // `ufbx_evaluate_opts` must be cleared to zero first! - ufbx_assert(ec->opts._begin_zero == 0 && ec->opts._end_zero == 0); - ufbxi_check_err_msg(&ec->error, ec->opts._begin_zero == 0 && ec->opts._end_zero == 0, "Uninitialized options"); - ec->scene = ec->src_scene; size_t num_elements = ec->scene.elements.count; @@ -24724,6 +24936,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_imp(ufbxi_eval_context node->mesh = (ufbx_mesh*)ufbxi_translate_element(ec, node->mesh); node->light = (ufbx_light*)ufbxi_translate_element(ec, node->light); node->camera = (ufbx_camera*)ufbxi_translate_element(ec, node->camera); + node->bone = (ufbx_bone*)ufbxi_translate_element(ec, node->bone); node->inherit_scale_node = (ufbx_node*)ufbxi_translate_element(ec, node->inherit_scale_node); node->scale_helper = (ufbx_node*)ufbxi_translate_element(ec, node->scale_helper); node->bind_pose = (ufbx_pose*)ufbxi_translate_element(ec, node->bind_pose); @@ -24780,6 +24993,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_imp(ufbxi_eval_context keys[i].shape = (ufbx_blend_shape*)ufbxi_translate_element(ec, keys[i].shape); } chan->keyframes.data = keys; + chan->target_shape = (ufbx_blend_shape*)ufbxi_translate_element(ec, chan->target_shape); } ufbxi_for_ptr_list(ufbx_cache_deformer, p_deformer, ec->scene.cache_deformers) { @@ -24791,14 +25005,8 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_imp(ufbxi_eval_context ufbx_material *material = *p_material; material->shader = (ufbx_shader*)ufbxi_translate_element(ec, material->shader); - for (size_t i = 0; i < UFBX_MATERIAL_FBX_MAP_COUNT; i++) { - ufbx_material_map *map = &material->fbx.maps[i]; - map->texture = (ufbx_texture*)ufbxi_translate_element(ec, map->texture); - } - for (size_t i = 0; i < UFBX_MATERIAL_PBR_MAP_COUNT; i++) { - ufbx_material_map *map = &material->pbr.maps[i]; - map->texture = (ufbx_texture*)ufbxi_translate_element(ec, map->texture); - } + ufbxi_translate_maps(ec, material->fbx.maps, UFBX_MATERIAL_FBX_MAP_COUNT); + ufbxi_translate_maps(ec, material->pbr.maps, UFBX_MATERIAL_PBR_MAP_COUNT); ufbx_material_texture *textures = ufbxi_push(&ec->result, ufbx_material_texture, material->textures.count); ufbxi_check_err(&ec->error, textures); @@ -24876,6 +25084,12 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_imp(ufbxi_eval_context constraint->targets.data = targets; } + ufbxi_for_ptr_list(ufbx_audio_layer, p_layer, ec->scene.audio_layers) { + ufbx_audio_layer *layer = *p_layer; + + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &layer->clips)); + } + ufbxi_for_ptr_list(ufbx_anim_stack, p_stack, ec->scene.anim_stacks) { ufbx_anim_stack *stack = *p_stack; @@ -25075,19 +25289,21 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_anim_string(ufbxi_create_an return 1; } -static int ufbxi_cmp_prop_override_prop_name(const void *va, const void *vb) +static bool ufbxi_prop_override_prop_name_less(void *user, const void *va, const void *vb) { + (void)user; const ufbx_prop_override *a = (const ufbx_prop_override*)va, *b = (const ufbx_prop_override*)vb; - if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key ? -1 : 1; - return ufbxi_str_cmp(a->prop_name, b->prop_name); + if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key; + return ufbxi_str_less(a->prop_name, b->prop_name); } -static int ufbxi_cmp_prop_override(const void *va, const void *vb) +static bool ufbxi_prop_override_less(void *user, const void *va, const void *vb) { + (void)user; const ufbx_prop_override *a = (const ufbx_prop_override*)va, *b = (const ufbx_prop_override*)vb; - if (a->element_id != b->element_id) return a->element_id < b->element_id ? -1 : 1; - if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key ? -1 : 1; - return strcmp(a->prop_name.data, b->prop_name.data); + if (a->element_id != b->element_id) return a->element_id < b->element_id; + if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key; + return strcmp(a->prop_name.data, b->prop_name.data) < 0; } static int ufbxi_cmp_transform_override(const void *va, const void *vb) @@ -25102,10 +25318,6 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_create_anim_imp(ufbxi_create_ani const ufbx_scene *scene = ac->scene; ufbx_anim *anim = &ac->anim; - // `ufbx_anim_opts` must be cleared to zero first! - ufbx_assert(ac->opts._begin_zero == 0 && ac->opts._end_zero == 0); - ufbxi_check_err_msg(&ac->error, ac->opts._begin_zero == 0 && ac->opts._end_zero == 0, "Uninitialized options"); - ufbxi_init_ator(&ac->error, &ac->ator_result, &ac->opts.result_allocator, "result"); ac->result.unordered = true; ac->result.ator = &ac->ator_result; @@ -25159,7 +25371,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_create_anim_imp(ufbxi_create_ani // Sort `anim->prop_overrides` first by `prop_name` only so we can deduplicate and // convert them to global strings in `ufbxi_strings[]` if possible. - qsort(anim->prop_overrides.data, anim->prop_overrides.count, sizeof(ufbx_prop_override), &ufbxi_cmp_prop_override_prop_name); + ufbxi_unstable_sort(anim->prop_overrides.data, anim->prop_overrides.count, sizeof(ufbx_prop_override), &ufbxi_prop_override_prop_name_less, NULL); const ufbx_string *global_str = ufbxi_strings, *global_end = global_str + ufbxi_arraycount(ufbxi_strings); ufbx_string prev_name = { ufbxi_empty_char }; @@ -25187,7 +25399,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_create_anim_imp(ufbxi_create_ani } // Sort `anim->prop_overrides` to the actual order expected by evaluation. - qsort(anim->prop_overrides.data, anim->prop_overrides.count, sizeof(ufbx_prop_override), &ufbxi_cmp_prop_override); + ufbxi_unstable_sort(anim->prop_overrides.data, anim->prop_overrides.count, sizeof(ufbx_prop_override), &ufbxi_prop_override_less, NULL); for (size_t i = 1; i < prop_overrides.count; i++) { const ufbx_prop_override *prev = &anim->prop_overrides.data[i - 1]; @@ -25230,7 +25442,12 @@ typedef struct { #if UFBXI_FEATURE_ANIMATION_BAKING -UFBX_LIST_TYPE(ufbxi_double_list, double); +typedef struct { + double time; + uint32_t flags; +} ufbxi_bake_time; + +UFBX_LIST_TYPE(ufbxi_bake_time_list, ufbxi_bake_time); typedef struct { ufbx_error error; @@ -25247,7 +25464,7 @@ typedef struct { ufbxi_buf tmp_props; ufbxi_buf tmp_bake_stack; - ufbxi_double_list layer_weight_times; + ufbxi_bake_time_list layer_weight_times; ufbx_baked_node **baked_nodes; bool *nodes_to_bake; @@ -25256,8 +25473,12 @@ typedef struct { const ufbx_anim *anim; ufbx_bake_opts opts; + double ktime_offset; + double time_begin; double time_end; + double time_min; + double time_max; ufbx_baked_anim bake; ufbxi_baked_anim_imp *imp; @@ -25280,23 +25501,36 @@ static int ufbxi_cmp_bake_prop(const void *va, const void *vb) return a->anim_value < b->anim_value; } -static int ufbxi_cmp_double(const void *va, const void *vb) +ufbx_static_assert(bake_step_left, UFBX_BAKED_KEY_STEP_LEFT == 0x1); +ufbx_static_assert(bake_step_right, UFBX_BAKED_KEY_STEP_RIGHT == 0x2); +ufbx_static_assert(bake_step_key, UFBX_BAKED_KEY_STEP_KEY == 0x4); +static ufbxi_forceinline int ufbxi_cmp_bake_time(ufbxi_bake_time a, ufbxi_bake_time b) { - const double a = *(const double*)va; - const double b = *(const double*)vb; - if (a != b) return a < b ? -1 : 1; + if (a.time != b.time) return a.time < b.time ? -1 : 1; + // Bit twiddling for a fast sorting of `0x1 (LEFT) < 0x0 < 0x2 (RIGHT)` + // by `step ^ 1`: `0x0 (LEFT) < 0x1 < 0x3 (RIGHT)` + uint32_t a_step = a.flags & 0x3, b_step = b.flags & 0x3; + if (a_step != b_step) return (a_step ^ 0x1) < (b_step ^ 0x1) ? -1 : 1; return 0; } -ufbxi_nodiscard static ufbxi_forceinline int ufbxi_bake_push_time(ufbxi_bake_context *bc, double time) +static int ufbxi_cmp_bake_time_fn(const void *va, const void *vb) +{ + const ufbxi_bake_time a = *(const ufbxi_bake_time*)va; + const ufbxi_bake_time b = *(const ufbxi_bake_time*)vb; + return ufbxi_cmp_bake_time(a, b); +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_bake_push_time(ufbxi_bake_context *bc, double time, uint32_t flags) { - double *p_key = ufbxi_push_fast(&bc->tmp_times, double, 1); + ufbxi_bake_time *p_key = ufbxi_push_fast(&bc->tmp_times, ufbxi_bake_time, 1); if (!p_key) return 0; - *p_key = time; + p_key->time = time; + p_key->flags = flags; return 1; } -ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_times(ufbxi_bake_context *bc, const ufbx_anim_value *anim_value, bool resample_linear) +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_times(ufbxi_bake_context *bc, const ufbx_anim_value *anim_value, bool resample_linear, uint32_t key_flag) { double sample_rate = bc->opts.resample_rate; double min_duration = bc->opts.minimum_sample_rate > 0.0 ? 1.0 / bc->opts.minimum_sample_rate : 0.0; @@ -25309,25 +25543,19 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_times(ufbxi_bake_context *b size_t num_keys = curve->keyframes.count; for (size_t key_ix = 0; key_ix < num_keys; key_ix++) { ufbx_keyframe a = keys[key_ix]; - double a_time = a.time - bc->opts.time_start_offset; - ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, a_time)); + double a_time = a.time; + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, a_time, key_flag)); if (key_ix + 1 >= num_keys) break; ufbx_keyframe b = keys[key_ix + 1]; - double b_time = b.time - bc->opts.time_start_offset; + double b_time = b.time; // Skip fully flat sections if (a.value == b.value && a.right.dy == 0.0f && b.left.dy == 0.0f) continue; if (a.interpolation == UFBX_INTERPOLATION_CONSTANT_PREV) { - double time = b_time - bc->opts.constant_timestep; - if (time >= b_time) time = ufbx_nextafter(time, -UFBX_INFINITY); - if (time <= a_time) time = ufbx_nextafter(a_time, UFBX_INFINITY); - ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, time)); + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, b_time, UFBX_BAKED_KEY_STEP_LEFT)); } else if (a.interpolation == UFBX_INTERPOLATION_CONSTANT_NEXT) { - double time = a_time + bc->opts.constant_timestep; - if (time <= a_time) time = ufbx_nextafter(time, UFBX_INFINITY); - if (time >= b_time) time = ufbx_nextafter(b_time, -UFBX_INFINITY); - ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, time)); + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, a_time, UFBX_BAKED_KEY_STEP_RIGHT)); } else if ((resample_linear || a.interpolation == UFBX_INTERPOLATION_CUBIC) && sample_rate > 0.0) { double duration = b_time - a_time; if (duration <= min_duration) continue; @@ -25343,7 +25571,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_times(ufbxi_bake_context *b for (size_t i = 0; i < bc->opts.max_keyframe_segments; i++) { double time = (start + (double)i * factor) / sample_rate; if (time >= stop) break; - ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, time)); + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, time, 0)); } } } @@ -25377,34 +25605,67 @@ ufbxi_nodiscard static ufbxi_noinline bool ufbxi_in_list(const char *const *item return false; } -ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_bake_times(ufbxi_bake_context *bc, ufbxi_double_list *p_dst) +ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_bake_times(ufbxi_bake_context *bc, ufbxi_bake_time_list *p_dst) { if (bc->layer_weight_times.count > 0) { - ufbxi_check_err(&bc->error, ufbxi_push_copy(&bc->tmp_times, double, bc->layer_weight_times.count, bc->layer_weight_times.data)); + ufbxi_check_err(&bc->error, ufbxi_push_copy(&bc->tmp_times, ufbxi_bake_time, bc->layer_weight_times.count, bc->layer_weight_times.data)); } if (bc->tmp_times.num_items == 0) { - ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, bc->time_begin)); - ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, bc->time_end)); + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, bc->time_begin, 0)); + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, bc->time_end, 0)); } size_t num_times = bc->tmp_times.num_items; - double *times = ufbxi_push_pop(&bc->tmp_prop, &bc->tmp_times, double, num_times); + ufbxi_bake_time *times = ufbxi_push_pop(&bc->tmp_prop, &bc->tmp_times, ufbxi_bake_time, num_times); ufbxi_check_err(&bc->error, times); // TODO: Something better - qsort(times, num_times, sizeof(double), &ufbxi_cmp_double); + qsort(times, num_times, sizeof(ufbxi_bake_time), &ufbxi_cmp_bake_time_fn); // Deduplicate times - { - size_t dst = 0, src = 0; - while (src < num_times) { - if (src + 1 < num_times && times[src] == times[src + 1]) { - src++; - } else if (dst != src) { - times[dst++] = times[src++]; - } else { - dst++; src++; + if (num_times > 0) { + size_t dst = 0; + ufbxi_bake_time prev = times[0]; + for (size_t src = 1; src < num_times; src++) { + ufbxi_bake_time next = times[src]; + // Merge keys with the same time and step flags `(0x1, 0x2)` + if (next.time == prev.time) { + if (((next.flags ^ prev.flags) & 0x3) == 0) { + prev.flags |= next.flags; + continue; + } else if (prev.flags & UFBX_BAKED_KEY_STEP_LEFT) { + next.flags |= UFBX_BAKED_KEY_STEP_KEY; + } else if (next.flags & UFBX_BAKED_KEY_STEP_RIGHT) { + prev.flags |= UFBX_BAKED_KEY_STEP_KEY; + } + } + + times[dst++] = prev; + prev = next; + } + times[dst++] = prev; + num_times = dst; + } + + // Cull too close resampled keys, these may arise during merging multiple times + if (num_times > 0) { + double min_dist = 0.25 / bc->opts.resample_rate; + uint32_t keep_flags = UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT|UFBX_BAKED_KEY_STEP_KEY|UFBX_BAKED_KEY_KEYFRAME; + + size_t dst = 0; + for (size_t src = 0; src < num_times; src++) { + ufbxi_bake_time cur = times[src]; + double delta = UFBX_INFINITY; + + bool keep = true; + if ((cur.flags & keep_flags) == 0) { + if (dst > 0) delta = cur.time - times[dst - 1].time; + if (src + 1 < num_times) delta = ufbx_fmin(delta, times[src + 1].time - cur.time); + if (delta < min_dist) keep = false; + } + if (keep) { + times[dst++] = cur; } } num_times = dst; @@ -25414,27 +25675,42 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_bake_times(ufbxi_bake_c if (bc->opts.maximum_sample_rate > 0.0) { const double epsilon = 0.0078125 / bc->opts.maximum_sample_rate; double sample_rate = bc->opts.maximum_sample_rate; + double max_interval = 1.0 / bc->opts.maximum_sample_rate; double min_interval = 1.0 / bc->opts.maximum_sample_rate - epsilon; size_t dst = 0, src = 0; - double prev_time = -UFBX_INFINITY; + // Pre-expand constant keyframes + for (size_t i = 0; i < num_times; i++) { + if ((times[i].flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) { + double sign = (times[i].flags & UFBX_BAKED_KEY_STEP_LEFT) != 0 ? -1.0 : 1.0; + double time = times[i].time + sign * max_interval; + if (i > 0) time = ufbx_fmax(time, times[i - 1].time); + if (i + 1 < num_times) time = ufbx_fmin(time, times[i + 1].time); + times[i].time = time; + times[i].flags = UFBX_BAKED_KEY_REDUCED; + } + } + + ufbxi_bake_time prev_time = { -UFBX_INFINITY }; while (src < num_times) { - double src_time = times[src]; + ufbxi_bake_time src_time = times[src]; src++; size_t start_src = src; - double next_time = ufbx_ceil(src_time * sample_rate - epsilon) / sample_rate; - while (src < num_times && times[src] <= next_time + epsilon) { + ufbxi_bake_time next_time; + next_time.time = ufbx_ceil(src_time.time * sample_rate - epsilon) / sample_rate; + next_time.flags = UFBX_BAKED_KEY_REDUCED; + while (src < num_times && times[src].time <= next_time.time + epsilon) { src++; } - if (src != start_src || src_time - prev_time <= min_interval) { + if (src != start_src || src_time.time - prev_time.time <= min_interval) { prev_time = next_time; } else { prev_time = src_time; } - if (dst == 0 || prev_time > times[dst - 1]) { + if (dst == 0 || prev_time.time > times[dst - 1].time) { times[dst++] = prev_time; } } @@ -25442,16 +25718,97 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_bake_times(ufbxi_bake_c num_times = dst; } + if (num_times > 0) { + if (times[0].time < bc->time_min) bc->time_min = times[0].time; + if (times[num_times - 1].time > bc->time_max) bc->time_max = times[num_times - 1].time; + } + p_dst->data = times; p_dst->count = num_times; return 1; } -ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_reduce_vec3(ufbxi_bake_context *bc, ufbx_baked_vec3_list *p_dst, bool *p_constant, ufbx_baked_vec3_list src) +#define ufbxi_add_epsilon(a, epsilon) ((a)>0 ? (a)*(epsilon) : (a)/(epsilon)) +#define ufbxi_sub_epsilon(a, epsilon) ((a)>0 ? (a)/(epsilon) : (a)*(epsilon)) + +static ufbxi_noinline bool ufbxi_postprocess_step(ufbxi_bake_context *bc, double prev_time, double next_time, double *p_time, uint32_t flags) +{ + ufbxi_dev_assert((flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0); + bool left = (flags & UFBX_BAKED_KEY_STEP_LEFT) != 0; + + double step = 0.001; + double epsilon = 1.0 + FLT_EPSILON * 4.0f; + + double time = *p_time; + switch (bc->opts.step_handling) { + case UFBX_BAKE_STEP_HANDLING_DEFAULT: + break; + case UFBX_BAKE_STEP_HANDLING_CUSTOM_DURATION: + step = bc->opts.step_custom_duration; + epsilon = 1.0 + bc->opts.step_custom_epsilon; + break; + case UFBX_BAKE_STEP_HANDLING_IDENTICAL_TIME: + return true; + case UFBX_BAKE_STEP_HANDLING_ADJACENT_DOUBLE: + if (left) { + *p_time = time = ufbx_nextafter(time, -UFBX_INFINITY); + return time > prev_time; + } else { + *p_time = time = ufbx_nextafter(time, UFBX_INFINITY); + return time < next_time; + } + case UFBX_BAKE_STEP_HANDLING_IGNORE: + return false; + default: + ufbxi_unreachable("Unhandled bake step handling"); + return false; + } + + if (left) { + double min_time = ufbx_fmax(prev_time + step, ufbxi_add_epsilon(prev_time, epsilon)); + *p_time = time = ufbx_fmin(time - step, ufbxi_sub_epsilon(time, epsilon)); + return time > min_time; + } else { + double max_time = ufbx_fmin(next_time - step, ufbxi_sub_epsilon(next_time, epsilon)); + *p_time = time = ufbx_fmax(time + step, ufbxi_add_epsilon(time, epsilon)); + return time < max_time; + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_postprocess_vec3(ufbxi_bake_context *bc, ufbx_baked_vec3_list *p_dst, bool *p_constant, ufbx_baked_vec3_list src) { if (src.count == 0) return 1; + // Offset times + if (bc->ktime_offset != 0.0) { + double scale = (double)bc->scene->metadata.ktime_second; + double offset = bc->ktime_offset; + for (size_t i = 0; i < src.count; i++) { + src.data[i].time = ufbx_rint(src.data[i].time * scale + offset) / scale; + } + } + + // Postprocess stepped tangents + { + size_t dst = 0; + double prev_time = src.data[0].time; + for (size_t i = 0; i < src.count; i++) { + ufbx_baked_vec3 cur = src.data[i]; + double next_time = i + 1 < src.count ? src.data[i + 1].time : UFBX_INFINITY; + bool keep = true; + if ((cur.flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) { + keep = ufbxi_postprocess_step(bc, prev_time, next_time, &cur.time, cur.flags); + } + if (keep) { + src.data[dst] = cur; + dst++; + prev_time = cur.time; + } + } + src.count = dst; + } + if (bc->opts.key_reduction_enabled) { double threshold = bc->opts.key_reduction_threshold * bc->opts.key_reduction_threshold; for (size_t pass = 0; pass < bc->opts.key_reduction_passes; pass++) { @@ -25501,10 +25858,39 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_reduce_vec3(ufbxi_bake_cont return 1; } -ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_reduce_quat(ufbxi_bake_context *bc, ufbx_baked_quat_list *p_dst, bool *p_constant, ufbx_baked_quat_list src) +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_postprocess_quat(ufbxi_bake_context *bc, ufbx_baked_quat_list *p_dst, bool *p_constant, ufbx_baked_quat_list src) { if (src.count == 0) return 1; + // Offset times + if (bc->ktime_offset != 0.0) { + double scale = (double)bc->scene->metadata.ktime_second; + double offset = bc->ktime_offset; + for (size_t i = 0; i < src.count; i++) { + src.data[i].time = ufbx_rint(src.data[i].time * scale + offset) / scale; + } + } + + // Postprocess stepped tangents + { + size_t dst = 0; + double prev_time = src.data[0].time; + for (size_t i = 0; i < src.count; i++) { + ufbx_baked_quat cur = src.data[i]; + double next_time = i + 1 < src.count ? src.data[i + 1].time : UFBX_INFINITY; + bool keep = true; + if ((cur.flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) { + keep = ufbxi_postprocess_step(bc, prev_time, next_time, &cur.time, cur.flags); + } + if (keep) { + prev_time = cur.time; + src.data[dst] = cur; + dst++; + } + } + src.count = dst; + } + // Fix quaternion antipodality for (size_t i = 1; i < src.count; i++) { src.data[i].value = ufbx_quat_fix_antipodal(src.data[i].value, src.data[i - 1].value); @@ -25574,6 +25960,38 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_reduce_quat(ufbxi_bake_cont return 1; } +static ufbxi_forceinline double ufbxi_bake_time_sample_time(ufbxi_bake_time time) +{ + // Move an infinitesimal step for stepped tangents + if ((time.flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) { + double dir = (time.flags & UFBX_BAKED_KEY_STEP_LEFT) != 0 ? -UFBX_INFINITY : UFBX_INFINITY; + return ufbx_nextafter(time.time, dir); + } else { + return time.time; + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_resampled_times(ufbxi_bake_context *bc, const ufbx_baked_vec3_list *p_keys) +{ + ufbx_baked_vec3_list keys = *p_keys; + + ufbxi_bake_time *times = ufbxi_push(&bc->tmp_times, ufbxi_bake_time, keys.count); + ufbxi_check_err(&bc->error, times); + for (size_t i = 0; i < keys.count; i++) { + uint32_t flags = keys.data[i].flags; + double time = keys.data[i].time; + if ((flags & UFBX_BAKED_KEY_STEP_LEFT) != 0 && i + 1 < keys.count && (keys.data[i + 1].flags & UFBX_BAKED_KEY_STEP_KEY) != 0) { + time = keys.data[i + 1].time; + } else if ((flags & UFBX_BAKED_KEY_STEP_RIGHT) != 0 && i > 0 && (keys.data[i - 1].flags & UFBX_BAKED_KEY_STEP_KEY) != 0) { + time = keys.data[i - 1].time; + } + times[i].time = time; + times[i].flags = flags & 0x7; + } + + return 1; +} + ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context *bc, uint32_t element_id, ufbxi_bake_prop *props, size_t count) { ufbx_assert(bc->baked_nodes && bc->nodes_to_bake); @@ -25606,7 +26024,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context } } - ufbxi_double_list times_t, times_r, times_s; + ufbxi_bake_time_list times_t, times_r, times_s; // Translation bool resample_translation = false; @@ -25621,13 +26039,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context if (!scale_helper_t->constant_scale) { resample_translation = true; } - - ufbx_baked_vec3_list scale_keys = scale_helper_t->scale_keys; - double *times = ufbxi_push(&bc->tmp_times, double, scale_keys.count); - ufbxi_check_err(&bc->error, times); - for (size_t i = 0; i < scale_keys.count; i++) { - times[i] = scale_keys.data[i].time; - } + ufbxi_check_err(&bc->error, ufbxi_push_resampled_times(bc, &scale_helper_t->scale_keys)); } else { constant_scale_t = node->parent->scale_helper->inherit_scale; } @@ -25638,13 +26050,14 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context // Literally any transform related property can affect complex translation if (ufbxi_in_list(ufbxi_transform_props, ufbxi_arraycount(ufbxi_transform_props), prop->prop_name)) { bool resample_linear = resample_translation || prop->prop_name != ufbxi_Lcl_Translation; - ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_linear)); + uint32_t key_flag = prop->prop_name == ufbxi_Lcl_Translation ? UFBX_BAKED_KEY_KEYFRAME : 0; + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_linear, key_flag)); } } } else { ufbxi_for(ufbxi_bake_prop, prop, props, count) { if (prop->prop_name == ufbxi_Lcl_Translation) { - ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_translation)); + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_translation, UFBX_BAKED_KEY_KEYFRAME)); } } } @@ -25656,13 +26069,14 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context ufbxi_for(ufbxi_bake_prop, prop, props, count) { if (ufbxi_in_list(ufbxi_complex_rotation_sources, ufbxi_arraycount(ufbxi_complex_rotation_sources), prop->prop_name)) { bool resample_linear = !bc->opts.no_resample_rotation || prop->prop_name != ufbxi_Lcl_Rotation; - ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_linear)); + uint32_t key_flag = prop->prop_name == ufbxi_Lcl_Rotation ? UFBX_BAKED_KEY_KEYFRAME : 0; + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_linear, key_flag)); } } } else { ufbxi_for(ufbxi_bake_prop, prop, props, count) { if (prop->prop_name == ufbxi_Lcl_Rotation) { - ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, !bc->opts.no_resample_rotation)); + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, !bc->opts.no_resample_rotation, UFBX_BAKED_KEY_KEYFRAME)); } } } @@ -25681,13 +26095,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context if (!scale_helper_s->constant_scale) { resample_scale = true; } - - ufbx_baked_vec3_list scale_keys = scale_helper_s->scale_keys; - double *times = ufbxi_push(&bc->tmp_times, double, scale_keys.count); - ufbxi_check_err(&bc->error, times); - for (size_t i = 0; i < scale_keys.count; i++) { - times[i] = scale_keys.data[i].time; - } + ufbxi_check_err(&bc->error, ufbxi_push_resampled_times(bc, &scale_helper_s->scale_keys)); } else { constant_scale_s = inherit_helper->local_transform.scale; } @@ -25695,7 +26103,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context ufbxi_for(ufbxi_bake_prop, prop, props, count) { if (prop->prop_name == ufbxi_Lcl_Scaling) { - ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_scale)); + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_scale, UFBX_BAKED_KEY_KEYFRAME)); } } ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, ×_s)); @@ -25718,21 +26126,51 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context size_t ix_t = 0, ix_r = 0, ix_s = 0; while (ix_t < times_t.count || ix_r < times_r.count || ix_s < times_s.count) { - double time = UFBX_INFINITY; - if (ix_t < times_t.count && time > times_t.data[ix_t]) time = times_t.data[ix_t]; - if (ix_r < times_r.count && time > times_r.data[ix_r]) time = times_r.data[ix_r]; - if (ix_s < times_s.count && time > times_s.data[ix_s]) time = times_s.data[ix_s]; + ufbxi_bake_time bake_time = { UFBX_INFINITY }; + uint32_t flags_r = 0, flags_t = 0, flags_s = 0; + + uint32_t flags = 0; + if (ix_r < times_r.count) { + bake_time = times_r.data[ix_r]; + flags_r = bake_time.flags; + bake_time.flags &= 0x7; + flags |= UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION; + } + if (ix_t < times_t.count) { + ufbxi_bake_time t = times_t.data[ix_t]; + int cmp = ufbxi_cmp_bake_time(t, bake_time); + if (cmp <= 0) { + if (cmp < 0) { + bake_time = t; + flags = 0; + } + bake_time.flags |= t.flags & 0x7; + flags_t = t.flags; + flags |= UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION; + } + } + if (ix_s < times_s.count) { + ufbxi_bake_time t = times_s.data[ix_s]; + int cmp = ufbxi_cmp_bake_time(t, bake_time); + if (cmp <= 0) { + if (cmp < 0) { + bake_time = t; + flags = 0; + } + bake_time.flags |= t.flags & 0x7; + flags_s = t.flags; + flags |= UFBX_TRANSFORM_FLAG_INCLUDE_SCALE; + } + } - uint32_t flags = UFBX_TRANSFORM_FLAG_IGNORE_SCALE_HELPER|UFBX_TRANSFORM_FLAG_IGNORE_COMPONENTWISE_SCALE|UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES; - if (ix_t < times_t.count && time == times_t.data[ix_t]) flags |= UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION; - if (ix_r < times_r.count && time == times_r.data[ix_r]) flags |= UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION; - if (ix_s < times_s.count && time == times_s.data[ix_s]) flags |= UFBX_TRANSFORM_FLAG_INCLUDE_SCALE; + flags |= UFBX_TRANSFORM_FLAG_IGNORE_SCALE_HELPER|UFBX_TRANSFORM_FLAG_IGNORE_COMPONENTWISE_SCALE|UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES; - ufbx_transform transform = ufbx_evaluate_transform_flags(bc->anim, node, time, flags); + double eval_time = ufbxi_bake_time_sample_time(bake_time); + ufbx_transform transform = ufbx_evaluate_transform_flags(bc->anim, node, eval_time, flags); if (flags & UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION) { if (scale_helper_t) { - ufbx_vec3 scale = ufbx_evaluate_baked_vec3(scale_helper_t->scale_keys, time); + ufbx_vec3 scale = ufbx_evaluate_baked_vec3(scale_helper_t->scale_keys, eval_time); transform.translation.x *= scale.x; transform.translation.y *= scale.y; transform.translation.z *= scale.z; @@ -25742,18 +26180,20 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context transform.translation.y *= constant_scale_t.y; transform.translation.z *= constant_scale_t.z; - keys_t.data[ix_t].time = time; + keys_t.data[ix_t].time = bake_time.time; keys_t.data[ix_t].value = transform.translation; + keys_t.data[ix_t].flags = (ufbx_baked_key_flags)(bake_time.flags | flags_t); ix_t++; } if (flags & UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION) { - keys_r.data[ix_r].time = time; + keys_r.data[ix_r].time = bake_time.time; keys_r.data[ix_r].value = transform.rotation; + keys_r.data[ix_r].flags = (ufbx_baked_key_flags)(bake_time.flags | flags_r); ix_r++; } if (flags & UFBX_TRANSFORM_FLAG_INCLUDE_SCALE) { if (scale_helper_s) { - ufbx_vec3 scale = ufbx_evaluate_baked_vec3(scale_helper_s->scale_keys, time); + ufbx_vec3 scale = ufbx_evaluate_baked_vec3(scale_helper_s->scale_keys, eval_time); transform.scale.x *= scale.x; transform.scale.y *= scale.y; transform.scale.z *= scale.z; @@ -25763,8 +26203,9 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context transform.scale.y *= constant_scale_s.y; transform.scale.z *= constant_scale_s.z; - keys_s.data[ix_s].time = time; + keys_s.data[ix_s].time = bake_time.time; keys_s.data[ix_s].value = transform.scale; + keys_s.data[ix_s].flags = (ufbx_baked_key_flags)(bake_time.flags | flags_s); ix_s++; } } @@ -25774,9 +26215,9 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context baked_node->element_id = node->element_id; baked_node->typed_id = node->typed_id; - ufbxi_check_err(&bc->error, ufbxi_bake_reduce_vec3(bc, &baked_node->translation_keys, &baked_node->constant_translation, keys_t)); - ufbxi_check_err(&bc->error, ufbxi_bake_reduce_quat(bc, &baked_node->rotation_keys, &baked_node->constant_rotation, keys_r)); - ufbxi_check_err(&bc->error, ufbxi_bake_reduce_vec3(bc, &baked_node->scale_keys, &baked_node->constant_scale, keys_s)); + ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_vec3(bc, &baked_node->translation_keys, &baked_node->constant_translation, keys_t)); + ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_quat(bc, &baked_node->rotation_keys, &baked_node->constant_rotation, keys_r)); + ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_vec3(bc, &baked_node->scale_keys, &baked_node->constant_scale, keys_s)); bc->baked_nodes[node->typed_id] = baked_node; @@ -25825,10 +26266,10 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node(ufbxi_bake_context *bc ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_prop(ufbxi_bake_context *bc, ufbx_element *element, const char *prop_name, ufbxi_bake_prop *props, size_t count) { ufbxi_for(ufbxi_bake_prop, prop, props, count) { - ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, false)); + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, false, UFBX_BAKED_KEY_KEYFRAME)); } - ufbxi_double_list times; + ufbxi_bake_time_list times; ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, ×)); ufbx_baked_vec3_list keys; @@ -25841,10 +26282,12 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_prop(ufbxi_bake_contex name.length = strlen(prop_name); for (size_t i = 0; i < times.count; i++) { - double time = times.data[i]; - ufbx_prop prop = ufbx_evaluate_prop_len(bc->anim, element, name.data, name.length, time); - keys.data[i].time = time; + ufbxi_bake_time bake_time = times.data[i]; + double eval_time = ufbxi_bake_time_sample_time(bake_time); + ufbx_prop prop = ufbx_evaluate_prop_len(bc->anim, element, name.data, name.length, eval_time); + keys.data[i].time = bake_time.time; keys.data[i].value = prop.value_vec3; + keys.data[i].flags = (ufbx_baked_key_flags)bake_time.flags; } ufbx_baked_prop *baked_prop = ufbxi_push_zero(&bc->tmp_props, ufbx_baked_prop, 1); @@ -25854,7 +26297,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_prop(ufbxi_bake_contex baked_prop->name.data = ufbxi_push_copy(&bc->result, char, baked_prop->name.length + 1, prop_name); ufbxi_check_err(&bc->error, baked_prop->name.data); - ufbxi_check_err(&bc->error, ufbxi_bake_reduce_vec3(bc, &baked_prop->keys, &baked_prop->constant_value, keys)); + ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_vec3(bc, &baked_prop->keys, &baked_prop->constant_value, keys)); ufbxi_buf_clear(&bc->tmp_prop); @@ -25900,12 +26343,22 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_element(ufbxi_bake_context return 1; } -ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim(ufbxi_bake_context *bc) +static ufbxi_noinline bool ufbxi_baked_node_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_baked_node *a = (const ufbx_baked_node*)va, *b = (const ufbx_baked_node*)vb; + return a->typed_id < b->typed_id; +} + +static ufbxi_noinline bool ufbxi_baked_element_less(void *user, const void *va, const void *vb) { - // `ufbx_bake_opts` must be cleared to zero first! - ufbx_assert(bc->opts._begin_zero == 0 && bc->opts._end_zero == 0); - ufbxi_check_err_msg(&bc->error, bc->opts._begin_zero == 0 && bc->opts._end_zero == 0, "Uninitialized options"); + (void)user; + const ufbx_baked_element *a = (const ufbx_baked_element*)va, *b = (const ufbx_baked_element*)vb; + return a->element_id < b->element_id; +} +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim(ufbxi_bake_context *bc) +{ const ufbx_anim *anim = bc->anim; const ufbx_scene *scene = bc->scene; @@ -25955,17 +26408,17 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim(ufbxi_bake_context *bc if (prop->prop_name != ufbxi_Weight) continue; ufbx_element *element = scene->elements.data[prop->element_id]; if (element->type == UFBX_ELEMENT_ANIM_LAYER) { - ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, true)); + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, true, 0)); has_weight_times = true; } } if (has_weight_times) { - ufbxi_double_list weight_times = { 0 }; + ufbxi_bake_time_list weight_times = { 0 }; ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, &weight_times)); bc->layer_weight_times.count = weight_times.count; - bc->layer_weight_times.data = ufbxi_push_copy(&bc->tmp, double, weight_times.count, weight_times.data); + bc->layer_weight_times.data = ufbxi_push_copy(&bc->tmp, ufbxi_bake_time, weight_times.count, weight_times.data); ufbxi_check_err(&bc->error, bc->layer_weight_times.data); ufbxi_buf_clear(&bc->tmp_prop); @@ -25994,6 +26447,20 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim(ufbxi_bake_context *bc bc->bake.elements.data = ufbxi_push_pop(&bc->result, &bc->tmp_elements, ufbx_baked_element, num_elements); ufbxi_check_err(&bc->error, bc->bake.elements.data); + ufbxi_unstable_sort(bc->bake.nodes.data, bc->bake.nodes.count, sizeof(ufbx_baked_node), &ufbxi_baked_node_less, NULL); + ufbxi_unstable_sort(bc->bake.elements.data, bc->bake.elements.count, sizeof(ufbx_baked_element), &ufbxi_baked_element_less, NULL); + + if (bc->time_min < bc->time_max) { + bc->bake.key_time_min = bc->time_min; + bc->bake.key_time_max = bc->time_max; + } + + if (bc->time_begin < bc->time_end) { + bc->bake.playback_time_begin = bc->time_begin; + bc->bake.playback_time_end = bc->time_end; + bc->bake.playback_duration = bc->time_end - bc->time_begin; + } + return 1; } @@ -26004,7 +26471,10 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_imp(ufbxi_bake_context if (bc->opts.max_keyframe_segments == 0) bc->opts.max_keyframe_segments = 32; if (bc->opts.key_reduction_threshold == 0) bc->opts.key_reduction_threshold = 0.000001; if (bc->opts.key_reduction_passes == 0) bc->opts.key_reduction_passes = 4; - if (bc->opts.constant_timestep <= 0.0) bc->opts.constant_timestep = 0.0; + + if (bc->opts.trim_start_time && anim->time_begin > 0.0) { + bc->ktime_offset = -anim->time_begin * (double)bc->scene->metadata.ktime_second; + } ufbxi_init_ator(&bc->error, &bc->ator_tmp, &bc->opts.temp_allocator, "temp"); ufbxi_init_ator(&bc->error, &bc->ator_result, &bc->opts.result_allocator, "result"); @@ -26027,8 +26497,12 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_imp(ufbxi_bake_context bc->tmp_bake_stack.ator = &bc->ator_tmp; bc->anim = anim; - bc->time_begin = anim->time_begin; - bc->time_end = anim->time_end; + if (anim->time_begin < anim->time_end) { + bc->time_begin = anim->time_begin; + bc->time_end = anim->time_end; + } + bc->time_min = UFBX_INFINITY; + bc->time_max = -UFBX_INFINITY; bc->imp = ufbxi_push(&bc->result, ufbxi_baked_anim_imp, 1); ufbxi_check_err(&bc->error, bc->imp); @@ -26037,10 +26511,16 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_imp(ufbxi_bake_context ufbxi_init_ref(&bc->imp->refcount, UFBXI_BAKED_ANIM_IMP_MAGIC, NULL); + bc->bake.metadata.result_memory_used = bc->ator_result.current_size; + bc->bake.metadata.temp_memory_used = bc->ator_tmp.current_size; + bc->bake.metadata.result_allocs = bc->ator_result.num_allocs; + bc->bake.metadata.temp_allocs = bc->ator_tmp.num_allocs; + bc->imp->magic = UFBXI_BAKED_ANIM_IMP_MAGIC; bc->imp->bake = bc->bake; bc->imp->refcount.ator = bc->ator_result; bc->imp->refcount.buf = bc->result; + return 1; } @@ -26119,10 +26599,6 @@ typedef struct { ufbxi_nodiscard static ufbxi_noinline int ufbxi_tessellate_nurbs_curve_imp(ufbxi_tessellate_curve_context *tc) { - // `ufbx_tessellate_opts` must be cleared to zero first! - ufbx_assert(tc->opts._begin_zero == 0 && tc->opts._end_zero == 0); - ufbxi_check_err_msg(&tc->error, tc->opts._begin_zero == 0 && tc->opts._end_zero == 0, "Uninitialized options"); - if (tc->opts.span_subdivision <= 0) { tc->opts.span_subdivision = 4; } @@ -26216,10 +26692,6 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_tessellate_nurbs_curve_imp(ufbxi ufbxi_nodiscard static ufbxi_noinline int ufbxi_tessellate_nurbs_surface_imp(ufbxi_tessellate_surface_context *tc) { - // `ufbx_tessellate_opts` must be cleared to zero first! - ufbx_assert(tc->opts._begin_zero == 0 && tc->opts._end_zero == 0); - ufbxi_check_err_msg(&tc->error, tc->opts._begin_zero == 0 && tc->opts._end_zero == 0, "Uninitialized options"); - if (tc->opts.span_subdivision_u <= 0) { tc->opts.span_subdivision_u = 4; } @@ -26534,7 +27006,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_tessellate_nurbs_surface_imp(ufb typedef struct { ufbx_real split; - uint32_t index; + uint32_t index_plus_one; // 0 for empty uint32_t slow_left; uint32_t slow_right; uint32_t slow_end; @@ -26559,8 +27031,10 @@ typedef struct { uint32_t indices[3]; } ufbxi_kd_triangle; -ufbxi_forceinline static ufbx_vec2 ufbxi_ngon_project(ufbxi_ngon_context *nc, ufbx_vec3 point) +ufbxi_noinline static ufbx_vec2 ufbxi_ngon_project(ufbxi_ngon_context *nc, uint32_t index) { + ufbx_vec3 point = nc->positions.values.data[nc->positions.indices.data[nc->face.index_begin + index]]; + ufbx_vec2 p; p.x = ufbxi_dot3(nc->axes[0], point); p.y = ufbxi_dot3(nc->axes[1], point); @@ -26572,10 +27046,10 @@ ufbxi_forceinline static ufbx_real ufbxi_orient2d(ufbx_vec2 a, ufbx_vec2 b, ufbx return (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x); } -ufbxi_forceinline static bool ufbxi_kd_check_point(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t index, ufbx_vec3 point) +ufbxi_noinline static bool ufbxi_kd_check_point(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t index) { if (index == tri->indices[0] || index == tri->indices[1] || index == tri->indices[2]) return false; - ufbx_vec2 p = ufbxi_ngon_project(nc, point); + ufbx_vec2 p = ufbxi_ngon_project(nc, index); ufbx_real u = ufbxi_orient2d(p, tri->points[0], tri->points[1]); ufbx_real v = ufbxi_orient2d(p, tri->points[1], tri->points[2]); @@ -26606,7 +27080,7 @@ ufbxi_noinline static bool ufbxi_kd_check_slow(ufbxi_ngon_context *nc, const ufb bool hit_right = tri->max_t[axis] >= split; if (hit_left && hit_right) { - if (ufbxi_kd_check_point(nc, tri, index, point)) { + if (ufbxi_kd_check_point(nc, tri, index)) { return true; } @@ -26632,11 +27106,9 @@ ufbxi_noinline static bool ufbxi_kd_check_fast(ufbxi_ngon_context *nc, const ufb ufbxi_recursive_function(bool, ufbxi_kd_check_fast, (nc, tri, kd_index, axis, depth), UFBXI_KD_FAST_DEPTH, (ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t kd_index, uint32_t axis, uint32_t depth)) { - ufbx_vertex_vec3 pos = nc->positions; - for (;;) { ufbxi_kd_node node = nc->kd_nodes[kd_index]; - if (node.slow_end == 0) return false; + if (node.index_plus_one == 0) return false; bool hit_left = tri->min_t[axis] <= node.split; bool hit_right = tri->max_t[axis] >= node.split; @@ -26646,8 +27118,8 @@ ufbxi_noinline static bool ufbxi_kd_check_fast(ufbxi_ngon_context *nc, const ufb if (hit_left && hit_right) { // Check for the point on the split plane - ufbx_vec3 point = pos.values.data[pos.indices.data[nc->face.index_begin + node.index]]; - if (ufbxi_kd_check_point(nc, tri, node.index, point)) { + uint32_t index = node.index_plus_one - 1; + if (ufbxi_kd_check_point(nc, tri, index)) { return true; } @@ -26677,6 +27149,22 @@ ufbxi_noinline static bool ufbxi_kd_check_fast(ufbxi_ngon_context *nc, const ufb } } +ufbxi_noinline static bool ufbxi_kd_check(ufbxi_ngon_context *nc, const ufbx_vec2 *points, const uint32_t *indices) +{ + ufbxi_kd_triangle tri; // ufbxi_uninit + tri.points[0] = points[0]; + tri.points[1] = points[1]; + tri.points[2] = points[2]; + tri.indices[0] = indices[0]; + tri.indices[1] = indices[1]; + tri.indices[2] = indices[2]; + tri.min_t[0] = ufbxi_min_real(ufbxi_min_real(points[0].x, points[1].x), points[2].x); + tri.min_t[1] = ufbxi_min_real(ufbxi_min_real(points[0].y, points[1].y), points[2].y); + tri.max_t[0] = ufbxi_max_real(ufbxi_max_real(points[0].x, points[1].x), points[2].x); + tri.max_t[1] = ufbxi_max_real(ufbxi_max_real(points[0].y, points[1].y), points[2].y); + return ufbxi_kd_check_fast(nc, &tri, 0, 0, 0); +} + ufbxi_noinline static bool ufbxi_kd_index_less(void *user, const void *va, const void *vb) { ufbxi_ngon_context *nc = (ufbxi_ngon_context*)user; @@ -26716,7 +27204,7 @@ ufbxi_noinline static void ufbxi_kd_build(ufbxi_ngon_context *nc, uint32_t *indi ufbxi_kd_node *kd = &nc->kd_nodes[fast_index]; kd->split = ufbxi_dot3(axis_dir, pos.values.data[pos.indices.data[face.index_begin + index]]); - kd->index = index; + kd->index_plus_one = index + 1; if (depth + 1 == UFBXI_KD_FAST_DEPTH) { kd->slow_left = (uint32_t)(indices - nc->kd_indices); @@ -26743,9 +27231,25 @@ ufbxi_noinline static void ufbxi_kd_build(ufbxi_ngon_context *nc, uint32_t *indi #if UFBXI_FEATURE_TRIANGULATION +ufbxi_noinline static ufbx_real ufbxi_ngon_tri_weight(const ufbx_vec2 *points) +{ + ufbx_vec2 p0 = points[0], p1 = points[1], p2 = points[2]; + ufbx_real orient = ufbxi_orient2d(p0, p1, p2); + if (orient <= 0.0f) return -1.0f; + + ufbx_real a = ufbxi_distsq2(p0, p1); + ufbx_real b = ufbxi_distsq2(p1, p2); + ufbx_real c = ufbxi_distsq2(p2, p0); + ufbx_real ab = (a + b - c) / (ufbx_real)ufbx_sqrt(4.0f * a * b); + ufbx_real bc = (b + c - a) / (ufbx_real)ufbx_sqrt(4.0f * b * c); + ufbx_real ca = (c + a - b) / (ufbx_real)ufbx_sqrt(4.0f * c * a); + return (ufbx_real)ufbx_fmax(UFBX_EPSILON, 2.0f - ufbx_fmax(ufbx_fmax(ab, bc), ca)); +} + ufbxi_noinline static uint32_t ufbxi_triangulate_ngon(ufbxi_ngon_context *nc, uint32_t *indices, uint32_t num_indices) { ufbx_face face = nc->face; + ufbx_assert(face.num_indices > 4); // Form an orthonormal basis to project the polygon into a 2D plane ufbx_vec3 normal = ufbx_get_weighted_face_normal(&nc->positions, face); @@ -26776,18 +27280,17 @@ ufbxi_noinline static uint32_t ufbxi_triangulate_ngon(ufbxi_ngon_context *nc, ui nc->kd_indices = kd_indices; uint32_t *kd_tmp = indices + face.num_indices; - ufbx_vertex_vec3 pos = nc->positions; // Collect all the reflex corners for intersection testing. uint32_t num_kd_indices = 0; { - ufbx_vec2 a = ufbxi_ngon_project(nc, pos.values.data[pos.indices.data[face.index_begin + face.num_indices - 1]]); - ufbx_vec2 b = ufbxi_ngon_project(nc, pos.values.data[pos.indices.data[face.index_begin + 0]]); + ufbx_vec2 a = ufbxi_ngon_project(nc, face.num_indices - 1); + ufbx_vec2 b = ufbxi_ngon_project(nc, 0); for (uint32_t i = 0; i < face.num_indices; i++) { uint32_t next = i + 1 < face.num_indices ? i + 1 : 0; - ufbx_vec2 c = ufbxi_ngon_project(nc, pos.values.data[pos.indices.data[face.index_begin + next]]); + ufbx_vec2 c = ufbxi_ngon_project(nc, next); - if (ufbxi_orient2d(a, b, c) < 0.0f) { + if (ufbxi_orient2d(a, b, c) <= 0.0f) { kd_indices[num_kd_indices++] = i; } @@ -26796,7 +27299,7 @@ ufbxi_noinline static uint32_t ufbxi_triangulate_ngon(ufbxi_ngon_context *nc, ui } } - // Build a KD-tree out of the collected reflex vertices. + // Build a KD-tree of the vertices. uint32_t num_skip_indices = (1u << (UFBXI_KD_FAST_DEPTH + 1)) - 1; uint32_t kd_slow_indices = num_kd_indices > num_skip_indices ? num_kd_indices - num_skip_indices : 0; ufbxi_ignore(kd_slow_indices); @@ -26822,73 +27325,64 @@ ufbxi_noinline static uint32_t ufbxi_triangulate_ngon(ufbxi_ngon_context *nc, ui // to iterate the polygon once if we move backwards one step every time we clip an ear. uint32_t indices_left = face.num_indices; { - ufbxi_kd_triangle tri; // ufbxi_uninit - - uint32_t ix = 1; - ufbx_vec2 a = ufbxi_ngon_project(nc, pos.values.data[pos.indices.data[face.index_begin + 0]]); - ufbx_vec2 b = ufbxi_ngon_project(nc, pos.values.data[pos.indices.data[face.index_begin + 1]]); - ufbx_vec2 c = ufbxi_ngon_project(nc, pos.values.data[pos.indices.data[face.index_begin + 2]]); - bool prev_was_reflex = false; + uint32_t point_indices[4] = { 0, 1, 2, 3 }; + ufbx_real weights[2]; // ufbxi_uninit + ufbx_vec2 points[4]; // ufbxi_uninit uint32_t num_steps = 0; - while (indices_left > 3) { - uint32_t prev = edges[ix*2 + 0]; - uint32_t next = edges[ix*2 + 1]; - if (ufbxi_orient2d(a, b, c) > 0.0f) { - - tri.points[0] = a; - tri.points[1] = b; - tri.points[2] = c; - tri.indices[0] = prev; - tri.indices[1] = ix; - tri.indices[2] = next; - tri.min_t[0] = ufbxi_min_real(ufbxi_min_real(a.x, b.x), c.x); - tri.min_t[1] = ufbxi_min_real(ufbxi_min_real(a.y, b.y), c.y); - tri.max_t[0] = ufbxi_max_real(ufbxi_max_real(a.x, b.x), c.x); - tri.max_t[1] = ufbxi_max_real(ufbxi_max_real(a.y, b.y), c.y); + points[0] = ufbxi_ngon_project(nc, point_indices[0]); + points[1] = ufbxi_ngon_project(nc, point_indices[1]); + points[2] = ufbxi_ngon_project(nc, point_indices[2]); + points[3] = ufbxi_ngon_project(nc, point_indices[3]); + + weights[0] = ufbxi_ngon_tri_weight(points + 0); + weights[1] = ufbxi_ngon_tri_weight(points + 1); + + uint32_t first_side = weights[1] > weights[0] ? 1 : 0; + bool clipped = false; + ufbxi_nounroll for (uint32_t side_ix = 0; side_ix < 2; side_ix++) { + uint32_t side = side_ix ^ first_side; + if (!(weights[side] >= 0.0f)) break; // If there is no reflex angle contained within the triangle formed // by `{ a, b, c }` connect the vertices `a - c` (prev, next) directly. - if (!ufbxi_kd_check_fast(nc, &tri, 0, 0, 0)) { + if (!ufbxi_kd_check(nc, points + side, point_indices + side)) { + uint32_t ia = point_indices[side + 0]; + uint32_t ib = point_indices[side + 1]; + uint32_t ic = point_indices[side + 2]; // Mark as clipped - edges[ix*2 + 0] |= 0x80000000; - edges[ix*2 + 1] |= 0x80000000; + edges[ib*2 + 0] |= 0x80000000; + edges[ib*2 + 1] |= 0x80000000; - edges[next*2 + 0] = prev; - edges[prev*2 + 1] = next; + edges[ic*2 + 0] = ia; + edges[ia*2 + 1] = ic; indices_left -= 1; - // Move backwards only if the previous was reflex as now it would - // cover a superset of the previous area, failing the intersection test. - if (prev_was_reflex) { - prev_was_reflex = false; - ix = prev; - b = a; - uint32_t prev_prev = edges[prev*2 + 0]; - a = ufbxi_ngon_project(nc, pos.values.data[pos.indices.data[face.index_begin + prev_prev]]); + // TODO: This may cause O(n^2) behavior! + num_steps = 0; + + if (side == 1) { + point_indices[2] = point_indices[3]; + point_indices[3] = edges[point_indices[3]*2 + 1]; } else { - ix = next; - b = c; - uint32_t next_next = edges[next*2 + 1]; - c = ufbxi_ngon_project(nc, pos.values.data[pos.indices.data[face.index_begin + next_next]]); + point_indices[1] = point_indices[0]; + point_indices[0] = edges[point_indices[0]*2 + 0]; } - continue; - } - prev_was_reflex = false; - } else { - prev_was_reflex = true; + clipped = true; + break; + } } + if (clipped) continue; // Continue forward - ix = next; - a = b; - b = c; - next = edges[next*2 + 1]; - c = ufbxi_ngon_project(nc, pos.values.data[pos.indices.data[face.index_begin + next]]); + point_indices[0] = point_indices[1]; + point_indices[1] = point_indices[2]; + point_indices[2] = point_indices[3]; + point_indices[3] = edges[point_indices[3]*2 + 1]; num_steps++; // If we have walked around the entire polygon it is irregular and @@ -26899,6 +27393,7 @@ ufbxi_noinline static uint32_t ufbxi_triangulate_ngon(ufbxi_ngon_context *nc, ui // Fallback: Cut non-ears until the polygon is completed. // TODO: Could do something better here.. + uint32_t ix = point_indices[1]; while (indices_left > 3) { uint32_t prev = edges[ix*2 + 0]; uint32_t next = edges[ix*2 + 1]; @@ -27155,20 +27650,6 @@ typedef struct { } ufbxi_subdivide_context; -static int ufbxi_subdivide_sum_real(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs) -{ - (void)user; - ufbx_real dst = 0.0f; - ufbxi_nounroll for (size_t i = 0; i != num_inputs; i++) { - ufbx_real src = *(const ufbx_real*)inputs[i].data; - ufbx_real weight = inputs[i].weight; - dst += src * weight; - } - *(ufbx_real*)output = dst; - - return 1; -} - static int ufbxi_subdivide_sum_vec2(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs) { (void)user; @@ -27260,8 +27741,8 @@ static int ufbxi_subdivide_sum_vertex_weights(void *user, void *output, const uf qsort(tmp_weights, num_weights, sizeof(ufbx_subdivision_weight), ufbxi_cmp_subdivision_weight); - if (num_weights > sc->max_vertex_weights) { - num_weights = sc->max_vertex_weights; + if (sc->max_vertex_weights != SIZE_MAX) { + num_weights = ufbxi_min_sz(sc->max_vertex_weights, num_weights); // Normalize weights ufbx_real prefix_weight = 0.0f; @@ -27284,8 +27765,8 @@ static int ufbxi_subdivide_sum_vertex_weights(void *user, void *output, const uf return 1; } -static ufbxi_subdivide_sum_fn *ufbxi_real_sum_fns[] = { - &ufbxi_subdivide_sum_real, +static ufbxi_subdivide_sum_fn *const ufbxi_real_sum_fns[] = { + NULL, &ufbxi_subdivide_sum_vec2, &ufbxi_subdivide_sum_vec3, &ufbxi_subdivide_sum_vec4, @@ -27745,7 +28226,7 @@ static ufbxi_noinline int ufbxi_subdivide_attrib(ufbxi_subdivide_context *sc, uf { if (!attrib->exists) return 1; - ufbx_assert(attrib->value_reals >= 1 && attrib->value_reals <= 4); + ufbx_assert(attrib->value_reals >= 2 && attrib->value_reals <= 4); ufbxi_subdivide_layer_input input; // ufbxi_uninit input.sum_fn = ufbxi_real_sum_fns[attrib->value_reals - 1]; @@ -28206,10 +28687,6 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_mesh_level(ufbxi_subdi ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_mesh_imp(ufbxi_subdivide_context *sc, size_t level) { - // `ufbx_subdivide_opts` must be cleared to zero first! - ufbx_assert(sc->opts._begin_zero == 0 && sc->opts._end_zero == 0); - ufbxi_check_err_msg(&sc->error, sc->opts._begin_zero == 0 && sc->opts._end_zero == 0, "Uninitialized options"); - if (sc->opts.boundary == UFBX_SUBDIVISION_BOUNDARY_DEFAULT) { sc->opts.boundary = sc->src_mesh.subdivision_boundary; } @@ -28246,8 +28723,6 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_mesh_imp(ufbxi_subdivi ufbxi_buf_free(&sc->tmp); ufbx_mesh *mesh = &sc->dst_mesh; - memset(&mesh->vertex_normal, 0, sizeof(mesh->vertex_normal)); - memset(&mesh->skinned_normal, 0, sizeof(mesh->skinned_normal)); // Subdivision always results in a mesh that consists only of quads mesh->max_face_triangles = 2; @@ -28255,6 +28730,11 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_mesh_imp(ufbxi_subdivi mesh->num_point_faces = 0; mesh->num_line_faces = 0; + if (!sc->opts.interpolate_normals) { + memset(&mesh->vertex_normal, 0, sizeof(mesh->vertex_normal)); + memset(&mesh->skinned_normal, 0, sizeof(mesh->skinned_normal)); + } + if (!sc->opts.interpolate_normals && !sc->opts.ignore_normals) { ufbx_topo_edge *topo = ufbxi_push(&sc->tmp, ufbx_topo_edge, mesh->num_indices); @@ -28581,36 +29061,66 @@ static ufbxi_noinline void ufbxi_release_ref(ufbxi_refcount *refcount) } } +static ufbxi_noinline void *ufbxi_uninitialized_options(ufbx_error *p_error) +{ + if (p_error) { + memset(p_error, 0, sizeof(ufbx_error)); + p_error->type = UFBX_ERROR_UNINITIALIZED_OPTIONS; + p_error->description.data = "Uninitialized options"; + p_error->description.length = strlen("Uninitialized options"); + } + return NULL; +} + +#define ufbxi_check_opts_ptr(m_type, m_opts, m_error) do { if (m_opts) { \ + uint32_t opts_cleared_to_zero = m_opts->_begin_zero | m_opts->_end_zero; \ + ufbx_assert(opts_cleared_to_zero == 0); \ + if (opts_cleared_to_zero != 0) return (m_type*)ufbxi_uninitialized_options(m_error); \ + } } while (0) + +#define ufbxi_check_opts_return(m_value, m_opts, m_error) do { if (m_opts) { \ + uint32_t opts_cleared_to_zero = m_opts->_begin_zero | m_opts->_end_zero; \ + ufbx_assert(opts_cleared_to_zero == 0); \ + if (opts_cleared_to_zero != 0) { \ + ufbxi_uninitialized_options(m_error); \ + return m_value; \ + } \ + } } while (0) + +#define ufbxi_check_opts_return_no_error(m_value, m_opts) do { if (m_opts) { \ + uint32_t opts_cleared_to_zero = m_opts->_begin_zero | m_opts->_end_zero; \ + if (opts_cleared_to_zero != 0) return m_value; \ + } } while (0) + // -- API #ifdef __cplusplus extern "C" { #endif -const ufbx_string ufbx_empty_string = { ufbxi_empty_char, 0 }; -const ufbx_blob ufbx_empty_blob = { NULL, 0 }; -const ufbx_matrix ufbx_identity_matrix = { 1,0,0, 0,1,0, 0,0,1, 0,0,0 }; -const ufbx_transform ufbx_identity_transform = { {0,0,0}, {0,0,0,1}, {1,1,1} }; -const ufbx_vec2 ufbx_zero_vec2 = { 0,0 }; -const ufbx_vec3 ufbx_zero_vec3 = { 0,0,0 }; -const ufbx_vec4 ufbx_zero_vec4 = { 0,0,0,0 }; -const ufbx_quat ufbx_identity_quat = { 0,0,0,1 }; +ufbx_abi_data_def const ufbx_string ufbx_empty_string = { ufbxi_empty_char, 0 }; +ufbx_abi_data_def const ufbx_blob ufbx_empty_blob = { NULL, 0 }; +ufbx_abi_data_def const ufbx_matrix ufbx_identity_matrix = { 1,0,0, 0,1,0, 0,0,1, 0,0,0 }; +ufbx_abi_data_def const ufbx_transform ufbx_identity_transform = { {0,0,0}, {0,0,0,1}, {1,1,1} }; +ufbx_abi_data_def const ufbx_vec2 ufbx_zero_vec2 = { 0,0 }; +ufbx_abi_data_def const ufbx_vec3 ufbx_zero_vec3 = { 0,0,0 }; +ufbx_abi_data_def const ufbx_vec4 ufbx_zero_vec4 = { 0,0,0,0 }; +ufbx_abi_data_def const ufbx_quat ufbx_identity_quat = { 0,0,0,1 }; -const ufbx_coordinate_axes ufbx_axes_right_handed_y_up = { +ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_right_handed_y_up = { UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Y, UFBX_COORDINATE_AXIS_POSITIVE_Z, }; -const ufbx_coordinate_axes ufbx_axes_right_handed_z_up = { +ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_right_handed_z_up = { UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Z, UFBX_COORDINATE_AXIS_NEGATIVE_Y, }; -const ufbx_coordinate_axes ufbx_axes_left_handed_y_up = { +ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_left_handed_y_up = { UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Y, UFBX_COORDINATE_AXIS_NEGATIVE_Z, }; -const ufbx_coordinate_axes ufbx_axes_left_handed_z_up = { +ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_left_handed_z_up = { UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Z, UFBX_COORDINATE_AXIS_POSITIVE_Y, }; - -const size_t ufbx_element_type_size[UFBX_ELEMENT_TYPE_COUNT] = { +ufbx_abi_data_def const size_t ufbx_element_type_size[UFBX_ELEMENT_TYPE_COUNT] = { sizeof(ufbx_unknown), sizeof(ufbx_node), sizeof(ufbx_mesh), @@ -28649,6 +29159,8 @@ const size_t ufbx_element_type_size[UFBX_ELEMENT_TYPE_COUNT] = { sizeof(ufbx_selection_node), sizeof(ufbx_character), sizeof(ufbx_constraint), + sizeof(ufbx_audio_layer), + sizeof(ufbx_audio_clip), sizeof(ufbx_pose), sizeof(ufbx_metadata_object), }; @@ -28736,6 +29248,7 @@ ufbx_abi bool ufbx_is_thread_safe(void) ufbx_abi ufbx_scene *ufbx_load_memory(const void *data, size_t size, const ufbx_load_opts *opts, ufbx_error *error) { + ufbxi_check_opts_ptr(ufbx_scene, opts, error); ufbxi_context uc = { UFBX_ERROR_NONE }; uc.data_begin = uc.data = (const char *)data; uc.data_size = size; @@ -28750,6 +29263,7 @@ ufbx_abi ufbx_scene *ufbx_load_file(const char *filename, const ufbx_load_opts * ufbx_abi ufbx_scene *ufbx_load_file_len(const char *filename, size_t filename_len, const ufbx_load_opts *opts, ufbx_error *error) { + ufbxi_check_opts_ptr(ufbx_scene, opts, error); ufbx_load_opts opts_copy; if (opts) { opts_copy = *opts; @@ -28795,6 +29309,7 @@ ufbx_abi ufbx_scene *ufbx_load_stdio(void *file_void, const ufbx_load_opts *opts ufbx_abi ufbx_scene *ufbx_load_stdio_prefix(void *file_void, const void *prefix, size_t prefix_size, const ufbx_load_opts *opts, ufbx_error *error) { + ufbxi_check_opts_ptr(ufbx_scene, opts, error); FILE *file = (FILE*)file_void; ufbxi_context uc = { UFBX_ERROR_NONE }; @@ -28834,6 +29349,7 @@ ufbx_abi ufbx_scene *ufbx_load_stream(const ufbx_stream *stream, const ufbx_load ufbx_abi ufbx_scene *ufbx_load_stream_prefix(const ufbx_stream *stream, const void *prefix, size_t prefix_size, const ufbx_load_opts *opts, ufbx_error *error) { + ufbxi_check_opts_ptr(ufbx_scene, opts, error); ufbxi_context uc = { UFBX_ERROR_NONE }; uc.data_begin = uc.data = (const char *)prefix; uc.data_size = prefix_size; @@ -29171,7 +29687,7 @@ ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time return curve->keyframes.data[curve->keyframes.count - 1].value; } -ufbx_abi ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_value, double time) +ufbx_abi ufbxi_noinline ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_value, double time) { if (!anim_value) { return 0.0f; @@ -29182,19 +29698,6 @@ ufbx_abi ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_val return res; } -ufbx_abi ufbxi_noinline ufbx_vec2 ufbx_evaluate_anim_value_vec2(const ufbx_anim_value *anim_value, double time) -{ - if (!anim_value) { - ufbx_vec2 zero = { 0.0f }; - return zero; - } - - ufbx_vec2 res = { anim_value->default_value.x, anim_value->default_value.y }; - if (anim_value->curves[0]) res.x = ufbx_evaluate_curve(anim_value->curves[0], time, res.x); - if (anim_value->curves[1]) res.y = ufbx_evaluate_curve(anim_value->curves[1], time, res.y); - return res; -} - ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_value *anim_value, double time) { if (!anim_value) { @@ -29418,6 +29921,7 @@ ufbx_abi ufbx_real ufbx_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_ ufbx_abi ufbx_scene *ufbx_evaluate_scene(const ufbx_scene *scene, const ufbx_anim *anim, double time, const ufbx_evaluate_opts *opts, ufbx_error *error) { + ufbxi_check_opts_ptr(ufbx_scene, opts, error); #if UFBXI_FEATURE_SCENE_EVALUATION ufbxi_eval_context ec = { 0 }; return ufbxi_evaluate_scene(&ec, (ufbx_scene*)scene, anim, time, opts, error); @@ -29433,6 +29937,7 @@ ufbx_abi ufbx_scene *ufbx_evaluate_scene(const ufbx_scene *scene, const ufbx_ani ufbx_abi ufbx_anim *ufbx_create_anim(const ufbx_scene *scene, const ufbx_anim_opts *opts, ufbx_error *error) { + ufbxi_check_opts_ptr(ufbx_anim, opts, error); ufbx_assert(scene); ufbxi_create_anim_context ac = { UFBX_ERROR_NONE }; @@ -29483,6 +29988,7 @@ ufbx_abi ufbx_baked_anim *ufbx_bake_anim(const ufbx_scene *scene, const ufbx_ani { ufbx_assert(scene); #if UFBXI_FEATURE_ANIMATION_BAKING + ufbxi_check_opts_ptr(ufbx_baked_anim, opts, error); if (!anim) { anim = scene->anim; } @@ -29547,6 +30053,35 @@ ufbx_abi void ufbx_free_baked_anim(ufbx_baked_anim *bake) ufbxi_release_ref(&imp->refcount); } + +ufbx_abi ufbx_baked_node *ufbx_find_baked_node_by_typed_id(ufbx_baked_anim *bake, uint32_t typed_id) +{ + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_baked_node, 8, &index, bake->nodes.data, 0, bake->nodes.count, + ( a->typed_id < typed_id ), ( a->typed_id == typed_id) ); + return index < SIZE_MAX ? &bake->nodes.data[index] : NULL; +} + +ufbx_abi ufbx_baked_node *ufbx_find_baked_node(ufbx_baked_anim *bake, ufbx_node *node) +{ + if (!bake || !node) return NULL; + return ufbx_find_baked_node_by_typed_id(bake, node->typed_id); +} + +ufbx_abi ufbx_baked_element *ufbx_find_baked_element_by_element_id(ufbx_baked_anim *bake, uint32_t element_id) +{ + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_baked_element, 8, &index, bake->elements.data, 0, bake->elements.count, + ( a->element_id < element_id ), ( a->element_id == element_id) ); + return index < SIZE_MAX ? &bake->elements.data[index] : NULL; +} + +ufbx_abi ufbx_baked_element *ufbx_find_baked_element(ufbx_baked_anim *bake, ufbx_element *element) +{ + if (!bake || !element) return NULL; + return ufbx_find_baked_element_by_element_id(bake, element->element_id); +} + ufbx_abi ufbx_vec3 ufbx_evaluate_baked_vec3(ufbx_baked_vec3_list keyframes, double time) { size_t begin = 0; @@ -29568,7 +30103,11 @@ ufbx_abi ufbx_vec3 ufbx_evaluate_baked_vec3(ufbx_baked_vec3_list keyframes, doub if (begin == 0) return next->value; const ufbx_baked_vec3 *prev = next - 1; + if (prev > keys && (prev->flags & UFBX_BAKED_KEY_STEP_RIGHT) != 0 && prev[-1].time == time) prev--; + if (time == prev->time) return prev->value; double t = (time - prev->time) / (next->time - prev->time); + if (prev->flags & UFBX_BAKED_KEY_STEP_LEFT) t = 0.0; + if (next->flags & UFBX_BAKED_KEY_STEP_RIGHT) t = 1.0; return ufbxi_lerp3(prev->value, next->value, (ufbx_real)t); } @@ -29596,7 +30135,12 @@ ufbx_abi ufbx_quat ufbx_evaluate_baked_quat(ufbx_baked_quat_list keyframes, doub if (begin == 0) return next->value; const ufbx_baked_quat *prev = next - 1; + if (prev > keys && prev[-1].time == time) prev--; + if (time == prev->time) return prev->value; double t = (time - prev->time) / (next->time - prev->time); + if (prev > keys && (prev->flags & UFBX_BAKED_KEY_STEP_RIGHT) != 0 && prev[-1].time == time) prev--; + if (prev->flags & UFBX_BAKED_KEY_STEP_LEFT) t = 0.0; + if (next->flags & UFBX_BAKED_KEY_STEP_RIGHT) t = 1.0; return ufbx_quat_slerp(prev->value, next->value, (ufbx_real)t); } @@ -29695,6 +30239,11 @@ ufbx_abi ufbx_quat ufbx_quat_mul(ufbx_quat a, ufbx_quat b) return ufbxi_mul_quat(a, b); } +ufbx_abi ufbx_vec3 ufbx_vec3_normalize(ufbx_vec3 v) +{ + return ufbxi_normalize3(v); +} + ufbx_abi ufbxi_noinline ufbx_real ufbx_quat_dot(ufbx_quat a, ufbx_quat b) { return a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w; @@ -29980,24 +30529,18 @@ ufbx_abi ufbx_matrix ufbx_matrix_invert(const ufbx_matrix *m) ufbx_abi ufbxi_noinline ufbx_matrix ufbx_matrix_for_normals(const ufbx_matrix *m) { ufbx_real det = ufbx_matrix_determinant(m); + ufbx_real det_sign = det >= 0.0f ? 1.0f : -1.0f; ufbx_matrix r; - if (ufbx_fabs(det) <= UFBX_EPSILON) { - memset(&r, 0, sizeof(r)); - return r; - } - - ufbx_real rcp_det = 1.0f / det; - - r.m00 = ( - m->m12*m->m21 + m->m11*m->m22) * rcp_det; - r.m01 = ( + m->m12*m->m20 - m->m10*m->m22) * rcp_det; - r.m02 = ( - m->m11*m->m20 + m->m10*m->m21) * rcp_det; - r.m10 = ( + m->m02*m->m21 - m->m01*m->m22) * rcp_det; - r.m11 = ( - m->m02*m->m20 + m->m00*m->m22) * rcp_det; - r.m12 = ( + m->m01*m->m20 - m->m00*m->m21) * rcp_det; - r.m20 = ( - m->m02*m->m11 + m->m01*m->m12) * rcp_det; - r.m21 = ( + m->m02*m->m10 - m->m00*m->m12) * rcp_det; - r.m22 = ( - m->m01*m->m10 + m->m00*m->m11) * rcp_det; + r.m00 = ( - m->m12*m->m21 + m->m11*m->m22) * det_sign; + r.m01 = ( + m->m12*m->m20 - m->m10*m->m22) * det_sign; + r.m02 = ( - m->m11*m->m20 + m->m10*m->m21) * det_sign; + r.m10 = ( + m->m02*m->m21 - m->m01*m->m22) * det_sign; + r.m11 = ( - m->m02*m->m20 + m->m00*m->m22) * det_sign; + r.m12 = ( + m->m01*m->m20 - m->m00*m->m21) * det_sign; + r.m20 = ( - m->m02*m->m11 + m->m01*m->m12) * det_sign; + r.m21 = ( + m->m02*m->m10 - m->m00*m->m12) * det_sign; + r.m22 = ( - m->m01*m->m10 + m->m00*m->m11) * det_sign; r.m03 = r.m13 = r.m23 = 0.0f; return r; @@ -30479,6 +31022,7 @@ ufbx_abi ufbxi_noinline ufbx_surface_point ufbx_evaluate_nurbs_surface(const ufb ufbx_abi ufbx_line_curve *ufbx_tessellate_nurbs_curve(const ufbx_nurbs_curve *curve, const ufbx_tessellate_curve_opts *opts, ufbx_error *error) { #if UFBXI_FEATURE_TESSELLATION + ufbxi_check_opts_ptr(ufbx_line_curve, opts, error); ufbx_assert(curve); if (!curve) return NULL; @@ -30518,6 +31062,7 @@ ufbx_abi ufbx_mesh *ufbx_tessellate_nurbs_surface(const ufbx_nurbs_surface *surf { #if UFBXI_FEATURE_TESSELLATION ufbx_assert(surface); + ufbxi_check_opts_ptr(ufbx_mesh, opts, error); if (!surface) return NULL; ufbxi_tessellate_surface_context tc = { UFBX_ERROR_NONE }; @@ -30703,45 +31248,23 @@ ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_catch_get_weighted_face_normal(ufbx_panic if (face.num_indices < 3) { return ufbx_zero_vec3; } else if (face.num_indices == 3) { - ufbx_vec3 a, b, c; - if (panic) { - a = ufbx_catch_get_vertex_vec3(panic, positions, face.index_begin + 0); - b = ufbx_catch_get_vertex_vec3(panic, positions, face.index_begin + 1); - c = ufbx_catch_get_vertex_vec3(panic, positions, face.index_begin + 2); - } else { - a = ufbx_get_vertex_vec3(positions, face.index_begin + 0); - b = ufbx_get_vertex_vec3(positions, face.index_begin + 1); - c = ufbx_get_vertex_vec3(positions, face.index_begin + 2); - } + ufbx_vec3 a = ufbx_get_vertex_vec3(positions, face.index_begin + 0); + ufbx_vec3 b = ufbx_get_vertex_vec3(positions, face.index_begin + 1); + ufbx_vec3 c = ufbx_get_vertex_vec3(positions, face.index_begin + 2); return ufbxi_cross3(ufbxi_sub3(b, a), ufbxi_sub3(c, a)); } else if (face.num_indices == 4) { - ufbx_vec3 a, b, c, d; - if (panic) { - a = ufbx_catch_get_vertex_vec3(panic, positions, face.index_begin + 0); - b = ufbx_catch_get_vertex_vec3(panic, positions, face.index_begin + 1); - c = ufbx_catch_get_vertex_vec3(panic, positions, face.index_begin + 2); - d = ufbx_catch_get_vertex_vec3(panic, positions, face.index_begin + 3); - } else { - a = ufbx_get_vertex_vec3(positions, face.index_begin + 0); - b = ufbx_get_vertex_vec3(positions, face.index_begin + 1); - c = ufbx_get_vertex_vec3(positions, face.index_begin + 2); - d = ufbx_get_vertex_vec3(positions, face.index_begin + 3); - } + ufbx_vec3 a = ufbx_get_vertex_vec3(positions, face.index_begin + 0); + ufbx_vec3 b = ufbx_get_vertex_vec3(positions, face.index_begin + 1); + ufbx_vec3 c = ufbx_get_vertex_vec3(positions, face.index_begin + 2); + ufbx_vec3 d = ufbx_get_vertex_vec3(positions, face.index_begin + 3); return ufbxi_cross3(ufbxi_sub3(c, a), ufbxi_sub3(d, b)); } else { - ufbx_vec3 a, b; - // Newell's Method ufbx_vec3 result = ufbx_zero_vec3; for (size_t i = 0; i < face.num_indices; i++) { size_t next = i + 1 < face.num_indices ? i + 1 : 0; - if (panic) { - a = ufbx_catch_get_vertex_vec3(panic, positions, face.index_begin + i); - b = ufbx_catch_get_vertex_vec3(panic, positions, face.index_begin + next); - } else { - a = ufbx_get_vertex_vec3(positions, face.index_begin + i); - b = ufbx_get_vertex_vec3(positions, face.index_begin + next); - } + ufbx_vec3 a = ufbx_get_vertex_vec3(positions, face.index_begin + i); + ufbx_vec3 b = ufbx_get_vertex_vec3(positions, face.index_begin + next); result.x += (a.y - b.y) * (a.z + b.z); result.y += (a.z - b.z) * (a.x + b.x); result.z += (a.x - b.x) * (a.y + b.y); @@ -30837,6 +31360,7 @@ ufbx_abi void ufbx_compute_normals(const ufbx_mesh *mesh, const ufbx_vertex_vec3 ufbx_abi ufbx_mesh *ufbx_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *opts, ufbx_error *error) { + ufbxi_check_opts_ptr(ufbx_mesh, opts, error); if (!mesh) return NULL; if (level == 0) return (ufbx_mesh*)mesh; return ufbxi_subdivide_mesh(mesh, level, opts, error); @@ -30876,6 +31400,7 @@ ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache_len( const char *filename, size_t filename_len, const ufbx_geometry_cache_opts *opts, ufbx_error *error) { + ufbxi_check_opts_ptr(ufbx_geometry_cache, opts, error); ufbx_string str = ufbxi_safe_string(filename, filename_len); return ufbxi_load_geometry_cache(str, opts, error); } @@ -30902,9 +31427,18 @@ ufbx_abi void ufbx_retain_geometry_cache(ufbx_geometry_cache *cache) ufbxi_retain_ref(&imp->refcount); } +typedef struct { + union { + double f64[UFBXI_GEOMETRY_CACHE_BUFFER_SIZE]; + float f32[UFBXI_GEOMETRY_CACHE_BUFFER_SIZE]; + } src; + ufbx_real dst[UFBXI_GEOMETRY_CACHE_BUFFER_SIZE]; +} ufbxi_geometry_cache_buffer; + ufbx_abi ufbxi_noinline size_t ufbx_read_geometry_cache_real(const ufbx_cache_frame *frame, ufbx_real *data, size_t count, const ufbx_geometry_cache_data_opts *user_opts) { #if UFBXI_FEATURE_GEOMETRY_CACHE + ufbxi_check_opts_return_no_error(0, user_opts); if (!frame || count == 0) return 0; ufbx_assert(data); if (!data) return 0; @@ -30920,10 +31454,6 @@ ufbx_abi ufbxi_noinline size_t ufbx_read_geometry_cache_real(const ufbx_cache_fr opts.open_file_cb.fn = ufbx_default_open_file; } - // `ufbx_geometry_cache_data_opts` must be cleared to zero first! - ufbx_assert(opts._begin_zero == 0 && opts._end_zero == 0); - if (!(opts._begin_zero == 0 && opts._end_zero == 0)) return 0; - bool use_double = false; size_t src_count = 0; @@ -30990,122 +31520,74 @@ ufbx_abi ufbxi_noinline size_t ufbx_read_geometry_cache_real(const ufbx_cache_fr ufbx_real *dst = data; size_t mirror_ix = (size_t)frame->mirror_axis - 1; - if (use_double) { - double buffer[512]; // ufbxi_uninit - while (src_count > 0) { - size_t to_read = ufbxi_min_sz(src_count, ufbxi_arraycount(buffer)); - src_count -= to_read; - size_t bytes_read = stream.read_fn(stream.user, buffer, to_read * sizeof(double)); + ufbxi_geometry_cache_buffer buffer; // ufbxi_uninit + while (src_count > 0) { + size_t to_read = ufbxi_min_sz(src_count, UFBXI_GEOMETRY_CACHE_BUFFER_SIZE); + src_count -= to_read; + size_t num_read = 0; + if (use_double) { + size_t bytes_read = stream.read_fn(stream.user, buffer.src.f64, to_read * sizeof(double)); if (bytes_read == SIZE_MAX) bytes_read = 0; - size_t num_read = bytes_read / sizeof(double); - + num_read = bytes_read / sizeof(double); if (src_big_endian != dst_big_endian) { for (size_t i = 0; i < num_read; i++) { - char t, *v = (char*)&buffer[i]; + char t, *v = (char*)&buffer.src.f64[i]; t = v[0]; v[0] = v[7]; v[7] = t; t = v[1]; v[1] = v[6]; v[6] = t; t = v[2]; v[2] = v[5]; v[5] = t; t = v[3]; v[3] = v[4]; v[4] = t; } } - - if (!opts.ignore_transform) { - double scale = frame->scale_factor; - if (scale != 1.0f) { - for (size_t i = 0; i < num_read; i++) { - buffer[i] *= scale; - } - } - if (frame->mirror_axis) { - while (mirror_ix < num_read) { - buffer[mirror_ix] = -buffer[mirror_ix]; - mirror_ix += 3; - } - mirror_ix -= num_read; - } + ufbxi_nounroll for (size_t i = 0; i < num_read; i++) { + buffer.dst[i] = (ufbx_real)buffer.src.f64[i]; } - - if (dst) { - ufbx_real weight = opts.weight; - if (opts.additive && opts.use_weight) { - for (size_t i = 0; i < num_read; i++) { - dst[i] += (ufbx_real)buffer[i] * weight; - } - } else if (opts.additive) { - for (size_t i = 0; i < num_read; i++) { - dst[i] += (ufbx_real)buffer[i]; - } - } else if (opts.use_weight) { - for (size_t i = 0; i < num_read; i++) { - dst[i] = (ufbx_real)buffer[i] * weight; - } - } else { - for (size_t i = 0; i < num_read; i++) { - dst[i] = (ufbx_real)buffer[i]; - } - } - dst += num_read; - } - - if (num_read != to_read) break; - } - } else { - float buffer[1024]; // ufbxi_uninit - while (src_count > 0) { - size_t to_read = ufbxi_min_sz(src_count, ufbxi_arraycount(buffer)); - src_count -= to_read; - size_t bytes_read = stream.read_fn(stream.user, buffer, to_read * sizeof(float)); + } else { + size_t bytes_read = stream.read_fn(stream.user, buffer.src.f32, to_read * sizeof(float)); if (bytes_read == SIZE_MAX) bytes_read = 0; - size_t num_read = bytes_read / sizeof(float); - + num_read = bytes_read / sizeof(float); if (src_big_endian != dst_big_endian) { for (size_t i = 0; i < num_read; i++) { - char t, *v = (char*)&buffer[i]; + char t, *v = (char*)&buffer.src.f32[i]; t = v[0]; v[0] = v[3]; v[3] = t; t = v[1]; v[1] = v[2]; v[2] = t; } } + ufbxi_nounroll for (size_t i = 0; i < num_read; i++) { + buffer.dst[i] = (ufbx_real)buffer.src.f32[i]; + } + } - if (!opts.ignore_transform) { - float scale = (float)frame->scale_factor; - if (scale != 1.0f) { - for (size_t i = 0; i < num_read; i++) { - buffer[i] *= scale; - } + if (!opts.ignore_transform) { + ufbx_real scale = frame->scale_factor; + if (scale != 1.0f) { + for (size_t i = 0; i < num_read; i++) { + buffer.dst[i] *= scale; } - if (frame->mirror_axis) { - while (mirror_ix < num_read) { - buffer[mirror_ix] = -buffer[mirror_ix]; - mirror_ix += 3; - } - mirror_ix -= num_read; + } + if (frame->mirror_axis) { + while (mirror_ix < num_read) { + buffer.dst[mirror_ix] = -buffer.dst[mirror_ix]; + mirror_ix += 3; } + mirror_ix -= num_read; } + } - if (dst) { - ufbx_real weight = opts.weight; - if (opts.additive && opts.use_weight) { - for (size_t i = 0; i < num_read; i++) { - dst[i] += (ufbx_real)buffer[i] * weight; - } - } else if (opts.additive) { - for (size_t i = 0; i < num_read; i++) { - dst[i] += (ufbx_real)buffer[i]; - } - } else if (opts.use_weight) { - for (size_t i = 0; i < num_read; i++) { - dst[i] = (ufbx_real)buffer[i] * weight; - } - } else { - for (size_t i = 0; i < num_read; i++) { - dst[i] = (ufbx_real)buffer[i]; - } + if (dst) { + ufbx_real weight = opts.use_weight ? opts.weight : 1.0f; + if (opts.additive) { + ufbxi_nounroll for (size_t i = 0; i < num_read; i++) { + dst[i] += buffer.dst[i] * weight; + } + } else { + ufbxi_nounroll for (size_t i = 0; i < num_read; i++) { + dst[i] = buffer.dst[i] * weight; } - dst += num_read; } - - if (num_read != to_read) break; + dst += num_read; } + + if (num_read != to_read) break; } if (stream.close_fn) { @@ -31121,6 +31603,7 @@ ufbx_abi ufbxi_noinline size_t ufbx_read_geometry_cache_real(const ufbx_cache_fr ufbx_abi ufbxi_noinline size_t ufbx_sample_geometry_cache_real(const ufbx_cache_channel *channel, double time, ufbx_real *data, size_t count, const ufbx_geometry_cache_data_opts *user_opts) { #if UFBXI_FEATURE_GEOMETRY_CACHE + ufbxi_check_opts_return_no_error(0, user_opts); if (!channel || count == 0) return 0; ufbx_assert(data); if (!data) return 0; @@ -31133,10 +31616,6 @@ ufbx_abi ufbxi_noinline size_t ufbx_sample_geometry_cache_real(const ufbx_cache_ memset(&opts, 0, sizeof(opts)); } - // `ufbx_geometry_cache_data_opts` must be cleared to zero first! - ufbx_assert(opts._begin_zero == 0 && opts._end_zero == 0); - if (!(opts._begin_zero == 0 && opts._end_zero == 0)) return 0; - size_t begin = 0; size_t end = channel->frames.count; const ufbx_cache_frame *frames = channel->frames.data; @@ -31253,7 +31732,7 @@ ufbx_abi void *ufbx_thread_pool_get_user_ptr(ufbx_thread_pool_context ctx) return pool->user_ptr; } -ufbx_abi ufbx_real ufbx_catch_get_vertex_real(ufbx_panic *panic, const ufbx_vertex_real *v, size_t index) +ufbx_abi ufbxi_noinline ufbx_real ufbx_catch_get_vertex_real(ufbx_panic *panic, const ufbx_vertex_real *v, size_t index) { if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return 0.0f; uint32_t ix = v->indices.data[index]; @@ -31261,7 +31740,7 @@ ufbx_abi ufbx_real ufbx_catch_get_vertex_real(ufbx_panic *panic, const ufbx_vert return v->values.data[(int32_t)ix]; } -ufbx_abi ufbx_vec2 ufbx_catch_get_vertex_vec2(ufbx_panic *panic, const ufbx_vertex_vec2 *v, size_t index) +ufbx_abi ufbxi_noinline ufbx_vec2 ufbx_catch_get_vertex_vec2(ufbx_panic *panic, const ufbx_vertex_vec2 *v, size_t index) { if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return ufbx_zero_vec2; uint32_t ix = v->indices.data[index]; @@ -31269,7 +31748,7 @@ ufbx_abi ufbx_vec2 ufbx_catch_get_vertex_vec2(ufbx_panic *panic, const ufbx_vert return v->values.data[(int32_t)ix]; } -ufbx_abi ufbx_vec3 ufbx_catch_get_vertex_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index) +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_catch_get_vertex_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index) { if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return ufbx_zero_vec3; uint32_t ix = v->indices.data[index]; @@ -31277,7 +31756,7 @@ ufbx_abi ufbx_vec3 ufbx_catch_get_vertex_vec3(ufbx_panic *panic, const ufbx_vert return v->values.data[(int32_t)ix]; } -ufbx_abi ufbx_vec4 ufbx_catch_get_vertex_vec4(ufbx_panic *panic, const ufbx_vertex_vec4 *v, size_t index) +ufbx_abi ufbxi_noinline ufbx_vec4 ufbx_catch_get_vertex_vec4(ufbx_panic *panic, const ufbx_vertex_vec4 *v, size_t index) { if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return ufbx_zero_vec4; uint32_t ix = v->indices.data[index]; @@ -31285,10 +31764,13 @@ ufbx_abi ufbx_vec4 ufbx_catch_get_vertex_vec4(ufbx_panic *panic, const ufbx_vert return v->values.data[(int32_t)ix]; } -ufbx_abi size_t ufbx_get_triangulate_face_num_indices(ufbx_face face) +ufbx_abi ufbx_real ufbx_catch_get_vertex_w_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index) { - if (face.num_indices < 3) return 0; - return (face.num_indices - 2) * 3; + if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return 0.0f; + if (v->values_w.count == 0) return 0.0f; + uint32_t ix = v->indices.data[index]; + if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return 0.0f; + return v->values_w.data[(int32_t)ix]; } ufbx_abi ufbx_unknown *ufbx_as_unknown(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_UNKNOWN ? (ufbx_unknown*)element : NULL; } @@ -31329,186 +31811,11 @@ ufbx_abi ufbx_selection_set *ufbx_as_selection_set(const ufbx_element *element) ufbx_abi ufbx_selection_node *ufbx_as_selection_node(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SELECTION_NODE ? (ufbx_selection_node*)element : NULL; } ufbx_abi ufbx_character *ufbx_as_character(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CHARACTER ? (ufbx_character*)element : NULL; } ufbx_abi ufbx_constraint *ufbx_as_constraint(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CONSTRAINT ? (ufbx_constraint*)element : NULL; } +ufbx_abi ufbx_audio_layer *ufbx_as_audio_layer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_AUDIO_LAYER ? (ufbx_audio_layer*)element : NULL; } +ufbx_abi ufbx_audio_clip *ufbx_as_audio_clip(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_AUDIO_CLIP ? (ufbx_audio_clip*)element : NULL; } ufbx_abi ufbx_pose *ufbx_as_pose(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_POSE ? (ufbx_pose*)element : NULL; } ufbx_abi ufbx_metadata_object *ufbx_as_metadata_object(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_METADATA_OBJECT ? (ufbx_metadata_object*)element : NULL; } -ufbx_abi void ufbx_ffi_find_int_len(int64_t *retval, const ufbx_props *props, const char *name, size_t name_len, const int64_t *def) -{ - *retval = ufbx_find_int_len(props, name, name_len, *def); -} - -ufbx_abi void ufbx_ffi_find_vec3_len(ufbx_vec3 *retval, const ufbx_props *props, const char *name, size_t name_len, const ufbx_vec3 *def) -{ - *retval = ufbx_find_vec3_len(props, name, name_len, *def); -} - -ufbx_abi void ufbx_ffi_find_string_len(ufbx_string *retval, const ufbx_props *props, const char *name, size_t name_len, const ufbx_string *def) -{ - *retval = ufbx_find_string_len(props, name, name_len, *def); -} - -ufbx_abi void ufbx_ffi_find_anim_props(ufbx_anim_prop_list *retval, const ufbx_anim_layer *layer, const ufbx_element *element) -{ - *retval = ufbx_find_anim_props(layer, element); -} - -ufbx_abi void ufbx_ffi_get_compatible_matrix_for_normals(ufbx_matrix *retval, const ufbx_node *node) -{ - *retval = ufbx_get_compatible_matrix_for_normals(node); -} - -ufbx_abi void ufbx_ffi_evaluate_anim_value_vec2(ufbx_vec2 *retval, const ufbx_anim_value *anim_value, double time) -{ - *retval = ufbx_evaluate_anim_value_vec2(anim_value, time); -} - -ufbx_abi void ufbx_ffi_evaluate_anim_value_vec3(ufbx_vec3 *retval, const ufbx_anim_value *anim_value, double time) -{ - *retval = ufbx_evaluate_anim_value_vec3(anim_value, time); -} - -ufbx_abi void ufbx_ffi_evaluate_prop_len(ufbx_prop *retval, const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time) -{ - *retval = ufbx_evaluate_prop_len(anim, element, name, name_len, time); -} - -ufbx_abi void ufbx_ffi_evaluate_props(ufbx_props *retval, const ufbx_anim *anim, ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size) -{ - *retval = ufbx_evaluate_props(anim, element, time, buffer, buffer_size); -} - -ufbx_abi void ufbx_ffi_evaluate_transform(ufbx_transform *retval, const ufbx_anim *anim, const ufbx_node *node, double time) -{ - *retval = ufbx_evaluate_transform(anim, node, time); -} - -ufbx_abi ufbx_real ufbx_ffi_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time) -{ - return ufbx_evaluate_blend_weight(anim, channel, time); -} - -ufbx_abi void ufbx_ffi_quat_mul(ufbx_quat *retval, const ufbx_quat *a, const ufbx_quat *b) -{ - *retval = ufbx_quat_mul(*a, *b); -} - -ufbx_abi void ufbx_ffi_quat_normalize(ufbx_quat *retval, const ufbx_quat *q) -{ - *retval = ufbx_quat_normalize(*q); -} - -ufbx_abi void ufbx_ffi_quat_fix_antipodal(ufbx_quat *retval, const ufbx_quat *q, const ufbx_quat *reference) -{ - *retval = ufbx_quat_fix_antipodal(*q, *reference); -} - -ufbx_abi void ufbx_ffi_quat_slerp(ufbx_quat *retval, const ufbx_quat *a, const ufbx_quat *b, ufbx_real t) -{ - *retval = ufbx_quat_slerp(*a, *b, t); -} - -ufbx_abi void ufbx_ffi_quat_rotate_vec3(ufbx_vec3 *retval, const ufbx_quat *q, const ufbx_vec3 *v) -{ - *retval = ufbx_quat_rotate_vec3(*q, *v); -} - -ufbx_abi void ufbx_ffi_quat_to_euler(ufbx_vec3 *retval, const ufbx_quat *q, ufbx_rotation_order order) -{ - *retval = ufbx_quat_to_euler(*q, order); -} - -ufbx_abi void ufbx_ffi_euler_to_quat(ufbx_quat *retval, const ufbx_vec3 *v, ufbx_rotation_order order) -{ - *retval = ufbx_euler_to_quat(*v, order); -} - -ufbx_abi void ufbx_ffi_matrix_mul(ufbx_matrix *retval, const ufbx_matrix *a, const ufbx_matrix *b) -{ - *retval = ufbx_matrix_mul(a, b); -} - -ufbx_abi void ufbx_ffi_matrix_invert(ufbx_matrix *retval, const ufbx_matrix *m) -{ - *retval = ufbx_matrix_invert(m); -} - -ufbx_abi void ufbx_ffi_matrix_for_normals(ufbx_matrix *retval, const ufbx_matrix *m) -{ - *retval = ufbx_matrix_for_normals(m); -} - -ufbx_abi void ufbx_ffi_transform_position(ufbx_vec3 *retval, const ufbx_matrix *m, const ufbx_vec3 *v) -{ - *retval = ufbx_transform_position(m, *v); -} - -ufbx_abi void ufbx_ffi_transform_direction(ufbx_vec3 *retval, const ufbx_matrix *m, const ufbx_vec3 *v) -{ - *retval = ufbx_transform_direction(m, *v); -} - -ufbx_abi void ufbx_ffi_transform_to_matrix(ufbx_matrix *retval, const ufbx_transform *t) -{ - *retval = ufbx_transform_to_matrix(t); -} - -ufbx_abi void ufbx_ffi_matrix_to_transform(ufbx_transform *retval, const ufbx_matrix *m) -{ - *retval = ufbx_matrix_to_transform(m); -} - -ufbx_abi void ufbx_ffi_get_skin_vertex_matrix(ufbx_matrix *retval, const ufbx_skin_deformer *skin, size_t vertex, const ufbx_matrix *fallback) -{ - *retval = ufbx_get_skin_vertex_matrix(skin, vertex, fallback); -} - -ufbx_abi void ufbx_ffi_get_blend_shape_vertex_offset(ufbx_vec3 *retval, const ufbx_blend_shape *shape, size_t vertex) -{ - *retval = ufbx_get_blend_shape_vertex_offset(shape, vertex); -} - -ufbx_abi void ufbx_ffi_get_blend_vertex_offset(ufbx_vec3 *retval, const ufbx_blend_deformer *blend, size_t vertex) -{ - *retval = ufbx_get_blend_vertex_offset(blend, vertex); -} - -ufbx_abi void ufbx_ffi_evaluate_nurbs_curve(ufbx_curve_point *retval, const ufbx_nurbs_curve *curve, ufbx_real u) -{ - *retval = ufbx_evaluate_nurbs_curve(curve, u); -} - -ufbx_abi void ufbx_ffi_evaluate_nurbs_surface(ufbx_surface_point *retval, const ufbx_nurbs_surface *surface, ufbx_real u, ufbx_real v) -{ - *retval = ufbx_evaluate_nurbs_surface(surface, u, v); -} - -ufbx_abi void ufbx_ffi_get_weighted_face_normal(ufbx_vec3 *retval, const ufbx_vertex_vec3 *positions, const ufbx_face *face) -{ - *retval = ufbx_get_weighted_face_normal(positions, *face); -} - -ufbx_abi uint32_t ufbx_ffi_triangulate_face(uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, const ufbx_face *face) -{ - return ufbx_triangulate_face(indices, num_indices, mesh, *face); -} - -ufbx_abi size_t ufbx_ffi_get_triangulate_face_num_indices(const ufbx_face *face) -{ - return ufbx_get_triangulate_face_num_indices(*face); -} - -ufbx_abi ufbx_vec3 ufbx_ffi_evaluate_baked_vec3(const ufbx_baked_vec3 *keyframes, size_t num_keyframes, double time) -{ - ufbx_baked_vec3_list list = { (ufbx_baked_vec3*)keyframes, num_keyframes }; - return ufbx_evaluate_baked_vec3(list, time); -} - -ufbx_abi ufbx_quat ufbx_ffi_evaluate_baked_quat(const ufbx_baked_quat *keyframes, size_t num_keyframes, double time) -{ - ufbx_baked_quat_list list = { (ufbx_baked_quat*)keyframes, num_keyframes }; - return ufbx_evaluate_baked_quat(list, time); -} - #ifdef __cplusplus } #endif diff --git a/thirdparty/ufbx/ufbx.h b/thirdparty/ufbx/ufbx.h index bb331102a597..072569068aae 100644 --- a/thirdparty/ufbx/ufbx.h +++ b/thirdparty/ufbx/ufbx.h @@ -94,6 +94,10 @@ #define ufbx_inline static #endif +// Assertion function used in ufbx, defaults to C standard `assert()`. +// You can define this to your custom preferred assert macro, but in that case +// make sure that it is also used within `ufbx.c`. +// Defining `UFBX_NO_ASSERT` to any value disables assertions. #ifndef ufbx_assert #if defined(UFBX_NO_ASSERT) #define ufbx_assert(cond) (void)0 @@ -110,21 +114,58 @@ // breaking API guarantees. #define ufbx_unsafe +// Linkage of the main ufbx API functions. +// Defaults to nothing, or `static` if `UFBX_STATIC` is defined. +// If you want to isolate ufbx to a single translation unit you can do the following: +// #define UFBX_STATIC +// #include "ufbx.h" +// #include "ufbx.c" #ifndef ufbx_abi - #define ufbx_abi + #if defined(UFBX_STATIC) + #define ufbx_abi static + #else + #define ufbx_abi + #endif +#endif + +// Linkage of the main ufbx data fields in the header. +// Defaults to `extern`, or `static` if `UFBX_STATIC` is defined. +#ifndef ufbx_abi_data + #if defined(UFBX_STATIC) + #define ufbx_abi_data static + #else + #define ufbx_abi_data extern + #endif +#endif + +// Linkage of the main ufbx data fields in the source. +// Defaults to nothing, or `static` if `UFBX_STATIC` is defined. +#ifndef ufbx_abi_data_definition + #if defined(UFBX_STATIC) + #define ufbx_abi_data_def static + #else + #define ufbx_abi_data_def + #endif #endif // -- Configuration -#if defined(UFBX_REAL_IS_FLOAT) - typedef float ufbx_real; -#else - typedef double ufbx_real; +#ifndef UFBX_REAL_TYPE + #if defined(UFBX_REAL_IS_FLOAT) + #define UFBX_REAL_TYPE float + #else + #define UFBX_REAL_TYPE double + #endif #endif +// Limits for embedded arrays within structures. #define UFBX_ERROR_STACK_MAX_DEPTH 8 #define UFBX_PANIC_MESSAGE_LENGTH 128 #define UFBX_ERROR_INFO_LENGTH 256 + +// Number of thread groups to use if threading is enabled. +// A thread group processes a number of tasks and is then waited and potentially +// re-used later. In essence, this controls the granularity of threading. #define UFBX_THREAD_GROUP_COUNT 4 // -- Language @@ -214,17 +255,27 @@ struct ufbx_converter { }; // -- Version +// Packing/unpacking for `UFBX_HEADER_VERSION` and `ufbx_source_version`. #define ufbx_pack_version(major, minor, patch) ((uint32_t)(major)*1000000u + (uint32_t)(minor)*1000u + (uint32_t)(patch)) #define ufbx_version_major(version) ((uint32_t)(version)/1000000u%1000u) #define ufbx_version_minor(version) ((uint32_t)(version)/1000u%1000u) #define ufbx_version_patch(version) ((uint32_t)(version)%1000u) -#define UFBX_HEADER_VERSION ufbx_pack_version(0, 11, 1) +// Version of the ufbx header. +// `UFBX_VERSION` is simply an alias of `UFBX_HEADER_VERSION`. +// `ufbx_source_version` contains the version of the corresponding source file. +// HINT: The version can be compared numerically to the result of `ufbx_pack_version()`, +// for example `#if UFBX_VERSION >= ufbx_pack_version(0, 12, 0)`. +#define UFBX_HEADER_VERSION ufbx_pack_version(0, 14, 0) #define UFBX_VERSION UFBX_HEADER_VERSION // -- Basic types -#define UFBX_NO_INDEX ((uint32_t)~0u) +// Main floating point type used everywhere in ufbx, defaults to `double`. +// If you define `UFBX_REAL_IS_FLOAT` to any value, `ufbx_real` will be defined +// as `float` instead. +// You can also manually define `UFBX_REAL_TYPE` to any floating point type. +typedef UFBX_REAL_TYPE ufbx_real; // Null-terminated UTF-8 encoded string within an FBX file typedef struct ufbx_string { @@ -340,6 +391,9 @@ UFBX_LIST_TYPE(ufbx_vec3_list, ufbx_vec3); UFBX_LIST_TYPE(ufbx_vec4_list, ufbx_vec4); UFBX_LIST_TYPE(ufbx_string_list, ufbx_string); +// Sentinel value used to represent a missing index. +#define UFBX_NO_INDEX ((uint32_t)~0u) + // -- Document object model typedef enum ufbx_dom_value_type UFBX_ENUM_REPR { @@ -578,6 +632,10 @@ typedef struct ufbx_selection_node ufbx_selection_node; typedef struct ufbx_character ufbx_character; typedef struct ufbx_constraint ufbx_constraint; +// Audio +typedef struct ufbx_audio_layer ufbx_audio_layer; +typedef struct ufbx_audio_clip ufbx_audio_clip; + // Miscellaneous typedef struct ufbx_pose ufbx_pose; typedef struct ufbx_metadata_object ufbx_metadata_object; @@ -621,6 +679,8 @@ UFBX_LIST_TYPE(ufbx_selection_set_list, ufbx_selection_set*); UFBX_LIST_TYPE(ufbx_selection_node_list, ufbx_selection_node*); UFBX_LIST_TYPE(ufbx_character_list, ufbx_character*); UFBX_LIST_TYPE(ufbx_constraint_list, ufbx_constraint*); +UFBX_LIST_TYPE(ufbx_audio_layer_list, ufbx_audio_layer*); +UFBX_LIST_TYPE(ufbx_audio_clip_list, ufbx_audio_clip*); UFBX_LIST_TYPE(ufbx_pose_list, ufbx_pose*); UFBX_LIST_TYPE(ufbx_metadata_object_list, ufbx_metadata_object*); @@ -663,6 +723,8 @@ typedef enum ufbx_element_type UFBX_ENUM_REPR { UFBX_ELEMENT_SELECTION_NODE, // < `ufbx_selection_node` UFBX_ELEMENT_CHARACTER, // < `ufbx_character` UFBX_ELEMENT_CONSTRAINT, // < `ufbx_constraint` + UFBX_ELEMENT_AUDIO_LAYER, // < `ufbx_audio_layer` + UFBX_ELEMENT_AUDIO_CLIP, // < `ufbx_audio_clip` UFBX_ELEMENT_POSE, // < `ufbx_pose` UFBX_ELEMENT_METADATA_OBJECT, // < `ufbx_metadata_object` @@ -690,7 +752,7 @@ UFBX_LIST_TYPE(ufbx_connection_list, ufbx_connection); // Some fields (like `connections_src`) are advanced and not visible // in the specialized element structs. // NOTE: The `element_id` value is consistent when loading the -// _same_ file, but re-exporting the file will invalidate them. (TOMOVE) +// _same_ file, but re-exporting the file will invalidate them. struct ufbx_element { ufbx_string name; ufbx_props props; @@ -756,6 +818,7 @@ typedef enum ufbx_inherit_mode UFBX_ENUM_REPR { UFBX_ENUM_TYPE(ufbx_inherit_mode, UFBX_INHERIT_MODE, UFBX_INHERIT_MODE_COMPONENTWISE_SCALE); +// Axis used to mirror transformations for handedness conversion. typedef enum ufbx_mirror_axis UFBX_ENUM_REPR { UFBX_MIRROR_AXIS_NONE, @@ -799,6 +862,7 @@ struct ufbx_node { ufbx_nullable ufbx_mesh *mesh; ufbx_nullable ufbx_light *light; ufbx_nullable ufbx_camera *camera; + ufbx_nullable ufbx_bone *bone; // Less common attributes use these fields. // @@ -930,11 +994,23 @@ struct ufbx_node { // single defined value per vertex accessible via: // attrib.values.data[attrib.indices.data[mesh->vertex_first_index[vertex_ix]] typedef struct ufbx_vertex_attrib { + // Is this attribute defined by the mesh. bool exists; + // List of values the attribute uses. ufbx_void_list values; + // Indices into `values[]`, indexed up to `ufbx_mesh.num_indices`. ufbx_uint32_list indices; + // Number of `ufbx_real` entries per value. size_t value_reals; + // `true` if this attribute is defined per vertex, instead of per index. bool unique_per_vertex; + // Optional 4th 'W' component for the attribute. + // May be defined for the following: + // ufbx_mesh.vertex_normal + // ufbx_mesh.vertex_tangent / ufbx_uv_set.vertex_tangent + // ufbx_mesh.vertex_bitangent / ufbx_uv_set.vertex_bitangent + // NOTE: This is not loaded by default, set `ufbx_load_opts.retain_vertex_attrib_w`. + ufbx_real_list values_w; } ufbx_vertex_attrib; // 1D vertex attribute, see `ufbx_vertex_attrib` for information @@ -944,6 +1020,7 @@ typedef struct ufbx_vertex_real { ufbx_uint32_list indices; size_t value_reals; bool unique_per_vertex; + ufbx_real_list values_w; UFBX_VERTEX_ATTRIB_IMPL(ufbx_real) } ufbx_vertex_real; @@ -955,6 +1032,7 @@ typedef struct ufbx_vertex_vec2 { ufbx_uint32_list indices; size_t value_reals; bool unique_per_vertex; + ufbx_real_list values_w; UFBX_VERTEX_ATTRIB_IMPL(ufbx_vec2) } ufbx_vertex_vec2; @@ -966,6 +1044,7 @@ typedef struct ufbx_vertex_vec3 { ufbx_uint32_list indices; size_t value_reals; bool unique_per_vertex; + ufbx_real_list values_w; UFBX_VERTEX_ATTRIB_IMPL(ufbx_vec3) } ufbx_vertex_vec3; @@ -977,6 +1056,7 @@ typedef struct ufbx_vertex_vec4 { ufbx_uint32_list indices; size_t value_reals; bool unique_per_vertex; + ufbx_real_list values_w; UFBX_VERTEX_ATTRIB_IMPL(ufbx_vec4) } ufbx_vertex_vec4; @@ -1244,6 +1324,11 @@ struct ufbx_mesh { // Segments for each face group. ufbx_mesh_part_list face_group_parts; + // Order of `material_parts` by first face that refers to it. + // Useful for compatibility with FBX SDK and various importers using it, + // as they use this material order by default. + ufbx_uint32_list material_part_usage_order; + // Skinned vertex positions, for efficiency the skinned positions are the // same as the static ones for non-skinned meshes and `skinned_is_local` // is set to true meaning you need to transform them manually using @@ -2272,6 +2357,7 @@ typedef enum ufbx_shader_type UFBX_ENUM_REPR { UFBX_SHADER_SHADERFX_GRAPH, // Variation of the FBX phong shader that can recover PBR properties like // `metalness` or `roughness` from the FBX non-physical values. + // NOTE: Enable `ufbx_load_opts.use_blender_pbr_material`. UFBX_SHADER_BLENDER_PHONG, // Wavefront .mtl format shader (used by .obj files) UFBX_SHADER_WAVEFRONT_MTL, @@ -2663,6 +2749,7 @@ typedef enum ufbx_shader_texture_type UFBX_ENUM_REPR { UFBX_ENUM_TYPE(ufbx_shader_texture_type, UFBX_SHADER_TEXTURE_TYPE, UFBX_SHADER_TEXTURE_OSL); +// Input to a shader texture, see `ufbx_shader_texture`. typedef struct ufbx_shader_texture_input { // Name of the input. @@ -2702,6 +2789,13 @@ typedef struct ufbx_shader_texture_input { UFBX_LIST_TYPE(ufbx_shader_texture_input_list, ufbx_shader_texture_input); +// Texture that emulates a shader graph node. +// 3ds Max exports some materials as node graphs serialized to textures. +// ufbx can parse a small subset of these, as normal maps are often hidden behind +// some kind of bump node. +// NOTE: These encode a lot of details of 3ds Max internals, not recommended for direct use. +// HINT: `ufbx_texture.file_textures[]` contains a list of "real" textures that are connected +// to the `ufbx_texture` that is pretending to be a shader node. typedef struct ufbx_shader_texture { // Type of this shader node. @@ -3246,6 +3340,52 @@ struct ufbx_constraint { ufbx_vec3 ik_pole_vector; }; +// -- Audio + +struct ufbx_audio_layer { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Clips contained in this layer. + ufbx_audio_clip_list clips; +}; + +struct ufbx_audio_clip { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Filename relative to the currently loaded file. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_string filename; + // Absolute filename specified in the file. + ufbx_string absolute_filename; + // Relative filename specified in the file. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_string relative_filename; + + // Filename relative to the loaded file, non-UTF-8 encoded. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_blob raw_filename; + // Absolute filename specified in the file, non-UTF-8 encoded. + ufbx_blob raw_absolute_filename; + // Relative filename specified in the file, non-UTF-8 encoded. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_blob raw_relative_filename; + + // Optional embedded content blob, eg. raw .png format data + ufbx_blob content; +}; + // -- Miscellaneous typedef struct ufbx_bone_pose { @@ -3313,12 +3453,11 @@ typedef enum ufbx_exporter UFBX_ENUM_REPR { UFBX_EXPORTER_BLENDER_BINARY, UFBX_EXPORTER_BLENDER_ASCII, UFBX_EXPORTER_MOTION_BUILDER, - UFBX_EXPORTER_BC_UNITY_EXPORTER, UFBX_ENUM_FORCE_WIDTH(UFBX_EXPORTER) } ufbx_exporter; -UFBX_ENUM_TYPE(ufbx_exporter, UFBX_EXPORTER, UFBX_EXPORTER_BC_UNITY_EXPORTER); +UFBX_ENUM_TYPE(ufbx_exporter, UFBX_EXPORTER, UFBX_EXPORTER_MOTION_BUILDER); typedef struct ufbx_application { ufbx_string vendor; @@ -3355,6 +3494,12 @@ typedef enum ufbx_warning_type UFBX_ENUM_REPR { // Duplicated connection between two elements that shouldn't have. UFBX_WARNING_DUPLICATE_CONNECTION, + // Vertex 'W' attribute length differs from main attribute. + UFBX_WARNING_BAD_VERTEX_W_ATTRIBUTE, + + // Missing polygon mapping type. + UFBX_WARNING_MISSING_POLYGON_MAPPING, + // Out-of-bounds index has been clamped to be in-bounds. // HINT: You can use `ufbx_index_error_handling` to adjust behavior. UFBX_WARNING_INDEX_CLAMPED, @@ -3524,6 +3669,9 @@ typedef struct ufbx_metadata { ufbx_real bone_prop_size_unit; bool bone_prop_limb_length_relative; + + ufbx_real ortho_size_unit; + int64_t ktime_second; // < One second in internal KTime units ufbx_string original_file_path; @@ -3606,11 +3754,14 @@ typedef struct ufbx_scene_settings { // HINT: Use `ufbx_load_opts.target_unit_meters` to normalize this. ufbx_real unit_meters; + // Frames per second the animation is defined at. double frames_per_second; ufbx_vec3 ambient_color; ufbx_string default_camera; + // Animation user interface settings. + // HINT: Use `ufbx_scene_settings.frames_per_second` instead of interpreting these yourself. ufbx_time_mode time_mode; ufbx_time_protocol time_protocol; ufbx_snap_mode snap_mode; @@ -3691,6 +3842,10 @@ struct ufbx_scene { ufbx_character_list characters; ufbx_constraint_list constraints; + // Audio + ufbx_audio_layer_list audio_layers; + ufbx_audio_clip_list audio_clips; + // Miscellaneous ufbx_pose_list poses; ufbx_metadata_object_list metadata_objects; @@ -3748,10 +3903,13 @@ typedef struct ufbx_topo_edge { ufbx_topo_flags flags; } ufbx_topo_edge; +// Vertex data array for `ufbx_generate_indices()`. +// NOTE: `ufbx_generate_indices()` compares the vertices using `memcmp()`, so +// any padding should be cleared to zero. typedef struct ufbx_vertex_stream { - void *data; - size_t vertex_count; - size_t vertex_size; + void *data; // < Data pointer of shape `char[vertex_count][vertex_size]`. + size_t vertex_count; // < Number of vertices in this stream, for sanity checking. + size_t vertex_size; // < Size of a vertex in bytes. } ufbx_vertex_stream; // -- Memory callbacks @@ -4179,6 +4337,10 @@ typedef enum ufbx_inherit_mode_handling UFBX_ENUM_REPR { // as `UFBX_INHERIT_MODE_HANDLING_HELPER_NODES`. UFBX_INHERIT_MODE_HANDLING_COMPENSATE, + // Attempt to compensate for bone scale by inversely scaling children. + // Will never create helper nodes. + UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK, + // Ignore non-standard inheritance modes. // Forces all nodes to have `UFBX_INHERIT_MODE_NORMAL` regardless of the // inherit mode specified in the file. This can be useful for emulating @@ -4207,51 +4369,120 @@ typedef enum ufbx_pivot_handling UFBX_ENUM_REPR { UFBX_ENUM_TYPE(ufbx_pivot_handling, UFBX_PIVOT_HANDLING, UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT); +typedef enum ufbx_baked_key_flags UFBX_FLAG_REPR { + // This keyframe represents a constant step from the left side + UFBX_BAKED_KEY_STEP_LEFT = 0x1, + // This keyframe represents a constant step from the right side + UFBX_BAKED_KEY_STEP_RIGHT = 0x2, + // This keyframe is the main part of a step + // Bordering either `UFBX_BAKED_KEY_STEP_LEFT` or `UFBX_BAKED_KEY_STEP_RIGHT`. + UFBX_BAKED_KEY_STEP_KEY = 0x4, + // This keyframe is a real keyframe in the source animation + UFBX_BAKED_KEY_KEYFRAME = 0x8, + // This keyframe has been reduced by maximum sample rate. + // See `ufbx_bake_opts.maximum_sample_rate`. + UFBX_BAKED_KEY_REDUCED = 0x10, + + UFBX_FLAG_FORCE_WIDTH(UFBX_BAKED_KEY) +} ufbx_baked_key_flags; + typedef struct ufbx_baked_vec3 { - double time; - ufbx_vec3 value; + double time; // < Time of the keyframe, in seconds + ufbx_vec3 value; // < Value at `time`, can be linearly interpolated + ufbx_baked_key_flags flags; // < Additional information about the keyframe } ufbx_baked_vec3; UFBX_LIST_TYPE(ufbx_baked_vec3_list, ufbx_baked_vec3); typedef struct ufbx_baked_quat { - double time; - ufbx_quat value; + double time; // < Time of the keyframe, in seconds + ufbx_quat value; // < Value at `time`, can be (spherically) linearly interpolated + ufbx_baked_key_flags flags; // < Additional information about the keyframe } ufbx_baked_quat; UFBX_LIST_TYPE(ufbx_baked_quat_list, ufbx_baked_quat); +// Baked transform animation for a single node. typedef struct ufbx_baked_node { + + // Typed ID of the node, maps to `ufbx_scene.nodes[]`. uint32_t typed_id; + // Element ID of the element, maps to `ufbx_scene.elements[]`. uint32_t element_id; + + // The translation channel has constant values for the whole animation. bool constant_translation; + // The rotation channel has constant values for the whole animation. bool constant_rotation; + // The scale channel has constant values for the whole animation. bool constant_scale; + + // Translation keys for the animation, maps to `ufbx_node.local_transform.translation`. ufbx_baked_vec3_list translation_keys; + // Rotation keyframes, maps to `ufbx_node.local_transform.rotation`. ufbx_baked_quat_list rotation_keys; + // Scale keyframes, maps to `ufbx_node.local_transform.scale`. ufbx_baked_vec3_list scale_keys; + } ufbx_baked_node; UFBX_LIST_TYPE(ufbx_baked_node_list, ufbx_baked_node); +// Baked property animation. typedef struct ufbx_baked_prop { + // Name of the property, eg. `"Visibility"`. ufbx_string name; + // The value of the property is constant for the whole animation. bool constant_value; + // Property value keys. ufbx_baked_vec3_list keys; } ufbx_baked_prop; UFBX_LIST_TYPE(ufbx_baked_prop_list, ufbx_baked_prop); +// Baked property animation for a single element. typedef struct ufbx_baked_element { + // Element ID of the element, maps to `ufbx_scene.elements[]`. uint32_t element_id; + // List of properties the animation modifies. ufbx_baked_prop_list props; } ufbx_baked_element; UFBX_LIST_TYPE(ufbx_baked_element_list, ufbx_baked_element); +typedef struct ufbx_baked_anim_metadata { + // Memory statistics + size_t result_memory_used; + size_t temp_memory_used; + size_t result_allocs; + size_t temp_allocs; +} ufbx_baked_anim_metadata; + +// Animation baked into linearly interpolated keyframes. +// See `ufbx_bake_anim()`. typedef struct ufbx_baked_anim { + + // Nodes that are modified by the animation. + // Some nodes may be missing if the specified animation does not transform them. + // Conversely, some non-obviously animated nodes may be included as exporters + // often may add dummy keyframes for objects. ufbx_baked_node_list nodes; + + // Element properties modified by the animation. ufbx_baked_element_list elements; + + // Playback time range for the animation. + double playback_time_begin; + double playback_time_end; + double playback_duration; + + // Keyframe time range. + double key_time_min; + double key_time_max; + + // Additional bake information. + ufbx_baked_anim_metadata metadata; + } ufbx_baked_anim; // -- Thread API @@ -4329,6 +4560,12 @@ typedef struct ufbx_load_opts { // Clean-up skin weights by removing negative, zero and NAN weights. bool clean_skin_weights; + // Read Blender materials as PBR values. + // Blender converts PBR materials to legacy FBX Phong materials in a deterministic way. + // If this setting is enabled, such materials will be read as `UFBX_SHADER_BLENDER_PHONG`, + // which means ufbx will be able to parse roughness and metallic textures. + bool use_blender_pbr_material; + // Don't adjust reading the FBX file depending on the detected exporter bool disable_quirks; @@ -4470,6 +4707,10 @@ typedef struct ufbx_load_opts { // Specify how to handle Unicode errors in strings. ufbx_unicode_error_handling unicode_error_handling; + // Retain the 'W' component of mesh normal/tangent/bitangent. + // See `ufbx_vertex_attrib.values_w`. + bool retain_vertex_attrib_w; + // Retain the raw document structure using `ufbx_dom_node`. bool retain_dom; @@ -4512,6 +4753,16 @@ typedef struct ufbx_load_opts { // (.obj) Data for the .mtl file. ufbx_blob obj_mtl_data; + // The world unit in meters that .obj files are assumed to be in. + // .obj files do not define the working units. By default the unit scale + // is read as zero, and no unit conversion is performed. + ufbx_real obj_unit_meters; + + // Coordinate space .obj files are assumed to be in. + // .obj files do not define the coordinate space they use. By default no + // coordinate space is assumed and no conversion is performed. + ufbx_coordinate_axes obj_axes; + uint32_t _end_zero; } ufbx_load_opts; @@ -4559,16 +4810,19 @@ UFBX_LIST_TYPE(ufbx_const_transform_override_list, const ufbx_transform_override typedef struct ufbx_anim_opts { uint32_t _begin_zero; - // Animation layers + // Animation layers indices. + // Corresponding to `ufbx_scene.anim_layers[]`, aka `ufbx_anim_layer.typed_id`. ufbx_const_uint32_list layer_ids; - // Override layer weights + // Override layer weights, parallel to `ufbx_anim_opts.layer_ids[]`. ufbx_const_real_list override_layer_weights; - // Property overrides + // Property overrides. + // These allow you to override FBX properties, such as 'UFBX_Lcl_Rotation`. ufbx_const_prop_override_desc_list prop_overrides; - // Transform overrides + // Transform overrides. + // These allow you to override individual nodes' `ufbx_node.local_transform`. ufbx_const_transform_override_list transform_overrides; // Ignore connected properties @@ -4579,16 +4833,49 @@ typedef struct ufbx_anim_opts { uint32_t _end_zero; } ufbx_anim_opts; +// Specifies how to handle stepped tangents. +typedef enum ufbx_bake_step_handling UFBX_ENUM_REPR { + + // One millisecond default step duration, with potential extra slack for converting to `float`. + UFBX_BAKE_STEP_HANDLING_DEFAULT, + + // Use a custom interpolation duration for the constant step. + // See `ufbx_bake_opts.step_custom_duration` and optionally `ufbx_bake_opts.step_custom_epsilon`. + UFBX_BAKE_STEP_HANDLING_CUSTOM_DURATION, + + // Stepped keyframes are represented as keyframes at the exact same time. + // Use flags `UFBX_BAKED_KEY_STEP_LEFT` and `UFBX_BAKED_KEY_STEP_RIGHT` to differentiate + // between the primary key and edge limits. + UFBX_BAKE_STEP_HANDLING_IDENTICAL_TIME, + + // Represent stepped keyframe times as the previous/next representable `double` value. + // Using this and robust linear interpolation will handle stepped tangents correctly + // without having to look at the key flags. + // NOTE: Casting these values to `float` or otherwise modifying them can collapse + // the keyframes to have the identical time. + UFBX_BAKE_STEP_HANDLING_ADJACENT_DOUBLE, + + // Treat all stepped tangents as linearly interpolated. + UFBX_BAKE_STEP_HANDLING_IGNORE, + + UFBX_ENUM_FORCE_WIDTH(ufbx_bake_step_handling) +} ufbx_bake_step_handling; + +UFBX_ENUM_TYPE(ufbx_bake_step_handling, UFBX_BAKE_STEP_HANDLING, UFBX_BAKE_STEP_HANDLING_IGNORE); + typedef struct ufbx_bake_opts { uint32_t _begin_zero; ufbx_allocator_opts temp_allocator; // < Allocator used during loading ufbx_allocator_opts result_allocator; // < Allocator used for the final baked animation - // Offset to start the evaluation from. - double time_start_offset; + // Move the keyframe times to start from zero regardless of the animation start time. + // For example, for an animation spanning between frames [30, 60] will be moved to + // [0, 30] in the baked animation. + // NOTE: This is in general not equivalent to subtracting `ufbx_anim.time_begin` + // from each keyframe, as this trimming is done exactly using internal FBX ticks. + bool trim_start_time; - // Sample rate in seconds. // Samples per second to use for resampling non-linear animation. // Default: 30 double resample_rate; @@ -4620,9 +4907,16 @@ typedef struct ufbx_bake_opts { // Default: 32 size_t max_keyframe_segments; - // Timestep in seconds for constant interpolation. - // Default of `0.0` uses the smallest representable time offset. - double constant_timestep; + // How to handle stepped tangents. + ufbx_bake_step_handling step_handling; + + // Interpolation duration used by `UFBX_BAKE_STEP_HANDLING_CUSTOM_DURATION`. + double step_custom_duration; + + // Interpolation epsilon used by `UFBX_BAKE_STEP_HANDLING_CUSTOM_DURATION`. + // Defined as the minimum fractional decrease/increase in key time, ie. + // `time / (1.0 + step_custom_epsilon)` and `time * (1.0 + step_custom_epsilon)`. + double step_custom_epsilon; // Enable key reduction. bool key_reduction_enabled; @@ -4640,10 +4934,6 @@ typedef struct ufbx_bake_opts { // Default: `4` size_t key_reduction_passes; - // Compensate for `UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE` by adjusting child scale. - // NOTE: This is an lossy operation, and properly works only for uniform scaling. - bool compensate_inherit_no_scale; - uint32_t _end_zero; } ufbx_bake_opts; @@ -4656,7 +4946,7 @@ typedef struct ufbx_tessellate_curve_opts { ufbx_allocator_opts result_allocator; // < Allocator used for the final line curve // How many segments tessellate each span in `ufbx_nurbs_basis.spans`. - uint32_t span_subdivision; + size_t span_subdivision; uint32_t _end_zero; } ufbx_tessellate_curve_opts; @@ -4674,8 +4964,8 @@ typedef struct ufbx_tessellate_surface_opts { // would make it easy to create an FBX file with an absurdly high subdivision // rate (similar to mesh subdivision). Please enforce copy the value yourself // enforcing whatever limits you deem reasonable. - uint32_t span_subdivision_u; - uint32_t span_subdivision_v; + size_t span_subdivision_u; + size_t span_subdivision_v; // Skip computing `ufbx_mesh.material_parts[]` bool skip_mesh_parts; @@ -4781,27 +5071,26 @@ extern "C" { #endif // Various zero/empty/identity values -extern const ufbx_string ufbx_empty_string; -extern const ufbx_blob ufbx_empty_blob; -extern const ufbx_matrix ufbx_identity_matrix; -extern const ufbx_transform ufbx_identity_transform; -extern const ufbx_vec2 ufbx_zero_vec2; -extern const ufbx_vec3 ufbx_zero_vec3; -extern const ufbx_vec4 ufbx_zero_vec4; -extern const ufbx_quat ufbx_identity_quat; - -// Commonly used coordinate axes - -extern const ufbx_coordinate_axes ufbx_axes_right_handed_y_up; -extern const ufbx_coordinate_axes ufbx_axes_right_handed_z_up; -extern const ufbx_coordinate_axes ufbx_axes_left_handed_y_up; -extern const ufbx_coordinate_axes ufbx_axes_left_handed_z_up; +ufbx_abi_data const ufbx_string ufbx_empty_string; +ufbx_abi_data const ufbx_blob ufbx_empty_blob; +ufbx_abi_data const ufbx_matrix ufbx_identity_matrix; +ufbx_abi_data const ufbx_transform ufbx_identity_transform; +ufbx_abi_data const ufbx_vec2 ufbx_zero_vec2; +ufbx_abi_data const ufbx_vec3 ufbx_zero_vec3; +ufbx_abi_data const ufbx_vec4 ufbx_zero_vec4; +ufbx_abi_data const ufbx_quat ufbx_identity_quat; + +// Commonly used coordinate axes. +ufbx_abi_data const ufbx_coordinate_axes ufbx_axes_right_handed_y_up; +ufbx_abi_data const ufbx_coordinate_axes ufbx_axes_right_handed_z_up; +ufbx_abi_data const ufbx_coordinate_axes ufbx_axes_left_handed_y_up; +ufbx_abi_data const ufbx_coordinate_axes ufbx_axes_left_handed_z_up; // Sizes of element types. eg `sizeof(ufbx_node)` -extern const size_t ufbx_element_type_size[UFBX_ELEMENT_TYPE_COUNT]; +ufbx_abi_data const size_t ufbx_element_type_size[UFBX_ELEMENT_TYPE_COUNT]; // Version of the source file, comparable to `UFBX_HEADER_VERSION` -extern const uint32_t ufbx_source_version; +ufbx_abi_data const uint32_t ufbx_source_version; // Practically always `true` (see below), if not you need to be careful with threads. // @@ -4958,7 +5247,6 @@ ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time // Evaluate a value from bundled animation curves. ufbx_abi ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_value, double time); -ufbx_abi ufbx_vec2 ufbx_evaluate_anim_value_vec2(const ufbx_anim_value *anim_value, double time); ufbx_abi ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_value *anim_value, double time); // Evaluate an animated property `name` from `element` at `time`. @@ -4973,6 +5261,7 @@ ufbx_inline ufbx_prop ufbx_evaluate_prop(const ufbx_anim *anim, const ufbx_eleme // `ufbx_props.defaults`. This lets you use `ufbx_find_prop/value()` for the results. ufbx_abi ufbx_props ufbx_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size); +// Flags to control `ufbx_evaluate_transform_flags()`. typedef enum ufbx_transform_flags UFBX_FLAG_REPR { // Ignore parent scale helper. @@ -4986,16 +5275,22 @@ typedef enum ufbx_transform_flags UFBX_FLAG_REPR { // Require explicit components UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES = 0x4, + // If `UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES`: Evaluate `ufbx_transform.translation`. UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION = 0x10, + // If `UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES`: Evaluate `ufbx_transform.rotation`. UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION = 0x20, + // If `UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES`: Evaluate `ufbx_transform.scale`. UFBX_TRANSFORM_FLAG_INCLUDE_SCALE = 0x40, UFBX_FLAG_FORCE_WIDTH(UFBX_TRANSFORM_FLAGS) } ufbx_transform_flags; +// Evaluate the animated transform of a node given a time. ufbx_abi ufbx_transform ufbx_evaluate_transform(const ufbx_anim *anim, const ufbx_node *node, double time); ufbx_abi ufbx_transform ufbx_evaluate_transform_flags(const ufbx_anim *anim, const ufbx_node *node, double time, uint32_t flags); +// Evaluate the blend shape weight of a blend channel. +// NOTE: Return value uses `1.0` for full weight, instead of `100.0` that the internal property `UFBX_Weight` uses. ufbx_abi ufbx_real ufbx_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time); // Evaluate the whole `scene` at a specific `time` in the animation `anim`. @@ -5007,42 +5302,70 @@ ufbx_abi ufbx_real ufbx_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_ // scene cannot be freed until all evaluated scenes are freed. ufbx_abi ufbx_scene *ufbx_evaluate_scene(const ufbx_scene *scene, const ufbx_anim *anim, double time, const ufbx_evaluate_opts *opts, ufbx_error *error); +// Create a custom animation descriptor. +// `ufbx_anim_opts` is used to specify animation layers and weights. +// HINT: You can also leave `ufbx_anim_opts.layer_ids[]` empty and only specify +// overrides to evaluate the scene with different properties or local transforms. ufbx_abi ufbx_anim *ufbx_create_anim(const ufbx_scene *scene, const ufbx_anim_opts *opts, ufbx_error *error); -ufbx_abi void ufbx_retain_anim(ufbx_anim *anim); +// Free an animation returned by `ufbx_create_anim()`. ufbx_abi void ufbx_free_anim(ufbx_anim *anim); +// Increase the animation reference count. +ufbx_abi void ufbx_retain_anim(ufbx_anim *anim); + // Animation baking +// "Bake" an animation to linearly interpolated keyframes. +// Composites the FBX transformation chain into quaternion rotations. ufbx_abi ufbx_baked_anim *ufbx_bake_anim(const ufbx_scene *scene, const ufbx_anim *anim, const ufbx_bake_opts *opts, ufbx_error *error); ufbx_abi void ufbx_retain_baked_anim(ufbx_baked_anim *bake); ufbx_abi void ufbx_free_baked_anim(ufbx_baked_anim *bake); +ufbx_abi ufbx_baked_node *ufbx_find_baked_node_by_typed_id(ufbx_baked_anim *bake, uint32_t typed_id); +ufbx_abi ufbx_baked_node *ufbx_find_baked_node(ufbx_baked_anim *bake, ufbx_node *node); + +ufbx_abi ufbx_baked_element *ufbx_find_baked_element_by_element_id(ufbx_baked_anim *bake, uint32_t element_id); +ufbx_abi ufbx_baked_element *ufbx_find_baked_element(ufbx_baked_anim *bake, ufbx_element *element); + +// Evaluate baked animation `keyframes` at `time`. +// Internally linearly interpolates between two adjacent keyframes. +// Handles stepped tangents cleanly, which is not strictly necessary for custom interpolation. ufbx_abi ufbx_vec3 ufbx_evaluate_baked_vec3(ufbx_baked_vec3_list keyframes, double time); + +// Evaluate baked animation `keyframes` at `time`. +// Internally spherically interpolates (`ufbx_quat_slerp()`) between two adjacent keyframes. +// Handles stepped tangents cleanly, which is not strictly necessary for custom interpolation. ufbx_abi ufbx_quat ufbx_evaluate_baked_quat(ufbx_baked_quat_list keyframes, double time); // Poses +// Retrieve the bone pose for `node`. +// Returns `NULL` if the pose does not contain `node`. ufbx_abi ufbx_bone_pose *ufbx_get_bone_pose(const ufbx_pose *pose, const ufbx_node *node); // Materials +// Find a texture for a given material FBX property. ufbx_abi ufbx_texture *ufbx_find_prop_texture_len(const ufbx_material *material, const char *name, size_t name_len); ufbx_inline ufbx_texture *ufbx_find_prop_texture(const ufbx_material *material, const char *name) { return ufbx_find_prop_texture_len(material, name, strlen(name)); } +// Find a texture for a given shader property. ufbx_abi ufbx_string ufbx_find_shader_prop_len(const ufbx_shader *shader, const char *name, size_t name_len); ufbx_inline ufbx_string ufbx_find_shader_prop(const ufbx_shader *shader, const char *name) { return ufbx_find_shader_prop_len(shader, name, strlen(name)); } +// Map from a shader property to material property. ufbx_abi ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings_len(const ufbx_shader *shader, const char *name, size_t name_len); ufbx_inline ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings(const ufbx_shader *shader, const char *name) { return ufbx_find_shader_prop_bindings_len(shader, name, strlen(name)); } +// Find an input in a shader texture. ufbx_abi ufbx_shader_texture_input *ufbx_find_shader_texture_input_len(const ufbx_shader_texture *shader, const char *name, size_t name_len); ufbx_inline ufbx_shader_texture_input *ufbx_find_shader_texture_input(const ufbx_shader_texture *shader, const char *name) { return ufbx_find_shader_texture_input_len(shader, name, strlen(name)); @@ -5050,8 +5373,13 @@ ufbx_inline ufbx_shader_texture_input *ufbx_find_shader_texture_input(const ufbx // Math +// Returns `true` if `axes` forms a valid coordinate space. ufbx_abi bool ufbx_coordinate_axes_valid(ufbx_coordinate_axes axes); +// Vector math utility functions. +ufbx_abi ufbx_vec3 ufbx_vec3_normalize(ufbx_vec3 v); + +// Quaternion math utility functions. ufbx_abi ufbx_real ufbx_quat_dot(ufbx_quat a, ufbx_quat b); ufbx_abi ufbx_quat ufbx_quat_mul(ufbx_quat a, ufbx_quat b); ufbx_abi ufbx_quat ufbx_quat_normalize(ufbx_quat q); @@ -5061,40 +5389,74 @@ ufbx_abi ufbx_vec3 ufbx_quat_rotate_vec3(ufbx_quat q, ufbx_vec3 v); ufbx_abi ufbx_vec3 ufbx_quat_to_euler(ufbx_quat q, ufbx_rotation_order order); ufbx_abi ufbx_quat ufbx_euler_to_quat(ufbx_vec3 v, ufbx_rotation_order order); +// Matrix math utility functions. ufbx_abi ufbx_matrix ufbx_matrix_mul(const ufbx_matrix *a, const ufbx_matrix *b); ufbx_abi ufbx_real ufbx_matrix_determinant(const ufbx_matrix *m); ufbx_abi ufbx_matrix ufbx_matrix_invert(const ufbx_matrix *m); + +// Get a matrix that can be used to transform geometry normals. +// NOTE: You must normalize the normals after transforming them with this matrix, +// eg. using `ufbx_vec3_normalize()`. +// NOTE: This function flips the normals if the determinant is negative. ufbx_abi ufbx_matrix ufbx_matrix_for_normals(const ufbx_matrix *m); + +// Matrix transformation utilities. ufbx_abi ufbx_vec3 ufbx_transform_position(const ufbx_matrix *m, ufbx_vec3 v); ufbx_abi ufbx_vec3 ufbx_transform_direction(const ufbx_matrix *m, ufbx_vec3 v); + +// Conversions between `ufbx_matrix` and `ufbx_transform`. ufbx_abi ufbx_matrix ufbx_transform_to_matrix(const ufbx_transform *t); ufbx_abi ufbx_transform ufbx_matrix_to_transform(const ufbx_matrix *m); // Skinning +// Get a matrix representing the deformation for a single vertex. +// Returns `fallback` if the vertex is not skinned. ufbx_abi ufbx_matrix ufbx_catch_get_skin_vertex_matrix(ufbx_panic *panic, const ufbx_skin_deformer *skin, size_t vertex, const ufbx_matrix *fallback); ufbx_inline ufbx_matrix ufbx_get_skin_vertex_matrix(const ufbx_skin_deformer *skin, size_t vertex, const ufbx_matrix *fallback) { return ufbx_catch_get_skin_vertex_matrix(NULL, skin, vertex, fallback); } +// Resolve the index into `ufbx_blend_shape.position_offsets[]` given a vertex. +// Returns `UFBX_NO_INDEX` if the vertex is not included in the blend shape. ufbx_abi uint32_t ufbx_get_blend_shape_offset_index(const ufbx_blend_shape *shape, size_t vertex); + +// Get the offset for a given vertex in the blend shape. +// Returns `ufbx_zero_vec3` if the vertex is not a included in the blend shape. ufbx_abi ufbx_vec3 ufbx_get_blend_shape_vertex_offset(const ufbx_blend_shape *shape, size_t vertex); + +// Get the _current_ blend offset given a blend deformer. +// NOTE: This depends on the current animated blend weight of the deformer. ufbx_abi ufbx_vec3 ufbx_get_blend_vertex_offset(const ufbx_blend_deformer *blend, size_t vertex); +// Apply the blend shape with `weight` to given vertices. ufbx_abi void ufbx_add_blend_shape_vertex_offsets(const ufbx_blend_shape *shape, ufbx_vec3 *vertices, size_t num_vertices, ufbx_real weight); + +// Apply the blend deformer with `weight` to given vertices. +// NOTE: This depends on the current animated blend weight of the deformer. ufbx_abi void ufbx_add_blend_vertex_offsets(const ufbx_blend_deformer *blend, ufbx_vec3 *vertices, size_t num_vertices, ufbx_real weight); // Curves/surfaces +// Low-level utility to evaluate NURBS the basis functions. ufbx_abi size_t ufbx_evaluate_nurbs_basis(const ufbx_nurbs_basis *basis, ufbx_real u, ufbx_real *weights, size_t num_weights, ufbx_real *derivatives, size_t num_derivatives); +// Evaluate a point on a NURBS curve given the parameter `u`. ufbx_abi ufbx_curve_point ufbx_evaluate_nurbs_curve(const ufbx_nurbs_curve *curve, ufbx_real u); + +// Evaluate a point on a NURBS surface given the parameter `u` and `v`. ufbx_abi ufbx_surface_point ufbx_evaluate_nurbs_surface(const ufbx_nurbs_surface *surface, ufbx_real u, ufbx_real v); +// Tessellate a NURBS curve into a polyline. ufbx_abi ufbx_line_curve *ufbx_tessellate_nurbs_curve(const ufbx_nurbs_curve *curve, const ufbx_tessellate_curve_opts *opts, ufbx_error *error); + +// Tessellate a NURBS surface into a mesh. ufbx_abi ufbx_mesh *ufbx_tessellate_nurbs_surface(const ufbx_nurbs_surface *surface, const ufbx_tessellate_surface_opts *opts, ufbx_error *error); +// Free a line returned by `ufbx_tessellate_nurbs_curve()`. ufbx_abi void ufbx_free_line_curve(ufbx_line_curve *curve); + +// Increase the refcount of the line. ufbx_abi void ufbx_retain_line_curve(ufbx_line_curve *curve); // Mesh Topology @@ -5103,6 +5465,9 @@ ufbx_abi void ufbx_retain_line_curve(ufbx_line_curve *curve); // Returns `UFBX_NO_INDEX` if out of bounds. ufbx_abi uint32_t ufbx_find_face_index(ufbx_mesh *mesh, size_t index); +// Triangulate a mesh face, returning the number of triangles. +// NOTE: You need to space for `(face.num_indices - 2) * 3 - 1` indices! +// HINT: Using `ufbx_mesh.max_face_triangles * 3` is always safe. ufbx_abi uint32_t ufbx_catch_triangulate_face(ufbx_panic *panic, uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, ufbx_face face); ufbx_inline uint32_t ufbx_triangulate_face(uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, ufbx_face face) { return ufbx_catch_triangulate_face(NULL, indices, num_indices, mesh, face); @@ -5117,21 +5482,27 @@ ufbx_inline void ufbx_compute_topology(const ufbx_mesh *mesh, ufbx_topo_edge *to // Get the next/previous edge around a vertex // NOTE: Does not return the half-edge on the opposite side (ie. `topo[index].twin`) +// Get the next half-edge in `topo`. ufbx_abi uint32_t ufbx_catch_topo_next_vertex_edge(ufbx_panic *panic, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index); ufbx_inline uint32_t ufbx_topo_next_vertex_edge(const ufbx_topo_edge *topo, size_t num_topo, uint32_t index) { return ufbx_catch_topo_next_vertex_edge(NULL, topo, num_topo, index); } +// Get the previous half-edge in `topo`. ufbx_abi uint32_t ufbx_catch_topo_prev_vertex_edge(ufbx_panic *panic, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index); ufbx_inline uint32_t ufbx_topo_prev_vertex_edge(const ufbx_topo_edge *topo, size_t num_topo, uint32_t index) { return ufbx_catch_topo_prev_vertex_edge(NULL, topo, num_topo, index); } +// Calculate a normal for a given face. +// The returned normal is weighted by face area. ufbx_abi ufbx_vec3 ufbx_catch_get_weighted_face_normal(ufbx_panic *panic, const ufbx_vertex_vec3 *positions, ufbx_face face); ufbx_inline ufbx_vec3 ufbx_get_weighted_face_normal(const ufbx_vertex_vec3 *positions, ufbx_face face) { return ufbx_catch_get_weighted_face_normal(NULL, positions, face); } +// Generate indices for normals from the topology. +// Respects smoothing groups. ufbx_abi size_t ufbx_catch_generate_normal_mapping(ufbx_panic *panic, const ufbx_mesh *mesh, const ufbx_topo_edge *topo, size_t num_topo, uint32_t *normal_indices, size_t num_normal_indices, bool assume_smooth); @@ -5139,6 +5510,8 @@ ufbx_abi size_t ufbx_generate_normal_mapping(const ufbx_mesh *mesh, const ufbx_topo_edge *topo, size_t num_topo, uint32_t *normal_indices, size_t num_normal_indices, bool assume_smooth); +// Compute normals given normal indices. +// You can use `ufbx_generate_normal_mapping()` to generate the normal indices. ufbx_abi void ufbx_catch_compute_normals(ufbx_panic *panic, const ufbx_mesh *mesh, const ufbx_vertex_vec3 *positions, const uint32_t *normal_indices, size_t num_normal_indices, ufbx_vec3 *normals, size_t num_normals); @@ -5146,13 +5519,20 @@ ufbx_abi void ufbx_compute_normals(const ufbx_mesh *mesh, const ufbx_vertex_vec3 const uint32_t *normal_indices, size_t num_normal_indices, ufbx_vec3 *normals, size_t num_normals); +// Subdivide a mesh using the Catmull-Clark subdivision `level` times. ufbx_abi ufbx_mesh *ufbx_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *opts, ufbx_error *error); +// Free a mesh returned from `ufbx_subdivide_mesh()` or `ufbx_tessellate_nurbs_surface()`. ufbx_abi void ufbx_free_mesh(ufbx_mesh *mesh); + +// Increase the mesh reference count. ufbx_abi void ufbx_retain_mesh(ufbx_mesh *mesh); // Geometry caches +// Load geometry cache information from a file. +// As geometry caches can be massive, this does not actually read the data, but +// only seeks through the files to form the metadata. ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache( const char *filename, const ufbx_geometry_cache_opts *opts, ufbx_error *error); @@ -5160,21 +5540,29 @@ ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache_len( const char *filename, size_t filename_len, const ufbx_geometry_cache_opts *opts, ufbx_error *error); +// Free a geometry cache returned from `ufbx_load_geometry_cache()`. ufbx_abi void ufbx_free_geometry_cache(ufbx_geometry_cache *cache); +// Increase the geometry cache reference count. ufbx_abi void ufbx_retain_geometry_cache(ufbx_geometry_cache *cache); +// Read a frame from a geometry cache. ufbx_abi size_t ufbx_read_geometry_cache_real(const ufbx_cache_frame *frame, ufbx_real *data, size_t num_data, const ufbx_geometry_cache_data_opts *opts); -ufbx_abi size_t ufbx_sample_geometry_cache_real(const ufbx_cache_channel *channel, double time, ufbx_real *data, size_t num_data, const ufbx_geometry_cache_data_opts *opts); ufbx_abi size_t ufbx_read_geometry_cache_vec3(const ufbx_cache_frame *frame, ufbx_vec3 *data, size_t num_data, const ufbx_geometry_cache_data_opts *opts); +// Sample the a geometry cache channel, linearly blending between adjacent frames. +ufbx_abi size_t ufbx_sample_geometry_cache_real(const ufbx_cache_channel *channel, double time, ufbx_real *data, size_t num_data, const ufbx_geometry_cache_data_opts *opts); ufbx_abi size_t ufbx_sample_geometry_cache_vec3(const ufbx_cache_channel *channel, double time, ufbx_vec3 *data, size_t num_data, const ufbx_geometry_cache_data_opts *opts); // DOM +// Find a DOM node given a name. ufbx_abi ufbx_dom_node *ufbx_dom_find_len(const ufbx_dom_node *parent, const char *name, size_t name_len); ufbx_inline ufbx_dom_node *ufbx_dom_find(const ufbx_dom_node *parent, const char *name) { return ufbx_dom_find_len(parent, name, strlen(name)); } // Utility +// Generate an index buffer for a flat vertex buffer. +// `streams` specifies one or more vertex data arrays, each stream must contain `num_indices` vertices. +// This function compacts the data within `streams` in-place, writing the deduplicated indices to `indices`. ufbx_abi size_t ufbx_generate_indices(const ufbx_vertex_stream *streams, size_t num_streams, uint32_t *indices, size_t num_indices, const ufbx_allocator_opts *allocator, ufbx_error *error); // Thread pool @@ -5183,23 +5571,30 @@ ufbx_abi size_t ufbx_generate_indices(const ufbx_vertex_stream *streams, size_t // See `ufbx_thread_pool_run_fn` for more information. ufbx_unsafe ufbx_abi void ufbx_thread_pool_run_task(ufbx_thread_pool_context ctx, uint32_t index); +// Get or set an arbitrary user pointer for the thread pool context. +// `ufbx_thread_pool_get_user_ptr()` returns `NULL` if unset. ufbx_unsafe ufbx_abi void ufbx_thread_pool_set_user_ptr(ufbx_thread_pool_context ctx, void *user_ptr); ufbx_unsafe ufbx_abi void *ufbx_thread_pool_get_user_ptr(ufbx_thread_pool_context ctx); // -- Inline API +// Utility functions for reading geometry data for a single index. ufbx_abi ufbx_real ufbx_catch_get_vertex_real(ufbx_panic *panic, const ufbx_vertex_real *v, size_t index); ufbx_abi ufbx_vec2 ufbx_catch_get_vertex_vec2(ufbx_panic *panic, const ufbx_vertex_vec2 *v, size_t index); ufbx_abi ufbx_vec3 ufbx_catch_get_vertex_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index); ufbx_abi ufbx_vec4 ufbx_catch_get_vertex_vec4(ufbx_panic *panic, const ufbx_vertex_vec4 *v, size_t index); +// Utility functions for reading geometry data for a single index. ufbx_inline ufbx_real ufbx_get_vertex_real(const ufbx_vertex_real *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values.data[(int32_t)v->indices.data[index]]; } ufbx_inline ufbx_vec2 ufbx_get_vertex_vec2(const ufbx_vertex_vec2 *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values.data[(int32_t)v->indices.data[index]]; } ufbx_inline ufbx_vec3 ufbx_get_vertex_vec3(const ufbx_vertex_vec3 *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values.data[(int32_t)v->indices.data[index]]; } ufbx_inline ufbx_vec4 ufbx_get_vertex_vec4(const ufbx_vertex_vec4 *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values.data[(int32_t)v->indices.data[index]]; } -ufbx_abi size_t ufbx_get_triangulate_face_num_indices(ufbx_face face); +ufbx_abi ufbx_real ufbx_catch_get_vertex_w_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index); +ufbx_inline ufbx_real ufbx_get_vertex_w_vec3(const ufbx_vertex_vec3 *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values_w.count > 0 ? v->values_w.data[(int32_t)v->indices.data[index]] : 0.0f; } +// Functions for converting an untyped `ufbx_element` to a concrete type. +// Returns `NULL` if the element is not that type. ufbx_abi ufbx_unknown *ufbx_as_unknown(const ufbx_element *element); ufbx_abi ufbx_node *ufbx_as_node(const ufbx_element *element); ufbx_abi ufbx_mesh *ufbx_as_mesh(const ufbx_element *element); @@ -5238,47 +5633,11 @@ ufbx_abi ufbx_selection_set *ufbx_as_selection_set(const ufbx_element *element); ufbx_abi ufbx_selection_node *ufbx_as_selection_node(const ufbx_element *element); ufbx_abi ufbx_character *ufbx_as_character(const ufbx_element *element); ufbx_abi ufbx_constraint *ufbx_as_constraint(const ufbx_element *element); +ufbx_abi ufbx_audio_layer *ufbx_as_audio_layer(const ufbx_element *element); +ufbx_abi ufbx_audio_clip *ufbx_as_audio_clip(const ufbx_element *element); ufbx_abi ufbx_pose *ufbx_as_pose(const ufbx_element *element); ufbx_abi ufbx_metadata_object *ufbx_as_metadata_object(const ufbx_element *element); -// -- FFI API - -ufbx_abi void ufbx_ffi_find_int_len(int64_t *retval, const ufbx_props *props, const char *name, size_t name_len, const int64_t *def); -ufbx_abi void ufbx_ffi_find_vec3_len(ufbx_vec3 *retval, const ufbx_props *props, const char *name, size_t name_len, const ufbx_vec3 *def); -ufbx_abi void ufbx_ffi_find_string_len(ufbx_string *retval, const ufbx_props *props, const char *name, size_t name_len, const ufbx_string *def); -ufbx_abi void ufbx_ffi_find_anim_props(ufbx_anim_prop_list *retval, const ufbx_anim_layer *layer, const ufbx_element *element); -ufbx_abi void ufbx_ffi_get_compatible_matrix_for_normals(ufbx_matrix *retval, const ufbx_node *node); -ufbx_abi void ufbx_ffi_evaluate_anim_value_vec2(ufbx_vec2 *retval, const ufbx_anim_value *anim_value, double time); -ufbx_abi void ufbx_ffi_evaluate_anim_value_vec3(ufbx_vec3 *retval, const ufbx_anim_value *anim_value, double time); -ufbx_abi void ufbx_ffi_evaluate_prop_len(ufbx_prop *retval, const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time); -ufbx_abi void ufbx_ffi_evaluate_props(ufbx_props *retval, const ufbx_anim *anim, ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size); -ufbx_abi void ufbx_ffi_evaluate_transform(ufbx_transform *retval, const ufbx_anim *anim, const ufbx_node *node, double time); -ufbx_abi ufbx_real ufbx_ffi_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time); -ufbx_abi void ufbx_ffi_quat_mul(ufbx_quat *retval, const ufbx_quat *a, const ufbx_quat *b); -ufbx_abi void ufbx_ffi_quat_normalize(ufbx_quat *retval, const ufbx_quat *q); -ufbx_abi void ufbx_ffi_quat_fix_antipodal(ufbx_quat *retval, const ufbx_quat *q, const ufbx_quat *reference); -ufbx_abi void ufbx_ffi_quat_slerp(ufbx_quat *retval, const ufbx_quat *a, const ufbx_quat *b, ufbx_real t); -ufbx_abi void ufbx_ffi_quat_rotate_vec3(ufbx_vec3 *retval, const ufbx_quat *q, const ufbx_vec3 *v); -ufbx_abi void ufbx_ffi_quat_to_euler(ufbx_vec3 *retval, const ufbx_quat *q, ufbx_rotation_order order); -ufbx_abi void ufbx_ffi_euler_to_quat(ufbx_quat *retval, const ufbx_vec3 *v, ufbx_rotation_order order); -ufbx_abi void ufbx_ffi_matrix_mul(ufbx_matrix *retval, const ufbx_matrix *a, const ufbx_matrix *b); -ufbx_abi void ufbx_ffi_matrix_invert(ufbx_matrix *retval, const ufbx_matrix *m); -ufbx_abi void ufbx_ffi_matrix_for_normals(ufbx_matrix *retval, const ufbx_matrix *m); -ufbx_abi void ufbx_ffi_transform_position(ufbx_vec3 *retval, const ufbx_matrix *m, const ufbx_vec3 *v); -ufbx_abi void ufbx_ffi_transform_direction(ufbx_vec3 *retval, const ufbx_matrix *m, const ufbx_vec3 *v); -ufbx_abi void ufbx_ffi_transform_to_matrix(ufbx_matrix *retval, const ufbx_transform *t); -ufbx_abi void ufbx_ffi_matrix_to_transform(ufbx_transform *retval, const ufbx_matrix *m); -ufbx_abi void ufbx_ffi_get_skin_vertex_matrix(ufbx_matrix *retval, const ufbx_skin_deformer *skin, size_t vertex, const ufbx_matrix *fallback); -ufbx_abi void ufbx_ffi_get_blend_shape_vertex_offset(ufbx_vec3 *retval, const ufbx_blend_shape *shape, size_t vertex); -ufbx_abi void ufbx_ffi_get_blend_vertex_offset(ufbx_vec3 *retval, const ufbx_blend_deformer *blend, size_t vertex); -ufbx_abi void ufbx_ffi_evaluate_nurbs_curve(ufbx_curve_point *retval, const ufbx_nurbs_curve *curve, ufbx_real u); -ufbx_abi void ufbx_ffi_evaluate_nurbs_surface(ufbx_surface_point *retval, const ufbx_nurbs_surface *surface, ufbx_real u, ufbx_real v); -ufbx_abi void ufbx_ffi_get_weighted_face_normal(ufbx_vec3 *retval, const ufbx_vertex_vec3 *positions, const ufbx_face *face); -ufbx_abi size_t ufbx_ffi_get_triangulate_face_num_indices(const ufbx_face *face); -ufbx_abi uint32_t ufbx_ffi_triangulate_face(uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, const ufbx_face *face); -ufbx_abi ufbx_vec3 ufbx_ffi_evaluate_baked_vec3(const ufbx_baked_vec3 *keyframes, size_t num_keyframes, double time); -ufbx_abi ufbx_quat ufbx_ffi_evaluate_baked_quat(const ufbx_baked_quat *keyframes, size_t num_keyframes, double time); - #ifdef __cplusplus } #endif