summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/static_checks.yml2
-rw-r--r--.mailmap2
-rw-r--r--AUTHORS.md5
-rw-r--r--COPYRIGHT.txt5
-rw-r--r--DONORS.md27
-rw-r--r--core/io/image.cpp10
-rw-r--r--core/io/image.h1
-rw-r--r--core/math/aabb.h2
-rw-r--r--core/math/delaunay_3d.h6
-rw-r--r--core/math/quick_hull.cpp2
-rw-r--r--core/math/rect2.h2
-rw-r--r--core/math/rect2i.h2
-rw-r--r--core/math/triangle_mesh.cpp2
-rw-r--r--core/math/vector2.cpp12
-rw-r--r--core/math/vector2.h10
-rw-r--r--core/math/vector2i.cpp12
-rw-r--r--core/math/vector2i.h10
-rw-r--r--core/math/vector3.cpp19
-rw-r--r--core/math/vector3.h11
-rw-r--r--core/math/vector3i.cpp14
-rw-r--r--core/math/vector3i.h10
-rw-r--r--core/math/vector4.cpp21
-rw-r--r--core/math/vector4.h11
-rw-r--r--core/math/vector4i.cpp16
-rw-r--r--core/math/vector4i.h10
-rw-r--r--core/templates/command_queue_mt.cpp29
-rw-r--r--core/templates/command_queue_mt.h73
-rw-r--r--core/variant/method_ptrcall.h10
-rw-r--r--core/variant/variant.cpp11
-rw-r--r--core/variant/variant.h1
-rw-r--r--core/variant/variant_call.cpp38
-rw-r--r--core/variant/variant_setget.cpp14
-rw-r--r--doc/classes/@GlobalScope.xml2
-rw-r--r--doc/classes/Animation.xml5
-rw-r--r--doc/classes/AnimationPlayer.xml10
-rw-r--r--doc/classes/AudioStreamWAV.xml5
-rw-r--r--doc/classes/BaseMaterial3D.xml3
-rw-r--r--doc/classes/CanvasItem.xml8
-rw-r--r--doc/classes/CodeEdit.xml38
-rw-r--r--doc/classes/DisplayServer.xml26
-rw-r--r--doc/classes/EditorSettings.xml2
-rw-r--r--doc/classes/HingeJoint3D.xml4
-rw-r--r--doc/classes/Node2D.xml3
-rw-r--r--doc/classes/PackedScene.xml4
-rw-r--r--doc/classes/ProjectSettings.xml5
-rw-r--r--doc/classes/Rect2.xml4
-rw-r--r--doc/classes/Rect2i.xml4
-rw-r--r--doc/classes/RenderingServer.xml6
-rw-r--r--doc/classes/ResourceImporterWAV.xml1
-rw-r--r--doc/classes/RichTextLabel.xml9
-rw-r--r--doc/classes/StatusIndicator.xml14
-rw-r--r--doc/classes/String.xml2
-rw-r--r--doc/classes/StringName.xml2
-rw-r--r--doc/classes/TextEdit.xml198
-rw-r--r--doc/classes/TileSetAtlasSource.xml9
-rw-r--r--doc/classes/Variant.xml4
-rw-r--r--doc/classes/Vector2.xml43
-rw-r--r--doc/classes/Vector2i.xml43
-rw-r--r--doc/classes/Vector3.xml49
-rw-r--r--doc/classes/Vector3i.xml43
-rw-r--r--doc/classes/Vector4.xml43
-rw-r--r--doc/classes/Vector4i.xml43
-rw-r--r--doc/classes/XRServer.xml4
-rw-r--r--drivers/gles3/effects/copy_effects.cpp6
-rw-r--r--drivers/gles3/rasterizer_scene_gles3.cpp5
-rw-r--r--drivers/gles3/shaders/scene.glsl8
-rw-r--r--drivers/gles3/storage/material_storage.cpp11
-rw-r--r--drivers/gles3/storage/material_storage.h1
-rw-r--r--drivers/gles3/storage/render_scene_buffers_gles3.cpp3
-rw-r--r--drivers/gles3/storage/texture_storage.cpp2
-rw-r--r--drivers/png/SCsub2
-rw-r--r--editor/code_editor.cpp414
-rw-r--r--editor/code_editor.h8
-rw-r--r--editor/editor_audio_buses.cpp19
-rw-r--r--editor/editor_dock_manager.cpp1
-rw-r--r--editor/editor_file_system.cpp65
-rw-r--r--editor/editor_file_system.h3
-rw-r--r--editor/editor_inspector.cpp6
-rw-r--r--editor/editor_node.cpp9
-rw-r--r--editor/editor_resource_picker.cpp52
-rw-r--r--editor/editor_resource_picker.h4
-rw-r--r--editor/engine_update_label.cpp52
-rw-r--r--editor/filesystem_dock.cpp29
-rw-r--r--editor/import/resource_importer_wav.cpp34
-rw-r--r--editor/plugins/canvas_item_editor_plugin.cpp4
-rw-r--r--editor/plugins/editor_preview_plugins.cpp2
-rw-r--r--editor/plugins/gizmos/navigation_link_3d_gizmo_plugin.cpp2
-rw-r--r--editor/plugins/gizmos/navigation_region_3d_gizmo_plugin.cpp4
-rw-r--r--editor/plugins/gizmos/occluder_instance_3d_gizmo_plugin.cpp4
-rw-r--r--editor/plugins/gradient_texture_2d_editor_plugin.cpp6
-rw-r--r--editor/plugins/navigation_obstacle_3d_editor_plugin.cpp4
-rw-r--r--editor/plugins/node_3d_editor_plugin.cpp10
-rw-r--r--editor/plugins/path_3d_editor_plugin.cpp4
-rw-r--r--editor/plugins/polygon_3d_editor_plugin.cpp4
-rw-r--r--editor/plugins/script_text_editor.cpp124
-rw-r--r--editor/plugins/text_editor.cpp20
-rw-r--r--editor/plugins/text_shader_editor.cpp10
-rw-r--r--editor/plugins/texture_region_editor_plugin.cpp4
-rw-r--r--editor/plugins/tiles/tile_data_editors.cpp2
-rw-r--r--editor/plugins/tiles/tile_proxies_manager_dialog.cpp4
-rw-r--r--editor/plugins/tiles/tile_set_atlas_source_editor.cpp14
-rw-r--r--editor/plugins/visual_shader_editor_plugin.cpp20
-rw-r--r--main/main.cpp11
-rw-r--r--methods.py94
-rw-r--r--misc/dist/html/full-size.html206
-rw-r--r--misc/extension_api_validation/4.2-stable.expected22
-rw-r--r--modules/astcenc/image_compress_astcenc.cpp2
-rw-r--r--modules/basis_universal/image_compress_basisu.cpp72
-rw-r--r--modules/cvtt/image_compress_cvtt.cpp2
-rw-r--r--modules/etcpak/image_compress_etcpak.cpp2
-rw-r--r--modules/gdscript/gdscript_byte_codegen.cpp52
-rw-r--r--modules/gdscript/gdscript_byte_codegen.h1
-rw-r--r--modules/gdscript/gdscript_codegen.h1
-rw-r--r--modules/gdscript/gdscript_compiler.cpp10
-rw-r--r--modules/gdscript/gdscript_disassembler.cpp44
-rw-r--r--modules/gdscript/gdscript_function.h2
-rw-r--r--modules/gdscript/gdscript_vm.cpp456
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out2
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out2
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd4
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd7
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out2
-rw-r--r--modules/mobile_vr/doc_classes/MobileVRInterface.xml3
-rw-r--r--modules/mobile_vr/mobile_vr_interface.cpp20
-rw-r--r--modules/mobile_vr/mobile_vr_interface.h5
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs9
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode(Of T)_ScriptProperties.generated.cs49
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AbstractGenericNode.cs7
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs6
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs14
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs5
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs2
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs14
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs14
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs94
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs109
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs87
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs116
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs109
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs121
-rw-r--r--modules/openxr/action_map/openxr_action_map.cpp164
-rw-r--r--modules/openxr/doc_classes/OpenXRAPIExtension.xml8
-rw-r--r--modules/openxr/doc_classes/OpenXRInterface.xml17
-rw-r--r--modules/openxr/extensions/openxr_composition_layer_extension.cpp4
-rw-r--r--modules/openxr/extensions/openxr_hand_interaction_extension.cpp97
-rw-r--r--modules/openxr/extensions/openxr_hand_interaction_extension.h72
-rw-r--r--modules/openxr/extensions/openxr_hand_tracking_extension.cpp2
-rw-r--r--modules/openxr/openxr_api.cpp448
-rw-r--r--modules/openxr/openxr_api.h90
-rw-r--r--modules/openxr/openxr_api_extension.cpp10
-rw-r--r--modules/openxr/openxr_api_extension.h1
-rw-r--r--modules/openxr/openxr_interface.cpp27
-rw-r--r--modules/openxr/openxr_interface.h26
-rw-r--r--modules/openxr/register_types.cpp2
-rw-r--r--modules/raycast/config.py5
-rw-r--r--modules/text_server_adv/gdextension_build/methods.py90
-rw-r--r--modules/text_server_fb/gdextension_build/methods.py90
-rw-r--r--platform/android/SCsub20
-rw-r--r--platform/linuxbsd/wayland/wayland_thread.cpp2
-rw-r--r--platform/linuxbsd/x11/display_server_x11.cpp2
-rw-r--r--platform/macos/display_server_macos.h8
-rw-r--r--platform/macos/display_server_macos.mm73
-rw-r--r--platform/macos/godot_status_item.h7
-rw-r--r--platform/macos/godot_status_item.mm61
-rw-r--r--platform/macos/native_menu_macos.h2
-rw-r--r--platform/macos/native_menu_macos.mm7
-rw-r--r--platform/web/export/export_plugin.cpp200
-rw-r--r--platform/web/export/export_plugin.h17
-rw-r--r--platform/windows/display_server_windows.cpp64
-rw-r--r--platform/windows/display_server_windows.h7
-rw-r--r--scene/2d/parallax_2d.cpp2
-rw-r--r--scene/3d/cpu_particles_3d.cpp2
-rw-r--r--scene/3d/decal.cpp2
-rw-r--r--scene/3d/fog_volume.cpp2
-rw-r--r--scene/3d/gpu_particles_collision_3d.cpp4
-rw-r--r--scene/3d/occluder_instance_3d.cpp4
-rw-r--r--scene/3d/voxel_gi.cpp2
-rw-r--r--scene/3d/xr_hand_modifier_3d.cpp24
-rw-r--r--scene/3d/xr_hand_modifier_3d.h3
-rw-r--r--scene/animation/animation_mixer.cpp9
-rw-r--r--scene/animation/animation_player.cpp119
-rw-r--r--scene/animation/animation_player.h8
-rw-r--r--scene/gui/code_edit.cpp630
-rw-r--r--scene/gui/code_edit.h8
-rw-r--r--scene/gui/control.cpp4
-rw-r--r--scene/gui/graph_edit.cpp6
-rw-r--r--scene/gui/graph_edit_arranger.cpp2
-rw-r--r--scene/gui/progress_bar.cpp2
-rw-r--r--scene/gui/rich_text_label.compat.inc5
-rw-r--r--scene/gui/rich_text_label.cpp43
-rw-r--r--scene/gui/rich_text_label.h4
-rw-r--r--scene/gui/tab_container.cpp22
-rw-r--r--scene/gui/text_edit.compat.inc41
-rw-r--r--scene/gui/text_edit.cpp2442
-rw-r--r--scene/gui/text_edit.h98
-rw-r--r--scene/gui/texture_button.cpp2
-rw-r--r--scene/gui/texture_progress_bar.cpp2
-rw-r--r--scene/gui/tree.cpp2
-rw-r--r--scene/main/canvas_item.compat.inc41
-rw-r--r--scene/main/canvas_item.cpp36
-rw-r--r--scene/main/canvas_item.h8
-rw-r--r--scene/main/status_indicator.cpp63
-rw-r--r--scene/main/status_indicator.h12
-rw-r--r--scene/main/viewport.cpp37
-rw-r--r--scene/main/viewport.h2
-rw-r--r--scene/main/window.cpp4
-rw-r--r--scene/resources/2d/tile_set.cpp6
-rw-r--r--scene/resources/animation.cpp29
-rw-r--r--scene/resources/animation.h5
-rw-r--r--scene/resources/audio_stream_wav.cpp144
-rw-r--r--scene/resources/audio_stream_wav.h23
-rw-r--r--scene/resources/material.cpp11
-rw-r--r--scene/resources/material.h1
-rw-r--r--scene/resources/particle_process_material.cpp8
-rw-r--r--scene/resources/visual_shader.cpp4
-rw-r--r--scene/resources/visual_shader.h1
-rw-r--r--servers/display_server.cpp15
-rw-r--r--servers/display_server.h6
-rw-r--r--servers/rendering/renderer_rd/effects/ss_effects.cpp3
-rw-r--r--servers/rendering/renderer_rd/environment/fog.cpp4
-rw-r--r--servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp2
-rw-r--r--servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp15
-rw-r--r--servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h3
-rw-r--r--servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp15
-rw-r--r--servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h1
-rw-r--r--servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl7
-rw-r--r--servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl6
-rw-r--r--servers/rendering/renderer_rd/storage_rd/material_storage.cpp8
-rw-r--r--servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp3
-rw-r--r--servers/rendering/renderer_rd/storage_rd/texture_storage.cpp17
-rw-r--r--servers/rendering/renderer_scene_occlusion_cull.h4
-rw-r--r--servers/rendering/rendering_device_graph.cpp9
-rw-r--r--servers/rendering/rendering_server_default.h4
-rw-r--r--servers/rendering/shader_types.cpp3
-rw-r--r--servers/rendering_server.cpp5
-rw-r--r--servers/rendering_server.h29
-rw-r--r--servers/xr/xr_interface.h10
-rw-r--r--servers/xr_server.cpp146
-rw-r--r--servers/xr_server.h39
-rw-r--r--tests/display_server_mock.h10
-rw-r--r--tests/scene/test_code_edit.h2016
-rw-r--r--tests/scene/test_text_edit.h5036
-rw-r--r--tests/test_macros.h9
-rw-r--r--thirdparty/README.md5
-rw-r--r--thirdparty/misc/patches/qoa-min-fix.patch155
-rw-r--r--thirdparty/misc/qoa.h728
247 files changed, 13180 insertions, 4481 deletions
diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml
index 0ed7432833..9b8a86b8e7 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -48,6 +48,8 @@ jobs:
- name: Style checks via pre-commit
uses: pre-commit/action@v3.0.1
+ with:
+ extra_args: --verbose --files ${{ env.CHANGED_FILES }}
- name: File formatting checks (file_format.sh)
run: |
diff --git a/.mailmap b/.mailmap
index 9e1ddb95e1..c4e7cf1743 100644
--- a/.mailmap
+++ b/.mailmap
@@ -14,6 +14,7 @@ Ariel Manzur <ariel@godotengine.org> <punto@godotengine.org>
Ariel Manzur <ariel@godotengine.org> <ariel@okamstudio.com>
Ariel Manzur <ariel@godotengine.org> <punto@Ariels-Mac-mini.local>
Ariel Manzur <ariel@godotengine.org> <punto@Ariels-Mac-mini-2.local>
+Arman Elgudzhyan <48544263+puchik@users.noreply.github.com>
A Thousand Ships <96648715+AThousandShips@users.noreply.github.com> <over999ships@gmail.com>
Bastiaan Olij <mux213@gmail.com>
Benjamin <mafortion.benjamin@gmail.com>
@@ -77,6 +78,7 @@ Jason Knight <00jknight@gmail.com> <jason@winterpixel.com>
Jean-Michel Bernard <jmb462@gmail.com>
Jérôme Gully <jerome.gully0@gmail.com>
JFonS <joan.fonssanchez@gmail.com>
+jitspoe <jitspoe@yahoo.com> <jitspoeAyahoooDcom>
Juan Linietsky <reduzio@gmail.com>
Juan Linietsky <reduzio@gmail.com> <juan@godotengine.org>
Juan Linietsky <reduzio@gmail.com> <juan@okamstudio.com>
diff --git a/AUTHORS.md b/AUTHORS.md
index f2fc58c1be..0f000a1a75 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -48,6 +48,7 @@ name is available.
Anutrix
Aren Villanueva (kurikaesu)
Ariel Manzur (punto-)
+ Arman Elgudzhyan (puchik)
AThousandShips
aXu-AP
Bartłomiej T. Listwon (Listwon)
@@ -141,6 +142,7 @@ name is available.
Jean-Michel Bernard (jmb462)
Jérôme Gully (Nutriz)
Jia Jun Chai (SkyLucilfer)
+ jitspoe
Joan Fons Sanchez (JFonS)
Johan Manuel (29jm)
Johannes Witt (HaSa1002)
@@ -217,6 +219,7 @@ name is available.
Omar El Sheikh (The-O-King)
Ovnuniarchos
Pascal Richter (ShyRed)
+ passivestar
Patrick Dawson (pkdawson)
Patrick Exner (FlameLizard)
Patrick (firefly2442)
@@ -232,6 +235,7 @@ name is available.
Poommetee Ketson (Noshyaar)
Przemysław Gołąb (n-pigeon)
Rafael M. G. (rafallus)
+ Raffaele Picca (RPicster)
Rafał Mikrut (qarmin)
Ralf Hölzemer (rollenrolm)
Ramesh Ravone (RameshRavone)
@@ -298,6 +302,7 @@ name is available.
Zae Chao (zaevi)
Zak Stam (zaksnet)
Zher Huei Lee (leezh)
+ Zi Ye (MajorMcDoom)
ZuBsPaCe
谢天 (jsjtxietian)
风青山 (Rindbee)
diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt
index 2e246e6634..cbb0ad179d 100644
--- a/COPYRIGHT.txt
+++ b/COPYRIGHT.txt
@@ -411,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 f12f6f4b3a..b629578675 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 <https://scp.games/pandemic/>
Broken Rules <https://brokenrul.es/>
Chasing Carrots <https://www.chasing-carrots.com/>
- Gamblify <https://www.gamblify.com/>
Indoor Astronaut <https://indoorastronaut.ch/>
Null <https://null.com/>
Orbital Knight <https://www.orbitalknight.com/>
@@ -56,6 +54,7 @@ generous deed immortalized in the next stable release of Godot Engine.
Garry Newman
Isaiah Smith <https://www.isaiahsmith.dev/>
Kenney <https://kenney.nl/>
+ Libretrend <https://libretrend.com/>
Life Art Studios <https://lifeartstudios.net/>
Lucid Silence Games
Matthew Campbell
@@ -99,19 +98,21 @@ generous deed immortalized in the next stable release of Godot Engine.
Scott Pezza
ShikadiGum
Silver Creek Entertainment
+ SolarLabyrinth
Stephan Kessler
Stephan Lanfermann
TigerJ
Tim Yuen
Violin Iliev
Vladimír Chvátil
- And 16 anonymous donors
+ And 15 anonymous donors
## Gold members
@reilaos
alMoo Games
Alva Majo
+ Amadan Interactive (Cillian Clifford)
Antti Vesanen
Artur Ilkaev
Asher Glick
@@ -124,21 +125,31 @@ generous deed immortalized in the next stable release of Godot Engine.
Brian Levinsen
Brut
Carlo del Mundo
+ Chickensoft
ClarkThyLord
Cosmin Munteanu
+ cowoder
Coy Humphrey
+ Daniel Eichler
David Chen Zhen
David Coles
David Hubber
David Snopek
+ Deakcor
Delton Ding
+ dfseifert
+ Don't You Know Who I Am? Inc.
+ Dono
Dustuu
+ Edelweiss
ElektroFox
endaye
Ends
+ Eren Öğrül
Eric Phy
Faisal Al-Kubaisi (QatariGameDev)
FeralBytes
+ Garrus Vakarian
GlassBrick
Grau
Guangzhou Lingchan
@@ -153,6 +164,7 @@ generous deed immortalized in the next stable release of Godot Engine.
John Gabriel
Jon Woodward
José Canepa
+ Justin Sasso
KAR Games
Karasu Studio
korinVR
@@ -167,6 +179,7 @@ generous deed immortalized in the next stable release of Godot Engine.
Mara Huldra
Martin Šenkeřík
Megabit Interactive
+ Michael Gooch
Modus Ponens
nezticle
Niklas Wahrman
@@ -183,19 +196,25 @@ generous deed immortalized in the next stable release of Godot Engine.
Robin Ward
Saltlight Studio
Samuel Judd
+ ScoreSpace
Silverclad Studios
Sofox
Space Kraken Studios
Spoony Panda
+ TANAKA Yu
+ TaraSophieDev
ThatGamer
ThePolyglotProgrammer
Tim Nedvyga
Tom Langwaldt
Trevor Slocum
tukon
+ Vagastella
Vincent Foulon
Weasel Games
WuotanStudios.com
+ Yury K.
+ Zee Weasel
Zhu Li
zikes
@@ -530,7 +549,7 @@ generous deed immortalized in the next stable release of Godot Engine.
ケルベロス
貴宏 小松
- And 208 anonymous donors
+ And 201 anonymous donors
## Silver and bronze donors
diff --git a/core/io/image.cpp b/core/io/image.cpp
index 6096211cff..5498b448d7 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 <typename Component, int CC, bool renormalize,
@@ -2616,7 +2616,11 @@ int Image::get_image_mipmap_offset_and_dimensions(int p_width, int p_height, For
}
bool Image::is_compressed() const {
- return format > 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 2cabbb767a..daddfac59d 100644
--- a/core/io/image.h
+++ b/core/io/image.h
@@ -376,6 +376,7 @@ public:
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/math/aabb.h b/core/math/aabb.h
index 48a883e64c..c2945a3ef1 100644
--- a/core/math/aabb.h
+++ b/core/math/aabb.h
@@ -101,7 +101,7 @@ struct _NO_DISCARD_ AABB {
_FORCE_INLINE_ void expand_to(const Vector3 &p_vector); /** expand to contain a point if necessary */
_FORCE_INLINE_ AABB abs() const {
- return AABB(position + size.min(Vector3()), size.abs());
+ return AABB(position + size.minf(0), size.abs());
}
Variant intersects_segment_bind(const Vector3 &p_from, const Vector3 &p_to) const;
diff --git a/core/math/delaunay_3d.h b/core/math/delaunay_3d.h
index 25bd4e8d89..4f21a665de 100644
--- a/core/math/delaunay_3d.h
+++ b/core/math/delaunay_3d.h
@@ -278,7 +278,7 @@ public:
}
Vector3i grid_pos = Vector3i(points[i] * proportions * ACCEL_GRID_SIZE);
- grid_pos = grid_pos.clamp(Vector3i(), Vector3i(ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1));
+ grid_pos = grid_pos.clampi(0, ACCEL_GRID_SIZE - 1);
for (List<Simplex *>::Element *E = acceleration_grid[grid_pos.x][grid_pos.y][grid_pos.z].front(); E;) {
List<Simplex *>::Element *N = E->next(); //may be deleted
@@ -335,8 +335,8 @@ public:
Vector3 extents = Vector3(radius2, radius2, radius2);
Vector3i from = Vector3i((center - extents) * proportions * ACCEL_GRID_SIZE);
Vector3i to = Vector3i((center + extents) * proportions * ACCEL_GRID_SIZE);
- from = from.clamp(Vector3i(), Vector3i(ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1));
- to = to.clamp(Vector3i(), Vector3i(ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1));
+ from = from.clampi(0, ACCEL_GRID_SIZE - 1);
+ to = to.clampi(0, ACCEL_GRID_SIZE - 1);
for (int32_t x = from.x; x <= to.x; x++) {
for (int32_t y = from.y; y <= to.y; y++) {
diff --git a/core/math/quick_hull.cpp b/core/math/quick_hull.cpp
index 4483f61bc4..6a60a5925d 100644
--- a/core/math/quick_hull.cpp
+++ b/core/math/quick_hull.cpp
@@ -55,7 +55,7 @@ Error QuickHull::build(const Vector<Vector3> &p_points, Geometry3D::MeshData &r_
HashSet<Vector3> valid_cache;
for (int i = 0; i < p_points.size(); i++) {
- Vector3 sp = p_points[i].snapped(Vector3(0.0001, 0.0001, 0.0001));
+ Vector3 sp = p_points[i].snappedf(0.0001);
if (valid_cache.has(sp)) {
valid_points.write[i] = false;
} else {
diff --git a/core/math/rect2.h b/core/math/rect2.h
index 7f410feb1c..b4069ae86a 100644
--- a/core/math/rect2.h
+++ b/core/math/rect2.h
@@ -278,7 +278,7 @@ struct _NO_DISCARD_ Rect2 {
}
_FORCE_INLINE_ Rect2 abs() const {
- return Rect2(position + size.min(Point2()), size.abs());
+ return Rect2(position + size.minf(0), size.abs());
}
_FORCE_INLINE_ Rect2 round() const {
diff --git a/core/math/rect2i.h b/core/math/rect2i.h
index 64806414c7..a1338da0bb 100644
--- a/core/math/rect2i.h
+++ b/core/math/rect2i.h
@@ -213,7 +213,7 @@ struct _NO_DISCARD_ Rect2i {
}
_FORCE_INLINE_ Rect2i abs() const {
- return Rect2i(position + size.min(Point2i()), size.abs());
+ return Rect2i(position + size.mini(0), size.abs());
}
_FORCE_INLINE_ void set_end(const Vector2i &p_end) {
diff --git a/core/math/triangle_mesh.cpp b/core/math/triangle_mesh.cpp
index 0da1b8c7ad..01b733183d 100644
--- a/core/math/triangle_mesh.cpp
+++ b/core/math/triangle_mesh.cpp
@@ -133,7 +133,7 @@ void TriangleMesh::create(const Vector<Vector3> &p_faces, const Vector<int32_t>
for (int j = 0; j < 3; j++) {
int vidx = -1;
- Vector3 vs = v[j].snapped(Vector3(0.0001, 0.0001, 0.0001));
+ Vector3 vs = v[j].snappedf(0.0001);
HashMap<Vector3, int>::Iterator E = db.find(vs);
if (E) {
vidx = E->value;
diff --git a/core/math/vector2.cpp b/core/math/vector2.cpp
index 198fd85d20..e86b97d6a8 100644
--- a/core/math/vector2.cpp
+++ b/core/math/vector2.cpp
@@ -135,12 +135,24 @@ Vector2 Vector2::clamp(const Vector2 &p_min, const Vector2 &p_max) const {
CLAMP(y, p_min.y, p_max.y));
}
+Vector2 Vector2::clampf(real_t p_min, real_t p_max) const {
+ return Vector2(
+ CLAMP(x, p_min, p_max),
+ CLAMP(y, p_min, p_max));
+}
+
Vector2 Vector2::snapped(const Vector2 &p_step) const {
return Vector2(
Math::snapped(x, p_step.x),
Math::snapped(y, p_step.y));
}
+Vector2 Vector2::snappedf(real_t p_step) const {
+ return Vector2(
+ Math::snapped(x, p_step),
+ Math::snapped(y, p_step));
+}
+
Vector2 Vector2::limit_length(real_t p_len) const {
const real_t l = length();
Vector2 v = *this;
diff --git a/core/math/vector2.h b/core/math/vector2.h
index 6ad003edd1..8851942cdd 100644
--- a/core/math/vector2.h
+++ b/core/math/vector2.h
@@ -89,10 +89,18 @@ struct _NO_DISCARD_ Vector2 {
return Vector2(MIN(x, p_vector2.x), MIN(y, p_vector2.y));
}
+ Vector2 minf(real_t p_scalar) const {
+ return Vector2(MIN(x, p_scalar), MIN(y, p_scalar));
+ }
+
Vector2 max(const Vector2 &p_vector2) const {
return Vector2(MAX(x, p_vector2.x), MAX(y, p_vector2.y));
}
+ Vector2 maxf(real_t p_scalar) const {
+ return Vector2(MAX(x, p_scalar), MAX(y, p_scalar));
+ }
+
real_t distance_to(const Vector2 &p_vector2) const;
real_t distance_squared_to(const Vector2 &p_vector2) const;
real_t angle_to(const Vector2 &p_vector2) const;
@@ -168,7 +176,9 @@ struct _NO_DISCARD_ Vector2 {
Vector2 ceil() const;
Vector2 round() const;
Vector2 snapped(const Vector2 &p_by) const;
+ Vector2 snappedf(real_t p_by) const;
Vector2 clamp(const Vector2 &p_min, const Vector2 &p_max) const;
+ Vector2 clampf(real_t p_min, real_t p_max) const;
real_t aspect() const { return width / height; }
operator String() const;
diff --git a/core/math/vector2i.cpp b/core/math/vector2i.cpp
index ba79d439dd..790f564734 100644
--- a/core/math/vector2i.cpp
+++ b/core/math/vector2i.cpp
@@ -39,12 +39,24 @@ Vector2i Vector2i::clamp(const Vector2i &p_min, const Vector2i &p_max) const {
CLAMP(y, p_min.y, p_max.y));
}
+Vector2i Vector2i::clampi(int32_t p_min, int32_t p_max) const {
+ return Vector2i(
+ CLAMP(x, p_min, p_max),
+ CLAMP(y, p_min, p_max));
+}
+
Vector2i Vector2i::snapped(const Vector2i &p_step) const {
return Vector2i(
Math::snapped(x, p_step.x),
Math::snapped(y, p_step.y));
}
+Vector2i Vector2i::snappedi(int32_t p_step) const {
+ return Vector2i(
+ Math::snapped(x, p_step),
+ Math::snapped(y, p_step));
+}
+
int64_t Vector2i::length_squared() const {
return x * (int64_t)x + y * (int64_t)y;
}
diff --git a/core/math/vector2i.h b/core/math/vector2i.h
index aa29263a65..aca9ae8272 100644
--- a/core/math/vector2i.h
+++ b/core/math/vector2i.h
@@ -81,10 +81,18 @@ struct _NO_DISCARD_ Vector2i {
return Vector2i(MIN(x, p_vector2i.x), MIN(y, p_vector2i.y));
}
+ Vector2i mini(int32_t p_scalar) const {
+ return Vector2i(MIN(x, p_scalar), MIN(y, p_scalar));
+ }
+
Vector2i max(const Vector2i &p_vector2i) const {
return Vector2i(MAX(x, p_vector2i.x), MAX(y, p_vector2i.y));
}
+ Vector2i maxi(int32_t p_scalar) const {
+ return Vector2i(MAX(x, p_scalar), MAX(y, p_scalar));
+ }
+
double distance_to(const Vector2i &p_to) const {
return (p_to - *this).length();
}
@@ -127,7 +135,9 @@ struct _NO_DISCARD_ Vector2i {
Vector2i sign() const { return Vector2i(SIGN(x), SIGN(y)); }
Vector2i abs() const { return Vector2i(Math::abs(x), Math::abs(y)); }
Vector2i clamp(const Vector2i &p_min, const Vector2i &p_max) const;
+ Vector2i clampi(int32_t p_min, int32_t p_max) const;
Vector2i snapped(const Vector2i &p_step) const;
+ Vector2i snappedi(int32_t p_step) const;
operator String() const;
operator Vector2() const;
diff --git a/core/math/vector3.cpp b/core/math/vector3.cpp
index fad5f2c0fb..1e90002665 100644
--- a/core/math/vector3.cpp
+++ b/core/math/vector3.cpp
@@ -52,6 +52,13 @@ Vector3 Vector3::clamp(const Vector3 &p_min, const Vector3 &p_max) const {
CLAMP(z, p_min.z, p_max.z));
}
+Vector3 Vector3::clampf(real_t p_min, real_t p_max) const {
+ return Vector3(
+ CLAMP(x, p_min, p_max),
+ CLAMP(y, p_min, p_max),
+ CLAMP(z, p_min, p_max));
+}
+
void Vector3::snap(const Vector3 &p_step) {
x = Math::snapped(x, p_step.x);
y = Math::snapped(y, p_step.y);
@@ -64,6 +71,18 @@ Vector3 Vector3::snapped(const Vector3 &p_step) const {
return v;
}
+void Vector3::snapf(real_t p_step) {
+ x = Math::snapped(x, p_step);
+ y = Math::snapped(y, p_step);
+ z = Math::snapped(z, p_step);
+}
+
+Vector3 Vector3::snappedf(real_t p_step) const {
+ Vector3 v = *this;
+ v.snapf(p_step);
+ return v;
+}
+
Vector3 Vector3::limit_length(real_t p_len) const {
const real_t l = length();
Vector3 v = *this;
diff --git a/core/math/vector3.h b/core/math/vector3.h
index f5d16984d9..2313eb557a 100644
--- a/core/math/vector3.h
+++ b/core/math/vector3.h
@@ -80,10 +80,18 @@ struct _NO_DISCARD_ Vector3 {
return Vector3(MIN(x, p_vector3.x), MIN(y, p_vector3.y), MIN(z, p_vector3.z));
}
+ Vector3 minf(real_t p_scalar) const {
+ return Vector3(MIN(x, p_scalar), MIN(y, p_scalar), MIN(z, p_scalar));
+ }
+
Vector3 max(const Vector3 &p_vector3) const {
return Vector3(MAX(x, p_vector3.x), MAX(y, p_vector3.y), MAX(z, p_vector3.z));
}
+ Vector3 maxf(real_t p_scalar) const {
+ return Vector3(MAX(x, p_scalar), MAX(y, p_scalar), MAX(z, p_scalar));
+ }
+
_FORCE_INLINE_ real_t length() const;
_FORCE_INLINE_ real_t length_squared() const;
@@ -96,7 +104,9 @@ struct _NO_DISCARD_ Vector3 {
_FORCE_INLINE_ void zero();
void snap(const Vector3 &p_step);
+ void snapf(real_t p_step);
Vector3 snapped(const Vector3 &p_step) const;
+ Vector3 snappedf(real_t p_step) const;
void rotate(const Vector3 &p_axis, real_t p_angle);
Vector3 rotated(const Vector3 &p_axis, real_t p_angle) const;
@@ -127,6 +137,7 @@ struct _NO_DISCARD_ Vector3 {
_FORCE_INLINE_ Vector3 ceil() const;
_FORCE_INLINE_ Vector3 round() const;
Vector3 clamp(const Vector3 &p_min, const Vector3 &p_max) const;
+ Vector3 clampf(real_t p_min, real_t p_max) const;
_FORCE_INLINE_ real_t distance_to(const Vector3 &p_to) const;
_FORCE_INLINE_ real_t distance_squared_to(const Vector3 &p_to) const;
diff --git a/core/math/vector3i.cpp b/core/math/vector3i.cpp
index f41460e623..93f9d15ac1 100644
--- a/core/math/vector3i.cpp
+++ b/core/math/vector3i.cpp
@@ -48,6 +48,13 @@ Vector3i Vector3i::clamp(const Vector3i &p_min, const Vector3i &p_max) const {
CLAMP(z, p_min.z, p_max.z));
}
+Vector3i Vector3i::clampi(int32_t p_min, int32_t p_max) const {
+ return Vector3i(
+ CLAMP(x, p_min, p_max),
+ CLAMP(y, p_min, p_max),
+ CLAMP(z, p_min, p_max));
+}
+
Vector3i Vector3i::snapped(const Vector3i &p_step) const {
return Vector3i(
Math::snapped(x, p_step.x),
@@ -55,6 +62,13 @@ Vector3i Vector3i::snapped(const Vector3i &p_step) const {
Math::snapped(z, p_step.z));
}
+Vector3i Vector3i::snappedi(int32_t p_step) const {
+ return Vector3i(
+ Math::snapped(x, p_step),
+ Math::snapped(y, p_step),
+ Math::snapped(z, p_step));
+}
+
Vector3i::operator String() const {
return "(" + itos(x) + ", " + itos(y) + ", " + itos(z) + ")";
}
diff --git a/core/math/vector3i.h b/core/math/vector3i.h
index a9f298bff1..035cfcf9e2 100644
--- a/core/math/vector3i.h
+++ b/core/math/vector3i.h
@@ -73,10 +73,18 @@ struct _NO_DISCARD_ Vector3i {
return Vector3i(MIN(x, p_vector3i.x), MIN(y, p_vector3i.y), MIN(z, p_vector3i.z));
}
+ Vector3i mini(int32_t p_scalar) const {
+ return Vector3i(MIN(x, p_scalar), MIN(y, p_scalar), MIN(z, p_scalar));
+ }
+
Vector3i max(const Vector3i &p_vector3i) const {
return Vector3i(MAX(x, p_vector3i.x), MAX(y, p_vector3i.y), MAX(z, p_vector3i.z));
}
+ Vector3i maxi(int32_t p_scalar) const {
+ return Vector3i(MAX(x, p_scalar), MAX(y, p_scalar), MAX(z, p_scalar));
+ }
+
_FORCE_INLINE_ int64_t length_squared() const;
_FORCE_INLINE_ double length() const;
@@ -85,7 +93,9 @@ struct _NO_DISCARD_ Vector3i {
_FORCE_INLINE_ Vector3i abs() const;
_FORCE_INLINE_ Vector3i sign() const;
Vector3i clamp(const Vector3i &p_min, const Vector3i &p_max) const;
+ Vector3i clampi(int32_t p_min, int32_t p_max) const;
Vector3i snapped(const Vector3i &p_step) const;
+ Vector3i snappedi(int32_t p_step) const;
_FORCE_INLINE_ double distance_to(const Vector3i &p_to) const;
_FORCE_INLINE_ int64_t distance_squared_to(const Vector3i &p_to) const;
diff --git a/core/math/vector4.cpp b/core/math/vector4.cpp
index e6f6dee42c..555ca6c66c 100644
--- a/core/math/vector4.cpp
+++ b/core/math/vector4.cpp
@@ -171,12 +171,25 @@ void Vector4::snap(const Vector4 &p_step) {
w = Math::snapped(w, p_step.w);
}
+void Vector4::snapf(real_t p_step) {
+ x = Math::snapped(x, p_step);
+ y = Math::snapped(y, p_step);
+ z = Math::snapped(z, p_step);
+ w = Math::snapped(w, p_step);
+}
+
Vector4 Vector4::snapped(const Vector4 &p_step) const {
Vector4 v = *this;
v.snap(p_step);
return v;
}
+Vector4 Vector4::snappedf(real_t p_step) const {
+ Vector4 v = *this;
+ v.snapf(p_step);
+ return v;
+}
+
Vector4 Vector4::inverse() const {
return Vector4(1.0f / x, 1.0f / y, 1.0f / z, 1.0f / w);
}
@@ -189,6 +202,14 @@ Vector4 Vector4::clamp(const Vector4 &p_min, const Vector4 &p_max) const {
CLAMP(w, p_min.w, p_max.w));
}
+Vector4 Vector4::clampf(real_t p_min, real_t p_max) const {
+ return Vector4(
+ CLAMP(x, p_min, p_max),
+ CLAMP(y, p_min, p_max),
+ CLAMP(z, p_min, p_max),
+ CLAMP(w, p_min, p_max));
+}
+
Vector4::operator String() const {
return "(" + String::num_real(x, false) + ", " + String::num_real(y, false) + ", " + String::num_real(z, false) + ", " + String::num_real(w, false) + ")";
}
diff --git a/core/math/vector4.h b/core/math/vector4.h
index 4dba3126cb..52699c6281 100644
--- a/core/math/vector4.h
+++ b/core/math/vector4.h
@@ -72,10 +72,18 @@ struct _NO_DISCARD_ Vector4 {
return Vector4(MIN(x, p_vector4.x), MIN(y, p_vector4.y), MIN(z, p_vector4.z), MIN(w, p_vector4.w));
}
+ Vector4 minf(real_t p_scalar) const {
+ return Vector4(MIN(x, p_scalar), MIN(y, p_scalar), MIN(z, p_scalar), MIN(w, p_scalar));
+ }
+
Vector4 max(const Vector4 &p_vector4) const {
return Vector4(MAX(x, p_vector4.x), MAX(y, p_vector4.y), MAX(z, p_vector4.z), MAX(w, p_vector4.w));
}
+ Vector4 maxf(real_t p_scalar) const {
+ return Vector4(MAX(x, p_scalar), MAX(y, p_scalar), MAX(z, p_scalar), MAX(w, p_scalar));
+ }
+
_FORCE_INLINE_ real_t length_squared() const;
bool is_equal_approx(const Vector4 &p_vec4) const;
bool is_zero_approx() const;
@@ -101,8 +109,11 @@ struct _NO_DISCARD_ Vector4 {
Vector4 posmod(real_t p_mod) const;
Vector4 posmodv(const Vector4 &p_modv) const;
void snap(const Vector4 &p_step);
+ void snapf(real_t p_step);
Vector4 snapped(const Vector4 &p_step) const;
+ Vector4 snappedf(real_t p_step) const;
Vector4 clamp(const Vector4 &p_min, const Vector4 &p_max) const;
+ Vector4 clampf(real_t p_min, real_t p_max) const;
Vector4 inverse() const;
_FORCE_INLINE_ real_t dot(const Vector4 &p_vec4) const;
diff --git a/core/math/vector4i.cpp b/core/math/vector4i.cpp
index 8e36c6b534..afa77a4988 100644
--- a/core/math/vector4i.cpp
+++ b/core/math/vector4i.cpp
@@ -65,6 +65,14 @@ Vector4i Vector4i::clamp(const Vector4i &p_min, const Vector4i &p_max) const {
CLAMP(w, p_min.w, p_max.w));
}
+Vector4i Vector4i::clampi(int32_t p_min, int32_t p_max) const {
+ return Vector4i(
+ CLAMP(x, p_min, p_max),
+ CLAMP(y, p_min, p_max),
+ CLAMP(z, p_min, p_max),
+ CLAMP(w, p_min, p_max));
+}
+
Vector4i Vector4i::snapped(const Vector4i &p_step) const {
return Vector4i(
Math::snapped(x, p_step.x),
@@ -73,6 +81,14 @@ Vector4i Vector4i::snapped(const Vector4i &p_step) const {
Math::snapped(w, p_step.w));
}
+Vector4i Vector4i::snappedi(int32_t p_step) const {
+ return Vector4i(
+ Math::snapped(x, p_step),
+ Math::snapped(y, p_step),
+ Math::snapped(z, p_step),
+ Math::snapped(w, p_step));
+}
+
Vector4i::operator String() const {
return "(" + itos(x) + ", " + itos(y) + ", " + itos(z) + ", " + itos(w) + ")";
}
diff --git a/core/math/vector4i.h b/core/math/vector4i.h
index 5a96d98d18..8a9c580bc1 100644
--- a/core/math/vector4i.h
+++ b/core/math/vector4i.h
@@ -75,10 +75,18 @@ struct _NO_DISCARD_ Vector4i {
return Vector4i(MIN(x, p_vector4i.x), MIN(y, p_vector4i.y), MIN(z, p_vector4i.z), MIN(w, p_vector4i.w));
}
+ Vector4i mini(int32_t p_scalar) const {
+ return Vector4i(MIN(x, p_scalar), MIN(y, p_scalar), MIN(z, p_scalar), MIN(w, p_scalar));
+ }
+
Vector4i max(const Vector4i &p_vector4i) const {
return Vector4i(MAX(x, p_vector4i.x), MAX(y, p_vector4i.y), MAX(z, p_vector4i.z), MAX(w, p_vector4i.w));
}
+ Vector4i maxi(int32_t p_scalar) const {
+ return Vector4i(MAX(x, p_scalar), MAX(y, p_scalar), MAX(z, p_scalar), MAX(w, p_scalar));
+ }
+
_FORCE_INLINE_ int64_t length_squared() const;
_FORCE_INLINE_ double length() const;
@@ -90,7 +98,9 @@ struct _NO_DISCARD_ Vector4i {
_FORCE_INLINE_ Vector4i abs() const;
_FORCE_INLINE_ Vector4i sign() const;
Vector4i clamp(const Vector4i &p_min, const Vector4i &p_max) const;
+ Vector4i clampi(int32_t p_min, int32_t p_max) const;
Vector4i snapped(const Vector4i &p_step) const;
+ Vector4i snappedi(int32_t p_step) const;
/* Operators */
diff --git a/core/templates/command_queue_mt.cpp b/core/templates/command_queue_mt.cpp
index 0c5c6394a1..d9e5e0b217 100644
--- a/core/templates/command_queue_mt.cpp
+++ b/core/templates/command_queue_mt.cpp
@@ -41,35 +41,6 @@ void CommandQueueMT::unlock() {
mutex.unlock();
}
-void CommandQueueMT::wait_for_flush() {
- // wait one millisecond for a flush to happen
- OS::get_singleton()->delay_usec(1000);
-}
-
-CommandQueueMT::SyncSemaphore *CommandQueueMT::_alloc_sync_sem() {
- int idx = -1;
-
- while (true) {
- lock();
- for (int i = 0; i < SYNC_SEMAPHORES; i++) {
- if (!sync_sems[i].in_use) {
- sync_sems[i].in_use = true;
- idx = i;
- break;
- }
- }
- unlock();
-
- if (idx == -1) {
- wait_for_flush();
- } else {
- break;
- }
- }
-
- return &sync_sems[idx];
-}
-
CommandQueueMT::CommandQueueMT() {
}
diff --git a/core/templates/command_queue_mt.h b/core/templates/command_queue_mt.h
index a4ac338bed..c149861467 100644
--- a/core/templates/command_queue_mt.h
+++ b/core/templates/command_queue_mt.h
@@ -32,9 +32,9 @@
#define COMMAND_QUEUE_MT_H
#include "core/object/worker_thread_pool.h"
+#include "core/os/condition_variable.h"
#include "core/os/memory.h"
#include "core/os/mutex.h"
-#include "core/os/semaphore.h"
#include "core/string/print_string.h"
#include "core/templates/local_vector.h"
#include "core/templates/simple_type.h"
@@ -251,14 +251,14 @@
#define DECL_PUSH(N) \
template <typename T, typename M COMMA(N) COMMA_SEP_LIST(TYPE_PARAM, N)> \
void push(T *p_instance, M p_method COMMA(N) COMMA_SEP_LIST(PARAM, N)) { \
- CMD_TYPE(N) *cmd = allocate_and_lock<CMD_TYPE(N)>(); \
+ MutexLock mlock(mutex); \
+ CMD_TYPE(N) *cmd = allocate<CMD_TYPE(N)>(); \
cmd->instance = p_instance; \
cmd->method = p_method; \
SEMIC_SEP_LIST(CMD_ASSIGN_PARAM, N); \
if (pump_task_id != WorkerThreadPool::INVALID_TASK_ID) { \
WorkerThreadPool::get_singleton()->notify_yield_over(pump_task_id); \
} \
- unlock(); \
}
#define CMD_RET_TYPE(N) CommandRet##N<T, M, COMMA_SEP_LIST(TYPE_ARG, N) COMMA(N) R>
@@ -266,19 +266,17 @@
#define DECL_PUSH_AND_RET(N) \
template <typename T, typename M, COMMA_SEP_LIST(TYPE_PARAM, N) COMMA(N) typename R> \
void push_and_ret(T *p_instance, M p_method, COMMA_SEP_LIST(PARAM, N) COMMA(N) R *r_ret) { \
- SyncSemaphore *ss = _alloc_sync_sem(); \
- CMD_RET_TYPE(N) *cmd = allocate_and_lock<CMD_RET_TYPE(N)>(); \
+ MutexLock mlock(mutex); \
+ CMD_RET_TYPE(N) *cmd = allocate<CMD_RET_TYPE(N)>(); \
cmd->instance = p_instance; \
cmd->method = p_method; \
SEMIC_SEP_LIST(CMD_ASSIGN_PARAM, N); \
cmd->ret = r_ret; \
- cmd->sync_sem = ss; \
if (pump_task_id != WorkerThreadPool::INVALID_TASK_ID) { \
WorkerThreadPool::get_singleton()->notify_yield_over(pump_task_id); \
} \
- unlock(); \
- ss->sem.wait(); \
- ss->in_use = false; \
+ sync_tail++; \
+ _wait_for_sync(mlock); \
}
#define CMD_SYNC_TYPE(N) CommandSync##N<T, M COMMA(N) COMMA_SEP_LIST(TYPE_ARG, N)>
@@ -286,39 +284,31 @@
#define DECL_PUSH_AND_SYNC(N) \
template <typename T, typename M COMMA(N) COMMA_SEP_LIST(TYPE_PARAM, N)> \
void push_and_sync(T *p_instance, M p_method COMMA(N) COMMA_SEP_LIST(PARAM, N)) { \
- SyncSemaphore *ss = _alloc_sync_sem(); \
- CMD_SYNC_TYPE(N) *cmd = allocate_and_lock<CMD_SYNC_TYPE(N)>(); \
+ MutexLock mlock(mutex); \
+ CMD_SYNC_TYPE(N) *cmd = allocate<CMD_SYNC_TYPE(N)>(); \
cmd->instance = p_instance; \
cmd->method = p_method; \
SEMIC_SEP_LIST(CMD_ASSIGN_PARAM, N); \
- cmd->sync_sem = ss; \
if (pump_task_id != WorkerThreadPool::INVALID_TASK_ID) { \
WorkerThreadPool::get_singleton()->notify_yield_over(pump_task_id); \
} \
- unlock(); \
- ss->sem.wait(); \
- ss->in_use = false; \
+ sync_tail++; \
+ _wait_for_sync(mlock); \
}
#define MAX_CMD_PARAMS 15
class CommandQueueMT {
- struct SyncSemaphore {
- Semaphore sem;
- bool in_use = false;
- };
-
struct CommandBase {
+ bool sync = false;
virtual void call() = 0;
- virtual SyncSemaphore *get_sync_semaphore() { return nullptr; }
virtual ~CommandBase() = default; // Won't be called.
};
struct SyncCommand : public CommandBase {
- SyncSemaphore *sync_sem = nullptr;
-
- virtual SyncSemaphore *get_sync_semaphore() override {
- return sync_sem;
+ virtual void call() override {}
+ SyncCommand() {
+ sync = true;
}
};
@@ -340,9 +330,11 @@ class CommandQueueMT {
SYNC_SEMAPHORES = 8
};
+ BinaryMutex mutex;
LocalVector<uint8_t> command_mem;
- SyncSemaphore sync_sems[SYNC_SEMAPHORES];
- Mutex mutex;
+ ConditionVariable sync_cond_var;
+ uint32_t sync_head = 0;
+ uint32_t sync_tail = 0;
WorkerThreadPool::TaskID pump_task_id = WorkerThreadPool::INVALID_TASK_ID;
uint64_t flush_read_ptr = 0;
@@ -357,32 +349,23 @@ class CommandQueueMT {
return cmd;
}
- template <typename T>
- T *allocate_and_lock() {
- lock();
- T *ret = allocate<T>();
- return ret;
- }
-
void _flush() {
- lock();
-
if (unlikely(flush_read_ptr)) {
// Re-entrant call.
- unlock();
return;
}
+ lock();
+
WorkerThreadPool::thread_enter_command_queue_mt_flush(this);
while (flush_read_ptr < command_mem.size()) {
uint64_t size = *(uint64_t *)&command_mem[flush_read_ptr];
flush_read_ptr += 8;
CommandBase *cmd = reinterpret_cast<CommandBase *>(&command_mem[flush_read_ptr]);
-
- SyncSemaphore *sync_sem = cmd->get_sync_semaphore();
cmd->call();
- if (sync_sem) {
- sync_sem->sem.post(); // Release in case it needs sync/ret.
+ if (unlikely(cmd->sync)) {
+ sync_head++;
+ sync_cond_var.notify_all();
}
flush_read_ptr += size;
@@ -394,8 +377,12 @@ class CommandQueueMT {
unlock();
}
- void wait_for_flush();
- SyncSemaphore *_alloc_sync_sem();
+ _FORCE_INLINE_ void _wait_for_sync(MutexLock<BinaryMutex> &p_lock) {
+ uint32_t sync_head_goal = sync_tail;
+ do {
+ sync_cond_var.wait(p_lock);
+ } while (sync_head != sync_head_goal); // Can't use lower-than because of wraparound.
+ }
public:
void lock();
diff --git a/core/variant/method_ptrcall.h b/core/variant/method_ptrcall.h
index 123f2067e2..c8d1241d3d 100644
--- a/core/variant/method_ptrcall.h
+++ b/core/variant/method_ptrcall.h
@@ -159,10 +159,7 @@ MAKE_PTRARG_BY_REFERENCE(Variant);
template <typename T>
struct PtrToArg<T *> {
_FORCE_INLINE_ static T *convert(const void *p_ptr) {
- if (p_ptr == nullptr) {
- return nullptr;
- }
- return const_cast<T *>(*reinterpret_cast<T *const *>(p_ptr));
+ return likely(p_ptr) ? const_cast<T *>(*reinterpret_cast<T *const *>(p_ptr)) : nullptr;
}
typedef Object *EncodeT;
_FORCE_INLINE_ static void encode(T *p_var, void *p_ptr) {
@@ -173,10 +170,7 @@ struct PtrToArg<T *> {
template <typename T>
struct PtrToArg<const T *> {
_FORCE_INLINE_ static const T *convert(const void *p_ptr) {
- if (p_ptr == nullptr) {
- return nullptr;
- }
- return *reinterpret_cast<T *const *>(p_ptr);
+ return likely(p_ptr) ? *reinterpret_cast<T *const *>(p_ptr) : nullptr;
}
typedef const Object *EncodeT;
_FORCE_INLINE_ static void encode(T *p_var, void *p_ptr) {
diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp
index fcbfdd4741..37eb16f9b2 100644
--- a/core/variant/variant.cpp
+++ b/core/variant/variant.cpp
@@ -3515,6 +3515,17 @@ bool Variant::is_shared() const {
return is_type_shared(type);
}
+bool Variant::is_read_only() const {
+ switch (type) {
+ case ARRAY:
+ return reinterpret_cast<const Array *>(_data._mem)->is_read_only();
+ case DICTIONARY:
+ return reinterpret_cast<const Dictionary *>(_data._mem)->is_read_only();
+ default:
+ return false;
+ }
+}
+
void Variant::_variant_call_error(const String &p_method, Callable::CallError &error) {
switch (error.error) {
case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT: {
diff --git a/core/variant/variant.h b/core/variant/variant.h
index ea6ae02c1e..93953c4e0e 100644
--- a/core/variant/variant.h
+++ b/core/variant/variant.h
@@ -349,6 +349,7 @@ public:
bool is_zero() const;
bool is_one() const;
bool is_null() const;
+ bool is_read_only() const;
// Make sure Variant is not implicitly cast when accessing it with bracket notation (GH-49469).
Variant &operator[](const Variant &p_key) = delete;
diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp
index d0d940c47d..986b34c662 100644
--- a/core/variant/variant_call.cpp
+++ b/core/variant/variant_call.cpp
@@ -1804,7 +1804,13 @@ static void _register_variant_builtin_methods() {
bind_method(Vector2, abs, sarray(), varray());
bind_method(Vector2, sign, sarray(), varray());
bind_method(Vector2, clamp, sarray("min", "max"), varray());
+ bind_method(Vector2, clampf, sarray("min", "max"), varray());
bind_method(Vector2, snapped, sarray("step"), varray());
+ bind_method(Vector2, snappedf, sarray("step"), varray());
+ bind_method(Vector2, min, sarray("with"), varray());
+ bind_method(Vector2, minf, sarray("with"), varray());
+ bind_method(Vector2, max, sarray("with"), varray());
+ bind_method(Vector2, maxf, sarray("with"), varray());
bind_static_method(Vector2, from_angle, sarray("angle"), varray());
@@ -1820,7 +1826,13 @@ static void _register_variant_builtin_methods() {
bind_method(Vector2i, sign, sarray(), varray());
bind_method(Vector2i, abs, sarray(), varray());
bind_method(Vector2i, clamp, sarray("min", "max"), varray());
+ bind_method(Vector2i, clampi, sarray("min", "max"), varray());
bind_method(Vector2i, snapped, sarray("step"), varray());
+ bind_method(Vector2i, snappedi, sarray("step"), varray());
+ bind_method(Vector2i, min, sarray("with"), varray());
+ bind_method(Vector2i, mini, sarray("with"), varray());
+ bind_method(Vector2i, max, sarray("with"), varray());
+ bind_method(Vector2i, maxi, sarray("with"), varray());
/* Rect2 */
@@ -1875,7 +1887,9 @@ static void _register_variant_builtin_methods() {
bind_method(Vector3, is_finite, sarray(), varray());
bind_method(Vector3, inverse, sarray(), varray());
bind_method(Vector3, clamp, sarray("min", "max"), varray());
+ bind_method(Vector3, clampf, sarray("min", "max"), varray());
bind_method(Vector3, snapped, sarray("step"), varray());
+ bind_method(Vector3, snappedf, sarray("step"), varray());
bind_method(Vector3, rotated, sarray("axis", "angle"), varray());
bind_method(Vector3, lerp, sarray("to", "weight"), varray());
bind_method(Vector3, slerp, sarray("to", "weight"), varray());
@@ -1896,9 +1910,13 @@ static void _register_variant_builtin_methods() {
bind_method(Vector3, project, sarray("b"), varray());
bind_method(Vector3, slide, sarray("n"), varray());
bind_method(Vector3, bounce, sarray("n"), varray());
- bind_method(Vector3, reflect, sarray("direction"), varray());
+ bind_method(Vector3, reflect, sarray("n"), varray());
bind_method(Vector3, sign, sarray(), varray());
bind_method(Vector3, octahedron_encode, sarray(), varray());
+ bind_method(Vector3, min, sarray("with"), varray());
+ bind_method(Vector3, minf, sarray("with"), varray());
+ bind_method(Vector3, max, sarray("with"), varray());
+ bind_method(Vector3, maxf, sarray("with"), varray());
bind_static_method(Vector3, octahedron_decode, sarray("uv"), varray());
/* Vector3i */
@@ -1912,7 +1930,13 @@ static void _register_variant_builtin_methods() {
bind_method(Vector3i, sign, sarray(), varray());
bind_method(Vector3i, abs, sarray(), varray());
bind_method(Vector3i, clamp, sarray("min", "max"), varray());
+ bind_method(Vector3i, clampi, sarray("min", "max"), varray());
bind_method(Vector3i, snapped, sarray("step"), varray());
+ bind_method(Vector3i, snappedi, sarray("step"), varray());
+ bind_method(Vector3i, min, sarray("with"), varray());
+ bind_method(Vector3i, mini, sarray("with"), varray());
+ bind_method(Vector3i, max, sarray("with"), varray());
+ bind_method(Vector3i, maxi, sarray("with"), varray());
/* Vector4 */
@@ -1931,7 +1955,9 @@ static void _register_variant_builtin_methods() {
bind_method(Vector4, posmod, sarray("mod"), varray());
bind_method(Vector4, posmodv, sarray("modv"), varray());
bind_method(Vector4, snapped, sarray("step"), varray());
+ bind_method(Vector4, snappedf, sarray("step"), varray());
bind_method(Vector4, clamp, sarray("min", "max"), varray());
+ bind_method(Vector4, clampf, sarray("min", "max"), varray());
bind_method(Vector4, normalized, sarray(), varray());
bind_method(Vector4, is_normalized, sarray(), varray());
bind_method(Vector4, direction_to, sarray("to"), varray());
@@ -1942,6 +1968,10 @@ static void _register_variant_builtin_methods() {
bind_method(Vector4, is_equal_approx, sarray("to"), varray());
bind_method(Vector4, is_zero_approx, sarray(), varray());
bind_method(Vector4, is_finite, sarray(), varray());
+ bind_method(Vector4, min, sarray("with"), varray());
+ bind_method(Vector4, minf, sarray("with"), varray());
+ bind_method(Vector4, max, sarray("with"), varray());
+ bind_method(Vector4, maxf, sarray("with"), varray());
/* Vector4i */
@@ -1952,7 +1982,13 @@ static void _register_variant_builtin_methods() {
bind_method(Vector4i, sign, sarray(), varray());
bind_method(Vector4i, abs, sarray(), varray());
bind_method(Vector4i, clamp, sarray("min", "max"), varray());
+ bind_method(Vector4i, clampi, sarray("min", "max"), varray());
bind_method(Vector4i, snapped, sarray("step"), varray());
+ bind_method(Vector4i, snappedi, sarray("step"), varray());
+ bind_method(Vector4i, min, sarray("with"), varray());
+ bind_method(Vector4i, mini, sarray("with"), varray());
+ bind_method(Vector4i, max, sarray("with"), varray());
+ bind_method(Vector4i, maxi, sarray("with"), varray());
bind_method(Vector4i, distance_to, sarray("to"), varray());
bind_method(Vector4i, distance_squared_to, sarray("to"), varray());
diff --git a/core/variant/variant_setget.cpp b/core/variant/variant_setget.cpp
index 9d5ed22b1a..f49e9e54b3 100644
--- a/core/variant/variant_setget.cpp
+++ b/core/variant/variant_setget.cpp
@@ -251,15 +251,21 @@ void Variant::set_named(const StringName &p_member, const Variant &p_value, bool
return;
}
} else if (type == Variant::DICTIONARY) {
- Variant *v = VariantGetInternalPtr<Dictionary>::get_ptr(this)->getptr(p_member);
+ Dictionary &dict = *VariantGetInternalPtr<Dictionary>::get_ptr(this);
+
+ if (dict.is_read_only()) {
+ r_valid = false;
+ return;
+ }
+
+ Variant *v = dict.getptr(p_member);
if (v) {
*v = p_value;
- r_valid = true;
} else {
- VariantGetInternalPtr<Dictionary>::get_ptr(this)->operator[](p_member) = p_value;
- r_valid = true;
+ dict[p_member] = p_value;
}
+ r_valid = true;
} else {
r_valid = false;
}
diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml
index 0307684588..f2a6517a0c 100644
--- a/doc/classes/@GlobalScope.xml
+++ b/doc/classes/@GlobalScope.xml
@@ -696,6 +696,7 @@
[codeblock]
max(1, 7, 3, -6, 5) # Returns 7
[/codeblock]
+ [b]Note:[/b] When using this on vectors it will [i]not[/i] perform component-wise maximum, and will pick the largest value when compared using [code]x &lt; y[/code]. To perform component-wise maximum, use [method Vector2.max], [method Vector2i.max], [method Vector3.max], [method Vector3i.max], [method Vector4.max], and [method Vector4i.max].
</description>
</method>
<method name="maxf">
@@ -729,6 +730,7 @@
[codeblock]
min(1, 7, 3, -6, 5) # Returns -6
[/codeblock]
+ [b]Note:[/b] When using this on vectors it will [i]not[/i] perform component-wise minimum, and will pick the smallest value when compared using [code]x &lt; y[/code]. To perform component-wise minimum, use [method Vector2.min], [method Vector2i.min], [method Vector3.min], [method Vector3i.min], [method Vector4.min], and [method Vector4i.min].
</description>
</method>
<method name="minf">
diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml
index 4e9b642674..26ed881502 100644
--- a/doc/classes/Animation.xml
+++ b/doc/classes/Animation.xml
@@ -595,6 +595,9 @@
</method>
</methods>
<members>
+ <member name="capture_included" type="bool" setter="_set_capture_included" getter="is_capture_included" default="false">
+ Returns [code]true[/code] if the capture track is included. This is a cached readonly value for performance.
+ </member>
<member name="length" type="float" setter="set_length" getter="get_length" default="1.0">
The total length of the animation (in seconds).
[b]Note:[/b] Length is not delimited by the last key, as this one may be before or after the end to ensure correct interpolation and looping.
@@ -658,7 +661,7 @@
Update at the keyframes.
</constant>
<constant name="UPDATE_CAPTURE" value="2" enum="UpdateMode">
- Same as [constant UPDATE_CONTINUOUS] but works as a flag to capture the value of the current object and perform interpolation in some methods. See also [method AnimationMixer.capture] and [method AnimationPlayer.play_with_capture].
+ Same as [constant UPDATE_CONTINUOUS] but works as a flag to capture the value of the current object and perform interpolation in some methods. See also [method AnimationMixer.capture], [member AnimationPlayer.playback_auto_capture], and [method AnimationPlayer.play_with_capture].
</constant>
<constant name="LOOP_NONE" value="0" enum="LoopMode">
At both ends of the animation, the animation will stop playing.
diff --git a/doc/classes/AnimationPlayer.xml b/doc/classes/AnimationPlayer.xml
index 1b742bea28..ef9c1ecebe 100644
--- a/doc/classes/AnimationPlayer.xml
+++ b/doc/classes/AnimationPlayer.xml
@@ -112,7 +112,7 @@
</method>
<method name="play_with_capture">
<return type="void" />
- <param index="0" name="name" type="StringName" />
+ <param index="0" name="name" type="StringName" default="&amp;&quot;&quot;" />
<param index="1" name="duration" type="float" default="-1.0" />
<param index="2" name="custom_blend" type="float" default="-1" />
<param index="3" name="custom_speed" type="float" default="1.0" />
@@ -120,12 +120,13 @@
<param index="5" name="trans_type" type="int" enum="Tween.TransitionType" default="0" />
<param index="6" name="ease_type" type="int" enum="Tween.EaseType" default="0" />
<description>
- See [method AnimationMixer.capture]. It is almost the same as the following:
+ See also [method AnimationMixer.capture].
+ You can use this method to use more detailed options for capture than those performed by [member playback_auto_capture]. When [member playback_auto_capture] is [code]false[/code], this method is almost the same as the following:
[codeblock]
capture(name, duration, trans_type, ease_type)
play(name, custom_blend, custom_speed, from_end)
[/codeblock]
- If name is blank, it specifies [member assigned_animation].
+ If [param name] is blank, it specifies [member assigned_animation].
If [param duration] is a negative value, the duration is set to the interval between the current position and the first key, when [param from_end] is [code]true[/code], uses the interval between the current position and the last key instead.
[b]Note:[/b] The [param duration] takes [member speed_scale] into account, but [param custom_speed] does not, because the capture cache is interpolated with the blend result and the result may contain multiple animations.
</description>
@@ -210,6 +211,9 @@
If [code]true[/code] and the engine is running in Movie Maker mode (see [MovieWriter]), exits the engine with [method SceneTree.quit] as soon as an animation is done playing in this [AnimationPlayer]. A message is printed when the engine quits for this reason.
[b]Note:[/b] This obeys the same logic as the [signal AnimationMixer.animation_finished] signal, so it will not quit the engine if the animation is set to be looping.
</member>
+ <member name="playback_auto_capture" type="bool" setter="set_auto_capture" getter="is_auto_capture" default="true">
+ If [code]true[/code], performs [method AnimationMixer.capture] before playback automatically. This means just [method play_with_capture] is executed with default arguments instead of [method play].
+ </member>
<member name="playback_default_blend_time" type="float" setter="set_default_blend_time" getter="get_default_blend_time" default="0.0">
The default time in which to blend animations. Ranges from 0 to 4096 with 0.01 precision.
</member>
diff --git a/doc/classes/AudioStreamWAV.xml b/doc/classes/AudioStreamWAV.xml
index 206b6361cc..3df814cb7f 100644
--- a/doc/classes/AudioStreamWAV.xml
+++ b/doc/classes/AudioStreamWAV.xml
@@ -15,7 +15,7 @@
<return type="int" enum="Error" />
<param index="0" name="path" type="String" />
<description>
- Saves the AudioStreamWAV as a WAV file to [param path]. Samples with IMA ADPCM format can't be saved.
+ Saves the AudioStreamWAV as a WAV file to [param path]. Samples with IMA ADPCM or QOA formats can't be saved.
[b]Note:[/b] A [code].wav[/code] extension is automatically appended to [param path] if it is missing.
</description>
</method>
@@ -56,6 +56,9 @@
<constant name="FORMAT_IMA_ADPCM" value="2" enum="Format">
Audio is compressed using IMA ADPCM.
</constant>
+ <constant name="FORMAT_QOA" value="3" enum="Format">
+ Audio is compressed as QOA ([url=https://qoaformat.org/]Quite OK Audio[/url]).
+ </constant>
<constant name="LOOP_DISABLED" value="0" enum="LoopMode">
Audio does not loop.
</constant>
diff --git a/doc/classes/BaseMaterial3D.xml b/doc/classes/BaseMaterial3D.xml
index 1bd4183b3c..fc8af02869 100644
--- a/doc/classes/BaseMaterial3D.xml
+++ b/doc/classes/BaseMaterial3D.xml
@@ -609,6 +609,9 @@
<constant name="BLEND_MODE_MUL" value="3" enum="BlendMode">
The color of the object is multiplied by the background.
</constant>
+ <constant name="BLEND_MODE_PREMULT_ALPHA" value="4" enum="BlendMode">
+ The color of the object is added to the background and the alpha channel is used to mask out the background. This is effectively a hybrid of the blend mix and add modes, useful for effects like fire where you want the flame to add but the smoke to mix. By default, this works with unshaded materials using premultiplied textures. For shaded materials, use the [code]PREMUL_ALPHA_FACTOR[/code] built-in so that lighting can be modulated as well.
+ </constant>
<constant name="ALPHA_ANTIALIASING_OFF" value="0" enum="AlphaAntiAliasing">
Disables Alpha AntiAliasing for the material.
</constant>
diff --git a/doc/classes/CanvasItem.xml b/doc/classes/CanvasItem.xml
index 18413c4be5..bf53cb2e14 100644
--- a/doc/classes/CanvasItem.xml
+++ b/doc/classes/CanvasItem.xml
@@ -77,8 +77,14 @@
<param index="0" name="position" type="Vector2" />
<param index="1" name="radius" type="float" />
<param index="2" name="color" type="Color" />
+ <param index="3" name="filled" type="bool" default="true" />
+ <param index="4" name="width" type="float" default="-1.0" />
+ <param index="5" name="antialiased" type="bool" default="false" />
<description>
- Draws a colored, filled circle. See also [method draw_arc], [method draw_polyline] and [method draw_polygon].
+ Draws a circle. See also [method draw_arc], [method draw_polyline], and [method draw_polygon].
+ If [param filled] is [code]true[/code], the circle will be filled with the [param color] specified. If [param filled] is [code]false[/code], the circle will be drawn as a stroke with the [param color] and [param width] specified.
+ If [param width] is negative, then two-point primitives will be drawn instead of a four-point ones. This means that when the CanvasItem is scaled, the lines will remain thin. If this behavior is not desired, then pass a positive [param width] like [code]1.0[/code].
+ [b]Note:[/b] [param width] is only effective if [param filled] is [code]false[/code].
</description>
</method>
<method name="draw_colored_polygon">
diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml
index 7c6f1a51c4..d455799c29 100644
--- a/doc/classes/CodeEdit.xml
+++ b/doc/classes/CodeEdit.xml
@@ -14,7 +14,7 @@
<return type="void" />
<param index="0" name="replace" type="bool" />
<description>
- Override this method to define how the selected entry should be inserted. If [param replace] is true, any existing text should be replaced.
+ Override this method to define how the selected entry should be inserted. If [param replace] is [code]true[/code], any existing text should be replaced.
</description>
</method>
<method name="_filter_code_completion_candidates" qualifiers="virtual const">
@@ -29,7 +29,7 @@
<return type="void" />
<param index="0" name="force" type="bool" />
<description>
- Override this method to define what happens when the user requests code completion. If [param force] is true, any checks should be bypassed.
+ Override this method to define what happens when the user requests code completion. If [param force] is [code]true[/code], any checks should be bypassed.
</description>
</method>
<method name="add_auto_brace_completion_pair">
@@ -123,7 +123,7 @@
<return type="void" />
<param index="0" name="replace" type="bool" default="false" />
<description>
- Inserts the selected entry into the text. If [param replace] is true, any existing text is replaced rather than merged.
+ Inserts the selected entry into the text. If [param replace] is [code]true[/code], any existing text is replaced rather than merged.
</description>
</method>
<method name="convert_indent">
@@ -144,6 +144,12 @@
Code regions are delimited using start and end tags (respectively [code]region[/code] and [code]endregion[/code] by default) preceded by one line comment delimiter. (eg. [code]#region[/code] and [code]#endregion[/code])
</description>
</method>
+ <method name="delete_lines">
+ <return type="void" />
+ <description>
+ Deletes all lines that are selected or have a caret on them.
+ </description>
+ </method>
<method name="do_indent">
<return type="void" />
<description>
@@ -156,6 +162,12 @@
Duplicates all lines currently selected with any caret. Duplicates the entire line beneath the current one no matter where the caret is within the line.
</description>
</method>
+ <method name="duplicate_selection">
+ <return type="void" />
+ <description>
+ Duplicates all selected text and duplicates all lines with a caret on them.
+ </description>
+ </method>
<method name="fold_all_lines">
<return type="void" />
<description>
@@ -379,6 +391,18 @@
Returns whether the line at the specified index is folded or not.
</description>
</method>
+ <method name="move_lines_down">
+ <return type="void" />
+ <description>
+ Moves all lines down that are selected or have a caret on them.
+ </description>
+ </method>
+ <method name="move_lines_up">
+ <return type="void" />
+ <description>
+ Moves all lines up that are selected or have a caret on them.
+ </description>
+ </method>
<method name="remove_comment_delimiter">
<return type="void" />
<param index="0" name="start_key" type="String" />
@@ -397,7 +421,7 @@
<return type="void" />
<param index="0" name="force" type="bool" default="false" />
<description>
- Emits [signal code_completion_requested], if [param force] is true will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path or signal.
+ Emits [signal code_completion_requested], if [param force] is [code]true[/code] will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path, or signal.
</description>
</method>
<method name="set_code_completion_selected_index">
@@ -467,6 +491,12 @@
Toggle the folding of the code block at the given line.
</description>
</method>
+ <method name="toggle_foldable_lines_at_carets">
+ <return type="void" />
+ <description>
+ Toggle the folding of the code block on all lines with a caret on them.
+ </description>
+ </method>
<method name="unfold_all_lines">
<return type="void" />
<description>
diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml
index e614a897d5..a054048266 100644
--- a/doc/classes/DisplayServer.xml
+++ b/doc/classes/DisplayServer.xml
@@ -58,7 +58,7 @@
</method>
<method name="create_status_indicator">
<return type="int" />
- <param index="0" name="icon" type="Image" />
+ <param index="0" name="icon" type="Texture2D" />
<param index="1" name="tooltip" type="String" />
<param index="2" name="callback" type="Callable" />
<description>
@@ -1167,20 +1167,41 @@
[b]Note:[/b] This method is implemented on Android, iOS, macOS, Windows, and Linux (X11/Wayland).
</description>
</method>
+ <method name="status_indicator_get_rect" qualifiers="const">
+ <return type="Rect2" />
+ <param index="0" name="id" type="int" />
+ <description>
+ Returns the rectangle for the given status indicator [param id] in screen coordinates. If the status indicator is not visible, returns an empty [Rect2].
+ [b]Note:[/b] This method is implemented on macOS and Windows.
+ </description>
+ </method>
<method name="status_indicator_set_callback">
<return type="void" />
<param index="0" name="id" type="int" />
<param index="1" name="callback" type="Callable" />
<description>
Sets the application status indicator activation callback.
+ [b]Note:[/b] This method is implemented on macOS and Windows.
</description>
</method>
<method name="status_indicator_set_icon">
<return type="void" />
<param index="0" name="id" type="int" />
- <param index="1" name="icon" type="Image" />
+ <param index="1" name="icon" type="Texture2D" />
<description>
Sets the application status indicator icon.
+ [b]Note:[/b] This method is implemented on macOS and Windows.
+ </description>
+ </method>
+ <method name="status_indicator_set_menu">
+ <return type="void" />
+ <param index="0" name="id" type="int" />
+ <param index="1" name="menu_rid" type="RID" />
+ <description>
+ Sets the application status indicator native popup menu.
+ [b]Note:[/b] On macOS, the menu is activated by any mouse button. Its activation callback is [i]not[/i] triggered.
+ [b]Note:[/b] On Windows, the menu is activated by the right mouse button, selecting the status icon and pressing [kbd]Shift + F10[/kbd], or the applications key. The menu's activation callback for the other mouse buttons is still triggered.
+ [b]Note:[/b] Native popup is only supported if [NativeMenu] supports the [constant NativeMenu.FEATURE_POPUP_MENU] feature.
</description>
</method>
<method name="status_indicator_set_tooltip">
@@ -1189,6 +1210,7 @@
<param index="1" name="tooltip" type="String" />
<description>
Sets the application status indicator tooltip.
+ [b]Note:[/b] This method is implemented on macOS and Windows.
</description>
</method>
<method name="tablet_get_current_driver" qualifiers="const">
diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index 93a7b09fce..0c08dd3605 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -881,7 +881,7 @@
All update modes will ignore builds with different major versions (e.g. Godot 4 -&gt; Godot 5).
</member>
<member name="network/connection/network_mode" type="int" setter="" getter="">
- Determines whether online features are enabled in the editor, such as the Asset Library or update checks. Disabling these online features helps alleviate privacy concerns by preventing the editor from making HTTP requests to the Godot website, GitHub, or third-party platforms hosting assets from the Asset Library.
+ Determines whether online features are enabled in the editor, such as the Asset Library or update checks. Disabling these online features helps alleviate privacy concerns by preventing the editor from making HTTP requests to the Godot website or third-party platforms hosting assets from the Asset Library.
</member>
<member name="network/debug/remote_host" type="String" setter="" getter="">
The address to listen to when starting the remote debugger. This can be set to [code]0.0.0.0[/code] to allow external clients to connect to the remote debugger (instead of restricting the remote debugger to connections from [code]localhost[/code]).
diff --git a/doc/classes/HingeJoint3D.xml b/doc/classes/HingeJoint3D.xml
index d150c79b78..f794853caf 100644
--- a/doc/classes/HingeJoint3D.xml
+++ b/doc/classes/HingeJoint3D.xml
@@ -53,7 +53,7 @@
<member name="angular_limit/relaxation" type="float" setter="set_param" getter="get_param" default="1.0">
The lower this value, the more the rotation gets slowed down.
</member>
- <member name="angular_limit/softness" type="float" setter="set_param" getter="get_param" default="0.9">
+ <member name="angular_limit/softness" type="float" setter="set_param" getter="get_param" default="0.9" deprecated="This property is never set by the engine and is kept for compatibility purposes.">
</member>
<member name="angular_limit/upper" type="float" setter="set_param" getter="get_param" default="1.5708">
The maximum rotation. Only active if [member angular_limit/enable] is [code]true[/code].
@@ -84,7 +84,7 @@
<constant name="PARAM_LIMIT_BIAS" value="3" enum="Param">
The speed with which the rotation across the axis perpendicular to the hinge gets corrected.
</constant>
- <constant name="PARAM_LIMIT_SOFTNESS" value="4" enum="Param">
+ <constant name="PARAM_LIMIT_SOFTNESS" value="4" enum="Param" deprecated="This property is never used by the engine and is kept for compatibility purpose.">
</constant>
<constant name="PARAM_LIMIT_RELAXATION" value="5" enum="Param">
The lower this value, the more the rotation gets slowed down.
diff --git a/doc/classes/Node2D.xml b/doc/classes/Node2D.xml
index 091acdf6f2..851290de7b 100644
--- a/doc/classes/Node2D.xml
+++ b/doc/classes/Node2D.xml
@@ -44,7 +44,8 @@
<return type="void" />
<param index="0" name="point" type="Vector2" />
<description>
- Rotates the node so it points towards the [param point], which is expected to use global coordinates.
+ Rotates the node so that its local +X axis points towards the [param point], which is expected to use global coordinates.
+ [param point] should not be the same as the node's position, otherwise the node always looks to the right.
</description>
</method>
<method name="move_local_x">
diff --git a/doc/classes/PackedScene.xml b/doc/classes/PackedScene.xml
index 579b4c5b9f..26d8fa8d5f 100644
--- a/doc/classes/PackedScene.xml
+++ b/doc/classes/PackedScene.xml
@@ -99,14 +99,14 @@
<return type="int" enum="Error" />
<param index="0" name="path" type="Node" />
<description>
- Pack will ignore any sub-nodes not owned by given node. See [member Node.owner].
+ Packs the [param path] node, and all owned sub-nodes, into this [PackedScene]. Any existing data will be cleared. See [member Node.owner].
</description>
</method>
</methods>
<members>
<member name="_bundled" type="Dictionary" setter="_set_bundled_scene" getter="_get_bundled_scene" default="{ &quot;conn_count&quot;: 0, &quot;conns&quot;: PackedInt32Array(), &quot;editable_instances&quot;: [], &quot;names&quot;: PackedStringArray(), &quot;node_count&quot;: 0, &quot;node_paths&quot;: [], &quot;nodes&quot;: PackedInt32Array(), &quot;variants&quot;: [], &quot;version&quot;: 3 }">
A dictionary representation of the scene contents.
- Available keys include "rnames" and "variants" for resources, "node_count", "nodes", "node_paths" for nodes, "editable_instances" for paths to overridden nodes, "conn_count" and "conns" for signal connections, and "version" for the format style of the PackedScene.
+ Available keys include "names" and "variants" for resources, "node_count", "nodes", "node_paths" for nodes, "editable_instances" for paths to overridden nodes, "conn_count" and "conns" for signal connections, and "version" for the format style of the PackedScene.
</member>
</members>
<constants>
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 0d4d8bbebb..e8d7dfb913 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -344,7 +344,7 @@
This setting can be overridden using the [code]--frame-delay &lt;ms;&gt;[/code] command line argument.
</member>
<member name="application/run/low_processor_mode" type="bool" setter="" getter="" default="false">
- If [code]true[/code], enables low-processor usage mode. This setting only works on desktop platforms. The screen is not redrawn if nothing changes visually. This is meant for writing applications and editors, but is pretty useless (and can hurt performance) in most games.
+ If [code]true[/code], enables low-processor usage mode. The screen is not redrawn if nothing changes visually. This is meant for writing applications and editors, but is pretty useless (and can hurt performance) in most games.
</member>
<member name="application/run/low_processor_mode_sleep_usec" type="int" setter="" getter="" default="6900">
Amount of sleeping between frames when the low-processor usage mode is enabled (in microseconds). Higher values will result in lower CPU usage.
@@ -2900,6 +2900,9 @@
<member name="xr/openxr/extensions/eye_gaze_interaction" type="bool" setter="" getter="" default="false">
Specify whether to enable eye tracking for this project. Depending on the platform, additional export configuration may be needed.
</member>
+ <member name="xr/openxr/extensions/hand_interaction_profile" type="bool" setter="" getter="" default="false">
+ If true the hand interaction profile extension will be activated if supported by the platform.
+ </member>
<member name="xr/openxr/extensions/hand_tracking" type="bool" setter="" getter="" default="true">
If true we enable the hand tracking extension if available.
</member>
diff --git a/doc/classes/Rect2.xml b/doc/classes/Rect2.xml
index 0471d73749..ba7dcab2d7 100644
--- a/doc/classes/Rect2.xml
+++ b/doc/classes/Rect2.xml
@@ -89,13 +89,13 @@
var rect = Rect2(0, 0, 5, 2)
rect = rect.expand(Vector2(10, 0)) # rect is Rect2(0, 0, 10, 2)
- rect = rect.expand(Vector2(-5, 5)) # rect is Rect2(-5, 0, 10, 5)
+ rect = rect.expand(Vector2(-5, 5)) # rect is Rect2(-5, 0, 15, 5)
[/gdscript]
[csharp]
var rect = new Rect2(0, 0, 5, 2);
rect = rect.Expand(new Vector2(10, 0)); // rect is Rect2(0, 0, 10, 2)
- rect = rect.Expand(new Vector2(-5, 5)); // rect is Rect2(-5, 0, 10, 5)
+ rect = rect.Expand(new Vector2(-5, 5)); // rect is Rect2(-5, 0, 15, 5)
[/csharp]
[/codeblocks]
</description>
diff --git a/doc/classes/Rect2i.xml b/doc/classes/Rect2i.xml
index c021a1be26..cbd09f8430 100644
--- a/doc/classes/Rect2i.xml
+++ b/doc/classes/Rect2i.xml
@@ -88,13 +88,13 @@
var rect = Rect2i(0, 0, 5, 2)
rect = rect.expand(Vector2i(10, 0)) # rect is Rect2i(0, 0, 10, 2)
- rect = rect.expand(Vector2i(-5, 5)) # rect is Rect2i(-5, 0, 10, 5)
+ rect = rect.expand(Vector2i(-5, 5)) # rect is Rect2i(-5, 0, 15, 5)
[/gdscript]
[csharp]
var rect = new Rect2I(0, 0, 5, 2);
rect = rect.Expand(new Vector2I(10, 0)); // rect is Rect2I(0, 0, 10, 2)
- rect = rect.Expand(new Vector2I(-5, 5)); // rect is Rect2I(-5, 0, 10, 5)
+ rect = rect.Expand(new Vector2I(-5, 5)); // rect is Rect2I(-5, 0, 15, 5)
[/csharp]
[/codeblocks]
</description>
diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml
index 5d90cd6b92..519bba4e7c 100644
--- a/doc/classes/RenderingServer.xml
+++ b/doc/classes/RenderingServer.xml
@@ -1979,6 +1979,12 @@
[b]Warning:[/b] This function is primarily intended for editor usage. For in-game use cases, prefer physics collision.
</description>
</method>
+ <method name="is_on_render_thread">
+ <return type="bool" />
+ <description>
+ Returns [code]true[/code] if our code is currently executing on the rendering thread.
+ </description>
+ </method>
<method name="light_directional_set_blend_splits">
<return type="void" />
<param index="0" name="light" type="RID" />
diff --git a/doc/classes/ResourceImporterWAV.xml b/doc/classes/ResourceImporterWAV.xml
index 5336c98d0f..d3dafb03b6 100644
--- a/doc/classes/ResourceImporterWAV.xml
+++ b/doc/classes/ResourceImporterWAV.xml
@@ -14,6 +14,7 @@
The compression mode to use on import.
[b]Disabled:[/b] Imports audio data without any compression. This results in the highest possible quality.
[b]RAM (Ima-ADPCM):[/b] Performs fast lossy compression on import. Low CPU cost, but quality is noticeably decreased compared to Ogg Vorbis or even MP3.
+ [b]QOA ([url=https://qoaformat.org/]Quite OK Audio[/url]):[/b] Performs lossy compression on import. CPU cost is slightly higher than IMA-ADPCM, but quality is much higher.
</member>
<member name="edit/loop_begin" type="int" setter="" getter="" default="0">
The begin loop point to use when [member edit/loop_mode] is [b]Forward[/b], [b]Ping-Pong[/b] or [b]Backward[/b]. This is set in seconds after the beginning of the audio file.
diff --git a/doc/classes/RichTextLabel.xml b/doc/classes/RichTextLabel.xml
index 01c4074e6d..f4e3c1209f 100644
--- a/doc/classes/RichTextLabel.xml
+++ b/doc/classes/RichTextLabel.xml
@@ -249,6 +249,13 @@
[/codeblock]
</description>
</method>
+ <method name="invalidate_paragraph">
+ <return type="bool" />
+ <param index="0" name="paragraph" type="int" />
+ <description>
+ Invalidates [param paragraph] and all subsequent paragraphs cache.
+ </description>
+ </method>
<method name="is_menu_visible" qualifiers="const">
<return type="bool" />
<description>
@@ -497,9 +504,11 @@
<method name="remove_paragraph">
<return type="bool" />
<param index="0" name="paragraph" type="int" />
+ <param index="1" name="no_invalidate" type="bool" default="false" />
<description>
Removes a paragraph of content from the label. Returns [code]true[/code] if the paragraph exists.
The [param paragraph] argument is the index of the paragraph to remove, it can take values in the interval [code][0, get_paragraph_count() - 1][/code].
+ If [param no_invalidate] is set to [code]true[/code], cache for the subsequent paragraphs is not invalidated. Use it for faster updates if deleted paragraph is fully self-contained (have no unclosed tags), or this call is part of the complex edit operation and [method invalidate_paragraph] will be called at the end of operation.
</description>
</method>
<method name="scroll_to_line">
diff --git a/doc/classes/StatusIndicator.xml b/doc/classes/StatusIndicator.xml
index e1fcc35ad7..688840b17f 100644
--- a/doc/classes/StatusIndicator.xml
+++ b/doc/classes/StatusIndicator.xml
@@ -8,10 +8,22 @@
</description>
<tutorials>
</tutorials>
+ <methods>
+ <method name="get_rect" qualifiers="const">
+ <return type="Rect2" />
+ <description>
+ Returns the status indicator rectangle in screen coordinates. If this status indicator is not visible, returns an empty [Rect2].
+ </description>
+ </method>
+ </methods>
<members>
- <member name="icon" type="Image" setter="set_icon" getter="get_icon">
+ <member name="icon" type="Texture2D" setter="set_icon" getter="get_icon">
Status indicator icon.
</member>
+ <member name="menu" type="NodePath" setter="set_menu" getter="get_menu" default="NodePath(&quot;&quot;)">
+ Status indicator native popup menu. If this is set, the [signal pressed] signal is not emitted.
+ [b]Note:[/b] Native popup is only supported if [NativeMenu] supports [constant NativeMenu.FEATURE_POPUP_MENU] feature.
+ </member>
<member name="tooltip" type="String" setter="set_tooltip" getter="get_tooltip" default="&quot;&quot;">
Status indicator tooltip.
</member>
diff --git a/doc/classes/String.xml b/doc/classes/String.xml
index a33a1aea41..59733e9696 100644
--- a/doc/classes/String.xml
+++ b/doc/classes/String.xml
@@ -747,7 +747,7 @@
<method name="reverse" qualifiers="const">
<return type="String" />
<description>
- Returns the copy of this string in reverse order.
+ Returns the copy of this string in reverse order. This operation works on unicode codepoints, rather than sequences of codepoints, and may break things like compound letters or emojis.
</description>
</method>
<method name="rfind" qualifiers="const">
diff --git a/doc/classes/StringName.xml b/doc/classes/StringName.xml
index e837b65199..e3c254fb48 100644
--- a/doc/classes/StringName.xml
+++ b/doc/classes/StringName.xml
@@ -648,7 +648,7 @@
<method name="reverse" qualifiers="const">
<return type="String" />
<description>
- Returns the copy of this string in reverse order.
+ Returns the copy of this string in reverse order. This operation works on unicode codepoints, rather than sequences of codepoints, and may break things like compound letters or emojis.
</description>
</method>
<method name="rfind" qualifiers="const">
diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml
index db0c1f17b0..2959ec4cfa 100644
--- a/doc/classes/TextEdit.xml
+++ b/doc/classes/TextEdit.xml
@@ -5,7 +5,7 @@
</brief_description>
<description>
A multiline text editor. It also has limited facilities for editing code, such as syntax highlighting support. For more advanced facilities for editing code, see [CodeEdit].
- [b]Note:[/b] Most viewport, caret and edit methods contain a [code]caret_index[/code] argument for [member caret_multiple] support. The argument should be one of the following: [code]-1[/code] for all carets, [code]0[/code] for the main caret, or greater than [code]0[/code] for secondary carets.
+ [b]Note:[/b] Most viewport, caret, and edit methods contain a [code]caret_index[/code] argument for [member caret_multiple] support. The argument should be one of the following: [code]-1[/code] for all carets, [code]0[/code] for the main caret, or greater than [code]0[/code] for secondary carets in the order they were created.
[b]Note:[/b] When holding down [kbd]Alt[/kbd], the vertical scroll wheel will scroll 5 times as fast as it would normally do. This also works in the Godot script editor.
</description>
<tutorials>
@@ -58,7 +58,7 @@
<method name="add_caret">
<return type="int" />
<param index="0" name="line" type="int" />
- <param index="1" name="col" type="int" />
+ <param index="1" name="column" type="int" />
<description>
Adds a new caret at the given location. Returns the index of the new caret, or [code]-1[/code] if the location is invalid.
</description>
@@ -67,7 +67,7 @@
<return type="void" />
<param index="0" name="below" type="bool" />
<description>
- Adds an additional caret above or below every caret. If [param below] is true the new caret will be added below and above otherwise.
+ Adds an additional caret above or below every caret. If [param below] is [code]true[/code] the new caret will be added below and above otherwise.
</description>
</method>
<method name="add_gutter">
@@ -83,7 +83,7 @@
Adds a selection and a caret for the next occurrence of the current selection. If there is no active selection, selects word under caret.
</description>
</method>
- <method name="adjust_carets_after_edit">
+ <method name="adjust_carets_after_edit" deprecated="No longer necessary since methods now adjust carets themselves.">
<return type="void" />
<param index="0" name="caret" type="int" />
<param index="1" name="from_line" type="int" />
@@ -91,7 +91,7 @@
<param index="3" name="to_line" type="int" />
<param index="4" name="to_col" type="int" />
<description>
- Reposition the carets affected by the edit. This assumes edits are applied in edit order, see [method get_caret_index_edit_order].
+ This method does nothing.
</description>
</method>
<method name="adjust_viewport_to_caret">
@@ -120,6 +120,23 @@
Starts a multipart edit. All edits will be treated as one action until [method end_complex_operation] is called.
</description>
</method>
+ <method name="begin_multicaret_edit">
+ <return type="void" />
+ <description>
+ Starts an edit for multiple carets. The edit must be ended with [method end_multicaret_edit]. Multicaret edits can be used to edit text at multiple carets and delay merging the carets until the end, so the caret indexes aren't affected immediately. [method begin_multicaret_edit] and [method end_multicaret_edit] can be nested, and the merge will happen at the last [method end_multicaret_edit].
+ Example usage:
+ [codeblock]
+ begin_complex_operation()
+ begin_multicaret_edit()
+ for i in range(get_caret_count()):
+ if multicaret_edit_ignore_caret(i):
+ continue
+ # Logic here.
+ end_multicaret_edit()
+ end_complex_operation()
+ [/codeblock]
+ </description>
+ </method>
<method name="cancel_ime">
<return type="void" />
<description>
@@ -145,6 +162,20 @@
Clears the undo history.
</description>
</method>
+ <method name="collapse_carets">
+ <return type="void" />
+ <param index="0" name="from_line" type="int" />
+ <param index="1" name="from_column" type="int" />
+ <param index="2" name="to_line" type="int" />
+ <param index="3" name="to_column" type="int" />
+ <param index="4" name="inclusive" type="bool" default="false" />
+ <description>
+ Collapse all carets in the given range to the [param from_line] and [param from_column] position.
+ [param inclusive] applies to both ends.
+ If [method is_in_mulitcaret_edit] is [code]true[/code], carets that are collapsed will be [code]true[/code] for [method multicaret_edit_ignore_caret].
+ [method merge_overlapping_carets] will be called if any carets were collapsed.
+ </description>
+ </method>
<method name="copy">
<return type="void" />
<param index="0" name="caret_index" type="int" default="-1" />
@@ -185,6 +216,12 @@
Ends a multipart edit, started with [method begin_complex_operation]. If called outside a complex operation, the current operation is pushed onto the undo/redo stack.
</description>
</method>
+ <method name="end_multicaret_edit">
+ <return type="void" />
+ <description>
+ Ends an edit for multiple carets, that was started with [method begin_multicaret_edit]. If this was the last [method end_multicaret_edit] and [method merge_overlapping_carets] was called, carets will be merged.
+ </description>
+ </method>
<method name="get_caret_column" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
@@ -205,7 +242,7 @@
Returns the caret pixel draw position.
</description>
</method>
- <method name="get_caret_index_edit_order">
+ <method name="get_caret_index_edit_order" deprecated="Carets no longer need to be edited in any specific order. If the carets need to be sorted, use [method get_sorted_carets] instead.">
<return type="PackedInt32Array" />
<description>
Returns a list of caret indexes in their edit order, this done from bottom to top. Edit order refers to the way actions such as [method insert_text_at_caret] are applied.
@@ -363,6 +400,15 @@
[b]Note:[/b] The return value is influenced by [theme_item line_spacing] and [theme_item font_size]. And it will not be less than [code]1[/code].
</description>
</method>
+ <method name="get_line_ranges_from_carets" qualifiers="const">
+ <return type="Vector2i[]" />
+ <param index="0" name="only_selections" type="bool" default="false" />
+ <param index="1" name="merge_adjacent" type="bool" default="true" />
+ <description>
+ Returns an [Array] of line ranges where [code]x[/code] is the first line and [code]y[/code] is the last line. All lines within these ranges will have a caret on them or be part of a selection. Each line will only be part of one line range, even if it has multiple carets on it.
+ If a selection's end column ([method get_selection_to_column]) is at column [code]0[/code], that line will not be included. If a selection begins on the line after another selection ends and [param merge_adjacent] is [code]true[/code], or they begin and end on the same line, one line range will include both selections.
+ </description>
+ </method>
<method name="get_line_width" qualifiers="const">
<return type="int" />
<param index="0" name="line" type="int" />
@@ -514,7 +560,18 @@
Returns the text inside the selection of a caret, or all the carets if [param caret_index] is its default value [code]-1[/code].
</description>
</method>
- <method name="get_selection_column" qualifiers="const">
+ <method name="get_selection_at_line_column" qualifiers="const">
+ <return type="int" />
+ <param index="0" name="line" type="int" />
+ <param index="1" name="column" type="int" />
+ <param index="2" name="include_edges" type="bool" default="true" />
+ <param index="3" name="only_selections" type="bool" default="true" />
+ <description>
+ Returns the caret index of the selection at the given [param line] and [param column], or [code]-1[/code] if there is none.
+ If [param include_edges] is [code]false[/code], the position must be inside the selection and not at either end. If [param only_selections] is [code]false[/code], carets without a selection will also be considered.
+ </description>
+ </method>
+ <method name="get_selection_column" qualifiers="const" deprecated="Use [method get_selection_origin_column] instead.">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
@@ -525,17 +582,17 @@
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
- Returns the selection begin column.
+ Returns the selection begin column. Returns the caret column if there is no selection.
</description>
</method>
<method name="get_selection_from_line" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
- Returns the selection begin line.
+ Returns the selection begin line. Returns the caret line if there is no selection.
</description>
</method>
- <method name="get_selection_line" qualifiers="const">
+ <method name="get_selection_line" qualifiers="const" deprecated="Use [method get_selection_origin_line] instead.">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
@@ -548,18 +605,40 @@
Returns the current selection mode.
</description>
</method>
+ <method name="get_selection_origin_column" qualifiers="const">
+ <return type="int" />
+ <param index="0" name="caret_index" type="int" default="0" />
+ <description>
+ Returns the origin column of the selection. This is the opposite end from the caret.
+ </description>
+ </method>
+ <method name="get_selection_origin_line" qualifiers="const">
+ <return type="int" />
+ <param index="0" name="caret_index" type="int" default="0" />
+ <description>
+ Returns the origin line of the selection. This is the opposite end from the caret.
+ </description>
+ </method>
<method name="get_selection_to_column" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
- Returns the selection end column.
+ Returns the selection end column. Returns the caret column if there is no selection.
</description>
</method>
<method name="get_selection_to_line" qualifiers="const">
<return type="int" />
<param index="0" name="caret_index" type="int" default="0" />
<description>
- Returns the selection end line.
+ Returns the selection end line. Returns the caret line if there is no selection.
+ </description>
+ </method>
+ <method name="get_sorted_carets" qualifiers="const">
+ <return type="PackedInt32Array" />
+ <param index="0" name="include_ignored_carets" type="bool" default="false" />
+ <description>
+ Returns the carets sorted by selection beginning from lowest line and column to highest (from top to bottom of text).
+ If [param include_ignored_carets] is [code]false[/code], carets from [method multicaret_edit_ignore_caret] will be ignored.
</description>
</method>
<method name="get_tab_size" qualifiers="const">
@@ -653,6 +732,19 @@
Inserts a new line with [param text] at [param line].
</description>
</method>
+ <method name="insert_text">
+ <return type="void" />
+ <param index="0" name="text" type="String" />
+ <param index="1" name="line" type="int" />
+ <param index="2" name="column" type="int" />
+ <param index="3" name="before_selection_begin" type="bool" default="true" />
+ <param index="4" name="before_selection_end" type="bool" default="false" />
+ <description>
+ Inserts the [param text] at [param line] and [param column].
+ If [param before_selection_begin] is [code]true[/code], carets and selections that begin at [param line] and [param column] will moved to the end of the inserted text, along with all carets after it.
+ If [param before_selection_end] is [code]true[/code], selections that end at [param line] and [param column] will be extended to the end of the inserted text. These parameters can be used to insert text inside of or outside of selections.
+ </description>
+ </method>
<method name="insert_text_at_caret">
<return type="void" />
<param index="0" name="text" type="String" />
@@ -661,6 +753,13 @@
Insert the specified text at the caret position.
</description>
</method>
+ <method name="is_caret_after_selection_origin" qualifiers="const">
+ <return type="bool" />
+ <param index="0" name="caret_index" type="int" default="0" />
+ <description>
+ Returns [code]true[/code] if the caret of the selection is after the selection origin. This can be used to determine the direction of the selection.
+ </description>
+ </method>
<method name="is_caret_visible" qualifiers="const">
<return type="bool" />
<param index="0" name="caret_index" type="int" default="0" />
@@ -671,7 +770,7 @@
<method name="is_dragging_cursor" qualifiers="const">
<return type="bool" />
<description>
- Returns [code]true[/code] if the user is dragging their mouse for scrolling or selecting.
+ Returns [code]true[/code] if the user is dragging their mouse for scrolling, selecting, or text dragging.
</description>
</method>
<method name="is_gutter_clickable" qualifiers="const">
@@ -695,6 +794,12 @@
Returns whether the gutter is overwritable.
</description>
</method>
+ <method name="is_in_mulitcaret_edit" qualifiers="const">
+ <return type="bool" />
+ <description>
+ Returns [code]true[/code] if a [method begin_multicaret_edit] has been called and [method end_multicaret_edit] has not yet been called.
+ </description>
+ </method>
<method name="is_line_gutter_clickable" qualifiers="const">
<return type="bool" />
<param index="0" name="line" type="int" />
@@ -749,9 +854,18 @@
<return type="void" />
<description>
Merges any overlapping carets. Will favor the newest caret, or the caret with a selection.
+ If [method is_in_mulitcaret_edit] is [code]true[/code], the merge will be queued to happen at the end of the multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit].
[b]Note:[/b] This is not called when a caret changes position but after certain actions, so it is possible to get into a state where carets overlap.
</description>
</method>
+ <method name="multicaret_edit_ignore_caret" qualifiers="const">
+ <return type="bool" />
+ <param index="0" name="caret_index" type="int" />
+ <description>
+ Returns [code]true[/code] if the given [param caret_index] should be ignored as part of a multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit]. Carets that should be ignored are ones that were part of removed text and will likely be merged at the end of the edit, or carets that were added during the edit.
+ It is recommended to [code]continue[/code] within a loop iterating on multiple carets if a caret should be ignored.
+ </description>
+ </method>
<method name="paste">
<return type="void" />
<param index="0" name="caret_index" type="int" default="-1" />
@@ -787,6 +901,15 @@
Removes the gutter from this [TextEdit].
</description>
</method>
+ <method name="remove_line_at">
+ <return type="void" />
+ <param index="0" name="line" type="int" />
+ <param index="1" name="move_carets_down" type="bool" default="true" />
+ <description>
+ Removes the line of text at [param line]. Carets on this line will attempt to match their previous visual x position.
+ If [param move_carets_down] is [code]true[/code] carets will move to the next line down, otherwise carets will move up.
+ </description>
+ </method>
<method name="remove_secondary_carets">
<return type="void" />
<description>
@@ -801,7 +924,6 @@
<param index="3" name="to_column" type="int" />
<description>
Removes text between the given positions.
- [b]Note:[/b] This does not adjust the caret or selection, which as a result it can end up in an invalid position.
</description>
</method>
<method name="search" qualifiers="const">
@@ -809,7 +931,7 @@
<param index="0" name="text" type="String" />
<param index="1" name="flags" type="int" />
<param index="2" name="from_line" type="int" />
- <param index="3" name="from_colum" type="int" />
+ <param index="3" name="from_column" type="int" />
<description>
Perform a search inside the text. Search flags can be specified in the [enum SearchFlags] enum.
In the returned vector, [code]x[/code] is the column, [code]y[/code] is the line. If no results are found, both are equal to [code]-1[/code].
@@ -835,14 +957,15 @@
</method>
<method name="select">
<return type="void" />
- <param index="0" name="from_line" type="int" />
- <param index="1" name="from_column" type="int" />
- <param index="2" name="to_line" type="int" />
- <param index="3" name="to_column" type="int" />
+ <param index="0" name="origin_line" type="int" />
+ <param index="1" name="origin_column" type="int" />
+ <param index="2" name="caret_line" type="int" />
+ <param index="3" name="caret_column" type="int" />
<param index="4" name="caret_index" type="int" default="0" />
<description>
- Perform selection, from line/column to line/column.
+ Selects text from [param origin_line] and [param origin_column] to [param caret_line] and [param caret_column] for the given [param caret_index]. This moves the selection origin and the caret. If the positions are the same, the selection will be deselected.
If [member selecting_enabled] is [code]false[/code], no selection will occur.
+ [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets].
</description>
</method>
<method name="select_all">
@@ -878,9 +1001,10 @@
<param index="3" name="wrap_index" type="int" default="0" />
<param index="4" name="caret_index" type="int" default="0" />
<description>
- Moves the caret to the specified [param line] index.
+ Moves the caret to the specified [param line] index. The caret column will be moved to the same visual position it was at the last time [method set_caret_column] was called, or clamped to the end of the line.
If [param adjust_viewport] is [code]true[/code], the viewport will center at the caret position after the move occurs.
If [param can_be_hidden] is [code]true[/code], the specified [param line] can be hidden.
+ If [param wrap_index] is [code]-1[/code], the caret column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_caret_column] was called.
[b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets].
</description>
</method>
@@ -945,7 +1069,8 @@
<param index="0" name="line" type="int" />
<param index="1" name="new_text" type="String" />
<description>
- Sets the text for a specific line.
+ Sets the text for a specific [param line].
+ Carets on the line will attempt to keep their visual x position.
</description>
</method>
<method name="set_line_as_center_visible">
@@ -1049,13 +1174,30 @@
<method name="set_selection_mode">
<return type="void" />
<param index="0" name="mode" type="int" enum="TextEdit.SelectionMode" />
- <param index="1" name="line" type="int" default="-1" />
- <param index="2" name="column" type="int" default="-1" />
- <param index="3" name="caret_index" type="int" default="0" />
<description>
Sets the current selection mode.
</description>
</method>
+ <method name="set_selection_origin_column">
+ <return type="void" />
+ <param index="0" name="column" type="int" />
+ <param index="1" name="caret_index" type="int" default="0" />
+ <description>
+ Sets the selection origin column to the [param column] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect.
+ </description>
+ </method>
+ <method name="set_selection_origin_line">
+ <return type="void" />
+ <param index="0" name="line" type="int" />
+ <param index="1" name="can_be_hidden" type="bool" default="true" />
+ <param index="2" name="wrap_index" type="int" default="-1" />
+ <param index="3" name="caret_index" type="int" default="0" />
+ <description>
+ Sets the selection origin line to the [param line] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect.
+ If [param can_be_hidden] is [code]false[/code], The line will be set to the nearest unhidden line below or above.
+ If [param wrap_index] is [code]-1[/code], the selection origin column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_selection_origin_column] or [method select] was called.
+ </description>
+ </method>
<method name="set_tab_size">
<return type="void" />
<param index="0" name="size" type="int" />
@@ -1089,7 +1231,7 @@
<param index="0" name="from_line" type="int" />
<param index="1" name="to_line" type="int" />
<description>
- Swaps the two lines.
+ Swaps the two lines. Carets will be swapped with the lines.
</description>
</method>
<method name="tag_saved_version">
@@ -1140,7 +1282,7 @@
If [code]true[/code], the selected text will be deselected when focus is lost.
</member>
<member name="drag_and_drop_selection_enabled" type="bool" setter="set_drag_and_drop_selection_enabled" getter="is_drag_and_drop_selection_enabled" default="true">
- If [code]true[/code], allow drag and drop of selected text.
+ If [code]true[/code], allow drag and drop of selected text. Text can still be dropped from other sources.
</member>
<member name="draw_control_chars" type="bool" setter="set_draw_control_chars" getter="get_draw_control_chars" default="false">
If [code]true[/code], control characters are displayed.
@@ -1231,7 +1373,7 @@
<signals>
<signal name="caret_changed">
<description>
- Emitted when the caret changes position.
+ Emitted when any caret changes position.
</description>
</signal>
<signal name="gutter_added">
diff --git a/doc/classes/TileSetAtlasSource.xml b/doc/classes/TileSetAtlasSource.xml
index 6f212274f8..a34ca0ce91 100644
--- a/doc/classes/TileSetAtlasSource.xml
+++ b/doc/classes/TileSetAtlasSource.xml
@@ -295,6 +295,15 @@
# If tile is not already flipped, flip it.
$TileMap.set_cell(0, Vector2i(2, 2), source_id, atlas_coords, alternate_id | TileSetAtlasSource.TRANSFORM_FLIP_H)
[/codeblock]
+ [b]Note:[/b] These transformations can be combined to do the equivalent of 0, 90, 180, and 270 degree rotations, as shown below:
+ [codeblock]
+ enum TileTransform {
+ ROTATE_0 = 0,
+ ROTATE_90 = TileSetAtlasSource.TRANSFORM_TRANSPOSE | TileSetAtlasSource.TRANSFORM_FLIP_H,
+ ROTATE_180 = TileSetAtlasSource.TRANSFORM_FLIP_H | TileSetAtlasSource.TRANSFORM_FLIP_V,
+ ROTATE_270 = TileSetAtlasSource.TRANSFORM_TRANSPOSE | TileSetAtlasSource.TRANSFORM_FLIP_V,
+ }
+ [/codeblock]
</constant>
<constant name="TRANSFORM_FLIP_V" value="8192">
Represents cell's vertical flip flag. See [constant TRANSFORM_FLIP_H] for usage.
diff --git a/doc/classes/Variant.xml b/doc/classes/Variant.xml
index eb837a4643..b420933285 100644
--- a/doc/classes/Variant.xml
+++ b/doc/classes/Variant.xml
@@ -42,8 +42,8 @@
# Note that Objects are their own special category.
# To get the name of the underlying Object type, you need the `get_class()` method.
print("foo is a(n) %s" % foo.get_class()) # inject the class name into a formatted string.
- # Note also that there is not yet any way to get a script's `class_name` string easily.
- # To fetch that value, you can use ProjectSettings.get_global_class_list().
+ # Note that this does not get the script's `class_name` global identifier.
+ # If the `class_name` is needed, use `foo.get_script().get_global_name()` instead.
[/gdscript]
[csharp]
Variant foo = 2;
diff --git a/doc/classes/Vector2.xml b/doc/classes/Vector2.xml
index 7b166a4fb0..5fadfd0d95 100644
--- a/doc/classes/Vector2.xml
+++ b/doc/classes/Vector2.xml
@@ -128,6 +128,14 @@
Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
</description>
</method>
+ <method name="clampf" qualifiers="const">
+ <return type="Vector2" />
+ <param index="0" name="min" type="float" />
+ <param index="1" name="max" type="float" />
+ <description>
+ Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
+ </description>
+ </method>
<method name="cross" qualifiers="const">
<return type="float" />
<param index="0" name="with" type="Vector2" />
@@ -265,18 +273,46 @@
Returns the vector with a maximum length by limiting its length to [param length].
</description>
</method>
+ <method name="max" qualifiers="const">
+ <return type="Vector2" />
+ <param index="0" name="with" type="Vector2" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector2(maxf(x, with.x), maxf(y, with.y))[/code].
+ </description>
+ </method>
<method name="max_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's highest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_X].
</description>
</method>
+ <method name="maxf" qualifiers="const">
+ <return type="Vector2" />
+ <param index="0" name="with" type="float" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector2(maxf(x, with), maxf(y, with))[/code].
+ </description>
+ </method>
+ <method name="min" qualifiers="const">
+ <return type="Vector2" />
+ <param index="0" name="with" type="Vector2" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector2(minf(x, with.x), minf(y, with.y))[/code].
+ </description>
+ </method>
<method name="min_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's lowest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_Y].
</description>
</method>
+ <method name="minf" qualifiers="const">
+ <return type="Vector2" />
+ <param index="0" name="with" type="float" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector2(minf(x, with), minf(y, with))[/code].
+ </description>
+ </method>
<method name="move_toward" qualifiers="const">
<return type="Vector2" />
<param index="0" name="to" type="Vector2" />
@@ -371,6 +407,13 @@
Returns a new vector with each component snapped to the nearest multiple of the corresponding component in [param step]. This can also be used to round the components to an arbitrary number of decimals.
</description>
</method>
+ <method name="snappedf" qualifiers="const">
+ <return type="Vector2" />
+ <param index="0" name="step" type="float" />
+ <description>
+ Returns a new vector with each component snapped to the nearest multiple of [param step]. This can also be used to round the components to an arbitrary number of decimals.
+ </description>
+ </method>
</methods>
<members>
<member name="x" type="float" setter="" getter="" default="0.0">
diff --git a/doc/classes/Vector2i.xml b/doc/classes/Vector2i.xml
index 18291e06a9..4afc62e038 100644
--- a/doc/classes/Vector2i.xml
+++ b/doc/classes/Vector2i.xml
@@ -64,6 +64,14 @@
Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
</description>
</method>
+ <method name="clampi" qualifiers="const">
+ <return type="Vector2i" />
+ <param index="0" name="min" type="int" />
+ <param index="1" name="max" type="int" />
+ <description>
+ Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
+ </description>
+ </method>
<method name="distance_squared_to" qualifiers="const">
<return type="int" />
<param index="0" name="to" type="Vector2i" />
@@ -92,18 +100,46 @@
This method runs faster than [method length], so prefer it if you need to compare vectors or need the squared distance for some formula.
</description>
</method>
+ <method name="max" qualifiers="const">
+ <return type="Vector2i" />
+ <param index="0" name="with" type="Vector2i" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector2i(maxi(x, with.x), maxi(y, with.y))[/code].
+ </description>
+ </method>
<method name="max_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's highest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_X].
</description>
</method>
+ <method name="maxi" qualifiers="const">
+ <return type="Vector2i" />
+ <param index="0" name="with" type="int" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector2i(maxi(x, with), maxi(y, with))[/code].
+ </description>
+ </method>
+ <method name="min" qualifiers="const">
+ <return type="Vector2i" />
+ <param index="0" name="with" type="Vector2i" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector2i(mini(x, with.x), mini(y, with.y))[/code].
+ </description>
+ </method>
<method name="min_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's lowest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_Y].
</description>
</method>
+ <method name="mini" qualifiers="const">
+ <return type="Vector2i" />
+ <param index="0" name="with" type="int" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector2i(mini(x, with), mini(y, with))[/code].
+ </description>
+ </method>
<method name="sign" qualifiers="const">
<return type="Vector2i" />
<description>
@@ -117,6 +153,13 @@
Returns a new vector with each component snapped to the closest multiple of the corresponding component in [param step].
</description>
</method>
+ <method name="snappedi" qualifiers="const">
+ <return type="Vector2i" />
+ <param index="0" name="step" type="int" />
+ <description>
+ Returns a new vector with each component snapped to the closest multiple of [param step].
+ </description>
+ </method>
</methods>
<members>
<member name="x" type="int" setter="" getter="" default="0">
diff --git a/doc/classes/Vector3.xml b/doc/classes/Vector3.xml
index 031d91af78..387359a97f 100644
--- a/doc/classes/Vector3.xml
+++ b/doc/classes/Vector3.xml
@@ -104,6 +104,14 @@
Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
</description>
</method>
+ <method name="clampf" qualifiers="const">
+ <return type="Vector3" />
+ <param index="0" name="min" type="float" />
+ <param index="1" name="max" type="float" />
+ <description>
+ Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
+ </description>
+ </method>
<method name="cross" qualifiers="const">
<return type="Vector3" />
<param index="0" name="with" type="Vector3" />
@@ -234,18 +242,46 @@
Returns the vector with a maximum length by limiting its length to [param length].
</description>
</method>
+ <method name="max" qualifiers="const">
+ <return type="Vector3" />
+ <param index="0" name="with" type="Vector3" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector3(maxf(x, with.x), maxf(y, with.y), maxf(z, with.z))[/code].
+ </description>
+ </method>
<method name="max_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's highest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_X].
</description>
</method>
+ <method name="maxf" qualifiers="const">
+ <return type="Vector3" />
+ <param index="0" name="with" type="float" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector3(maxf(x, with), maxf(y, with), maxf(z, with))[/code].
+ </description>
+ </method>
+ <method name="min" qualifiers="const">
+ <return type="Vector3" />
+ <param index="0" name="with" type="Vector3" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector3(minf(x, with.x), minf(y, with.y), minf(z, with.z))[/code].
+ </description>
+ </method>
<method name="min_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's lowest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_Z].
</description>
</method>
+ <method name="minf" qualifiers="const">
+ <return type="Vector3" />
+ <param index="0" name="with" type="float" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector3(minf(x, with), minf(y, with), minf(z, with))[/code].
+ </description>
+ </method>
<method name="move_toward" qualifiers="const">
<return type="Vector3" />
<param index="0" name="to" type="Vector3" />
@@ -307,10 +343,10 @@
</method>
<method name="reflect" qualifiers="const">
<return type="Vector3" />
- <param index="0" name="direction" type="Vector3" />
+ <param index="0" name="n" type="Vector3" />
<description>
- Returns the result of reflecting the vector from a plane defined by the given direction vector [param direction].
- [b]Note:[/b] [method reflect] differs from what other engines and frameworks call [code skip-lint]reflect()[/code]. In other engines, [code skip-lint]reflect()[/code] takes a normal direction which is a direction perpendicular to the plane. In Godot, you specify a direction parallel to the plane. See also [method bounce] which does what most engines call [code skip-lint]reflect()[/code].
+ Returns the result of reflecting the vector through a plane defined by the given normal vector [param n].
+ [b]Note:[/b] [method reflect] differs from what other engines and frameworks call [code skip-lint]reflect()[/code]. In other engines, [code skip-lint]reflect()[/code] returns the result of the vector reflected by the given plane. The reflection thus passes through the given normal. While in Godot the reflection passes through the plane and can be thought of as bouncing off the normal. See also [method bounce] which does what most engines call [code skip-lint]reflect()[/code].
</description>
</method>
<method name="rotated" qualifiers="const">
@@ -365,6 +401,13 @@
Returns a new vector with each component snapped to the nearest multiple of the corresponding component in [param step]. This can also be used to round the components to an arbitrary number of decimals.
</description>
</method>
+ <method name="snappedf" qualifiers="const">
+ <return type="Vector3" />
+ <param index="0" name="step" type="float" />
+ <description>
+ Returns a new vector with each component snapped to the nearest multiple of [param step]. This can also be used to round the components to an arbitrary number of decimals.
+ </description>
+ </method>
</methods>
<members>
<member name="x" type="float" setter="" getter="" default="0.0">
diff --git a/doc/classes/Vector3i.xml b/doc/classes/Vector3i.xml
index ffebd3e1f3..df4624dbb1 100644
--- a/doc/classes/Vector3i.xml
+++ b/doc/classes/Vector3i.xml
@@ -59,6 +59,14 @@
Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
</description>
</method>
+ <method name="clampi" qualifiers="const">
+ <return type="Vector3i" />
+ <param index="0" name="min" type="int" />
+ <param index="1" name="max" type="int" />
+ <description>
+ Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
+ </description>
+ </method>
<method name="distance_squared_to" qualifiers="const">
<return type="int" />
<param index="0" name="to" type="Vector3i" />
@@ -87,18 +95,46 @@
This method runs faster than [method length], so prefer it if you need to compare vectors or need the squared distance for some formula.
</description>
</method>
+ <method name="max" qualifiers="const">
+ <return type="Vector3i" />
+ <param index="0" name="with" type="Vector3i" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector3i(maxi(x, with.x), maxi(y, with.y), maxi(z, with.z))[/code].
+ </description>
+ </method>
<method name="max_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's highest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_X].
</description>
</method>
+ <method name="maxi" qualifiers="const">
+ <return type="Vector3i" />
+ <param index="0" name="with" type="int" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector3i(maxi(x, with), maxi(y, with), maxi(z, with))[/code].
+ </description>
+ </method>
+ <method name="min" qualifiers="const">
+ <return type="Vector3i" />
+ <param index="0" name="with" type="Vector3i" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector3i(mini(x, with.x), mini(y, with.y), mini(z, with.z))[/code].
+ </description>
+ </method>
<method name="min_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's lowest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_Z].
</description>
</method>
+ <method name="mini" qualifiers="const">
+ <return type="Vector3i" />
+ <param index="0" name="with" type="int" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector3i(mini(x, with), mini(y, with), mini(z, with))[/code].
+ </description>
+ </method>
<method name="sign" qualifiers="const">
<return type="Vector3i" />
<description>
@@ -112,6 +148,13 @@
Returns a new vector with each component snapped to the closest multiple of the corresponding component in [param step].
</description>
</method>
+ <method name="snappedi" qualifiers="const">
+ <return type="Vector3i" />
+ <param index="0" name="step" type="int" />
+ <description>
+ Returns a new vector with each component snapped to the closest multiple of [param step].
+ </description>
+ </method>
</methods>
<members>
<member name="x" type="int" setter="" getter="" default="0">
diff --git a/doc/classes/Vector4.xml b/doc/classes/Vector4.xml
index b31cdb01c9..87af370462 100644
--- a/doc/classes/Vector4.xml
+++ b/doc/classes/Vector4.xml
@@ -64,6 +64,14 @@
Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
</description>
</method>
+ <method name="clampf" qualifiers="const">
+ <return type="Vector4" />
+ <param index="0" name="min" type="float" />
+ <param index="1" name="max" type="float" />
+ <description>
+ Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
+ </description>
+ </method>
<method name="cubic_interpolate" qualifiers="const">
<return type="Vector4" />
<param index="0" name="b" type="Vector4" />
@@ -176,18 +184,46 @@
Returns the result of the linear interpolation between this vector and [param to] by amount [param weight]. [param weight] is on the range of [code]0.0[/code] to [code]1.0[/code], representing the amount of interpolation.
</description>
</method>
+ <method name="max" qualifiers="const">
+ <return type="Vector4" />
+ <param index="0" name="with" type="Vector4" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector4(maxf(x, with.x), maxf(y, with.y), maxf(z, with.z), maxf(w, with.w))[/code].
+ </description>
+ </method>
<method name="max_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's highest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_X].
</description>
</method>
+ <method name="maxf" qualifiers="const">
+ <return type="Vector4" />
+ <param index="0" name="with" type="float" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector4(maxf(x, with), maxf(y, with), maxf(z, with), maxf(w, with))[/code].
+ </description>
+ </method>
+ <method name="min" qualifiers="const">
+ <return type="Vector4" />
+ <param index="0" name="with" type="Vector4" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector4(minf(x, with.x), minf(y, with.y), minf(z, with.z), minf(w, with.w))[/code].
+ </description>
+ </method>
<method name="min_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's lowest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_W].
</description>
</method>
+ <method name="minf" qualifiers="const">
+ <return type="Vector4" />
+ <param index="0" name="with" type="float" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector4(minf(x, with), minf(y, with), minf(z, with), minf(w, with))[/code].
+ </description>
+ </method>
<method name="normalized" qualifiers="const">
<return type="Vector4" />
<description>
@@ -228,6 +264,13 @@
Returns a new vector with each component snapped to the nearest multiple of the corresponding component in [param step]. This can also be used to round the components to an arbitrary number of decimals.
</description>
</method>
+ <method name="snappedf" qualifiers="const">
+ <return type="Vector4" />
+ <param index="0" name="step" type="float" />
+ <description>
+ Returns a new vector with each component snapped to the nearest multiple of [param step]. This can also be used to round the components to an arbitrary number of decimals.
+ </description>
+ </method>
</methods>
<members>
<member name="w" type="float" setter="" getter="" default="0.0">
diff --git a/doc/classes/Vector4i.xml b/doc/classes/Vector4i.xml
index f8a0026066..8f54b767e0 100644
--- a/doc/classes/Vector4i.xml
+++ b/doc/classes/Vector4i.xml
@@ -57,6 +57,14 @@
Returns a new vector with all components clamped between the components of [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
</description>
</method>
+ <method name="clampi" qualifiers="const">
+ <return type="Vector4i" />
+ <param index="0" name="min" type="int" />
+ <param index="1" name="max" type="int" />
+ <description>
+ Returns a new vector with all components clamped between [param min] and [param max], by running [method @GlobalScope.clamp] on each component.
+ </description>
+ </method>
<method name="distance_squared_to" qualifiers="const">
<return type="int" />
<param index="0" name="to" type="Vector4i" />
@@ -85,18 +93,46 @@
This method runs faster than [method length], so prefer it if you need to compare vectors or need the squared distance for some formula.
</description>
</method>
+ <method name="max" qualifiers="const">
+ <return type="Vector4i" />
+ <param index="0" name="with" type="Vector4i" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector4i(maxi(x, with.x), maxi(y, with.y), maxi(z, with.z), maxi(w, with.w))[/code].
+ </description>
+ </method>
<method name="max_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's highest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_X].
</description>
</method>
+ <method name="maxi" qualifiers="const">
+ <return type="Vector4i" />
+ <param index="0" name="with" type="int" />
+ <description>
+ Returns the component-wise maximum of this and [param with], equivalent to [code]Vector4i(maxi(x, with), maxi(y, with), maxi(z, with), maxi(w, with))[/code].
+ </description>
+ </method>
+ <method name="min" qualifiers="const">
+ <return type="Vector4i" />
+ <param index="0" name="with" type="Vector4i" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector4i(mini(x, with.x), mini(y, with.y), mini(z, with.z), mini(w, with.w))[/code].
+ </description>
+ </method>
<method name="min_axis_index" qualifiers="const">
<return type="int" />
<description>
Returns the axis of the vector's lowest value. See [code]AXIS_*[/code] constants. If all components are equal, this method returns [constant AXIS_W].
</description>
</method>
+ <method name="mini" qualifiers="const">
+ <return type="Vector4i" />
+ <param index="0" name="with" type="int" />
+ <description>
+ Returns the component-wise minimum of this and [param with], equivalent to [code]Vector4i(mini(x, with), mini(y, with), mini(z, with), mini(w, with))[/code].
+ </description>
+ </method>
<method name="sign" qualifiers="const">
<return type="Vector4i" />
<description>
@@ -110,6 +146,13 @@
Returns a new vector with each component snapped to the closest multiple of the corresponding component in [param step].
</description>
</method>
+ <method name="snappedi" qualifiers="const">
+ <return type="Vector4i" />
+ <param index="0" name="step" type="int" />
+ <description>
+ Returns a new vector with each component snapped to the closest multiple of [param step].
+ </description>
+ </method>
</methods>
<members>
<member name="w" type="int" setter="" getter="" default="0">
diff --git a/doc/classes/XRServer.xml b/doc/classes/XRServer.xml
index d5714980c3..4179ba821c 100644
--- a/doc/classes/XRServer.xml
+++ b/doc/classes/XRServer.xml
@@ -37,8 +37,8 @@
You should call this method after a few seconds have passed. For example, when the user requests a realignment of the display holding a designated button on a controller for a short period of time, or when implementing a teleport mechanism.
</description>
</method>
- <method name="clear_reference_frame" qualifiers="const">
- <return type="Transform3D" />
+ <method name="clear_reference_frame">
+ <return type="void" />
<description>
Clears the reference frame that was set by previous calls to [method center_on_hmd].
</description>
diff --git a/drivers/gles3/effects/copy_effects.cpp b/drivers/gles3/effects/copy_effects.cpp
index 6e64652982..47ca832bd7 100644
--- a/drivers/gles3/effects/copy_effects.cpp
+++ b/drivers/gles3/effects/copy_effects.cpp
@@ -198,8 +198,7 @@ void CopyEffects::bilinear_blur(GLuint p_source_texture, int p_mipmap_count, con
for (int i = 1; i < p_mipmap_count; i++) {
dest_region.position.x >>= 1;
dest_region.position.y >>= 1;
- dest_region.size.x = MAX(1, dest_region.size.x >> 1);
- dest_region.size.y = MAX(1, dest_region.size.y >> 1);
+ dest_region.size = Size2i(dest_region.size.x >> 1, dest_region.size.y >> 1).maxi(1);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffers[i % 2]);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, p_source_texture, i);
glBlitFramebuffer(source_region.position.x, source_region.position.y, source_region.position.x + source_region.size.x, source_region.position.y + source_region.size.y,
@@ -238,8 +237,7 @@ void CopyEffects::gaussian_blur(GLuint p_source_texture, int p_mipmap_count, con
for (int i = 1; i < p_mipmap_count; i++) {
dest_region.position.x >>= 1;
dest_region.position.y >>= 1;
- dest_region.size.x = MAX(1, dest_region.size.x >> 1);
- dest_region.size.y = MAX(1, dest_region.size.y >> 1);
+ dest_region.size = Size2i(dest_region.size.x >> 1, dest_region.size.y >> 1).maxi(1);
base_size.x >>= 1;
base_size.y >>= 1;
diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp
index 606bcee775..ee770be3da 100644
--- a/drivers/gles3/rasterizer_scene_gles3.cpp
+++ b/drivers/gles3/rasterizer_scene_gles3.cpp
@@ -3041,6 +3041,11 @@ void RasterizerSceneGLES3::_render_list_template(RenderListParameters *p_params,
}
} break;
+ case GLES3::SceneShaderData::BLEND_MODE_PREMULT_ALPHA: {
+ glBlendEquation(GL_FUNC_ADD);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+
+ } break;
case GLES3::SceneShaderData::BLEND_MODE_ALPHA_TO_COVERAGE: {
// Do nothing for now.
} break;
diff --git a/drivers/gles3/shaders/scene.glsl b/drivers/gles3/shaders/scene.glsl
index 797b9066a9..2b372cb88d 100644
--- a/drivers/gles3/shaders/scene.glsl
+++ b/drivers/gles3/shaders/scene.glsl
@@ -1442,6 +1442,9 @@ void main() {
float clearcoat_roughness = 0.0;
float anisotropy = 0.0;
vec2 anisotropy_flow = vec2(1.0, 0.0);
+#ifdef PREMUL_ALPHA_USED
+ float premul_alpha = 1.0;
+#endif // PREMUL_ALPHA_USED
#ifndef FOG_DISABLED
vec4 fog = vec4(0.0);
#endif // !FOG_DISABLED
@@ -2141,9 +2144,12 @@ void main() {
frag_color.rgb += additive_light_color;
#endif // USE_ADDITIVE_LIGHTING
-
frag_color.rgb *= scene_data.luminance_multiplier;
#endif // !RENDER_MATERIAL
#endif // !MODE_RENDER_DEPTH
+
+#ifdef PREMUL_ALPHA_USED
+ frag_color.rgb *= premul_alpha;
+#endif // PREMUL_ALPHA_USED
}
diff --git a/drivers/gles3/storage/material_storage.cpp b/drivers/gles3/storage/material_storage.cpp
index 62d22dac4d..996c205042 100644
--- a/drivers/gles3/storage/material_storage.cpp
+++ b/drivers/gles3/storage/material_storage.cpp
@@ -1243,6 +1243,7 @@ MaterialStorage::MaterialStorage() {
actions.renames["NORMAL_MAP_DEPTH"] = "normal_map_depth";
actions.renames["ALBEDO"] = "albedo";
actions.renames["ALPHA"] = "alpha";
+ actions.renames["PREMUL_ALPHA_FACTOR"] = "premul_alpha";
actions.renames["METALLIC"] = "metallic";
actions.renames["SPECULAR"] = "specular";
actions.renames["ROUGHNESS"] = "roughness";
@@ -1327,6 +1328,7 @@ MaterialStorage::MaterialStorage() {
actions.usage_defines["ALPHA_HASH_SCALE"] = "#define ALPHA_HASH_USED\n";
actions.usage_defines["ALPHA_ANTIALIASING_EDGE"] = "#define ALPHA_ANTIALIASING_EDGE_USED\n";
actions.usage_defines["ALPHA_TEXTURE_COORDINATE"] = "@ALPHA_ANTIALIASING_EDGE";
+ actions.usage_defines["PREMULT_ALPHA_FACTOR"] = "#define PREMULT_ALPHA_USED";
actions.usage_defines["SSS_STRENGTH"] = "#define ENABLE_SSS\n";
actions.usage_defines["SSS_TRANSMITTANCE_DEPTH"] = "#define ENABLE_TRANSMITTANCE\n";
@@ -1964,13 +1966,9 @@ void MaterialStorage::global_shader_parameters_load_settings(bool p_load_texture
Variant value = d["value"];
if (gvtype >= RS::GLOBAL_VAR_TYPE_SAMPLER2D) {
- //textire
- if (!p_load_textures) {
- continue;
- }
-
String path = value;
- if (path.is_empty()) {
+ // Don't load the textures, but still add the parameter so shaders compile correctly while loading.
+ if (!p_load_textures || path.is_empty()) {
value = RID();
} else {
Ref<Resource> resource = ResourceLoader::load(path);
@@ -2908,6 +2906,7 @@ void SceneShaderData::set_code(const String &p_code) {
actions.render_mode_values["blend_mix"] = Pair<int *, int>(&blend_modei, BLEND_MODE_MIX);
actions.render_mode_values["blend_sub"] = Pair<int *, int>(&blend_modei, BLEND_MODE_SUB);
actions.render_mode_values["blend_mul"] = Pair<int *, int>(&blend_modei, BLEND_MODE_MUL);
+ actions.render_mode_values["blend_premul_alpha"] = Pair<int *, int>(&blend_modei, BLEND_MODE_PREMULT_ALPHA);
actions.render_mode_values["alpha_to_coverage"] = Pair<int *, int>(&alpha_antialiasing_modei, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE);
actions.render_mode_values["alpha_to_coverage_and_one"] = Pair<int *, int>(&alpha_antialiasing_modei, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE_AND_TO_ONE);
diff --git a/drivers/gles3/storage/material_storage.h b/drivers/gles3/storage/material_storage.h
index 02aecf33d6..392ebcc570 100644
--- a/drivers/gles3/storage/material_storage.h
+++ b/drivers/gles3/storage/material_storage.h
@@ -248,6 +248,7 @@ struct SceneShaderData : public ShaderData {
BLEND_MODE_ADD,
BLEND_MODE_SUB,
BLEND_MODE_MUL,
+ BLEND_MODE_PREMULT_ALPHA,
BLEND_MODE_ALPHA_TO_COVERAGE
};
diff --git a/drivers/gles3/storage/render_scene_buffers_gles3.cpp b/drivers/gles3/storage/render_scene_buffers_gles3.cpp
index 6803c92dc9..cb194933ed 100644
--- a/drivers/gles3/storage/render_scene_buffers_gles3.cpp
+++ b/drivers/gles3/storage/render_scene_buffers_gles3.cpp
@@ -577,8 +577,7 @@ void RenderSceneBuffersGLES3::check_glow_buffers() {
GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton();
Size2i level_size = internal_size;
for (int i = 0; i < 4; i++) {
- level_size.x = MAX(level_size.x >> 1, 4);
- level_size.y = MAX(level_size.y >> 1, 4);
+ level_size = Size2i(level_size.x >> 1, level_size.y >> 1).maxi(4);
glow.levels[i].size = level_size;
diff --git a/drivers/gles3/storage/texture_storage.cpp b/drivers/gles3/storage/texture_storage.cpp
index 6f32e4d49d..373df8d8de 100644
--- a/drivers/gles3/storage/texture_storage.cpp
+++ b/drivers/gles3/storage/texture_storage.cpp
@@ -2812,7 +2812,7 @@ void TextureStorage::_render_target_allocate_sdf(RenderTarget *rt) {
}
rt->process_size = size * scale / 100;
- rt->process_size = rt->process_size.max(Size2i(1, 1));
+ rt->process_size = rt->process_size.maxi(1);
glGenTextures(2, rt->sdf_texture_process);
glBindTexture(GL_TEXTURE_2D, rt->sdf_texture_process[0]);
diff --git a/drivers/png/SCsub b/drivers/png/SCsub
index dd4777a19b..e38f3c4760 100644
--- a/drivers/png/SCsub
+++ b/drivers/png/SCsub
@@ -39,7 +39,7 @@ if env["builtin_libpng"]:
if env["arch"].startswith("arm"):
if env.msvc: # Can't compile assembly files with MSVC.
- env_thirdparty.Append(CPPDEFINES=[("PNG_ARM_NEON_OPT"), 0])
+ env_thirdparty.Append(CPPDEFINES=[("PNG_ARM_NEON_OPT", 0)])
else:
env_neon = env_thirdparty.Clone()
if "S_compiler" in env:
diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp
index 49896d66d8..cfeb495690 100644
--- a/editor/code_editor.cpp
+++ b/editor/code_editor.cpp
@@ -33,7 +33,6 @@
#include "core/input/input.h"
#include "core/os/keyboard.h"
#include "core/string/string_builder.h"
-#include "core/templates/pair.h"
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/plugins/script_editor_plugin.h"
@@ -173,10 +172,8 @@ bool FindReplaceBar::_search(uint32_t p_flags, int p_from_line, int p_from_col)
if (pos.x != -1) {
if (!preserve_cursor && !is_selection_only()) {
text_editor->unfold_line(pos.y);
- text_editor->set_caret_line(pos.y, false);
- text_editor->set_caret_column(pos.x + text.length(), false);
- text_editor->center_viewport_to_caret(0);
text_editor->select(pos.y, pos.x, pos.y, pos.x + text.length());
+ text_editor->center_viewport_to_caret(0);
line_col_changed_for_result = true;
}
@@ -216,7 +213,7 @@ void FindReplaceBar::_replace() {
text_editor->begin_complex_operation();
if (selection_enabled && is_selection_only()) {
// Restrict search_current() to selected region.
- text_editor->set_caret_line(selection_begin.width, false, true, 0, 0);
+ text_editor->set_caret_line(selection_begin.width, false, true, -1, 0);
text_editor->set_caret_column(selection_begin.height, true, 0);
}
@@ -285,10 +282,10 @@ void FindReplaceBar::_replace_all() {
text_editor->begin_complex_operation();
if (selection_enabled && is_selection_only()) {
- text_editor->set_caret_line(selection_begin.width, false, true, 0, 0);
+ text_editor->set_caret_line(selection_begin.width, false, true, -1, 0);
text_editor->set_caret_column(selection_begin.height, true, 0);
} else {
- text_editor->set_caret_line(0, false, true, 0, 0);
+ text_editor->set_caret_line(0, false, true, -1, 0);
text_editor->set_caret_column(0, true, 0);
}
@@ -812,22 +809,22 @@ void CodeTextEditor::input(const Ref<InputEvent> &event) {
}
if (ED_IS_SHORTCUT("script_text_editor/move_up", key_event)) {
- move_lines_up();
+ text_editor->move_lines_up();
accept_event();
return;
}
if (ED_IS_SHORTCUT("script_text_editor/move_down", key_event)) {
- move_lines_down();
+ text_editor->move_lines_down();
accept_event();
return;
}
if (ED_IS_SHORTCUT("script_text_editor/delete_line", key_event)) {
- delete_lines();
+ text_editor->delete_lines();
accept_event();
return;
}
if (ED_IS_SHORTCUT("script_text_editor/duplicate_selection", key_event)) {
- duplicate_selection();
+ text_editor->duplicate_selection();
accept_event();
return;
}
@@ -1116,31 +1113,23 @@ void CodeTextEditor::trim_trailing_whitespace() {
break;
}
}
- text_editor->set_line(i, line.substr(0, end));
+ text_editor->remove_text(i, end, i, line.length());
}
}
if (trimmed_whitespace) {
text_editor->merge_overlapping_carets();
text_editor->end_complex_operation();
- text_editor->queue_redraw();
}
}
void CodeTextEditor::insert_final_newline() {
int final_line = text_editor->get_line_count() - 1;
-
String line = text_editor->get_line(final_line);
// Length 0 means it's already an empty line, no need to add a newline.
if (line.length() > 0 && !line.ends_with("\n")) {
- text_editor->begin_complex_operation();
-
- line += "\n";
- text_editor->set_line(final_line, line);
-
- text_editor->end_complex_operation();
- text_editor->queue_redraw();
+ text_editor->insert_text("\n", final_line, line.length(), false);
}
}
@@ -1149,9 +1138,12 @@ void CodeTextEditor::convert_case(CaseStyle p_case) {
return;
}
text_editor->begin_complex_operation();
+ text_editor->begin_multicaret_edit();
- Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order();
- for (const int &c : caret_edit_order) {
+ for (int c = 0; c < text_editor->get_caret_count(); c++) {
+ if (text_editor->multicaret_edit_ignore_caret(c)) {
+ continue;
+ }
if (!text_editor->has_selection(c)) {
continue;
}
@@ -1192,6 +1184,7 @@ void CodeTextEditor::convert_case(CaseStyle p_case) {
text_editor->set_line(i, new_line);
}
}
+ text_editor->end_multicaret_edit();
text_editor->end_complex_operation();
}
@@ -1200,308 +1193,24 @@ void CodeTextEditor::set_indent_using_spaces(bool p_use_spaces) {
indentation_txt->set_text(p_use_spaces ? TTR("Spaces", "Indentation") : TTR("Tabs", "Indentation"));
}
-void CodeTextEditor::move_lines_up() {
- text_editor->begin_complex_operation();
-
- Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order();
-
- // Lists of carets representing each group.
- Vector<Vector<int>> caret_groups;
- Vector<Pair<int, int>> group_borders;
-
- // Search for groups of carets and their selections residing on the same lines.
- for (int i = 0; i < caret_edit_order.size(); i++) {
- int c = caret_edit_order[i];
-
- Vector<int> new_group{ c };
- Pair<int, int> group_border;
- group_border.first = _get_affected_lines_from(c);
- group_border.second = _get_affected_lines_to(c);
-
- for (int j = i; j < caret_edit_order.size() - 1; j++) {
- int c_current = caret_edit_order[j];
- int c_next = caret_edit_order[j + 1];
-
- int next_start_pos = _get_affected_lines_from(c_next);
- int next_end_pos = _get_affected_lines_to(c_next);
-
- int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current);
-
- i = j;
- if (next_end_pos != current_start_pos && next_end_pos + 1 != current_start_pos) {
- break;
- }
- group_border.first = next_start_pos;
- new_group.push_back(c_next);
- // If the last caret is added to the current group there is no need to process it again.
- if (j + 1 == caret_edit_order.size() - 1) {
- i++;
- }
- }
- group_borders.push_back(group_border);
- caret_groups.push_back(new_group);
- }
-
- for (int i = group_borders.size() - 1; i >= 0; i--) {
- if (group_borders[i].first - 1 < 0) {
- continue;
- }
-
- // If the group starts overlapping with the upper group don't move it.
- if (i < group_borders.size() - 1 && group_borders[i].first - 1 <= group_borders[i + 1].second) {
- continue;
- }
-
- // We have to remember caret positions and selections prior to line swapping.
- Vector<Vector<int>> caret_group_parameters;
-
- for (int j = 0; j < caret_groups[i].size(); j++) {
- int c = caret_groups[i][j];
- int cursor_line = text_editor->get_caret_line(c);
- int cursor_column = text_editor->get_caret_column(c);
-
- if (!text_editor->has_selection(c)) {
- caret_group_parameters.push_back(Vector<int>{ -1, -1, -1, -1, cursor_line, cursor_column });
- continue;
- }
- int from_line = text_editor->get_selection_from_line(c);
- int from_col = text_editor->get_selection_from_column(c);
- int to_line = text_editor->get_selection_to_line(c);
- int to_column = text_editor->get_selection_to_column(c);
- caret_group_parameters.push_back(Vector<int>{ from_line, from_col, to_line, to_column, cursor_line, cursor_column });
- }
-
- for (int line_id = group_borders[i].first; line_id <= group_borders[i].second; line_id++) {
- text_editor->unfold_line(line_id);
- text_editor->unfold_line(line_id - 1);
-
- text_editor->swap_lines(line_id - 1, line_id);
- }
-
- for (int j = 0; j < caret_groups[i].size(); j++) {
- int c = caret_groups[i][j];
- const Vector<int> &caret_parameters = caret_group_parameters[j];
- text_editor->set_caret_line(caret_parameters[4] - 1, c == 0, true, 0, c);
- text_editor->set_caret_column(caret_parameters[5], c == 0, c);
-
- if (caret_parameters[0] >= 0) {
- text_editor->select(caret_parameters[0] - 1, caret_parameters[1], caret_parameters[2] - 1, caret_parameters[3], c);
- }
- }
- }
-
- text_editor->end_complex_operation();
- text_editor->merge_overlapping_carets();
- text_editor->queue_redraw();
-}
-
-void CodeTextEditor::move_lines_down() {
- text_editor->begin_complex_operation();
-
- Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order();
-
- // Lists of carets representing each group.
- Vector<Vector<int>> caret_groups;
- Vector<Pair<int, int>> group_borders;
- Vector<int> group_border_ends;
- // Search for groups of carets and their selections residing on the same lines.
- for (int i = 0; i < caret_edit_order.size(); i++) {
- int c = caret_edit_order[i];
-
- Vector<int> new_group{ c };
- Pair<int, int> group_border;
- group_border.first = _get_affected_lines_from(c);
- group_border.second = _get_affected_lines_to(c);
-
- for (int j = i; j < caret_edit_order.size() - 1; j++) {
- int c_current = caret_edit_order[j];
- int c_next = caret_edit_order[j + 1];
-
- int next_start_pos = _get_affected_lines_from(c_next);
- int next_end_pos = _get_affected_lines_to(c_next);
-
- int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current);
-
- i = j;
- if (next_end_pos == current_start_pos || next_end_pos + 1 == current_start_pos) {
- group_border.first = next_start_pos;
- new_group.push_back(c_next);
- // If the last caret is added to the current group there is no need to process it again.
- if (j + 1 == caret_edit_order.size() - 1) {
- i++;
- }
- } else {
- break;
- }
- }
- group_borders.push_back(group_border);
- group_border_ends.push_back(text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c));
- caret_groups.push_back(new_group);
- }
-
- for (int i = 0; i < group_borders.size(); i++) {
- if (group_border_ends[i] + 1 > text_editor->get_line_count() - 1) {
- continue;
- }
-
- // If the group starts overlapping with the upper group don't move it.
- if (i > 0 && group_border_ends[i] + 1 >= group_borders[i - 1].first) {
- continue;
- }
-
- // We have to remember caret positions and selections prior to line swapping.
- Vector<Vector<int>> caret_group_parameters;
-
- for (int j = 0; j < caret_groups[i].size(); j++) {
- int c = caret_groups[i][j];
- int cursor_line = text_editor->get_caret_line(c);
- int cursor_column = text_editor->get_caret_column(c);
-
- if (!text_editor->has_selection(c)) {
- caret_group_parameters.push_back(Vector<int>{ -1, -1, -1, -1, cursor_line, cursor_column });
- continue;
- }
- int from_line = text_editor->get_selection_from_line(c);
- int from_col = text_editor->get_selection_from_column(c);
- int to_line = text_editor->get_selection_to_line(c);
- int to_column = text_editor->get_selection_to_column(c);
- caret_group_parameters.push_back(Vector<int>{ from_line, from_col, to_line, to_column, cursor_line, cursor_column });
- }
-
- for (int line_id = group_borders[i].second; line_id >= group_borders[i].first; line_id--) {
- text_editor->unfold_line(line_id);
- text_editor->unfold_line(line_id + 1);
-
- text_editor->swap_lines(line_id + 1, line_id);
- }
-
- for (int j = 0; j < caret_groups[i].size(); j++) {
- int c = caret_groups[i][j];
- const Vector<int> &caret_parameters = caret_group_parameters[j];
- text_editor->set_caret_line(caret_parameters[4] + 1, c == 0, true, 0, c);
- text_editor->set_caret_column(caret_parameters[5], c == 0, c);
-
- if (caret_parameters[0] >= 0) {
- text_editor->select(caret_parameters[0] + 1, caret_parameters[1], caret_parameters[2] + 1, caret_parameters[3], c);
- }
- }
- }
-
- text_editor->merge_overlapping_carets();
- text_editor->end_complex_operation();
- text_editor->queue_redraw();
-}
-
-void CodeTextEditor::delete_lines() {
- text_editor->begin_complex_operation();
-
- Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order();
- Vector<int> lines;
- int last_line = INT_MAX;
- for (const int &c : caret_edit_order) {
- for (int line = _get_affected_lines_to(c); line >= _get_affected_lines_from(c); line--) {
- if (line >= last_line) {
- continue;
- }
- last_line = line;
- lines.append(line);
- }
- }
-
- for (const int &line : lines) {
- if (line != text_editor->get_line_count() - 1) {
- text_editor->remove_text(line, 0, line + 1, 0);
- } else {
- text_editor->remove_text(line - 1, text_editor->get_line(line - 1).length(), line, text_editor->get_line(line).length());
- }
- // Readjust carets.
- int new_line = MIN(line, text_editor->get_line_count() - 1);
- text_editor->unfold_line(new_line);
- for (const int &c : caret_edit_order) {
- if (text_editor->get_caret_line(c) == line || (text_editor->get_caret_line(c) == line + 1 && text_editor->get_caret_column(c) == 0)) {
- text_editor->deselect(c);
- text_editor->set_caret_line(new_line, c == 0, true, 0, c);
- continue;
- }
- if (text_editor->get_caret_line(c) > line) {
- text_editor->set_caret_line(text_editor->get_caret_line(c) - 1, c == 0, true, 0, c);
- continue;
- }
- break;
- }
- }
- text_editor->merge_overlapping_carets();
- text_editor->end_complex_operation();
-}
-
-void CodeTextEditor::duplicate_selection() {
- text_editor->begin_complex_operation();
-
- Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order();
- for (const int &c : caret_edit_order) {
- const int cursor_column = text_editor->get_caret_column(c);
- int from_line = text_editor->get_caret_line(c);
- int to_line = text_editor->get_caret_line(c);
- int from_column = 0;
- int to_column = 0;
- int cursor_new_line = to_line + 1;
- int cursor_new_column = text_editor->get_caret_column(c);
- String new_text = "\n" + text_editor->get_line(from_line);
- bool selection_active = false;
-
- text_editor->set_caret_column(text_editor->get_line(from_line).length(), c == 0, c);
- if (text_editor->has_selection(c)) {
- from_column = text_editor->get_selection_from_column(c);
- to_column = text_editor->get_selection_to_column(c);
-
- from_line = text_editor->get_selection_from_line(c);
- to_line = text_editor->get_selection_to_line(c);
- cursor_new_line = to_line + text_editor->get_caret_line(c) - from_line;
- cursor_new_column = to_column == cursor_column ? 2 * to_column - from_column : to_column;
- new_text = text_editor->get_selected_text(c);
- selection_active = true;
-
- text_editor->set_caret_line(to_line, c == 0, true, 0, c);
- text_editor->set_caret_column(to_column, c == 0, c);
- }
-
- for (int i = from_line; i <= to_line; i++) {
- text_editor->unfold_line(i);
- }
- text_editor->deselect(c);
- text_editor->insert_text_at_caret(new_text, c);
- text_editor->set_caret_line(cursor_new_line, c == 0, true, 0, c);
- text_editor->set_caret_column(cursor_new_column, c == 0, c);
- if (selection_active) {
- text_editor->select(to_line, to_column, 2 * to_line - from_line, to_line == from_line ? 2 * to_column - from_column : to_column, c);
- }
- }
- text_editor->merge_overlapping_carets();
- text_editor->end_complex_operation();
- text_editor->queue_redraw();
-}
-
void CodeTextEditor::toggle_inline_comment(const String &delimiter) {
text_editor->begin_complex_operation();
+ text_editor->begin_multicaret_edit();
- Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order();
- caret_edit_order.reverse();
- int last_line = -1;
+ Vector<Point2i> line_ranges = text_editor->get_line_ranges_from_carets();
int folded_to = 0;
- for (const int &c1 : caret_edit_order) {
- int from = _get_affected_lines_from(c1);
- from += from == last_line ? 1 + folded_to : 0;
- int to = _get_affected_lines_to(c1);
- last_line = to;
+ for (Point2i line_range : line_ranges) {
+ int from_line = line_range.x;
+ int to_line = line_range.y;
// If last line is folded, extends to the end of the folded section
- if (text_editor->is_line_folded(to)) {
- folded_to = text_editor->get_next_visible_line_offset_from(to + 1, 1) - 1;
- to += folded_to;
+ if (text_editor->is_line_folded(to_line)) {
+ folded_to = text_editor->get_next_visible_line_offset_from(to_line + 1, 1) - 1;
+ to_line += folded_to;
}
// Check first if there's any uncommented lines in selection.
bool is_commented = true;
bool is_all_empty = true;
- for (int line = from; line <= to; line++) {
+ for (int line = from_line; line <= to_line; line++) {
// `+ delimiter.length()` here because comment delimiter is not actually `in comment` so we check first character after it
int delimiter_idx = text_editor->is_in_comment(line, text_editor->get_first_non_whitespace_column(line) + delimiter.length());
// Empty lines should not be counted.
@@ -1517,58 +1226,24 @@ void CodeTextEditor::toggle_inline_comment(const String &delimiter) {
// Special case for commenting empty lines, treat it/them as uncommented lines.
is_commented = is_commented && !is_all_empty;
- // Caret positions need to be saved since they could be moved at the eol.
- Vector<int> caret_cols;
- Vector<int> selection_to_cols;
- for (const int &c2 : caret_edit_order) {
- if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) {
- caret_cols.append(text_editor->get_caret_column(c2));
- }
- if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) {
- selection_to_cols.append(text_editor->get_selection_to_column(c2));
- }
- }
-
// Comment/uncomment.
- for (int line = from; line <= to; line++) {
- String line_text = text_editor->get_line(line);
+ for (int line = from_line; line <= to_line; line++) {
if (is_all_empty) {
- text_editor->set_line(line, delimiter);
+ text_editor->insert_text(delimiter, line, 0);
continue;
}
if (is_commented) {
- text_editor->set_line(line, line_text.replace_first(delimiter, ""));
+ int delimiter_column = text_editor->get_line(line).find(delimiter);
+ text_editor->remove_text(line, delimiter_column, line, delimiter_column + delimiter.length());
} else {
- text_editor->set_line(line, line_text.insert(text_editor->get_first_non_whitespace_column(line), delimiter));
- }
- }
-
- // Readjust carets and selections.
- int caret_i = 0;
- int selection_i = 0;
- int offset = (is_commented ? -1 : 1) * delimiter.length();
- for (const int &c2 : caret_edit_order) {
- bool is_line_selection = text_editor->has_selection(c2) && text_editor->get_selection_from_line(c2) < text_editor->get_selection_to_line(c2);
- if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) {
- int caret_col = caret_cols[caret_i++];
- caret_col += (is_line_selection && caret_col == 0) ? 0 : offset;
- text_editor->set_caret_column(caret_col, c2 == 0, c2);
- }
- if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) {
- int from_col = text_editor->get_selection_from_column(c2);
- from_col += (is_line_selection && from_col == 0) ? 0 : offset;
- int to_col = selection_to_cols[selection_i++];
- to_col += (to_col == 0) ? 0 : offset;
- text_editor->select(
- text_editor->get_selection_from_line(c2), from_col,
- text_editor->get_selection_to_line(c2), to_col, c2);
+ text_editor->insert_text(delimiter, line, text_editor->get_first_non_whitespace_column(line));
}
}
}
- text_editor->merge_overlapping_carets();
+
+ text_editor->end_multicaret_edit();
text_editor->end_complex_operation();
- text_editor->queue_redraw();
}
void CodeTextEditor::goto_line(int p_line) {
@@ -1815,22 +1490,6 @@ void CodeTextEditor::_toggle_scripts_pressed() {
update_toggle_scripts_button();
}
-int CodeTextEditor::_get_affected_lines_from(int p_caret) {
- return text_editor->has_selection(p_caret) ? text_editor->get_selection_from_line(p_caret) : text_editor->get_caret_line(p_caret);
-}
-
-int CodeTextEditor::_get_affected_lines_to(int p_caret) {
- if (!text_editor->has_selection(p_caret)) {
- return text_editor->get_caret_line(p_caret);
- }
- int line = text_editor->get_selection_to_line(p_caret);
- // Don't affect a line with no selected characters.
- if (text_editor->get_selection_to_column(p_caret) == 0) {
- line--;
- }
- return line;
-}
-
void CodeTextEditor::_error_pressed(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
@@ -1879,13 +1538,12 @@ void CodeTextEditor::set_warning_count(int p_warning_count) {
}
void CodeTextEditor::toggle_bookmark() {
- Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order();
- caret_edit_order.reverse();
+ Vector<int> sorted_carets = text_editor->get_sorted_carets();
int last_line = -1;
- for (const int &c : caret_edit_order) {
- int from = text_editor->has_selection(c) ? text_editor->get_selection_from_line(c) : text_editor->get_caret_line(c);
+ for (const int &c : sorted_carets) {
+ int from = text_editor->get_selection_from_line(c);
from += from == last_line ? 1 : 0;
- int to = text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c);
+ int to = text_editor->get_selection_to_line(c);
if (to < from) {
continue;
}
diff --git a/editor/code_editor.h b/editor/code_editor.h
index c36eedb580..75a2a68d58 100644
--- a/editor/code_editor.h
+++ b/editor/code_editor.h
@@ -207,9 +207,6 @@ class CodeTextEditor : public VBoxContainer {
void _toggle_scripts_pressed();
- int _get_affected_lines_from(int p_caret);
- int _get_affected_lines_to(int p_caret);
-
protected:
virtual void _load_theme_settings() {}
virtual void _validate_script() {}
@@ -238,11 +235,6 @@ public:
void set_indent_using_spaces(bool p_use_spaces);
- void move_lines_up();
- void move_lines_down();
- void delete_lines();
- void duplicate_selection();
-
/// Toggle inline comment on currently selected lines, or on current line if nothing is selected,
/// by adding or removing comment delimiter
void toggle_inline_comment(const String &delimiter);
diff --git a/editor/editor_audio_buses.cpp b/editor/editor_audio_buses.cpp
index b4e9faa4fd..658bc33ddc 100644
--- a/editor/editor_audio_buses.cpp
+++ b/editor/editor_audio_buses.cpp
@@ -87,9 +87,14 @@ void EditorAudioBus::_notification(int p_what) {
disabled_vu = get_editor_theme_icon(SNAME("BusVuFrozen"));
- Color solo_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.0, 0.92, 0.44);
- Color mute_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(1.0, 0.44, 0.44);
- Color bypass_color = EditorThemeManager::is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(0.44, 0.87, 1.0);
+ Color solo_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.9, 1.74, 0.83);
+ Color mute_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(2.35, 1.03, 1.03);
+ Color bypass_color = EditorThemeManager::is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(1.03, 2.04, 2.35);
+ float darkening_factor = EditorThemeManager::is_dark_theme() ? 0.15 : 0.65;
+
+ Ref<StyleBoxFlat>(solo->get_theme_stylebox("pressed"))->set_border_color(solo_color.darkened(darkening_factor));
+ Ref<StyleBoxFlat>(mute->get_theme_stylebox("pressed"))->set_border_color(mute_color.darkened(darkening_factor));
+ Ref<StyleBoxFlat>(bypass->get_theme_stylebox("pressed"))->set_border_color(bypass_color.darkened(darkening_factor));
solo->set_icon(get_editor_theme_icon(SNAME("AudioBusSolo")));
solo->add_theme_color_override("icon_pressed_color", solo_color);
@@ -835,7 +840,13 @@ EditorAudioBus::EditorAudioBus(EditorAudioBuses *p_buses, bool p_is_master) {
child->add_theme_style_override("normal", sbempty);
child->add_theme_style_override("hover", sbempty);
child->add_theme_style_override("focus", sbempty);
- child->add_theme_style_override("pressed", sbempty);
+
+ Ref<StyleBoxFlat> sbflat = memnew(StyleBoxFlat);
+ sbflat->set_content_margin_all(0);
+ sbflat->set_bg_color(Color(1, 1, 1, 0));
+ sbflat->set_border_width(Side::SIDE_BOTTOM, Math::round(3 * EDSCALE));
+ child->add_theme_style_override("pressed", sbflat);
+
child->end_bulk_theme_override();
}
diff --git a/editor/editor_dock_manager.cpp b/editor/editor_dock_manager.cpp
index b6250671ee..06dd33d8ab 100644
--- a/editor/editor_dock_manager.cpp
+++ b/editor/editor_dock_manager.cpp
@@ -147,7 +147,6 @@ void EditorDockManager::_update_layout() {
if (!dock_context_popup->is_inside_tree() || EditorNode::get_singleton()->is_exiting()) {
return;
}
- EditorNode::get_singleton()->edit_current();
dock_context_popup->docks_updated();
_update_docks_menu();
EditorNode::get_singleton()->save_editor_layout_delayed();
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index 14190a43a5..1e91a326b3 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -1578,31 +1578,7 @@ void EditorFileSystem::_update_script_classes() {
update_script_mutex.lock();
for (const String &path : update_script_paths) {
- ScriptServer::remove_global_class_by_path(path); // First remove, just in case it changed
-
- int index = -1;
- EditorFileSystemDirectory *efd = find_file(path, &index);
-
- if (!efd || index < 0) {
- // The file was removed
- continue;
- }
-
- if (!efd->files[index]->script_class_name.is_empty()) {
- String lang;
- for (int j = 0; j < ScriptServer::get_language_count(); j++) {
- if (ScriptServer::get_language(j)->handles_global_class_type(efd->files[index]->type)) {
- lang = ScriptServer::get_language(j)->get_name();
- }
- }
- if (lang.is_empty()) {
- continue; // No lang found that can handle this global class
- }
-
- ScriptServer::add_global_class(efd->files[index]->script_class_name, efd->files[index]->script_class_extends, lang, path);
- EditorNode::get_editor_data().script_class_set_icon_path(efd->files[index]->script_class_name, efd->files[index]->script_class_icon_path);
- EditorNode::get_editor_data().script_class_set_name(path, efd->files[index]->script_class_name);
- }
+ EditorFileSystem::get_singleton()->register_global_class_script(path, path);
}
// Parse documentation second, as it requires the class names to be correct and registered
@@ -1844,6 +1820,34 @@ HashSet<String> EditorFileSystem::get_valid_extensions() const {
return valid_extensions;
}
+void EditorFileSystem::register_global_class_script(const String &p_search_path, const String &p_target_path) {
+ ScriptServer::remove_global_class_by_path(p_search_path); // First remove, just in case it changed
+
+ int index = -1;
+ EditorFileSystemDirectory *efd = find_file(p_search_path, &index);
+
+ if (!efd || index < 0) {
+ // The file was removed
+ return;
+ }
+
+ if (!efd->files[index]->script_class_name.is_empty()) {
+ String lang;
+ for (int j = 0; j < ScriptServer::get_language_count(); j++) {
+ if (ScriptServer::get_language(j)->handles_global_class_type(efd->files[index]->type)) {
+ lang = ScriptServer::get_language(j)->get_name();
+ }
+ }
+ if (lang.is_empty()) {
+ return; // No lang found that can handle this global class
+ }
+
+ ScriptServer::add_global_class(efd->files[index]->script_class_name, efd->files[index]->script_class_extends, lang, p_target_path);
+ EditorNode::get_editor_data().script_class_set_icon_path(efd->files[index]->script_class_name, efd->files[index]->script_class_icon_path);
+ EditorNode::get_editor_data().script_class_set_name(p_target_path, efd->files[index]->script_class_name);
+ }
+}
+
Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector<String> &p_files) {
String importer_name;
@@ -2324,8 +2328,9 @@ void EditorFileSystem::reimport_file_with_custom_parameters(const String &p_file
}
void EditorFileSystem::_reimport_thread(uint32_t p_index, ImportThreadData *p_import_data) {
- p_import_data->max_index = MAX(p_import_data->reimport_from + int(p_index), p_import_data->max_index);
- _reimport_file(p_import_data->reimport_files[p_import_data->reimport_from + p_index].path);
+ int current_max = p_import_data->reimport_from + int(p_index);
+ p_import_data->max_index.exchange_if_greater(current_max);
+ _reimport_file(p_import_data->reimport_files[current_max].path);
}
void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
@@ -2405,15 +2410,15 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
importer->import_threaded_begin();
ImportThreadData tdata;
- tdata.max_index = from;
+ tdata.max_index.set(from);
tdata.reimport_from = from;
tdata.reimport_files = reimport_files.ptr();
WorkerThreadPool::GroupID group_task = WorkerThreadPool::get_singleton()->add_template_group_task(this, &EditorFileSystem::_reimport_thread, &tdata, i - from + 1, -1, false, vformat(TTR("Import resources of type: %s"), reimport_files[from].importer));
int current_index = from - 1;
do {
- if (current_index < tdata.max_index) {
- current_index = tdata.max_index;
+ if (current_index < tdata.max_index.get()) {
+ current_index = tdata.max_index.get();
pr.step(reimport_files[current_index].path.get_file(), current_index);
}
OS::get_singleton()->delay_usec(1);
diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h
index 782d3eee38..ad0e3f10ef 100644
--- a/editor/editor_file_system.h
+++ b/editor/editor_file_system.h
@@ -282,7 +282,7 @@ class EditorFileSystem : public Node {
struct ImportThreadData {
const ImportFile *reimport_files;
int reimport_from;
- int max_index = 0;
+ SafeNumeric<int> max_index;
};
void _reimport_thread(uint32_t p_index, ImportThreadData *p_import_data);
@@ -310,6 +310,7 @@ public:
void scan_changes();
void update_file(const String &p_file);
HashSet<String> get_valid_extensions() const;
+ void register_global_class_script(const String &p_search_path, const String &p_target_path);
EditorFileSystemDirectory *get_filesystem_path(const String &p_path);
String get_file_type(const String &p_file) const;
diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp
index 41396a2ed1..f1e487d79b 100644
--- a/editor/editor_inspector.cpp
+++ b/editor/editor_inspector.cpp
@@ -4024,14 +4024,16 @@ void EditorInspector::_notification(int p_what) {
} break;
case NOTIFICATION_PREDELETE: {
- edit(nullptr); //just in case
+ if (EditorNode::get_singleton() && !EditorNode::get_singleton()->is_exiting()) {
+ // Don't need to clean up if exiting, and object may already be freed.
+ edit(nullptr);
+ }
} break;
case NOTIFICATION_EXIT_TREE: {
if (!sub_inspector) {
get_tree()->disconnect("node_removed", callable_mp(this, &EditorInspector::_node_removed));
}
- edit(nullptr);
} break;
case NOTIFICATION_VISIBILITY_CHANGED: {
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index b9ac7b6d42..5bb9aa91d2 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -3486,6 +3486,10 @@ void EditorNode::remove_editor_plugin(EditorPlugin *p_editor, bool p_config_chan
}
}
+ if (singleton->editor_plugin_screen == p_editor) {
+ singleton->editor_plugin_screen = nullptr;
+ }
+
singleton->editor_table.erase(p_editor);
}
p_editor->make_visible(false);
@@ -3512,6 +3516,7 @@ void EditorNode::add_extension_editor_plugin(const StringName &p_class_name) {
EditorPlugin *plugin = Object::cast_to<EditorPlugin>(ClassDB::instantiate(p_class_name));
singleton->editor_data.add_extension_editor_plugin(p_class_name, plugin);
add_editor_plugin(plugin);
+ plugin->enable_plugin();
}
void EditorNode::remove_extension_editor_plugin(const StringName &p_class_name) {
@@ -7255,7 +7260,9 @@ EditorNode::EditorNode() {
add_editor_plugin(memnew(AudioBusesEditorPlugin(audio_bus_editor)));
for (int i = 0; i < EditorPlugins::get_plugin_count(); i++) {
- add_editor_plugin(EditorPlugins::create(i));
+ EditorPlugin *plugin = EditorPlugins::create(i);
+ add_editor_plugin(plugin);
+ plugin->enable_plugin();
}
for (const StringName &extension_class_name : GDExtensionEditorPlugins::get_extension_classes()) {
diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp
index 9e4bf2b7d9..e082366c44 100644
--- a/editor/editor_resource_picker.cpp
+++ b/editor/editor_resource_picker.cpp
@@ -488,8 +488,8 @@ void EditorResourcePicker::set_create_options(Object *p_menu_node) {
if (!base_type.is_empty()) {
int idx = 0;
- HashSet<StringName> allowed_types;
- _get_allowed_types(false, &allowed_types);
+ _ensure_allowed_types();
+ HashSet<StringName> allowed_types = allowed_types_without_convert;
for (const StringName &E : allowed_types) {
const String &t = E;
@@ -593,23 +593,29 @@ static void _add_allowed_type(const StringName &p_type, HashSet<StringName> *p_v
}
}
-void EditorResourcePicker::_get_allowed_types(bool p_with_convert, HashSet<StringName> *p_vector) const {
+void EditorResourcePicker::_ensure_allowed_types() const {
+ if (!allowed_types_without_convert.is_empty()) {
+ return;
+ }
+
Vector<String> allowed_types = base_type.split(",");
int size = allowed_types.size();
for (int i = 0; i < size; i++) {
- String base = allowed_types[i].strip_edges();
+ const String base = allowed_types[i].strip_edges();
+ _add_allowed_type(base, &allowed_types_without_convert);
+ }
- _add_allowed_type(base, p_vector);
+ allowed_types_with_convert = HashSet<StringName>(allowed_types_without_convert);
- if (p_with_convert) {
- if (base == "BaseMaterial3D") {
- p_vector->insert("Texture2D");
- } else if (base == "ShaderMaterial") {
- p_vector->insert("Shader");
- } else if (base == "Texture2D") {
- p_vector->insert("Image");
- }
+ for (int i = 0; i < size; i++) {
+ const String base = allowed_types[i].strip_edges();
+ if (base == "BaseMaterial3D") {
+ allowed_types_with_convert.insert("Texture2D");
+ } else if (base == "ShaderMaterial") {
+ allowed_types_with_convert.insert("Shader");
+ } else if (base == "Texture2D") {
+ allowed_types_with_convert.insert("Image");
}
}
}
@@ -645,8 +651,8 @@ bool EditorResourcePicker::_is_drop_valid(const Dictionary &p_drag_data) const {
}
}
- HashSet<StringName> allowed_types;
- _get_allowed_types(true, &allowed_types);
+ _ensure_allowed_types();
+ HashSet<StringName> allowed_types = allowed_types_with_convert;
if (res.is_valid()) {
String res_type = _get_resource_type(res);
@@ -713,8 +719,8 @@ void EditorResourcePicker::drop_data_fw(const Point2 &p_point, const Variant &p_
}
if (dropped_resource.is_valid()) {
- HashSet<StringName> allowed_types;
- _get_allowed_types(false, &allowed_types);
+ _ensure_allowed_types();
+ HashSet<StringName> allowed_types = allowed_types_without_convert;
String res_type = _get_resource_type(dropped_resource);
@@ -835,8 +841,8 @@ void EditorResourcePicker::set_base_type(const String &p_base_type) {
// There is a possibility that the new base type is conflicting with the existing value.
// Keep the value, but warn the user that there is a potential mistake.
if (!base_type.is_empty() && edited_resource.is_valid()) {
- HashSet<StringName> allowed_types;
- _get_allowed_types(true, &allowed_types);
+ _ensure_allowed_types();
+ HashSet<StringName> allowed_types = allowed_types_with_convert;
StringName custom_class;
bool is_custom = false;
@@ -857,8 +863,8 @@ String EditorResourcePicker::get_base_type() const {
}
Vector<String> EditorResourcePicker::get_allowed_types() const {
- HashSet<StringName> allowed_types;
- _get_allowed_types(false, &allowed_types);
+ _ensure_allowed_types();
+ HashSet<StringName> allowed_types = allowed_types_without_convert;
Vector<String> types;
types.resize(allowed_types.size());
@@ -881,8 +887,8 @@ void EditorResourcePicker::set_edited_resource(Ref<Resource> p_resource) {
}
if (!base_type.is_empty()) {
- HashSet<StringName> allowed_types;
- _get_allowed_types(true, &allowed_types);
+ _ensure_allowed_types();
+ HashSet<StringName> allowed_types = allowed_types_with_convert;
StringName custom_class;
bool is_custom = false;
diff --git a/editor/editor_resource_picker.h b/editor/editor_resource_picker.h
index 333d9c9915..28229e6b37 100644
--- a/editor/editor_resource_picker.h
+++ b/editor/editor_resource_picker.h
@@ -52,6 +52,8 @@ class EditorResourcePicker : public HBoxContainer {
bool dropping = false;
Vector<String> inheritors_array;
+ mutable HashSet<StringName> allowed_types_without_convert;
+ mutable HashSet<StringName> allowed_types_with_convert;
Button *assign_button = nullptr;
TextureRect *preview_rect = nullptr;
@@ -97,7 +99,7 @@ class EditorResourcePicker : public HBoxContainer {
void _button_input(const Ref<InputEvent> &p_event);
String _get_resource_type(const Ref<Resource> &p_resource) const;
- void _get_allowed_types(bool p_with_convert, HashSet<StringName> *p_vector) const;
+ void _ensure_allowed_types() const;
bool _is_drop_valid(const Dictionary &p_drag_data) const;
bool _is_type_valid(const String &p_type_name, const HashSet<StringName> &p_allowed_types) const;
diff --git a/editor/engine_update_label.cpp b/editor/engine_update_label.cpp
index 1d7df806de..9984d6f02f 100644
--- a/editor/engine_update_label.cpp
+++ b/editor/engine_update_label.cpp
@@ -30,6 +30,7 @@
#include "engine_update_label.h"
+#include "core/io/json.h"
#include "core/os/time.h"
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
@@ -46,7 +47,7 @@ bool EngineUpdateLabel::_can_check_updates() const {
void EngineUpdateLabel::_check_update() {
checked_update = true;
_set_status(UpdateStatus::BUSY);
- http->request("https://raw.githubusercontent.com/godotengine/godot-website/master/_data/versions.yml");
+ http->request("https://godotengine.org/versions.json");
}
void EngineUpdateLabel::_http_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body) {
@@ -62,12 +63,24 @@ void EngineUpdateLabel::_http_request_completed(int p_result, int p_response_cod
return;
}
- PackedStringArray lines;
+ Array version_data;
{
String s;
const uint8_t *r = p_body.ptr();
s.parse_utf8((const char *)r, p_body.size());
- lines = s.split("\n");
+
+ Variant result = JSON::parse_string(s);
+ if (result == Variant()) {
+ _set_status(UpdateStatus::ERROR);
+ _set_message(TTR("Failed to parse version JSON."), theme_cache.error_color);
+ return;
+ }
+ if (result.get_type() != Variant::ARRAY) {
+ _set_status(UpdateStatus::ERROR);
+ _set_message(TTR("Received JSON data is not a valid version array."), theme_cache.error_color);
+ return;
+ }
+ version_data = result;
}
UpdateMode update_mode = UpdateMode(int(EDITOR_GET("network/connection/engine_version_update_mode")));
@@ -78,14 +91,11 @@ void EngineUpdateLabel::_http_request_completed(int p_result, int p_response_cod
int current_minor = version_info["minor"];
int current_patch = version_info["patch"];
- int current_version_line = -1;
- for (int i = 0; i < lines.size(); i++) {
- const String &line = lines[i];
- if (!line.begins_with("- name")) {
- continue;
- }
+ Dictionary found_version_info;
+ for (const Variant &data_bit : version_data) {
+ const Dictionary info = data_bit;
- const String version_string = _extract_sub_string(line);
+ const String version_string = info["name"];
const PackedStringArray version_bits = version_string.split(".");
if (version_bits.size() < 2) {
@@ -111,7 +121,7 @@ void EngineUpdateLabel::_http_request_completed(int p_result, int p_response_cod
}
if (minor > current_minor || patch > current_patch) {
- String version_type = _extract_sub_string(lines[i + 1]);
+ String version_type = info["flavor"];
if (stable_only && _get_version_type(version_type, nullptr) != VersionType::STABLE) {
continue;
}
@@ -120,17 +130,17 @@ void EngineUpdateLabel::_http_request_completed(int p_result, int p_response_cod
found_version += "-" + version_type;
break;
} else if (minor == current_minor && patch == current_patch) {
- current_version_line = i;
+ found_version_info = info;
found_version = version_string;
break;
}
}
- if (current_version_line == -1 && !found_version.is_empty()) {
+ if (found_version_info.is_empty() && !found_version.is_empty()) {
_set_status(UpdateStatus::UPDATE_AVAILABLE);
_set_message(vformat(TTR("Update available: %s."), found_version), theme_cache.update_color);
return;
- } else if (current_version_line == -1 || stable_only) {
+ } else if (found_version_info.is_empty() || stable_only) {
_set_status(UpdateStatus::UP_TO_DATE);
return;
}
@@ -138,17 +148,11 @@ void EngineUpdateLabel::_http_request_completed(int p_result, int p_response_cod
int current_version_index;
VersionType current_version_type = _get_version_type(version_info["status"], &current_version_index);
- for (int i = current_version_line + 1; i < lines.size(); i++) {
- const String &line = lines[i];
- if (line.begins_with("- name")) {
- break;
- }
-
- if (!line.begins_with(" - name") && !line.begins_with(" flavor")) {
- continue;
- }
+ const Array releases = found_version_info["releases"];
+ for (const Variant &data_bit : version_data) {
+ const Dictionary info = data_bit;
- const String version_string = _extract_sub_string(line);
+ const String version_string = info["name"];
int version_index;
VersionType version_type = _get_version_type(version_string, &version_index);
diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp
index 3f96d934a8..ac4991755b 100644
--- a/editor/filesystem_dock.cpp
+++ b/editor/filesystem_dock.cpp
@@ -1568,34 +1568,7 @@ void FileSystemDock::_update_resource_paths_after_move(const HashMap<String, Str
if (I) {
ResourceUID::get_singleton()->set_id(I->value, new_path);
}
-
- ScriptServer::remove_global_class_by_path(old_path);
-
- int index = -1;
- EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->find_file(old_path, &index);
-
- if (!efd || index < 0) {
- // The file was removed.
- continue;
- }
-
- // Update paths for global classes.
- if (!efd->get_file_script_class_name(index).is_empty()) {
- String lang;
- for (int i = 0; i < ScriptServer::get_language_count(); i++) {
- if (ScriptServer::get_language(i)->handles_global_class_type(efd->get_file_type(index))) {
- lang = ScriptServer::get_language(i)->get_name();
- break;
- }
- }
- if (lang.is_empty()) {
- continue; // No language found that can handle this global class.
- }
-
- ScriptServer::add_global_class(efd->get_file_script_class_name(index), efd->get_file_script_class_extends(index), lang, new_path);
- EditorNode::get_editor_data().script_class_set_icon_path(efd->get_file_script_class_name(index), efd->get_file_script_class_icon_path(index));
- EditorNode::get_editor_data().script_class_set_name(new_path, efd->get_file_script_class_name(index));
- }
+ EditorFileSystem::get_singleton()->register_global_class_script(old_path, new_path);
}
// Rename all resources loaded, be it subresources or actual resources.
diff --git a/editor/import/resource_importer_wav.cpp b/editor/import/resource_importer_wav.cpp
index c8add6ab20..6d3d474cee 100644
--- a/editor/import/resource_importer_wav.cpp
+++ b/editor/import/resource_importer_wav.cpp
@@ -90,7 +90,7 @@ void ResourceImporterWAV::get_import_options(const String &p_path, List<ImportOp
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_mode", PROPERTY_HINT_ENUM, "Detect From WAV,Disabled,Forward,Ping-Pong,Backward", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_begin"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_end"), -1));
- r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Disabled,RAM (Ima-ADPCM)"), 0));
+ r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Disabled,RAM (Ima-ADPCM),QOA (Quite OK Audio)"), 0));
}
Error ResourceImporterWAV::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) {
@@ -454,13 +454,13 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s
is16 = false;
}
- Vector<uint8_t> dst_data;
+ Vector<uint8_t> pcm_data;
AudioStreamWAV::Format dst_format;
if (compression == 1) {
dst_format = AudioStreamWAV::FORMAT_IMA_ADPCM;
if (format_channels == 1) {
- _compress_ima_adpcm(data, dst_data);
+ _compress_ima_adpcm(data, pcm_data);
} else {
//byte interleave
Vector<float> left;
@@ -482,9 +482,9 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s
_compress_ima_adpcm(right, bright);
int dl = bleft.size();
- dst_data.resize(dl * 2);
+ pcm_data.resize(dl * 2);
- uint8_t *w = dst_data.ptrw();
+ uint8_t *w = pcm_data.ptrw();
const uint8_t *rl = bleft.ptr();
const uint8_t *rr = bright.ptr();
@@ -496,13 +496,14 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s
} else {
dst_format = is16 ? AudioStreamWAV::FORMAT_16_BITS : AudioStreamWAV::FORMAT_8_BITS;
- dst_data.resize(data.size() * (is16 ? 2 : 1));
+ bool enforce16 = is16 || compression == 2;
+ pcm_data.resize(data.size() * (enforce16 ? 2 : 1));
{
- uint8_t *w = dst_data.ptrw();
+ uint8_t *w = pcm_data.ptrw();
int ds = data.size();
for (int i = 0; i < ds; i++) {
- if (is16) {
+ if (enforce16) {
int16_t v = CLAMP(data[i] * 32768, -32768, 32767);
encode_uint16(v, &w[i * 2]);
} else {
@@ -513,6 +514,23 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s
}
}
+ Vector<uint8_t> dst_data;
+ if (compression == 2) {
+ dst_format = AudioStreamWAV::FORMAT_QOA;
+ qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } };
+ uint32_t qoa_len = 0;
+
+ desc.samplerate = rate;
+ desc.samples = frames;
+ desc.channels = format_channels;
+
+ void *encoded = qoa_encode((short *)pcm_data.ptrw(), &desc, &qoa_len);
+ dst_data.resize(qoa_len);
+ memcpy(dst_data.ptrw(), encoded, qoa_len);
+ } else {
+ dst_data = pcm_data;
+ }
+
Ref<AudioStreamWAV> sample;
sample.instantiate();
sample->set_data(dst_data);
diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp
index 5ac5dd6ee6..79a4269f01 100644
--- a/editor/plugins/canvas_item_editor_plugin.cpp
+++ b/editor/plugins/canvas_item_editor_plugin.cpp
@@ -467,7 +467,7 @@ Point2 CanvasItemEditor::snap_point(Point2 p_target, unsigned int p_modes, unsig
if (((snap_pixel && (p_modes & SNAP_PIXEL)) || (p_forced_modes & SNAP_PIXEL)) && rotation == 0.0) {
// Pixel
- output = output.snapped(Size2(1, 1));
+ output = output.snappedf(1);
}
snap_transform = Transform2D(rotation, output);
@@ -1625,7 +1625,7 @@ bool CanvasItemEditor::_gui_input_anchors(const Ref<InputEvent> &p_event) {
previous_anchor = xform.affine_inverse().xform(_anchor_to_position(control, previous_anchor));
Vector2 new_anchor = xform.xform(snap_point(previous_anchor + (drag_to - drag_from), SNAP_GRID | SNAP_OTHER_NODES, SNAP_NODE_PARENT | SNAP_NODE_SIDES | SNAP_NODE_CENTER, control));
- new_anchor = _position_to_anchor(control, new_anchor).snapped(Vector2(0.001, 0.001));
+ new_anchor = _position_to_anchor(control, new_anchor).snappedf(0.001);
bool use_single_axis = m->is_shift_pressed();
Vector2 drag_vector = xform.xform(drag_to) - xform.xform(drag_from);
diff --git a/editor/plugins/editor_preview_plugins.cpp b/editor/plugins/editor_preview_plugins.cpp
index 9fb4d86fa2..f2b38536b5 100644
--- a/editor/plugins/editor_preview_plugins.cpp
+++ b/editor/plugins/editor_preview_plugins.cpp
@@ -130,7 +130,7 @@ Ref<Texture2D> EditorTexturePreviewPlugin::generate(const Ref<Resource> &p_from,
if (new_size.y > p_size.y) {
new_size = Vector2(new_size.x * p_size.y / new_size.y, p_size.y);
}
- Vector2i new_size_i = Vector2i(new_size).max(Vector2i(1, 1));
+ Vector2i new_size_i = Vector2i(new_size).maxi(1);
img->resize(new_size_i.x, new_size_i.y, Image::INTERPOLATE_CUBIC);
post_process_preview(img);
diff --git a/editor/plugins/gizmos/navigation_link_3d_gizmo_plugin.cpp b/editor/plugins/gizmos/navigation_link_3d_gizmo_plugin.cpp
index 3717b4b1a3..51d15a0a70 100644
--- a/editor/plugins/gizmos/navigation_link_3d_gizmo_plugin.cpp
+++ b/editor/plugins/gizmos/navigation_link_3d_gizmo_plugin.cpp
@@ -165,7 +165,7 @@ void NavigationLink3DGizmoPlugin::set_handle(const EditorNode3DGizmo *p_gizmo, i
if (Node3DEditor::get_singleton()->is_snap_enabled()) {
double snap = Node3DEditor::get_singleton()->get_translate_snap();
- intersection.snap(Vector3(snap, snap, snap));
+ intersection.snapf(snap);
}
position = gi.xform(intersection);
diff --git a/editor/plugins/gizmos/navigation_region_3d_gizmo_plugin.cpp b/editor/plugins/gizmos/navigation_region_3d_gizmo_plugin.cpp
index 088b09ea46..14105f0b3b 100644
--- a/editor/plugins/gizmos/navigation_region_3d_gizmo_plugin.cpp
+++ b/editor/plugins/gizmos/navigation_region_3d_gizmo_plugin.cpp
@@ -108,8 +108,8 @@ void NavigationRegion3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) {
for (int j = 0; j < 3; j++) {
tw[tidx++] = f.vertex[j];
_EdgeKey ek;
- ek.from = f.vertex[j].snapped(Vector3(CMP_EPSILON, CMP_EPSILON, CMP_EPSILON));
- ek.to = f.vertex[(j + 1) % 3].snapped(Vector3(CMP_EPSILON, CMP_EPSILON, CMP_EPSILON));
+ ek.from = f.vertex[j].snappedf(CMP_EPSILON);
+ ek.to = f.vertex[(j + 1) % 3].snappedf(CMP_EPSILON);
if (ek.from < ek.to) {
SWAP(ek.from, ek.to);
}
diff --git a/editor/plugins/gizmos/occluder_instance_3d_gizmo_plugin.cpp b/editor/plugins/gizmos/occluder_instance_3d_gizmo_plugin.cpp
index 37c3a05b50..d6f649ab9c 100644
--- a/editor/plugins/gizmos/occluder_instance_3d_gizmo_plugin.cpp
+++ b/editor/plugins/gizmos/occluder_instance_3d_gizmo_plugin.cpp
@@ -163,9 +163,9 @@ void OccluderInstance3DGizmoPlugin::set_handle(const EditorNode3DGizmo *p_gizmo,
if (p_id == 2) {
Vector2 s = Vector2(intersection.x, intersection.y) * 2.0f;
if (snap_enabled) {
- s = s.snapped(Vector2(snap, snap));
+ s = s.snappedf(snap);
}
- s = s.max(Vector2(0.001, 0.001));
+ s = s.maxf(0.001);
qo->set_size(s);
} else {
float d = intersection[p_id];
diff --git a/editor/plugins/gradient_texture_2d_editor_plugin.cpp b/editor/plugins/gradient_texture_2d_editor_plugin.cpp
index bb6096ea34..ebc00c49bb 100644
--- a/editor/plugins/gradient_texture_2d_editor_plugin.cpp
+++ b/editor/plugins/gradient_texture_2d_editor_plugin.cpp
@@ -42,7 +42,7 @@
Point2 GradientTexture2DEdit::_get_handle_pos(const Handle p_handle) {
// Get the handle's mouse position in pixels relative to offset.
- return (p_handle == HANDLE_FROM ? texture->get_fill_from() : texture->get_fill_to()).clamp(Vector2(), Vector2(1, 1)) * size;
+ return (p_handle == HANDLE_FROM ? texture->get_fill_from() : texture->get_fill_to()).clampf(0, 1) * size;
}
GradientTexture2DEdit::Handle GradientTexture2DEdit::get_handle_at(const Vector2 &p_pos) {
@@ -112,9 +112,9 @@ void GradientTexture2DEdit::gui_input(const Ref<InputEvent> &p_event) {
return;
}
- Vector2 new_pos = (mpos / size).clamp(Vector2(0, 0), Vector2(1, 1));
+ Vector2 new_pos = (mpos / size).clampf(0, 1);
if (snap_enabled || mm->is_command_or_control_pressed()) {
- new_pos = new_pos.snapped(Vector2(1.0 / snap_count, 1.0 / snap_count));
+ new_pos = new_pos.snappedf(1.0 / snap_count);
}
// Allow to snap to an axis with Shift.
diff --git a/editor/plugins/navigation_obstacle_3d_editor_plugin.cpp b/editor/plugins/navigation_obstacle_3d_editor_plugin.cpp
index 869f5b3b10..61b43eaaf1 100644
--- a/editor/plugins/navigation_obstacle_3d_editor_plugin.cpp
+++ b/editor/plugins/navigation_obstacle_3d_editor_plugin.cpp
@@ -330,9 +330,7 @@ EditorPlugin::AfterGUIInput NavigationObstacle3DEditor::forward_3d_gui_input(Cam
}
if (!snap_ignore && Node3DEditor::get_singleton()->is_snap_enabled()) {
- cpoint = cpoint.snapped(Vector2(
- Node3DEditor::get_singleton()->get_translate_snap(),
- Node3DEditor::get_singleton()->get_translate_snap()));
+ cpoint = cpoint.snappedf(Node3DEditor::get_singleton()->get_translate_snap());
}
edited_point_pos = cpoint;
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index be14132185..4a418e62ca 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -1448,7 +1448,7 @@ Transform3D Node3DEditorViewport::_compute_transform(TransformMode p_mode, const
switch (p_mode) {
case TRANSFORM_SCALE: {
if (_edit.snap || spatial_editor->is_snap_enabled()) {
- p_motion.snap(Vector3(p_extra, p_extra, p_extra));
+ p_motion.snapf(p_extra);
}
Transform3D s;
if (p_local) {
@@ -1469,7 +1469,7 @@ Transform3D Node3DEditorViewport::_compute_transform(TransformMode p_mode, const
}
case TRANSFORM_TRANSLATE: {
if (_edit.snap || spatial_editor->is_snap_enabled()) {
- p_motion.snap(Vector3(p_extra, p_extra, p_extra));
+ p_motion.snapf(p_extra);
}
if (p_local) {
@@ -4786,7 +4786,7 @@ void Node3DEditorViewport::update_transform(bool p_shift) {
snap = spatial_editor->get_scale_snap() / 100;
}
Vector3 motion_snapped = motion;
- motion_snapped.snap(Vector3(snap, snap, snap));
+ motion_snapped.snapf(snap);
// This might not be necessary anymore after issue #288 is solved (in 4.0?).
// TRANSLATORS: Refers to changing the scale of a node in the 3D editor.
set_message(TTR("Scaling:") + " (" + String::num(motion_snapped.x, snap_step_decimals) + ", " +
@@ -4858,7 +4858,7 @@ void Node3DEditorViewport::update_transform(bool p_shift) {
snap = spatial_editor->get_translate_snap();
}
Vector3 motion_snapped = motion;
- motion_snapped.snap(Vector3(snap, snap, snap));
+ motion_snapped.snapf(snap);
// TRANSLATORS: Refers to changing the position of a node in the 3D editor.
set_message(TTR("Translating:") + " (" + String::num(motion_snapped.x, snap_step_decimals) + ", " +
String::num(motion_snapped.y, snap_step_decimals) + ", " + String::num(motion_snapped.z, snap_step_decimals) + ")");
@@ -8997,7 +8997,7 @@ void Node3DEditorPlugin::set_state(const Dictionary &p_state) {
Vector3 Node3DEditor::snap_point(Vector3 p_target, Vector3 p_start) const {
if (is_snap_enabled()) {
real_t snap = get_translate_snap();
- p_target.snap(Vector3(snap, snap, snap));
+ p_target.snapf(snap);
}
return p_target;
}
diff --git a/editor/plugins/path_3d_editor_plugin.cpp b/editor/plugins/path_3d_editor_plugin.cpp
index 4e317114a3..f09318277a 100644
--- a/editor/plugins/path_3d_editor_plugin.cpp
+++ b/editor/plugins/path_3d_editor_plugin.cpp
@@ -117,7 +117,7 @@ void Path3DGizmo::set_handle(int p_id, bool p_secondary, Camera3D *p_camera, con
if (p.intersects_ray(ray_from, ray_dir, &inters)) {
if (Node3DEditor::get_singleton()->is_snap_enabled()) {
float snap = Node3DEditor::get_singleton()->get_translate_snap();
- inters.snap(Vector3(snap, snap, snap));
+ inters.snapf(snap);
}
Vector3 local = gi.xform(inters);
@@ -146,7 +146,7 @@ void Path3DGizmo::set_handle(int p_id, bool p_secondary, Camera3D *p_camera, con
Vector3 local = gi.xform(inters) - base;
if (Node3DEditor::get_singleton()->is_snap_enabled()) {
float snap = Node3DEditor::get_singleton()->get_translate_snap();
- local.snap(Vector3(snap, snap, snap));
+ local.snapf(snap);
}
if (info.type == HandleType::HANDLE_TYPE_IN) {
diff --git a/editor/plugins/polygon_3d_editor_plugin.cpp b/editor/plugins/polygon_3d_editor_plugin.cpp
index da84afc4d7..7c41093774 100644
--- a/editor/plugins/polygon_3d_editor_plugin.cpp
+++ b/editor/plugins/polygon_3d_editor_plugin.cpp
@@ -334,9 +334,7 @@ EditorPlugin::AfterGUIInput Polygon3DEditor::forward_3d_gui_input(Camera3D *p_ca
}
if (!snap_ignore && Node3DEditor::get_singleton()->is_snap_enabled()) {
- cpoint = cpoint.snapped(Vector2(
- Node3DEditor::get_singleton()->get_translate_snap(),
- Node3DEditor::get_singleton()->get_translate_snap()));
+ cpoint = cpoint.snappedf(Node3DEditor::get_singleton()->get_translate_snap());
}
edited_point_pos = cpoint;
diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp
index a642f35d6f..561edcf8bf 100644
--- a/editor/plugins/script_text_editor.cpp
+++ b/editor/plugins/script_text_editor.cpp
@@ -284,8 +284,7 @@ void ScriptTextEditor::_warning_clicked(const Variant &p_line) {
if (prev_line.contains("@warning_ignore")) {
const int closing_bracket_idx = prev_line.find(")");
const String text_to_insert = ", " + code.quote(quote_style);
- prev_line = prev_line.insert(closing_bracket_idx, text_to_insert);
- text_editor->set_line(line - 1, prev_line);
+ text_editor->insert_text(text_to_insert, line - 1, closing_bracket_idx);
} else {
const int indent = text_editor->get_indent_level(line) / text_editor->get_indent_size();
String annotation_indent;
@@ -352,22 +351,26 @@ void ScriptTextEditor::add_callback(const String &p_function, const PackedString
if (!language->can_make_function()) {
return;
}
-
+ code_editor->get_text_editor()->begin_complex_operation();
+ code_editor->get_text_editor()->remove_secondary_carets();
+ code_editor->get_text_editor()->deselect();
String code = code_editor->get_text_editor()->get_text();
int pos = language->find_function(p_function, code);
- code_editor->get_text_editor()->remove_secondary_carets();
if (pos == -1) {
- //does not exist
- code_editor->get_text_editor()->deselect();
- pos = code_editor->get_text_editor()->get_line_count() + 2;
+ // Function does not exist, create it at the end of the file.
+ int last_line = code_editor->get_text_editor()->get_line_count() - 1;
String func = language->make_function("", p_function, p_args);
- //code=code+func;
- code_editor->get_text_editor()->set_caret_line(pos + 1);
- code_editor->get_text_editor()->set_caret_column(1000000); //none shall be that big
- code_editor->get_text_editor()->insert_text_at_caret("\n\n" + func);
+ code_editor->get_text_editor()->insert_text("\n\n" + func, last_line, code_editor->get_text_editor()->get_line(last_line).length());
+ pos = last_line + 3;
+ }
+ // Put caret on the line after the function, after the indent.
+ int indent_column = 1;
+ if (EDITOR_GET("text_editor/behavior/indent/type")) {
+ indent_column = EDITOR_GET("text_editor/behavior/indent/size");
}
- code_editor->get_text_editor()->set_caret_line(pos);
- code_editor->get_text_editor()->set_caret_column(1);
+ code_editor->get_text_editor()->set_caret_line(pos, true, true, -1);
+ code_editor->get_text_editor()->set_caret_column(indent_column);
+ code_editor->get_text_editor()->end_complex_operation();
}
bool ScriptTextEditor::show_members_overview() {
@@ -1335,10 +1338,10 @@ void ScriptTextEditor::_edit_option(int p_op) {
callable_mp((Control *)tx, &Control::grab_focus).call_deferred();
} break;
case EDIT_MOVE_LINE_UP: {
- code_editor->move_lines_up();
+ code_editor->get_text_editor()->move_lines_up();
} break;
case EDIT_MOVE_LINE_DOWN: {
- code_editor->move_lines_down();
+ code_editor->get_text_editor()->move_lines_down();
} break;
case EDIT_INDENT: {
Ref<Script> scr = script;
@@ -1355,24 +1358,16 @@ void ScriptTextEditor::_edit_option(int p_op) {
tx->unindent_lines();
} break;
case EDIT_DELETE_LINE: {
- code_editor->delete_lines();
+ code_editor->get_text_editor()->delete_lines();
} break;
case EDIT_DUPLICATE_SELECTION: {
- code_editor->duplicate_selection();
+ code_editor->get_text_editor()->duplicate_selection();
} break;
case EDIT_DUPLICATE_LINES: {
code_editor->get_text_editor()->duplicate_lines();
} break;
case EDIT_TOGGLE_FOLD_LINE: {
- int prev_line = -1;
- for (int caret_idx : tx->get_caret_index_edit_order()) {
- int line_idx = tx->get_caret_line(caret_idx);
- if (line_idx != prev_line) {
- tx->toggle_foldable_line(line_idx);
- prev_line = line_idx;
- }
- }
- tx->queue_redraw();
+ tx->toggle_foldable_lines_at_carets();
} break;
case EDIT_FOLD_ALL_LINES: {
tx->fold_all_lines();
@@ -1399,24 +1394,34 @@ void ScriptTextEditor::_edit_option(int p_op) {
}
tx->begin_complex_operation();
- int begin, end;
+ tx->begin_multicaret_edit();
+ int begin = tx->get_line_count() - 1, end = 0;
if (tx->has_selection()) {
- begin = tx->get_selection_from_line();
- end = tx->get_selection_to_line();
- // ignore if the cursor is not past the first column
- if (tx->get_selection_to_column() == 0) {
- end--;
+ // Auto indent all lines that have a caret or selection on it.
+ Vector<Point2i> line_ranges = tx->get_line_ranges_from_carets();
+ for (Point2i line_range : line_ranges) {
+ scr->get_language()->auto_indent_code(text, line_range.x, line_range.y);
+ if (line_range.x < begin) {
+ begin = line_range.x;
+ }
+ if (line_range.y > end) {
+ end = line_range.y;
+ }
}
} else {
+ // Auto indent entire text.
begin = 0;
end = tx->get_line_count() - 1;
+ scr->get_language()->auto_indent_code(text, begin, end);
}
- scr->get_language()->auto_indent_code(text, begin, end);
+
+ // Apply auto indented code.
Vector<String> lines = text.split("\n");
for (int i = begin; i <= end; ++i) {
tx->set_line(i, lines[i]);
}
+ tx->end_multicaret_edit();
tx->end_complex_operation();
} break;
case EDIT_TRIM_TRAILING_WHITESAPCE: {
@@ -1515,13 +1520,12 @@ void ScriptTextEditor::_edit_option(int p_op) {
code_editor->remove_all_bookmarks();
} break;
case DEBUG_TOGGLE_BREAKPOINT: {
- Vector<int> caret_edit_order = tx->get_caret_index_edit_order();
- caret_edit_order.reverse();
+ Vector<int> sorted_carets = tx->get_sorted_carets();
int last_line = -1;
- for (const int &c : caret_edit_order) {
- int from = tx->has_selection(c) ? tx->get_selection_from_line(c) : tx->get_caret_line(c);
+ for (const int &c : sorted_carets) {
+ int from = tx->get_selection_from_line(c);
from += from == last_line ? 1 : 0;
- int to = tx->has_selection(c) ? tx->get_selection_to_line(c) : tx->get_caret_line(c);
+ int to = tx->get_selection_to_line(c);
if (to < from) {
continue;
}
@@ -2008,45 +2012,32 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
tx->apply_ime();
Point2i pos = tx->get_line_column_at_pos(local_pos);
- int row = pos.y;
- int col = pos.x;
+ int mouse_line = pos.y;
+ int mouse_column = pos.x;
tx->set_move_caret_on_right_click_enabled(EDITOR_GET("text_editor/behavior/navigation/move_caret_on_right_click"));
- int caret_clicked = -1;
+ int selection_clicked = -1;
if (tx->is_move_caret_on_right_click_enabled()) {
- if (tx->has_selection()) {
- for (int i = 0; i < tx->get_caret_count(); i++) {
- int from_line = tx->get_selection_from_line(i);
- int to_line = tx->get_selection_to_line(i);
- int from_column = tx->get_selection_from_column(i);
- int to_column = tx->get_selection_to_column(i);
-
- if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) {
- // Right click in one of the selected text
- caret_clicked = i;
- break;
- }
- }
- }
- if (caret_clicked < 0) {
+ selection_clicked = tx->get_selection_at_line_column(mouse_line, mouse_column, true);
+ if (selection_clicked < 0) {
tx->deselect();
tx->remove_secondary_carets();
- caret_clicked = 0;
- tx->set_caret_line(row, false, false);
- tx->set_caret_column(col);
+ selection_clicked = 0;
+ tx->set_caret_line(mouse_line, false, false, -1);
+ tx->set_caret_column(mouse_column);
}
}
String word_at_pos = tx->get_word_at_pos(local_pos);
if (word_at_pos.is_empty()) {
- word_at_pos = tx->get_word_under_caret(caret_clicked);
+ word_at_pos = tx->get_word_under_caret(selection_clicked);
}
if (word_at_pos.is_empty()) {
- word_at_pos = tx->get_selected_text(caret_clicked);
+ word_at_pos = tx->get_selected_text(selection_clicked);
}
bool has_color = (word_at_pos == "Color");
- bool foldable = tx->can_fold_line(row) || tx->is_line_folded(row);
+ bool foldable = tx->can_fold_line(mouse_line) || tx->is_line_folded(mouse_line);
bool open_docs = false;
bool goto_definition = false;
@@ -2064,9 +2055,9 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
}
if (has_color) {
- String line = tx->get_line(row);
- color_position.x = row;
- color_position.y = col;
+ String line = tx->get_line(mouse_line);
+ color_position.x = mouse_line;
+ color_position.y = mouse_column;
int begin = -1;
int end = -1;
@@ -2076,7 +2067,7 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
COLOR_NAME, // Color.COLOR_NAME
} expression_pattern = NOT_PARSED;
- for (int i = col; i < line.length(); i++) {
+ for (int i = mouse_column; i < line.length(); i++) {
if (line[i] == '(') {
if (expression_pattern == NOT_PARSED) {
begin = i;
@@ -2155,7 +2146,6 @@ void ScriptTextEditor::_color_changed(const Color &p_color) {
code_editor->get_text_editor()->begin_complex_operation();
code_editor->get_text_editor()->set_line(color_position.x, line_with_replaced_args);
code_editor->get_text_editor()->end_complex_operation();
- code_editor->get_text_editor()->queue_redraw();
}
void ScriptTextEditor::_prepare_edit_menu() {
diff --git a/editor/plugins/text_editor.cpp b/editor/plugins/text_editor.cpp
index 6070e08739..e19d9d933a 100644
--- a/editor/plugins/text_editor.cpp
+++ b/editor/plugins/text_editor.cpp
@@ -380,10 +380,10 @@ void TextEditor::_edit_option(int p_op) {
callable_mp((Control *)tx, &Control::grab_focus).call_deferred();
} break;
case EDIT_MOVE_LINE_UP: {
- code_editor->move_lines_up();
+ code_editor->get_text_editor()->move_lines_up();
} break;
case EDIT_MOVE_LINE_DOWN: {
- code_editor->move_lines_down();
+ code_editor->get_text_editor()->move_lines_down();
} break;
case EDIT_INDENT: {
tx->indent_lines();
@@ -392,24 +392,16 @@ void TextEditor::_edit_option(int p_op) {
tx->unindent_lines();
} break;
case EDIT_DELETE_LINE: {
- code_editor->delete_lines();
+ code_editor->get_text_editor()->delete_lines();
} break;
case EDIT_DUPLICATE_SELECTION: {
- code_editor->duplicate_selection();
+ code_editor->get_text_editor()->duplicate_selection();
} break;
case EDIT_DUPLICATE_LINES: {
code_editor->get_text_editor()->duplicate_lines();
} break;
case EDIT_TOGGLE_FOLD_LINE: {
- int previous_line = -1;
- for (int caret_idx : tx->get_caret_index_edit_order()) {
- int line_idx = tx->get_caret_line(caret_idx);
- if (line_idx != previous_line) {
- tx->toggle_foldable_line(line_idx);
- previous_line = line_idx;
- }
- }
- tx->queue_redraw();
+ tx->toggle_foldable_lines_at_carets();
} break;
case EDIT_FOLD_ALL_LINES: {
tx->fold_all_lines();
@@ -531,7 +523,7 @@ void TextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
}
}
if (!tx->has_selection()) {
- tx->set_caret_line(row, true, false);
+ tx->set_caret_line(row, true, false, -1);
tx->set_caret_column(col);
}
}
diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp
index 40bd51a442..83a1700306 100644
--- a/editor/plugins/text_shader_editor.cpp
+++ b/editor/plugins/text_shader_editor.cpp
@@ -653,10 +653,10 @@ void TextShaderEditor::_menu_option(int p_option) {
code_editor->get_text_editor()->select_all();
} break;
case EDIT_MOVE_LINE_UP: {
- code_editor->move_lines_up();
+ code_editor->get_text_editor()->move_lines_up();
} break;
case EDIT_MOVE_LINE_DOWN: {
- code_editor->move_lines_down();
+ code_editor->get_text_editor()->move_lines_down();
} break;
case EDIT_INDENT: {
if (shader.is_null() && shader_inc.is_null()) {
@@ -671,10 +671,10 @@ void TextShaderEditor::_menu_option(int p_option) {
code_editor->get_text_editor()->unindent_lines();
} break;
case EDIT_DELETE_LINE: {
- code_editor->delete_lines();
+ code_editor->get_text_editor()->delete_lines();
} break;
case EDIT_DUPLICATE_SELECTION: {
- code_editor->duplicate_selection();
+ code_editor->get_text_editor()->duplicate_selection();
} break;
case EDIT_DUPLICATE_LINES: {
code_editor->get_text_editor()->duplicate_lines();
@@ -1010,7 +1010,7 @@ void TextShaderEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
}
}
if (!tx->has_selection()) {
- tx->set_caret_line(row, true, false);
+ tx->set_caret_line(row, true, false, -1);
tx->set_caret_column(col);
}
}
diff --git a/editor/plugins/texture_region_editor_plugin.cpp b/editor/plugins/texture_region_editor_plugin.cpp
index 9498e980ec..5557eec694 100644
--- a/editor/plugins/texture_region_editor_plugin.cpp
+++ b/editor/plugins/texture_region_editor_plugin.cpp
@@ -328,7 +328,7 @@ void TextureRegionEditor::_texture_overlay_input(const Ref<InputEvent> &p_input)
drag_from = mtx.affine_inverse().xform(mb->get_position());
if (snap_mode == SNAP_PIXEL) {
- drag_from = drag_from.snapped(Vector2(1, 1));
+ drag_from = drag_from.snappedf(1);
} else if (snap_mode == SNAP_GRID) {
drag_from = snap_point(drag_from);
}
@@ -566,7 +566,7 @@ void TextureRegionEditor::_texture_overlay_input(const Ref<InputEvent> &p_input)
} else {
Vector2 new_pos = mtx.affine_inverse().xform(mm->get_position());
if (snap_mode == SNAP_PIXEL) {
- new_pos = new_pos.snapped(Vector2(1, 1));
+ new_pos = new_pos.snappedf(1);
} else if (snap_mode == SNAP_GRID) {
new_pos = snap_point(new_pos);
}
diff --git a/editor/plugins/tiles/tile_data_editors.cpp b/editor/plugins/tiles/tile_data_editors.cpp
index f047e4ff16..86a291db0e 100644
--- a/editor/plugins/tiles/tile_data_editors.cpp
+++ b/editor/plugins/tiles/tile_data_editors.cpp
@@ -458,7 +458,7 @@ void GenericTilePolygonEditor::_snap_point(Point2 &r_point) {
break;
case SNAP_HALF_PIXEL:
- r_point = r_point.snapped(Vector2(0.5, 0.5));
+ r_point = r_point.snappedf(0.5);
break;
case SNAP_GRID: {
diff --git a/editor/plugins/tiles/tile_proxies_manager_dialog.cpp b/editor/plugins/tiles/tile_proxies_manager_dialog.cpp
index ca4ffeecc2..a75ab45106 100644
--- a/editor/plugins/tiles/tile_proxies_manager_dialog.cpp
+++ b/editor/plugins/tiles/tile_proxies_manager_dialog.cpp
@@ -257,13 +257,13 @@ bool TileProxiesManagerDialog::_set(const StringName &p_name, const Variant &p_v
if (p_name == "from_source") {
from.source_id = MAX(int(p_value), -1);
} else if (p_name == "from_coords") {
- from.set_atlas_coords(Vector2i(p_value).max(Vector2i(-1, -1)));
+ from.set_atlas_coords(Vector2i(p_value).maxi(-1));
} else if (p_name == "from_alternative") {
from.alternative_tile = MAX(int(p_value), -1);
} else if (p_name == "to_source") {
to.source_id = MAX(int(p_value), 0);
} else if (p_name == "to_coords") {
- to.set_atlas_coords(Vector2i(p_value).max(Vector2i(0, 0)));
+ to.set_atlas_coords(Vector2i(p_value).maxi(0));
} else if (p_name == "to_alternative") {
to.alternative_tile = MAX(int(p_value), 0);
} else {
diff --git a/editor/plugins/tiles/tile_set_atlas_source_editor.cpp b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp
index 1da2f89c1c..1a1b14bb84 100644
--- a/editor/plugins/tiles/tile_set_atlas_source_editor.cpp
+++ b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp
@@ -1088,7 +1088,7 @@ void TileSetAtlasSourceEditor::_tile_atlas_control_gui_input(const Ref<InputEven
if (drag_type == DRAG_TYPE_CREATE_BIG_TILE) {
// Create big tile.
- new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1));
+ new_base_tiles_coords = new_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1));
Rect2i new_rect = Rect2i(start_base_tiles_coords, new_base_tiles_coords - start_base_tiles_coords).abs();
new_rect.size += Vector2i(1, 1);
@@ -1100,8 +1100,8 @@ void TileSetAtlasSourceEditor::_tile_atlas_control_gui_input(const Ref<InputEven
}
} else if (drag_type == DRAG_TYPE_CREATE_TILES) {
// Create tiles.
- last_base_tiles_coords = last_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1));
- new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1));
+ last_base_tiles_coords = last_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1));
+ new_base_tiles_coords = new_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1));
Vector<Point2i> line = Geometry2D::bresenham_line(last_base_tiles_coords, new_base_tiles_coords);
for (int i = 0; i < line.size(); i++) {
@@ -1115,8 +1115,8 @@ void TileSetAtlasSourceEditor::_tile_atlas_control_gui_input(const Ref<InputEven
} else if (drag_type == DRAG_TYPE_REMOVE_TILES) {
// Remove tiles.
- last_base_tiles_coords = last_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1));
- new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1));
+ last_base_tiles_coords = last_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1));
+ new_base_tiles_coords = new_base_tiles_coords.maxi(0).min(grid_size - Vector2i(1, 1));
Vector<Point2i> line = Geometry2D::bresenham_line(last_base_tiles_coords, new_base_tiles_coords);
for (int i = 0; i < line.size(); i++) {
@@ -1838,7 +1838,7 @@ void TileSetAtlasSourceEditor::_tile_atlas_control_draw() {
Vector2i separation = tile_set_atlas_source->get_separation();
Vector2i tile_size = tile_set_atlas_source->get_texture_region_size();
Vector2i origin = margins + (area.position * (tile_size + separation));
- Vector2i size = area.size * tile_size + (area.size - Vector2i(1, 1)).max(Vector2i(0, 0)) * separation;
+ Vector2i size = area.size * tile_size + (area.size - Vector2i(1, 1)).maxi(0) * separation;
TilesEditorUtils::draw_selection_rect(tile_atlas_control, Rect2i(origin, size));
} else {
Vector2i grid_size = tile_set_atlas_source->get_atlas_grid_size();
@@ -2157,7 +2157,7 @@ void TileSetAtlasSourceEditor::_undo_redo_inspector_callback(Object *p_undo_redo
Vector2i TileSetAtlasSourceEditor::_get_drag_offset_tile_coords(const Vector2i &p_offset) const {
Vector2i half_tile_size = tile_set->get_tile_size() / 2;
Vector2i new_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position() + half_tile_size * p_offset);
- return new_base_tiles_coords.max(Vector2i(-1, -1)).min(tile_set_atlas_source->get_atlas_grid_size());
+ return new_base_tiles_coords.maxi(-1).min(tile_set_atlas_source->get_atlas_grid_size());
}
void TileSetAtlasSourceEditor::edit(Ref<TileSet> p_tile_set, TileSetAtlasSource *p_tile_set_atlas_source, int p_source_id) {
diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp
index 0dd845270a..9e706ce623 100644
--- a/editor/plugins/visual_shader_editor_plugin.cpp
+++ b/editor/plugins/visual_shader_editor_plugin.cpp
@@ -1002,7 +1002,7 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool
button->hide();
}
- if (i == 0 && custom_editor) {
+ if (j == 0 && custom_editor) {
hb->add_child(custom_editor);
custom_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL);
} else {
@@ -1019,22 +1019,22 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool
type_box->add_item(TTR("Boolean"));
type_box->add_item(TTR("Transform"));
type_box->add_item(TTR("Sampler"));
- type_box->select(group_node->get_input_port_type(i));
+ type_box->select(group_node->get_input_port_type(j));
type_box->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
- type_box->connect("item_selected", callable_mp(editor, &VisualShaderEditor::_change_input_port_type).bind(p_id, i), CONNECT_DEFERRED);
+ type_box->connect("item_selected", callable_mp(editor, &VisualShaderEditor::_change_input_port_type).bind(p_id, j), CONNECT_DEFERRED);
LineEdit *name_box = memnew(LineEdit);
hb->add_child(name_box);
name_box->set_custom_minimum_size(Size2(65 * EDSCALE, 0));
name_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
name_box->set_text(name_left);
- name_box->connect("text_submitted", callable_mp(editor, &VisualShaderEditor::_change_input_port_name).bind(name_box, p_id, i), CONNECT_DEFERRED);
- name_box->connect("focus_exited", callable_mp(editor, &VisualShaderEditor::_port_name_focus_out).bind(name_box, p_id, i, false), CONNECT_DEFERRED);
+ name_box->connect("text_submitted", callable_mp(editor, &VisualShaderEditor::_change_input_port_name).bind(name_box, p_id, j), CONNECT_DEFERRED);
+ name_box->connect("focus_exited", callable_mp(editor, &VisualShaderEditor::_port_name_focus_out).bind(name_box, p_id, j, false), CONNECT_DEFERRED);
Button *remove_btn = memnew(Button);
remove_btn->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("Remove"), EditorStringName(EditorIcons)));
remove_btn->set_tooltip_text(TTR("Remove") + " " + name_left);
- remove_btn->connect("pressed", callable_mp(editor, &VisualShaderEditor::_remove_input_port).bind(p_id, i), CONNECT_DEFERRED);
+ remove_btn->connect("pressed", callable_mp(editor, &VisualShaderEditor::_remove_input_port).bind(p_id, j), CONNECT_DEFERRED);
hb->add_child(remove_btn);
} else {
Label *label = memnew(Label);
@@ -1043,7 +1043,7 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool
label->add_theme_style_override("normal", editor->get_theme_stylebox(SNAME("label_style"), SNAME("VShaderEditor"))); //more compact
hb->add_child(label);
- if (vsnode->is_input_port_default(i, mode) && !port_left_used) {
+ if (vsnode->is_input_port_default(j, mode) && !port_left_used) {
Label *hint_label = memnew(Label);
hint_label->set_text(TTR("[default]"));
hint_label->add_theme_color_override("font_color", editor->get_theme_color(SNAME("font_readonly_color"), SNAME("TextEdit")));
@@ -4673,7 +4673,7 @@ void VisualShaderEditor::_show_members_dialog(bool at_mouse_pos, VisualShaderNod
// Keep dialog within window bounds.
Rect2 window_rect = Rect2(get_window()->get_position(), get_window()->get_size());
Rect2 dialog_rect = Rect2(members_dialog->get_position(), members_dialog->get_size());
- Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).max(Vector2());
+ Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).maxf(0);
members_dialog->set_position(members_dialog->get_position() - difference);
callable_mp((Control *)node_filter, &Control::grab_focus).call_deferred(); // Still not visible.
@@ -4702,7 +4702,7 @@ void VisualShaderEditor::_show_add_varying_dialog() {
// Keep dialog within window bounds.
Rect2 window_rect = Rect2(DisplayServer::get_singleton()->window_get_position(), DisplayServer::get_singleton()->window_get_size());
Rect2 dialog_rect = Rect2(add_varying_dialog->get_position(), add_varying_dialog->get_size());
- Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).max(Vector2());
+ Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).maxf(0);
add_varying_dialog->set_position(add_varying_dialog->get_position() - difference);
}
@@ -4713,7 +4713,7 @@ void VisualShaderEditor::_show_remove_varying_dialog() {
// Keep dialog within window bounds.
Rect2 window_rect = Rect2(DisplayServer::get_singleton()->window_get_position(), DisplayServer::get_singleton()->window_get_size());
Rect2 dialog_rect = Rect2(remove_varying_dialog->get_position(), remove_varying_dialog->get_size());
- Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).max(Vector2());
+ Vector2 difference = (dialog_rect.get_end() - window_rect.get_end()).maxf(0);
remove_varying_dialog->set_position(remove_varying_dialog->get_position() - difference);
}
diff --git a/main/main.cpp b/main/main.cpp
index eee634086e..90c84114f2 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -2405,6 +2405,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
// OpenXR project extensions settings.
GLOBAL_DEF_BASIC("xr/openxr/extensions/hand_tracking", true);
+ GLOBAL_DEF_RST_BASIC("xr/openxr/extensions/hand_interaction_profile", false);
GLOBAL_DEF_BASIC("xr/openxr/extensions/eye_gaze_interaction", false);
#ifdef TOOLS_ENABLED
@@ -3182,6 +3183,7 @@ int Main::start() {
#ifdef TOOLS_ENABLED
String doc_tool_path;
+ bool doc_tool_implicit_cwd = false;
BitField<DocTools::GenerateFlags> gen_flags;
String _export_preset;
bool export_debug = false;
@@ -3252,6 +3254,7 @@ int Main::start() {
if (doc_tool_path.begins_with("-")) {
// Assuming other command line arg, so default to cwd.
doc_tool_path = ".";
+ doc_tool_implicit_cwd = true;
parsed_pair = false;
}
#ifdef MODULE_GDSCRIPT_ENABLED
@@ -3282,6 +3285,7 @@ int Main::start() {
// Handle case where no path is given to --doctool.
else if (args[i] == "--doctool") {
doc_tool_path = ".";
+ doc_tool_implicit_cwd = true;
}
#endif
}
@@ -3308,6 +3312,11 @@ int Main::start() {
{
Ref<DirAccess> da = DirAccess::open(doc_tool_path);
ERR_FAIL_COND_V_MSG(da.is_null(), EXIT_FAILURE, "Argument supplied to --doctool must be a valid directory path.");
+ // Ensure that doctool is running in the root dir, but only if
+ // user did not manually specify a path as argument.
+ if (doc_tool_implicit_cwd) {
+ ERR_FAIL_COND_V_MSG(!da->dir_exists("doc"), EXIT_FAILURE, "--doctool must be run from the Godot repository's root folder, or specify a path that points there.");
+ }
}
#ifndef MODULE_MONO_ENABLED
@@ -3636,7 +3645,7 @@ int Main::start() {
}
}
- if (doc_tool_path == ".") {
+ if (doc_tool_implicit_cwd) {
doc_tool_path = "./docs";
}
diff --git a/methods.py b/methods.py
index 30c7cb0331..0c29632f10 100644
--- a/methods.py
+++ b/methods.py
@@ -34,39 +34,41 @@ class ANSI(Enum):
internal value, or an empty string in a non-colorized scope.
"""
- GRAY = "\x1b[0;30m"
- RED = "\x1b[0;31m"
- GREEN = "\x1b[0;32m"
- YELLOW = "\x1b[0;33m"
- BLUE = "\x1b[0;34m"
- PURPLE = "\x1b[0;35m"
- CYAN = "\x1b[0;36m"
- WHITE = "\x1b[0;37m"
-
- BOLD_GRAY = "\x1b[1;90m"
- BOLD_RED = "\x1b[1;91m"
- BOLD_GREEN = "\x1b[1;92m"
- BOLD_YELLOW = "\x1b[1;93m"
- BOLD_BLUE = "\x1b[1;94m"
- BOLD_PURPLE = "\x1b[1;95m"
- BOLD_CYAN = "\x1b[1;96m"
- BOLD_WHITE = "\x1b[1;97m"
-
RESET = "\x1b[0m"
- def __str__(self):
+ 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 self.value if _colorize else ""
+ return str(self.value) if _colorize else ""
def print_warning(*values: object) -> None:
"""Prints a warning message with formatting."""
- print(f"{ANSI.BOLD_YELLOW}WARNING:{ANSI.YELLOW}", *values, ANSI.RESET, file=sys.stderr)
+ print(f"{ANSI.YELLOW}{ANSI.BOLD}WARNING:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr)
def print_error(*values: object) -> None:
"""Prints an error message with formatting."""
- print(f"{ANSI.BOLD_RED}ERROR:{ANSI.RED}", *values, ANSI.RESET, file=sys.stderr)
+ print(f"{ANSI.RED}{ANSI.BOLD}ERROR:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr)
def add_source_files_orig(self, sources, files, allow_gen=False):
@@ -647,33 +649,33 @@ def use_windows_spawn_fix(self, platform=None):
def no_verbose(env):
- colors = [ANSI.BLUE, ANSI.BOLD_BLUE, ANSI.RESET]
+ 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 = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors)
- java_compile_source_message = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors)
- compile_shared_source_message = "{0}Compiling shared {1}$SOURCE{0} ...{2}".format(*colors)
- link_program_message = "{0}Linking Program {1}$TARGET{0} ...{2}".format(*colors)
- link_library_message = "{0}Linking Static Library {1}$TARGET{0} ...{2}".format(*colors)
- ranlib_library_message = "{0}Ranlib Library {1}$TARGET{0} ...{2}".format(*colors)
- link_shared_library_message = "{0}Linking Shared Library {1}$TARGET{0} ...{2}".format(*colors)
- java_library_message = "{0}Creating Java Archive {1}$TARGET{0} ...{2}".format(*colors)
- compiled_resource_message = "{0}Creating Compiled Resource {1}$TARGET{0} ...{2}".format(*colors)
- generated_file_message = "{0}Generating {1}$TARGET{0} ...{2}".format(*colors)
-
- env.Append(CXXCOMSTR=compile_source_message)
- env.Append(CCCOMSTR=compile_source_message)
- env.Append(SHCCCOMSTR=compile_shared_source_message)
- env.Append(SHCXXCOMSTR=compile_shared_source_message)
- env.Append(ARCOMSTR=link_library_message)
- env.Append(RANLIBCOMSTR=ranlib_library_message)
- env.Append(SHLINKCOMSTR=link_shared_library_message)
- env.Append(LINKCOMSTR=link_program_message)
- env.Append(JARCOMSTR=java_library_message)
- env.Append(JAVACCOMSTR=java_compile_source_message)
- env.Append(RCCOMSTR=compiled_resource_message)
- env.Append(GENCOMSTR=generated_file_message)
+ compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors)
+ java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors)
+ compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors)
+ link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors)
+ link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors)
+ ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors)
+ link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors)
+ java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors)
+ compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors)
+ generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors)
+
+ env["CXXCOMSTR"] = compile_source_message
+ env["CCCOMSTR"] = compile_source_message
+ env["SHCCCOMSTR"] = compile_shared_source_message
+ env["SHCXXCOMSTR"] = compile_shared_source_message
+ env["ARCOMSTR"] = link_library_message
+ env["RANLIBCOMSTR"] = ranlib_library_message
+ env["SHLINKCOMSTR"] = link_shared_library_message
+ env["LINKCOMSTR"] = link_program_message
+ env["JARCOMSTR"] = java_library_message
+ env["JAVACCOMSTR"] = java_compile_source_message
+ env["RCCOMSTR"] = compiled_resource_message
+ env["GENCOMSTR"] = generated_file_message
def detect_visual_c_compiler_version(tools_env):
diff --git a/misc/dist/html/full-size.html b/misc/dist/html/full-size.html
index 8ae25362f8..874fe2695e 100644
--- a/misc/dist/html/full-size.html
+++ b/misc/dist/html/full-size.html
@@ -2,135 +2,95 @@
<html lang="en">
<head>
<meta charset="utf-8">
- <meta name="viewport" content="width=device-width, user-scalable=no">
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<title>$GODOT_PROJECT_NAME</title>
<style>
-body {
- touch-action: none;
+html, body, #canvas {
margin: 0;
- border: 0 none;
padding: 0;
- text-align: center;
+ border: 0;
+}
+
+body {
+ color: white;
background-color: black;
+ overflow: hidden;
+ touch-action: none;
}
#canvas {
display: block;
- margin: 0;
- color: white;
}
#canvas:focus {
outline: none;
}
-.godot {
- font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
- color: #e0e0e0;
- background-color: #3b3943;
- background-image: linear-gradient(to bottom, #403e48, #35333c);
- border: 1px solid #45434e;
- box-shadow: 0 0 1px 1px #2f2d35;
-}
-
-/* Status display */
-
-#status {
+#status, #status-splash, #status-progress {
position: absolute;
left: 0;
- top: 0;
right: 0;
+}
+
+#status, #status-splash {
+ top: 0;
bottom: 0;
+}
+
+#status {
+ background-color: #38363A;
display: flex;
+ flex-direction: column;
justify-content: center;
align-items: center;
- /* don't consume click events - make children visible explicitly */
visibility: hidden;
}
-#status-progress {
- width: 366px;
- height: 7px;
- background-color: #38363A;
- border: 1px solid #444246;
- padding: 1px;
- box-shadow: 0 0 2px 1px #1B1C22;
- border-radius: 2px;
- visibility: visible;
-}
-
-@media only screen and (orientation:portrait) {
- #status-progress {
- width: 61.8%;
- }
+#status-splash {
+ max-height: 100%;
+ max-width: 100%;
+ margin: auto;
}
-#status-progress-inner {
- height: 100%;
- width: 0;
- box-sizing: border-box;
- transition: width 0.5s linear;
- background-color: #202020;
- border: 1px solid #222223;
- box-shadow: 0 0 1px 1px #27282E;
- border-radius: 3px;
+#status-progress, #status-notice {
+ display: none;
}
-#status-indeterminate {
- height: 42px;
- visibility: visible;
- position: relative;
-}
-
-#status-indeterminate > div {
- width: 4.5px;
- height: 0;
- border-style: solid;
- border-width: 9px 3px 0 3px;
- border-color: #2b2b2b transparent transparent transparent;
- transform-origin: center 21px;
- position: absolute;
+#status-progress {
+ bottom: 10%;
+ width: 50%;
+ margin: 0 auto;
}
-#status-indeterminate > div:nth-child(1) { transform: rotate( 22.5deg); }
-#status-indeterminate > div:nth-child(2) { transform: rotate( 67.5deg); }
-#status-indeterminate > div:nth-child(3) { transform: rotate(112.5deg); }
-#status-indeterminate > div:nth-child(4) { transform: rotate(157.5deg); }
-#status-indeterminate > div:nth-child(5) { transform: rotate(202.5deg); }
-#status-indeterminate > div:nth-child(6) { transform: rotate(247.5deg); }
-#status-indeterminate > div:nth-child(7) { transform: rotate(292.5deg); }
-#status-indeterminate > div:nth-child(8) { transform: rotate(337.5deg); }
-
#status-notice {
- margin: 0 100px;
+ background-color: #5b3943;
+ border-radius: 0.5rem;
+ border: 1px solid #9b3943;
+ color: #e0e0e0;
+ font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
line-height: 1.3;
- visibility: visible;
- padding: 4px 6px;
- visibility: visible;
+ margin: 0 2rem;
+ overflow: hidden;
+ padding: 1rem;
+ text-align: center;
+ z-index: 1;
}
</style>
$GODOT_HEAD_INCLUDE
</head>
<body>
<canvas id="canvas">
- HTML5 canvas appears to be unsupported in the current browser.<br >
- Please try updating or use a different browser.
+ Your browser does not support the canvas tag.
</canvas>
+
+ <noscript>
+ Your browser does not support JavaScript.
+ </noscript>
+
<div id="status">
- <div id="status-progress" style="display: none;" oncontextmenu="event.preventDefault();">
- <div id ="status-progress-inner"></div>
- </div>
- <div id="status-indeterminate" style="display: none;" oncontextmenu="event.preventDefault();">
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- </div>
- <div id="status-notice" class="godot" style="display: none;"></div>
+ <img id="status-splash" src="$GODOT_SPLASH" alt="">
+ <progress id="status-progress"></progress>
+ <div id="status-notice"></div>
</div>
<script src="$GODOT_URL"></script>
@@ -140,58 +100,25 @@ const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED;
const engine = new Engine(GODOT_CONFIG);
(function () {
- const INDETERMINATE_STATUS_STEP_MS = 100;
+ const statusOverlay = document.getElementById('status');
const statusProgress = document.getElementById('status-progress');
- const statusProgressInner = document.getElementById('status-progress-inner');
- const statusIndeterminate = document.getElementById('status-indeterminate');
const statusNotice = document.getElementById('status-notice');
let initializing = true;
- let statusMode = 'hidden';
-
- let animationCallbacks = [];
- function animate(time) {
- animationCallbacks.forEach((callback) => callback(time));
- requestAnimationFrame(animate);
- }
- requestAnimationFrame(animate);
-
- function animateStatusIndeterminate(ms) {
- const i = Math.floor((ms / INDETERMINATE_STATUS_STEP_MS) % 8);
- if (statusIndeterminate.children[i].style.borderTopColor === '') {
- Array.prototype.slice.call(statusIndeterminate.children).forEach((child) => {
- child.style.borderTopColor = '';
- });
- statusIndeterminate.children[i].style.borderTopColor = '#dfdfdf';
- }
- }
+ let statusMode = '';
function setStatusMode(mode) {
if (statusMode === mode || !initializing) {
return;
}
- [statusProgress, statusIndeterminate, statusNotice].forEach((elem) => {
- elem.style.display = 'none';
- });
- animationCallbacks = animationCallbacks.filter(function (value) {
- return (value !== animateStatusIndeterminate);
- });
- switch (mode) {
- case 'progress':
- statusProgress.style.display = 'block';
- break;
- case 'indeterminate':
- statusIndeterminate.style.display = 'block';
- animationCallbacks.push(animateStatusIndeterminate);
- break;
- case 'notice':
- statusNotice.style.display = 'block';
- break;
- case 'hidden':
- break;
- default:
- throw new Error('Invalid status mode');
+ if (mode === 'hidden') {
+ statusOverlay.remove();
+ initializing = false;
+ return;
}
+ statusOverlay.style.visibility = 'visible';
+ statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
+ statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
statusMode = mode;
}
@@ -217,6 +144,7 @@ const engine = new Engine(GODOT_CONFIG);
const missing = Engine.getMissingFeatures({
threads: GODOT_THREADS_ENABLED,
});
+
if (missing.length !== 0) {
if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
// There's a chance that installing the service worker would fix the issue
@@ -242,25 +170,19 @@ const engine = new Engine(GODOT_CONFIG);
displayFailureNotice(missingMsg + missing.join('\n'));
}
} else {
- setStatusMode('indeterminate');
+ setStatusMode('progress');
engine.startGame({
'onProgress': function (current, total) {
- if (total > 0) {
- statusProgressInner.style.width = `${(current / total) * 100}%`;
- setStatusMode('progress');
- if (current === total) {
- // wait for progress bar animation
- setTimeout(() => {
- setStatusMode('indeterminate');
- }, 500);
- }
+ if (current > 0 && total > 0) {
+ statusProgress.value = current;
+ statusProgress.max = total;
} else {
- setStatusMode('indeterminate');
+ statusProgress.removeAttribute('value');
+ statusProgress.removeAttribute('max');
}
},
}).then(() => {
setStatusMode('hidden');
- initializing = false;
}, displayFailureNotice);
}
}());
diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected
index 4706ed37f0..5a9976dc71 100644
--- a/misc/extension_api_validation/4.2-stable.expected
+++ b/misc/extension_api_validation/4.2-stable.expected
@@ -315,3 +315,25 @@ 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.
diff --git a/modules/astcenc/image_compress_astcenc.cpp b/modules/astcenc/image_compress_astcenc.cpp
index 31df83efae..941c1f44be 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 72e7977eef..d8ef1c0414 100644
--- a/modules/basis_universal/image_compress_basisu.cpp
+++ b/modules/basis_universal/image_compress_basisu.cpp
@@ -96,17 +96,74 @@ Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedCha
} break;
}
+ // Copy the source image data with mipmaps into BasisU.
{
- // Encode the image with mipmaps.
+ const int orig_width = image->get_width();
+ const int orig_height = image->get_height();
+
+ bool is_res_div_4 = (orig_width % 4 == 0) && (orig_height % 4 == 0);
+
+ // Image's resolution rounded up to the nearest values divisible by 4.
+ int next_width = orig_width <= 2 ? orig_width : (orig_width + 3) & ~3;
+ int next_height = orig_height <= 2 ? orig_height : (orig_height + 3) & ~3;
+
Vector<uint8_t> image_data = image->get_data();
basisu::vector<basisu::image> basisu_mipmaps;
+ // Buffer for storing padded mipmap data.
+ Vector<uint32_t> mip_data_padded;
+
for (int32_t i = 0; i <= image->get_mipmap_count(); i++) {
int ofs, size, width, height;
image->get_mipmap_offset_size_and_dimensions(i, ofs, size, width, height);
+ const uint8_t *image_mip_data = image_data.ptr() + ofs;
+
+ // Pad the mipmap's data if its resolution isn't divisible by 4.
+ if (image->has_mipmaps() && !is_res_div_4 && (width > 2 && height > 2) && (width != next_width || height != next_height)) {
+ // Source mip's data interpreted as 32-bit RGBA blocks to help with copying pixel data.
+ const uint32_t *mip_src_data = reinterpret_cast<const uint32_t *>(image_mip_data);
+
+ // Reserve space in the padded buffer.
+ mip_data_padded.resize(next_width * next_height);
+ uint32_t *data_padded_ptr = mip_data_padded.ptrw();
+
+ // Pad mipmap to the nearest block by smearing.
+ int x = 0, y = 0;
+ for (y = 0; y < height; y++) {
+ for (x = 0; x < width; x++) {
+ data_padded_ptr[next_width * y + x] = mip_src_data[width * y + x];
+ }
+
+ // First, smear in x.
+ for (; x < next_width; x++) {
+ data_padded_ptr[next_width * y + x] = data_padded_ptr[next_width * y + x - 1];
+ }
+ }
+
+ // Then, smear in y.
+ for (; y < next_height; y++) {
+ for (x = 0; x < next_width; x++) {
+ data_padded_ptr[next_width * y + x] = data_padded_ptr[next_width * y + x - next_width];
+ }
+ }
+
+ // Override the image_mip_data pointer with our temporary Vector.
+ image_mip_data = reinterpret_cast<const uint8_t *>(mip_data_padded.ptr());
+
+ // Override the mipmap's properties.
+ width = next_width;
+ height = next_height;
+ size = mip_data_padded.size() * 4;
+ }
+
+ // Get the next mipmap's resolution.
+ next_width /= 2;
+ next_height /= 2;
+
+ // Copy the source mipmap's data to a BasisU image.
basisu::image basisu_image(width, height);
- memcpy(basisu_image.get_ptr(), image_data.ptr() + ofs, size);
+ memcpy(basisu_image.get_ptr(), image_mip_data, size);
if (i == 0) {
params.m_source_images.push_back(basisu_image);
@@ -132,10 +189,10 @@ Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedCha
// Copy the encoded data to the buffer.
{
- uint8_t *w = basisu_data.ptrw();
- *(uint32_t *)w = decompress_format;
+ uint8_t *wb = basisu_data.ptrw();
+ *(uint32_t *)wb = decompress_format;
- memcpy(w + 4, basisu_out.get_ptr(), basisu_out.size());
+ memcpy(wb + 4, basisu_out.get_ptr(), basisu_out.size());
}
return basisu_data;
@@ -238,12 +295,11 @@ Ref<Image> basis_universal_unpacker_ptr(const uint8_t *p_data, int p_size) {
uint8_t *dst = out_data.ptrw();
memset(dst, 0, out_data.size());
- uint32_t mip_count = Image::get_image_required_mipmaps(basisu_info.m_orig_width, basisu_info.m_orig_height, image_format);
- for (uint32_t i = 0; i <= mip_count; i++) {
+ for (uint32_t i = 0; i < basisu_info.m_total_levels; i++) {
basist::basisu_image_level_info basisu_level;
transcoder.get_image_level_info(src_ptr, src_size, basisu_level, 0, i);
- 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/cvtt/image_compress_cvtt.cpp b/modules/cvtt/image_compress_cvtt.cpp
index e9a7009d7c..7335315c51 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/etcpak/image_compress_etcpak.cpp b/modules/etcpak/image_compress_etcpak.cpp
index dcd73101c2..4ce0cf50d9 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/gdscript/gdscript_byte_codegen.cpp b/modules/gdscript/gdscript_byte_codegen.cpp
index bfe090edb0..5a50bd8648 100644
--- a/modules/gdscript/gdscript_byte_codegen.cpp
+++ b/modules/gdscript/gdscript_byte_codegen.cpp
@@ -1196,23 +1196,49 @@ void GDScriptByteCodeGenerator::write_call_builtin_type_static(const Address &p_
}
void GDScriptByteCodeGenerator::write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector<Address> &p_arguments) {
- bool is_validated = false;
-
MethodBind *method = ClassDB::get_method(p_class, p_method);
- if (!is_validated) {
- // Perform regular call.
- append_opcode_and_argcount(GDScriptFunction::OPCODE_CALL_NATIVE_STATIC, p_arguments.size() + 1);
- for (int i = 0; i < p_arguments.size(); i++) {
- append(p_arguments[i]);
+ // Perform regular call.
+ append_opcode_and_argcount(GDScriptFunction::OPCODE_CALL_NATIVE_STATIC, p_arguments.size() + 1);
+ for (int i = 0; i < p_arguments.size(); i++) {
+ append(p_arguments[i]);
+ }
+ CallTarget ct = get_call_target(p_target);
+ append(ct.target);
+ append(method);
+ append(p_arguments.size());
+ ct.cleanup();
+ return;
+}
+
+void GDScriptByteCodeGenerator::write_call_native_static_validated(const GDScriptCodeGenerator::Address &p_target, MethodBind *p_method, const Vector<GDScriptCodeGenerator::Address> &p_arguments) {
+ Variant::Type return_type = Variant::NIL;
+ bool has_return = p_method->has_return();
+
+ if (has_return) {
+ PropertyInfo return_info = p_method->get_return_info();
+ return_type = return_info.type;
+ }
+
+ CallTarget ct = get_call_target(p_target, return_type);
+
+ if (has_return) {
+ Variant::Type temp_type = temporaries[ct.target.address].type;
+ if (temp_type != return_type) {
+ write_type_adjust(ct.target, return_type);
}
- CallTarget ct = get_call_target(p_target);
- append(ct.target);
- append(method);
- append(p_arguments.size());
- ct.cleanup();
- return;
}
+
+ GDScriptFunction::Opcode code = p_method->has_return() ? GDScriptFunction::OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN : GDScriptFunction::OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN;
+ append_opcode_and_argcount(code, 1 + p_arguments.size());
+
+ for (int i = 0; i < p_arguments.size(); i++) {
+ append(p_arguments[i]);
+ }
+ append(ct.target);
+ append(p_arguments.size());
+ append(p_method);
+ ct.cleanup();
}
void GDScriptByteCodeGenerator::write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) {
diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h
index 5a736b2554..34f56a2f5c 100644
--- a/modules/gdscript/gdscript_byte_codegen.h
+++ b/modules/gdscript/gdscript_byte_codegen.h
@@ -518,6 +518,7 @@ public:
virtual void write_call_builtin_type(const Address &p_target, const Address &p_base, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) override;
virtual void write_call_builtin_type_static(const Address &p_target, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) override;
virtual void write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector<Address> &p_arguments) override;
+ virtual void write_call_native_static_validated(const Address &p_target, MethodBind *p_method, const Vector<Address> &p_arguments) override;
virtual void write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) override;
virtual void write_call_method_bind_validated(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) override;
virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) override;
diff --git a/modules/gdscript/gdscript_codegen.h b/modules/gdscript/gdscript_codegen.h
index 4c33ed499a..c1c0b61395 100644
--- a/modules/gdscript/gdscript_codegen.h
+++ b/modules/gdscript/gdscript_codegen.h
@@ -131,6 +131,7 @@ public:
virtual void write_call_builtin_type(const Address &p_target, const Address &p_base, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) = 0;
virtual void write_call_builtin_type_static(const Address &p_target, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) = 0;
virtual void write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector<Address> &p_arguments) = 0;
+ virtual void write_call_native_static_validated(const Address &p_target, MethodBind *p_method, const Vector<Address> &p_arguments) = 0;
virtual void write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) = 0;
virtual void write_call_method_bind_validated(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) = 0;
virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) = 0;
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index 734e37bc09..a8a7f3d9f7 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -673,7 +673,15 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
} else if (!call->is_super && subscript->base->type == GDScriptParser::Node::IDENTIFIER && call->function_name != SNAME("new") &&
ClassDB::class_exists(static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name) && !Engine::get_singleton()->has_singleton(static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name)) {
// It's a static native method call.
- gen->write_call_native_static(result, static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name, subscript->attribute->name, arguments);
+ StringName class_name = static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name;
+ MethodBind *method = ClassDB::get_method(class_name, subscript->attribute->name);
+ if (_can_use_validate_call(method, arguments)) {
+ // Exact arguments, use validated call.
+ gen->write_call_native_static_validated(result, method, arguments);
+ } else {
+ // Not exact arguments, use regular static call
+ gen->write_call_native_static(result, class_name, subscript->attribute->name, arguments);
+ }
} else {
GDScriptCodeGenerator::Address base = _parse_expression(codegen, r_error, subscript->base);
if (r_error) {
diff --git a/modules/gdscript/gdscript_disassembler.cpp b/modules/gdscript/gdscript_disassembler.cpp
index c7873dcd52..8dd04c76dd 100644
--- a/modules/gdscript/gdscript_disassembler.cpp
+++ b/modules/gdscript/gdscript_disassembler.cpp
@@ -678,6 +678,50 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const {
incr += 4 + argc;
} break;
+ case OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN: {
+ int instr_var_args = _code_ptr[++ip];
+ text += "call native static method validated (return) ";
+ MethodBind *method = _methods_ptr[_code_ptr[ip + 2 + instr_var_args]];
+ int argc = _code_ptr[ip + 1 + instr_var_args];
+ text += DADDR(1 + argc) + " = ";
+ text += method->get_instance_class();
+ text += ".";
+ text += method->get_name();
+ text += "(";
+ for (int i = 0; i < argc; i++) {
+ if (i > 0)
+ text += ", ";
+ text += DADDR(1 + i);
+ }
+ text += ")";
+ incr = 4 + argc;
+ } break;
+
+ case OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN: {
+ int instr_var_args = _code_ptr[++ip];
+
+ text += "call native static method validated (no return) ";
+
+ MethodBind *method = _methods_ptr[_code_ptr[ip + 2 + instr_var_args]];
+
+ int argc = _code_ptr[ip + 1 + instr_var_args];
+
+ text += method->get_instance_class();
+ text += ".";
+ text += method->get_name();
+ text += "(";
+
+ for (int i = 0; i < argc; i++) {
+ if (i > 0) {
+ text += ", ";
+ }
+ text += DADDR(1 + i);
+ }
+ text += ")";
+
+ incr = 4 + argc;
+ } break;
+
case OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN: {
int instr_var_args = _code_ptr[++ip];
text += "call method-bind validated (return) ";
diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h
index 184d256bcd..430b96115b 100644
--- a/modules/gdscript/gdscript_function.h
+++ b/modules/gdscript/gdscript_function.h
@@ -264,6 +264,8 @@ public:
OPCODE_CALL_METHOD_BIND_RET,
OPCODE_CALL_BUILTIN_STATIC,
OPCODE_CALL_NATIVE_STATIC,
+ OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN,
+ OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN,
OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN,
OPCODE_CALL_METHOD_BIND_VALIDATED_NO_RETURN,
OPCODE_AWAIT,
diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp
index 842975698b..4e76965889 100644
--- a/modules/gdscript/gdscript_vm.cpp
+++ b/modules/gdscript/gdscript_vm.cpp
@@ -211,156 +211,158 @@ void (*type_init_function_table[])(Variant *) = {
};
#if defined(__GNUC__)
-#define OPCODES_TABLE \
- static const void *switch_table_ops[] = { \
- &&OPCODE_OPERATOR, \
- &&OPCODE_OPERATOR_VALIDATED, \
- &&OPCODE_TYPE_TEST_BUILTIN, \
- &&OPCODE_TYPE_TEST_ARRAY, \
- &&OPCODE_TYPE_TEST_NATIVE, \
- &&OPCODE_TYPE_TEST_SCRIPT, \
- &&OPCODE_SET_KEYED, \
- &&OPCODE_SET_KEYED_VALIDATED, \
- &&OPCODE_SET_INDEXED_VALIDATED, \
- &&OPCODE_GET_KEYED, \
- &&OPCODE_GET_KEYED_VALIDATED, \
- &&OPCODE_GET_INDEXED_VALIDATED, \
- &&OPCODE_SET_NAMED, \
- &&OPCODE_SET_NAMED_VALIDATED, \
- &&OPCODE_GET_NAMED, \
- &&OPCODE_GET_NAMED_VALIDATED, \
- &&OPCODE_SET_MEMBER, \
- &&OPCODE_GET_MEMBER, \
- &&OPCODE_SET_STATIC_VARIABLE, \
- &&OPCODE_GET_STATIC_VARIABLE, \
- &&OPCODE_ASSIGN, \
- &&OPCODE_ASSIGN_NULL, \
- &&OPCODE_ASSIGN_TRUE, \
- &&OPCODE_ASSIGN_FALSE, \
- &&OPCODE_ASSIGN_TYPED_BUILTIN, \
- &&OPCODE_ASSIGN_TYPED_ARRAY, \
- &&OPCODE_ASSIGN_TYPED_NATIVE, \
- &&OPCODE_ASSIGN_TYPED_SCRIPT, \
- &&OPCODE_CAST_TO_BUILTIN, \
- &&OPCODE_CAST_TO_NATIVE, \
- &&OPCODE_CAST_TO_SCRIPT, \
- &&OPCODE_CONSTRUCT, \
- &&OPCODE_CONSTRUCT_VALIDATED, \
- &&OPCODE_CONSTRUCT_ARRAY, \
- &&OPCODE_CONSTRUCT_TYPED_ARRAY, \
- &&OPCODE_CONSTRUCT_DICTIONARY, \
- &&OPCODE_CALL, \
- &&OPCODE_CALL_RETURN, \
- &&OPCODE_CALL_ASYNC, \
- &&OPCODE_CALL_UTILITY, \
- &&OPCODE_CALL_UTILITY_VALIDATED, \
- &&OPCODE_CALL_GDSCRIPT_UTILITY, \
- &&OPCODE_CALL_BUILTIN_TYPE_VALIDATED, \
- &&OPCODE_CALL_SELF_BASE, \
- &&OPCODE_CALL_METHOD_BIND, \
- &&OPCODE_CALL_METHOD_BIND_RET, \
- &&OPCODE_CALL_BUILTIN_STATIC, \
- &&OPCODE_CALL_NATIVE_STATIC, \
- &&OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN, \
- &&OPCODE_CALL_METHOD_BIND_VALIDATED_NO_RETURN, \
- &&OPCODE_AWAIT, \
- &&OPCODE_AWAIT_RESUME, \
- &&OPCODE_CREATE_LAMBDA, \
- &&OPCODE_CREATE_SELF_LAMBDA, \
- &&OPCODE_JUMP, \
- &&OPCODE_JUMP_IF, \
- &&OPCODE_JUMP_IF_NOT, \
- &&OPCODE_JUMP_TO_DEF_ARGUMENT, \
- &&OPCODE_JUMP_IF_SHARED, \
- &&OPCODE_RETURN, \
- &&OPCODE_RETURN_TYPED_BUILTIN, \
- &&OPCODE_RETURN_TYPED_ARRAY, \
- &&OPCODE_RETURN_TYPED_NATIVE, \
- &&OPCODE_RETURN_TYPED_SCRIPT, \
- &&OPCODE_ITERATE_BEGIN, \
- &&OPCODE_ITERATE_BEGIN_INT, \
- &&OPCODE_ITERATE_BEGIN_FLOAT, \
- &&OPCODE_ITERATE_BEGIN_VECTOR2, \
- &&OPCODE_ITERATE_BEGIN_VECTOR2I, \
- &&OPCODE_ITERATE_BEGIN_VECTOR3, \
- &&OPCODE_ITERATE_BEGIN_VECTOR3I, \
- &&OPCODE_ITERATE_BEGIN_STRING, \
- &&OPCODE_ITERATE_BEGIN_DICTIONARY, \
- &&OPCODE_ITERATE_BEGIN_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_PACKED_BYTE_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_PACKED_INT32_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_PACKED_INT64_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT32_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT64_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_PACKED_STRING_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR2_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR3_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_PACKED_COLOR_ARRAY, \
- &&OPCODE_ITERATE_BEGIN_OBJECT, \
- &&OPCODE_ITERATE, \
- &&OPCODE_ITERATE_INT, \
- &&OPCODE_ITERATE_FLOAT, \
- &&OPCODE_ITERATE_VECTOR2, \
- &&OPCODE_ITERATE_VECTOR2I, \
- &&OPCODE_ITERATE_VECTOR3, \
- &&OPCODE_ITERATE_VECTOR3I, \
- &&OPCODE_ITERATE_STRING, \
- &&OPCODE_ITERATE_DICTIONARY, \
- &&OPCODE_ITERATE_ARRAY, \
- &&OPCODE_ITERATE_PACKED_BYTE_ARRAY, \
- &&OPCODE_ITERATE_PACKED_INT32_ARRAY, \
- &&OPCODE_ITERATE_PACKED_INT64_ARRAY, \
- &&OPCODE_ITERATE_PACKED_FLOAT32_ARRAY, \
- &&OPCODE_ITERATE_PACKED_FLOAT64_ARRAY, \
- &&OPCODE_ITERATE_PACKED_STRING_ARRAY, \
- &&OPCODE_ITERATE_PACKED_VECTOR2_ARRAY, \
- &&OPCODE_ITERATE_PACKED_VECTOR3_ARRAY, \
- &&OPCODE_ITERATE_PACKED_COLOR_ARRAY, \
- &&OPCODE_ITERATE_OBJECT, \
- &&OPCODE_STORE_GLOBAL, \
- &&OPCODE_STORE_NAMED_GLOBAL, \
- &&OPCODE_TYPE_ADJUST_BOOL, \
- &&OPCODE_TYPE_ADJUST_INT, \
- &&OPCODE_TYPE_ADJUST_FLOAT, \
- &&OPCODE_TYPE_ADJUST_STRING, \
- &&OPCODE_TYPE_ADJUST_VECTOR2, \
- &&OPCODE_TYPE_ADJUST_VECTOR2I, \
- &&OPCODE_TYPE_ADJUST_RECT2, \
- &&OPCODE_TYPE_ADJUST_RECT2I, \
- &&OPCODE_TYPE_ADJUST_VECTOR3, \
- &&OPCODE_TYPE_ADJUST_VECTOR3I, \
- &&OPCODE_TYPE_ADJUST_TRANSFORM2D, \
- &&OPCODE_TYPE_ADJUST_VECTOR4, \
- &&OPCODE_TYPE_ADJUST_VECTOR4I, \
- &&OPCODE_TYPE_ADJUST_PLANE, \
- &&OPCODE_TYPE_ADJUST_QUATERNION, \
- &&OPCODE_TYPE_ADJUST_AABB, \
- &&OPCODE_TYPE_ADJUST_BASIS, \
- &&OPCODE_TYPE_ADJUST_TRANSFORM3D, \
- &&OPCODE_TYPE_ADJUST_PROJECTION, \
- &&OPCODE_TYPE_ADJUST_COLOR, \
- &&OPCODE_TYPE_ADJUST_STRING_NAME, \
- &&OPCODE_TYPE_ADJUST_NODE_PATH, \
- &&OPCODE_TYPE_ADJUST_RID, \
- &&OPCODE_TYPE_ADJUST_OBJECT, \
- &&OPCODE_TYPE_ADJUST_CALLABLE, \
- &&OPCODE_TYPE_ADJUST_SIGNAL, \
- &&OPCODE_TYPE_ADJUST_DICTIONARY, \
- &&OPCODE_TYPE_ADJUST_ARRAY, \
- &&OPCODE_TYPE_ADJUST_PACKED_BYTE_ARRAY, \
- &&OPCODE_TYPE_ADJUST_PACKED_INT32_ARRAY, \
- &&OPCODE_TYPE_ADJUST_PACKED_INT64_ARRAY, \
- &&OPCODE_TYPE_ADJUST_PACKED_FLOAT32_ARRAY, \
- &&OPCODE_TYPE_ADJUST_PACKED_FLOAT64_ARRAY, \
- &&OPCODE_TYPE_ADJUST_PACKED_STRING_ARRAY, \
- &&OPCODE_TYPE_ADJUST_PACKED_VECTOR2_ARRAY, \
- &&OPCODE_TYPE_ADJUST_PACKED_VECTOR3_ARRAY, \
- &&OPCODE_TYPE_ADJUST_PACKED_COLOR_ARRAY, \
- &&OPCODE_ASSERT, \
- &&OPCODE_BREAKPOINT, \
- &&OPCODE_LINE, \
- &&OPCODE_END \
- }; \
+#define OPCODES_TABLE \
+ static const void *switch_table_ops[] = { \
+ &&OPCODE_OPERATOR, \
+ &&OPCODE_OPERATOR_VALIDATED, \
+ &&OPCODE_TYPE_TEST_BUILTIN, \
+ &&OPCODE_TYPE_TEST_ARRAY, \
+ &&OPCODE_TYPE_TEST_NATIVE, \
+ &&OPCODE_TYPE_TEST_SCRIPT, \
+ &&OPCODE_SET_KEYED, \
+ &&OPCODE_SET_KEYED_VALIDATED, \
+ &&OPCODE_SET_INDEXED_VALIDATED, \
+ &&OPCODE_GET_KEYED, \
+ &&OPCODE_GET_KEYED_VALIDATED, \
+ &&OPCODE_GET_INDEXED_VALIDATED, \
+ &&OPCODE_SET_NAMED, \
+ &&OPCODE_SET_NAMED_VALIDATED, \
+ &&OPCODE_GET_NAMED, \
+ &&OPCODE_GET_NAMED_VALIDATED, \
+ &&OPCODE_SET_MEMBER, \
+ &&OPCODE_GET_MEMBER, \
+ &&OPCODE_SET_STATIC_VARIABLE, \
+ &&OPCODE_GET_STATIC_VARIABLE, \
+ &&OPCODE_ASSIGN, \
+ &&OPCODE_ASSIGN_NULL, \
+ &&OPCODE_ASSIGN_TRUE, \
+ &&OPCODE_ASSIGN_FALSE, \
+ &&OPCODE_ASSIGN_TYPED_BUILTIN, \
+ &&OPCODE_ASSIGN_TYPED_ARRAY, \
+ &&OPCODE_ASSIGN_TYPED_NATIVE, \
+ &&OPCODE_ASSIGN_TYPED_SCRIPT, \
+ &&OPCODE_CAST_TO_BUILTIN, \
+ &&OPCODE_CAST_TO_NATIVE, \
+ &&OPCODE_CAST_TO_SCRIPT, \
+ &&OPCODE_CONSTRUCT, \
+ &&OPCODE_CONSTRUCT_VALIDATED, \
+ &&OPCODE_CONSTRUCT_ARRAY, \
+ &&OPCODE_CONSTRUCT_TYPED_ARRAY, \
+ &&OPCODE_CONSTRUCT_DICTIONARY, \
+ &&OPCODE_CALL, \
+ &&OPCODE_CALL_RETURN, \
+ &&OPCODE_CALL_ASYNC, \
+ &&OPCODE_CALL_UTILITY, \
+ &&OPCODE_CALL_UTILITY_VALIDATED, \
+ &&OPCODE_CALL_GDSCRIPT_UTILITY, \
+ &&OPCODE_CALL_BUILTIN_TYPE_VALIDATED, \
+ &&OPCODE_CALL_SELF_BASE, \
+ &&OPCODE_CALL_METHOD_BIND, \
+ &&OPCODE_CALL_METHOD_BIND_RET, \
+ &&OPCODE_CALL_BUILTIN_STATIC, \
+ &&OPCODE_CALL_NATIVE_STATIC, \
+ &&OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN, \
+ &&OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN, \
+ &&OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN, \
+ &&OPCODE_CALL_METHOD_BIND_VALIDATED_NO_RETURN, \
+ &&OPCODE_AWAIT, \
+ &&OPCODE_AWAIT_RESUME, \
+ &&OPCODE_CREATE_LAMBDA, \
+ &&OPCODE_CREATE_SELF_LAMBDA, \
+ &&OPCODE_JUMP, \
+ &&OPCODE_JUMP_IF, \
+ &&OPCODE_JUMP_IF_NOT, \
+ &&OPCODE_JUMP_TO_DEF_ARGUMENT, \
+ &&OPCODE_JUMP_IF_SHARED, \
+ &&OPCODE_RETURN, \
+ &&OPCODE_RETURN_TYPED_BUILTIN, \
+ &&OPCODE_RETURN_TYPED_ARRAY, \
+ &&OPCODE_RETURN_TYPED_NATIVE, \
+ &&OPCODE_RETURN_TYPED_SCRIPT, \
+ &&OPCODE_ITERATE_BEGIN, \
+ &&OPCODE_ITERATE_BEGIN_INT, \
+ &&OPCODE_ITERATE_BEGIN_FLOAT, \
+ &&OPCODE_ITERATE_BEGIN_VECTOR2, \
+ &&OPCODE_ITERATE_BEGIN_VECTOR2I, \
+ &&OPCODE_ITERATE_BEGIN_VECTOR3, \
+ &&OPCODE_ITERATE_BEGIN_VECTOR3I, \
+ &&OPCODE_ITERATE_BEGIN_STRING, \
+ &&OPCODE_ITERATE_BEGIN_DICTIONARY, \
+ &&OPCODE_ITERATE_BEGIN_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_PACKED_BYTE_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_PACKED_INT32_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_PACKED_INT64_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT32_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT64_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_PACKED_STRING_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR2_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR3_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_PACKED_COLOR_ARRAY, \
+ &&OPCODE_ITERATE_BEGIN_OBJECT, \
+ &&OPCODE_ITERATE, \
+ &&OPCODE_ITERATE_INT, \
+ &&OPCODE_ITERATE_FLOAT, \
+ &&OPCODE_ITERATE_VECTOR2, \
+ &&OPCODE_ITERATE_VECTOR2I, \
+ &&OPCODE_ITERATE_VECTOR3, \
+ &&OPCODE_ITERATE_VECTOR3I, \
+ &&OPCODE_ITERATE_STRING, \
+ &&OPCODE_ITERATE_DICTIONARY, \
+ &&OPCODE_ITERATE_ARRAY, \
+ &&OPCODE_ITERATE_PACKED_BYTE_ARRAY, \
+ &&OPCODE_ITERATE_PACKED_INT32_ARRAY, \
+ &&OPCODE_ITERATE_PACKED_INT64_ARRAY, \
+ &&OPCODE_ITERATE_PACKED_FLOAT32_ARRAY, \
+ &&OPCODE_ITERATE_PACKED_FLOAT64_ARRAY, \
+ &&OPCODE_ITERATE_PACKED_STRING_ARRAY, \
+ &&OPCODE_ITERATE_PACKED_VECTOR2_ARRAY, \
+ &&OPCODE_ITERATE_PACKED_VECTOR3_ARRAY, \
+ &&OPCODE_ITERATE_PACKED_COLOR_ARRAY, \
+ &&OPCODE_ITERATE_OBJECT, \
+ &&OPCODE_STORE_GLOBAL, \
+ &&OPCODE_STORE_NAMED_GLOBAL, \
+ &&OPCODE_TYPE_ADJUST_BOOL, \
+ &&OPCODE_TYPE_ADJUST_INT, \
+ &&OPCODE_TYPE_ADJUST_FLOAT, \
+ &&OPCODE_TYPE_ADJUST_STRING, \
+ &&OPCODE_TYPE_ADJUST_VECTOR2, \
+ &&OPCODE_TYPE_ADJUST_VECTOR2I, \
+ &&OPCODE_TYPE_ADJUST_RECT2, \
+ &&OPCODE_TYPE_ADJUST_RECT2I, \
+ &&OPCODE_TYPE_ADJUST_VECTOR3, \
+ &&OPCODE_TYPE_ADJUST_VECTOR3I, \
+ &&OPCODE_TYPE_ADJUST_TRANSFORM2D, \
+ &&OPCODE_TYPE_ADJUST_VECTOR4, \
+ &&OPCODE_TYPE_ADJUST_VECTOR4I, \
+ &&OPCODE_TYPE_ADJUST_PLANE, \
+ &&OPCODE_TYPE_ADJUST_QUATERNION, \
+ &&OPCODE_TYPE_ADJUST_AABB, \
+ &&OPCODE_TYPE_ADJUST_BASIS, \
+ &&OPCODE_TYPE_ADJUST_TRANSFORM3D, \
+ &&OPCODE_TYPE_ADJUST_PROJECTION, \
+ &&OPCODE_TYPE_ADJUST_COLOR, \
+ &&OPCODE_TYPE_ADJUST_STRING_NAME, \
+ &&OPCODE_TYPE_ADJUST_NODE_PATH, \
+ &&OPCODE_TYPE_ADJUST_RID, \
+ &&OPCODE_TYPE_ADJUST_OBJECT, \
+ &&OPCODE_TYPE_ADJUST_CALLABLE, \
+ &&OPCODE_TYPE_ADJUST_SIGNAL, \
+ &&OPCODE_TYPE_ADJUST_DICTIONARY, \
+ &&OPCODE_TYPE_ADJUST_ARRAY, \
+ &&OPCODE_TYPE_ADJUST_PACKED_BYTE_ARRAY, \
+ &&OPCODE_TYPE_ADJUST_PACKED_INT32_ARRAY, \
+ &&OPCODE_TYPE_ADJUST_PACKED_INT64_ARRAY, \
+ &&OPCODE_TYPE_ADJUST_PACKED_FLOAT32_ARRAY, \
+ &&OPCODE_TYPE_ADJUST_PACKED_FLOAT64_ARRAY, \
+ &&OPCODE_TYPE_ADJUST_PACKED_STRING_ARRAY, \
+ &&OPCODE_TYPE_ADJUST_PACKED_VECTOR2_ARRAY, \
+ &&OPCODE_TYPE_ADJUST_PACKED_VECTOR3_ARRAY, \
+ &&OPCODE_TYPE_ADJUST_PACKED_COLOR_ARRAY, \
+ &&OPCODE_ASSERT, \
+ &&OPCODE_BREAKPOINT, \
+ &&OPCODE_LINE, \
+ &&OPCODE_END \
+ }; \
static_assert((sizeof(switch_table_ops) / sizeof(switch_table_ops[0]) == (OPCODE_END + 1)), "Opcodes in jump table aren't the same as opcodes in enum.");
#define OPCODE(m_op) \
@@ -882,23 +884,27 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
#endif
#ifdef DEBUG_ENABLED
if (!valid) {
- Object *obj = dst->get_validated_object();
- String v = index->operator String();
- bool read_only_property = false;
- if (obj) {
- read_only_property = ClassDB::has_property(obj->get_class_name(), v) && (ClassDB::get_property_setter(obj->get_class_name(), v) == StringName());
- }
- if (read_only_property) {
- err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", v, _get_var_type(dst));
+ if (dst->is_read_only()) {
+ err_text = "Invalid assignment on read-only value (on base: '" + _get_var_type(dst) + "').";
} else {
- if (!v.is_empty()) {
- v = "'" + v + "'";
- } else {
- v = "of type '" + _get_var_type(index) + "'";
+ Object *obj = dst->get_validated_object();
+ String v = index->operator String();
+ bool read_only_property = false;
+ if (obj) {
+ read_only_property = ClassDB::has_property(obj->get_class_name(), v) && (ClassDB::get_property_setter(obj->get_class_name(), v) == StringName());
}
- err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'.";
- if (err_code == Variant::VariantSetError::SET_INDEXED_ERR) {
- err_text = "Invalid assignment of index " + v + " (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'.";
+ if (read_only_property) {
+ err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", v, _get_var_type(dst));
+ } else {
+ if (!v.is_empty()) {
+ v = "'" + v + "'";
+ } else {
+ v = "of type '" + _get_var_type(index) + "'";
+ }
+ err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'.";
+ if (err_code == Variant::VariantSetError::SET_INDEXED_ERR) {
+ err_text = "Invalid assignment of index " + v + " (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'.";
+ }
}
}
OPCODE_BREAK;
@@ -924,13 +930,17 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
#ifdef DEBUG_ENABLED
if (!valid) {
- String v = index->operator String();
- if (!v.is_empty()) {
- v = "'" + v + "'";
+ if (dst->is_read_only()) {
+ err_text = "Invalid assignment on read-only value (on base: '" + _get_var_type(dst) + "').";
} else {
- v = "of type '" + _get_var_type(index) + "'";
+ String v = index->operator String();
+ if (!v.is_empty()) {
+ v = "'" + v + "'";
+ } else {
+ v = "of type '" + _get_var_type(index) + "'";
+ }
+ err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'.";
}
- err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'.";
OPCODE_BREAK;
}
#endif
@@ -956,13 +966,17 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
#ifdef DEBUG_ENABLED
if (oob) {
- String v = index->operator String();
- if (!v.is_empty()) {
- v = "'" + v + "'";
+ if (dst->is_read_only()) {
+ err_text = "Invalid assignment on read-only value (on base: '" + _get_var_type(dst) + "').";
} else {
- v = "of type '" + _get_var_type(index) + "'";
+ String v = index->operator String();
+ if (!v.is_empty()) {
+ v = "'" + v + "'";
+ } else {
+ v = "of type '" + _get_var_type(index) + "'";
+ }
+ err_text = "Out of bounds set index " + v + " (on base: '" + _get_var_type(dst) + "')";
}
- err_text = "Out of bounds set index " + v + " (on base: '" + _get_var_type(dst) + "')";
OPCODE_BREAK;
}
#endif
@@ -1090,15 +1104,19 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
#ifdef DEBUG_ENABLED
if (!valid) {
- Object *obj = dst->get_validated_object();
- bool read_only_property = false;
- if (obj) {
- read_only_property = ClassDB::has_property(obj->get_class_name(), *index) && (ClassDB::get_property_setter(obj->get_class_name(), *index) == StringName());
- }
- if (read_only_property) {
- err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", String(*index), _get_var_type(dst));
+ if (dst->is_read_only()) {
+ err_text = "Invalid assignment on read-only value (on base: '" + _get_var_type(dst) + "').";
} else {
- err_text = "Invalid assignment of property or key '" + String(*index) + "' with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'.";
+ Object *obj = dst->get_validated_object();
+ bool read_only_property = false;
+ if (obj) {
+ read_only_property = ClassDB::has_property(obj->get_class_name(), *index) && (ClassDB::get_property_setter(obj->get_class_name(), *index) == StringName());
+ }
+ if (read_only_property) {
+ err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", String(*index), _get_var_type(dst));
+ } else {
+ err_text = "Invalid assignment of property or key '" + String(*index) + "' with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'.";
+ }
}
OPCODE_BREAK;
}
@@ -1956,6 +1974,78 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
}
DISPATCH_OPCODE;
+ OPCODE(OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN) {
+ LOAD_INSTRUCTION_ARGS
+ CHECK_SPACE(3 + instr_arg_count);
+
+ ip += instr_arg_count;
+
+ int argc = _code_ptr[ip + 1];
+ GD_ERR_BREAK(argc < 0);
+
+ GD_ERR_BREAK(_code_ptr[ip + 2] < 0 || _code_ptr[ip + 2] >= _methods_count);
+ MethodBind *method = _methods_ptr[_code_ptr[ip + 2]];
+
+ Variant **argptrs = instruction_args;
+
+#ifdef DEBUG_ENABLED
+ uint64_t call_time = 0;
+ if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) {
+ call_time = OS::get_singleton()->get_ticks_usec();
+ }
+#endif
+
+ GET_INSTRUCTION_ARG(ret, argc);
+ method->validated_call(nullptr, (const Variant **)argptrs, ret);
+
+#ifdef DEBUG_ENABLED
+ if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) {
+ uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time;
+ _profile_native_call(t_taken, method->get_name(), method->get_instance_class());
+ function_call_time += t_taken;
+ }
+#endif
+
+ ip += 3;
+ }
+ DISPATCH_OPCODE;
+
+ OPCODE(OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN) {
+ LOAD_INSTRUCTION_ARGS
+ CHECK_SPACE(3 + instr_arg_count);
+
+ ip += instr_arg_count;
+
+ int argc = _code_ptr[ip + 1];
+ GD_ERR_BREAK(argc < 0);
+
+ GD_ERR_BREAK(_code_ptr[ip + 2] < 0 || _code_ptr[ip + 2] >= _methods_count);
+ MethodBind *method = _methods_ptr[_code_ptr[ip + 2]];
+
+ Variant **argptrs = instruction_args;
+#ifdef DEBUG_ENABLED
+ uint64_t call_time = 0;
+ if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) {
+ call_time = OS::get_singleton()->get_ticks_usec();
+ }
+#endif
+
+ GET_INSTRUCTION_ARG(ret, argc);
+ VariantInternal::initialize(ret, Variant::NIL);
+ method->validated_call(nullptr, (const Variant **)argptrs, nullptr);
+
+#ifdef DEBUG_ENABLED
+ if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) {
+ uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time;
+ _profile_native_call(t_taken, method->get_name(), method->get_instance_class());
+ function_call_time += t_taken;
+ }
+#endif
+
+ ip += 3;
+ }
+ DISPATCH_OPCODE;
+
OPCODE(OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN) {
LOAD_INSTRUCTION_ARGS
CHECK_SPACE(3 + instr_arg_count);
diff --git a/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out b/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out
index c524a1ae6b..350d5d1d45 100644
--- a/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out
+++ b/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out
@@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR
>> on function: test()
>> runtime/errors/constant_array_is_deep.gd
>> 6
->> Invalid assignment of property or key '0' with value of type 'int' on a base object of type 'Dictionary'.
+>> Invalid assignment on read-only value (on base: 'Dictionary').
diff --git a/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out b/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out
index cf51b0262d..5f1f372b0a 100644
--- a/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out
+++ b/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out
@@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR
>> on function: test()
>> runtime/errors/constant_dictionary_is_deep.gd
>> 6
->> Invalid assignment of index '0' (on base: 'Array') with value of type 'int'.
+>> Invalid assignment on read-only value (on base: 'Array').
diff --git a/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd
new file mode 100644
index 0000000000..2f31ecc52f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd
@@ -0,0 +1,4 @@
+func test():
+ var dictionary := { "a": 0 }
+ dictionary.make_read_only()
+ dictionary.a = 1
diff --git a/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out
new file mode 100644
index 0000000000..f7d531e119
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out
@@ -0,0 +1,6 @@
+GDTEST_RUNTIME_ERROR
+>> SCRIPT ERROR
+>> on function: test()
+>> runtime/errors/read_only_dictionary.gd
+>> 4
+>> Invalid assignment on read-only value (on base: 'Dictionary').
diff --git a/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd
new file mode 100644
index 0000000000..35e4dbd6a0
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd
@@ -0,0 +1,7 @@
+func test():
+ # Validated native static call with return value.
+ print(FileAccess.file_exists("some_file"))
+
+ # Validated native static call without return value.
+ Node.print_orphan_nodes()
+
diff --git a/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out
new file mode 100644
index 0000000000..44302c8137
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+false
diff --git a/modules/mobile_vr/doc_classes/MobileVRInterface.xml b/modules/mobile_vr/doc_classes/MobileVRInterface.xml
index 1be8cc828d..8338054142 100644
--- a/modules/mobile_vr/doc_classes/MobileVRInterface.xml
+++ b/modules/mobile_vr/doc_classes/MobileVRInterface.xml
@@ -34,6 +34,9 @@
<member name="k2" type="float" setter="set_k2" getter="get_k2" default="0.215">
The k2 lens factor, see k1.
</member>
+ <member name="offset_rect" type="Rect2" setter="set_offset_rect" getter="get_offset_rect" default="Rect2(0, 0, 1, 1)">
+ Set the offset rect relative to the area being rendered. A length of 1 represents the whole rendering area on that axis.
+ </member>
<member name="oversample" type="float" setter="set_oversample" getter="get_oversample" default="1.5">
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.
</member>
diff --git a/modules/mobile_vr/mobile_vr_interface.cpp b/modules/mobile_vr/mobile_vr_interface.cpp
index bba56f6468..d23edcd1d1 100644
--- a/modules/mobile_vr/mobile_vr_interface.cpp
+++ b/modules/mobile_vr/mobile_vr_interface.cpp
@@ -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,6 +246,7 @@ 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");
@@ -256,6 +260,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;
};
@@ -483,6 +495,8 @@ Vector<BlitToScreen> MobileVRInterface::post_draw_viewport(RID p_render_target,
// Because we are rendering to our device we must use our main viewport!
ERR_FAIL_COND_V(p_screen_rect == Rect2(), 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;
blit.render_target = p_render_target;
@@ -494,16 +508,16 @@ Vector<BlitToScreen> 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);
diff --git a/modules/mobile_vr/mobile_vr_interface.h b/modules/mobile_vr/mobile_vr_interface.h
index f680d8aa11..e1d43fff74 100644
--- a/modules/mobile_vr/mobile_vr_interface.h
+++ b/modules/mobile_vr/mobile_vr_interface.h
@@ -62,6 +62,8 @@ private:
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;
@@ -121,6 +123,9 @@ public:
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;
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 3cc5841097..724fb164e0 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 @@ public class ScriptPropertiesGeneratorTests
"ScriptBoilerplate_ScriptProperties.generated.cs", "OuterClass.NestedClass_ScriptProperties.generated.cs"
);
}
+
+ [Fact]
+ public async void AbstractGenericNode()
+ {
+ await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.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 0000000000..a561c5fc0d
--- /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<T>
+{
+#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword
+ /// <summary>
+ /// Cached StringNames for the properties and fields contained in this class, for fast lookup.
+ /// </summary>
+ public new class PropertyName : global::Godot.Node.PropertyName {
+ /// <summary>
+ /// Cached name for the 'MyArray' property.
+ /// </summary>
+ public new static readonly global::Godot.StringName MyArray = "MyArray";
+ }
+ /// <inheritdoc/>
+ [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<T>(value);
+ return true;
+ }
+ return base.SetGodotClassPropertyValue(name, value);
+ }
+ /// <inheritdoc/>
+ [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);
+ }
+ /// <summary>
+ /// 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.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList()
+ {
+ var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>();
+ 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 0000000000..cee4f67921
--- /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<T> 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 462da31d66..2b5eecab8a 100644
--- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs
@@ -66,6 +66,12 @@ public class MustBeVariantGD0301
Method<Rid[]>();
}
+ public void MethodCallDynamic()
+ {
+ dynamic self = this;
+ self.Method<object>();
+ }
+
public void Method<[MustBeVariant] T>()
{
}
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs
index 95eaca4d3d..e894e7a86c 100644
--- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs
@@ -50,8 +50,18 @@ namespace Godot.SourceGenerators
var typeSymbol = sm.GetSymbolInfo(typeSyntax).Symbol as ITypeSymbol;
Helper.ThrowIfNull(typeSymbol);
- var parentSymbol = sm.GetSymbolInfo(parentSyntax).Symbol;
- Helper.ThrowIfNull(parentSymbol);
+ var parentSymbolInfo = sm.GetSymbolInfo(parentSyntax);
+ var parentSymbol = parentSymbolInfo.Symbol;
+ if (parentSymbol == null)
+ {
+ if (parentSymbolInfo.CandidateReason == CandidateReason.LateBound)
+ {
+ // Invocations on dynamic are late bound so we can't retrieve the symbol.
+ continue;
+ }
+
+ Helper.ThrowIfNull(parentSymbol);
+ }
if (!ShouldCheckTypeArgument(context, parentSyntax, parentSymbol, typeSyntax, typeSymbol, i))
{
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs
index a0e410e31a..21223654f3 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 @@ namespace Godot.SourceGenerators
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/glue/GodotSharp/GodotSharp/Core/Aabb.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs
index feaa1d07da..ab7f8ede44 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs
@@ -69,7 +69,7 @@ namespace Godot
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/Rect2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs
index 9d9065911e..19721b6cca 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs
@@ -69,7 +69,7 @@ namespace Godot
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 @@ namespace Godot
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 @@ namespace Godot
{
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 65704b3da7..7ee9ff8552 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs
@@ -69,7 +69,7 @@ namespace Godot
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 @@ namespace Godot
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 @@ namespace Godot
{
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/Vector2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs
index 856fd54352..50bf56d832 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs
@@ -192,6 +192,23 @@ namespace Godot
}
/// <summary>
+ /// Returns a new vector with all components clamped between the
+ /// <paramref name="min"/> and <paramref name="max"/> using
+ /// <see cref="Mathf.Clamp(real_t, real_t, real_t)"/>.
+ /// </summary>
+ /// <param name="min">The minimum allowed value.</param>
+ /// <param name="max">The maximum allowed value.</param>
+ /// <returns>The vector with all components clamped.</returns>
+ public readonly Vector2 Clamp(real_t min, real_t max)
+ {
+ return new Vector2
+ (
+ Mathf.Clamp(X, min, max),
+ Mathf.Clamp(Y, min, max)
+ );
+ }
+
+ /// <summary>
/// Returns the cross product of this vector and <paramref name="with"/>.
/// </summary>
/// <param name="with">The other vector.</param>
@@ -413,6 +430,70 @@ namespace Godot
}
/// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector2(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ public readonly Vector2 Max(Vector2 with)
+ {
+ return new Vector2
+ (
+ Mathf.Max(X, with.X),
+ Mathf.Max(Y, with.Y)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector2(Mathf.Max(X, with), Mathf.Max(Y, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ public readonly Vector2 Max(real_t with)
+ {
+ return new Vector2
+ (
+ Mathf.Max(X, with),
+ Mathf.Max(Y, with)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector2(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ public readonly Vector2 Min(Vector2 with)
+ {
+ return new Vector2
+ (
+ Mathf.Min(X, with.X),
+ Mathf.Min(Y, with.Y)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector2(Mathf.Min(X, with), Mathf.Min(Y, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ public readonly Vector2 Min(real_t with)
+ {
+ return new Vector2
+ (
+ Mathf.Min(X, with),
+ Mathf.Min(Y, with)
+ );
+ }
+
+ /// <summary>
/// Returns the axis of the vector's highest value. See <see cref="Axis"/>.
/// If both components are equal, this method returns <see cref="Axis.X"/>.
/// </summary>
@@ -600,7 +681,7 @@ namespace Godot
}
/// <summary>
- /// Returns this vector with each component snapped to the nearest multiple of <paramref name="step"/>.
+ /// Returns a new vector with each component snapped to the nearest multiple of the corresponding component in <paramref name="step"/>.
/// This can also be used to round to an arbitrary number of decimals.
/// </summary>
/// <param name="step">A vector value representing the step size to snap to.</param>
@@ -611,6 +692,17 @@ namespace Godot
}
/// <summary>
+ /// Returns a new vector with each component snapped to the nearest multiple of <paramref name="step"/>.
+ /// This can also be used to round to an arbitrary number of decimals.
+ /// </summary>
+ /// <param name="step">The step size to snap to.</param>
+ /// <returns>The snapped vector.</returns>
+ public readonly Vector2 Snapped(real_t step)
+ {
+ return new Vector2(Mathf.Snapped(X, step), Mathf.Snapped(Y, step));
+ }
+
+ /// <summary>
/// Returns a perpendicular vector rotated 90 degrees counter-clockwise
/// compared to the original, with the same length.
/// </summary>
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs
index 511cc7971c..9442db4d86 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs
@@ -125,6 +125,23 @@ namespace Godot
}
/// <summary>
+ /// Returns a new vector with all components clamped between the
+ /// <paramref name="min"/> and <paramref name="max"/> using
+ /// <see cref="Mathf.Clamp(int, int, int)"/>.
+ /// </summary>
+ /// <param name="min">The minimum allowed value.</param>
+ /// <param name="max">The maximum allowed value.</param>
+ /// <returns>The vector with all components clamped.</returns>
+ public readonly Vector2I Clamp(int min, int max)
+ {
+ return new Vector2I
+ (
+ Mathf.Clamp(X, min, max),
+ Mathf.Clamp(Y, min, max)
+ );
+ }
+
+ /// <summary>
/// Returns the squared distance between this vector and <paramref name="to"/>.
/// This method runs faster than <see cref="DistanceTo"/>, so prefer it if
/// you need to compare vectors or need the squared distance for some formula.
@@ -175,6 +192,70 @@ namespace Godot
}
/// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector2I(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ public readonly Vector2I Max(Vector2I with)
+ {
+ return new Vector2I
+ (
+ Mathf.Max(X, with.X),
+ Mathf.Max(Y, with.Y)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector2I(Mathf.Max(X, with), Mathf.Max(Y, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ public readonly Vector2I Max(int with)
+ {
+ return new Vector2I
+ (
+ Mathf.Max(X, with),
+ Mathf.Max(Y, with)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector2I(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ public readonly Vector2I Min(Vector2I with)
+ {
+ return new Vector2I
+ (
+ Mathf.Min(X, with.X),
+ Mathf.Min(Y, with.Y)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector2I(Mathf.Min(X, with), Mathf.Min(Y, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ public readonly Vector2I Min(int with)
+ {
+ return new Vector2I
+ (
+ Mathf.Min(X, with),
+ Mathf.Min(Y, with)
+ );
+ }
+
+ /// <summary>
/// Returns the axis of the vector's highest value. See <see cref="Axis"/>.
/// If both components are equal, this method returns <see cref="Axis.X"/>.
/// </summary>
@@ -208,6 +289,34 @@ namespace Godot
return v;
}
+ /// <summary>
+ /// Returns a new vector with each component snapped to the closest multiple of the corresponding component in <paramref name="step"/>.
+ /// </summary>
+ /// <param name="step">A vector value representing the step size to snap to.</param>
+ /// <returns>The snapped vector.</returns>
+ public readonly Vector2I Snapped(Vector2I step)
+ {
+ return new Vector2I
+ (
+ (int)Mathf.Snapped((double)X, (double)step.X),
+ (int)Mathf.Snapped((double)Y, (double)step.Y)
+ );
+ }
+
+ /// <summary>
+ /// Returns a new vector with each component snapped to the closest multiple of <paramref name="step"/>.
+ /// </summary>
+ /// <param name="step">The step size to snap to.</param>
+ /// <returns>The snapped vector.</returns>
+ public readonly Vector2I Snapped(int step)
+ {
+ return new Vector2I
+ (
+ (int)Mathf.Snapped((double)X, (double)step),
+ (int)Mathf.Snapped((double)Y, (double)step)
+ );
+ }
+
// Constants
private static readonly Vector2I _minValue = new Vector2I(int.MinValue, int.MinValue);
private static readonly Vector2I _maxValue = new Vector2I(int.MaxValue, int.MaxValue);
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs
index 6300705107..27f2713efa 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs
@@ -179,6 +179,24 @@ namespace Godot
}
/// <summary>
+ /// Returns a new vector with all components clamped between the
+ /// <paramref name="min"/> and <paramref name="max"/> using
+ /// <see cref="Mathf.Clamp(real_t, real_t, real_t)"/>.
+ /// </summary>
+ /// <param name="min">The minimum allowed value.</param>
+ /// <param name="max">The maximum allowed value.</param>
+ /// <returns>The vector with all components clamped.</returns>
+ public readonly Vector3 Clamp(real_t min, real_t max)
+ {
+ return new Vector3
+ (
+ Mathf.Clamp(X, min, max),
+ Mathf.Clamp(Y, min, max),
+ Mathf.Clamp(Z, min, max)
+ );
+ }
+
+ /// <summary>
/// Returns the cross product of this vector and <paramref name="with"/>.
/// </summary>
/// <param name="with">The other vector.</param>
@@ -419,6 +437,57 @@ namespace Godot
}
/// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector3(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y), Mathf.Max(Z, with.Z))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ public readonly Vector3 Max(Vector3 with)
+ {
+ return new Vector3
+ (
+ Mathf.Max(X, with.X),
+ Mathf.Max(Y, with.Y),
+ Mathf.Max(Z, with.Z)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector3(Mathf.Max(X, with), Mathf.Max(Y, with), Mathf.Max(Z, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ public readonly Vector3 Max(real_t with)
+ {
+ return new Vector3
+ (
+ Mathf.Max(X, with),
+ Mathf.Max(Y, with),
+ Mathf.Max(Z, with)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector3(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y), Mathf.Min(Z, with.Z))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ public readonly Vector3 Min(Vector3 with)
+ {
+ return new Vector3
+ (
+ Mathf.Min(X, with.X),
+ Mathf.Min(Y, with.Y),
+ Mathf.Min(Z, with.Z)
+ );
+ }
+
+ /// <summary>
/// Returns the axis of the vector's highest value. See <see cref="Axis"/>.
/// If all components are equal, this method returns <see cref="Axis.X"/>.
/// </summary>
@@ -643,7 +712,7 @@ namespace Godot
}
/// <summary>
- /// Returns this vector with each component snapped to the nearest multiple of <paramref name="step"/>.
+ /// Returns a new vector with each component snapped to the nearest multiple of the corresponding component in <paramref name="step"/>.
/// This can also be used to round to an arbitrary number of decimals.
/// </summary>
/// <param name="step">A vector value representing the step size to snap to.</param>
@@ -658,6 +727,22 @@ namespace Godot
);
}
+ /// <summary>
+ /// Returns a new vector with each component snapped to the nearest multiple of <paramref name="step"/>.
+ /// This can also be used to round to an arbitrary number of decimals.
+ /// </summary>
+ /// <param name="step">The step size to snap to.</param>
+ /// <returns>The snapped vector.</returns>
+ public readonly Vector3 Snapped(real_t step)
+ {
+ return new Vector3
+ (
+ Mathf.Snapped(X, step),
+ Mathf.Snapped(Y, step),
+ Mathf.Snapped(Z, step)
+ );
+ }
+
// Constants
private static readonly Vector3 _zero = new Vector3(0, 0, 0);
private static readonly Vector3 _one = new Vector3(1, 1, 1);
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs
index aea46efc5b..8312e2c231 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs
@@ -133,6 +133,24 @@ namespace Godot
}
/// <summary>
+ /// Returns a new vector with all components clamped between the
+ /// <paramref name="min"/> and <paramref name="max"/> using
+ /// <see cref="Mathf.Clamp(int, int, int)"/>.
+ /// </summary>
+ /// <param name="min">The minimum allowed value.</param>
+ /// <param name="max">The maximum allowed value.</param>
+ /// <returns>The vector with all components clamped.</returns>
+ public readonly Vector3I Clamp(int min, int max)
+ {
+ return new Vector3I
+ (
+ Mathf.Clamp(X, min, max),
+ Mathf.Clamp(Y, min, max),
+ Mathf.Clamp(Z, min, max)
+ );
+ }
+
+ /// <summary>
/// Returns the squared distance between this vector and <paramref name="to"/>.
/// This method runs faster than <see cref="DistanceTo"/>, so prefer it if
/// you need to compare vectors or need the squared distance for some formula.
@@ -185,6 +203,74 @@ namespace Godot
}
/// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector3I(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y), Mathf.Max(Z, with.Z))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ public readonly Vector3I Max(Vector3I with)
+ {
+ return new Vector3I
+ (
+ Mathf.Max(X, with.X),
+ Mathf.Max(Y, with.Y),
+ Mathf.Max(Z, with.Z)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector3I(Mathf.Max(X, with), Mathf.Max(Y, with), Mathf.Max(Z, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ public readonly Vector3I Max(int with)
+ {
+ return new Vector3I
+ (
+ Mathf.Max(X, with),
+ Mathf.Max(Y, with),
+ Mathf.Max(Z, with)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector3I(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y), Mathf.Min(Z, with.Z))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ public readonly Vector3I Min(Vector3I with)
+ {
+ return new Vector3I
+ (
+ Mathf.Min(X, with.X),
+ Mathf.Min(Y, with.Y),
+ Mathf.Min(Z, with.Z)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector3I(Mathf.Min(X, with), Mathf.Min(Y, with), Mathf.Min(Z, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ public readonly Vector3I Min(int with)
+ {
+ return new Vector3I
+ (
+ Mathf.Min(X, with),
+ Mathf.Min(Y, with),
+ Mathf.Min(Z, with)
+ );
+ }
+
+ /// <summary>
/// Returns the axis of the vector's highest value. See <see cref="Axis"/>.
/// If all components are equal, this method returns <see cref="Axis.X"/>.
/// </summary>
@@ -219,6 +305,36 @@ namespace Godot
return v;
}
+ /// <summary>
+ /// Returns a new vector with each component snapped to the closest multiple of the corresponding component in <paramref name="step"/>.
+ /// </summary>
+ /// <param name="step">A vector value representing the step size to snap to.</param>
+ /// <returns>The snapped vector.</returns>
+ public readonly Vector3I Snapped(Vector3I step)
+ {
+ return new Vector3I
+ (
+ (int)Mathf.Snapped((double)X, (double)step.X),
+ (int)Mathf.Snapped((double)Y, (double)step.Y),
+ (int)Mathf.Snapped((double)Z, (double)step.Z)
+ );
+ }
+
+ /// <summary>
+ /// Returns a new vector with each component snapped to the closest multiple of <paramref name="step"/>.
+ /// </summary>
+ /// <param name="step">The step size to snap to.</param>
+ /// <returns>The snapped vector.</returns>
+ public readonly Vector3I Snapped(int step)
+ {
+ return new Vector3I
+ (
+ (int)Mathf.Snapped((double)X, (double)step),
+ (int)Mathf.Snapped((double)Y, (double)step),
+ (int)Mathf.Snapped((double)Z, (double)step)
+ );
+ }
+
// Constants
private static readonly Vector3I _minValue = new Vector3I(int.MinValue, int.MinValue, int.MinValue);
private static readonly Vector3I _maxValue = new Vector3I(int.MaxValue, int.MaxValue, int.MaxValue);
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs
index 7c4832943c..ec59197fa3 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs
@@ -177,6 +177,25 @@ namespace Godot
}
/// <summary>
+ /// Returns a new vector with all components clamped between the
+ /// <paramref name="min"/> and <paramref name="max"/> using
+ /// <see cref="Mathf.Clamp(real_t, real_t, real_t)"/>.
+ /// </summary>
+ /// <param name="min">The minimum allowed value.</param>
+ /// <param name="max">The maximum allowed value.</param>
+ /// <returns>The vector with all components clamped.</returns>
+ public readonly Vector4 Clamp(real_t min, real_t max)
+ {
+ return new Vector4
+ (
+ Mathf.Clamp(X, min, max),
+ Mathf.Clamp(Y, min, max),
+ Mathf.Clamp(Z, min, max),
+ Mathf.Clamp(W, min, max)
+ );
+ }
+
+ /// <summary>
/// Performs a cubic interpolation between vectors <paramref name="preA"/>, this vector,
/// <paramref name="b"/>, and <paramref name="postB"/>, by the given amount <paramref name="weight"/>.
/// </summary>
@@ -352,6 +371,78 @@ namespace Godot
}
/// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector4(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y), Mathf.Max(Z, with.Z), Mathf.Max(W, with.W))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ 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)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector4(Mathf.Max(X, with), Mathf.Max(Y, with), Mathf.Max(Z, with), Mathf.Max(W, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ 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)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector4(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y), Mathf.Min(Z, with.Z), Mathf.Min(W, with.W))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ 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)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector4(Mathf.Min(X, with), Mathf.Min(Y, with), Mathf.Min(Z, with), Mathf.Min(W, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ 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)
+ );
+ }
+
+ /// <summary>
/// Returns the axis of the vector's highest value. See <see cref="Axis"/>.
/// If all components are equal, this method returns <see cref="Axis.X"/>.
/// </summary>
@@ -465,7 +556,7 @@ namespace Godot
}
/// <summary>
- /// Returns this vector with each component snapped to the nearest multiple of <paramref name="step"/>.
+ /// Returns a new vector with each component snapped to the nearest multiple of the corresponding component in <paramref name="step"/>.
/// This can also be used to round to an arbitrary number of decimals.
/// </summary>
/// <param name="step">A vector value representing the step size to snap to.</param>
@@ -480,6 +571,22 @@ namespace Godot
);
}
+ /// <summary>
+ /// Returns a new vector with each component snapped to the nearest multiple of <paramref name="step"/>.
+ /// This can also be used to round to an arbitrary number of decimals.
+ /// </summary>
+ /// <param name="step">The step size to snap to.</param>
+ /// <returns>The snapped vector.</returns>
+ public readonly Vector4 Snapped(real_t step)
+ {
+ return new Vector4(
+ Mathf.Snapped(X, step),
+ Mathf.Snapped(Y, step),
+ Mathf.Snapped(Z, step),
+ Mathf.Snapped(W, step)
+ );
+ }
+
// Constants
private static readonly Vector4 _zero = new Vector4(0, 0, 0, 0);
private static readonly Vector4 _one = new Vector4(1, 1, 1, 1);
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs
index 27aa86b7e4..ba8e54b88b 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs
@@ -150,6 +150,25 @@ namespace Godot
}
/// <summary>
+ /// Returns a new vector with all components clamped between
+ /// <paramref name="min"/> and <paramref name="max"/> using
+ /// <see cref="Mathf.Clamp(int, int, int)"/>.
+ /// </summary>
+ /// <param name="min">The minimum allowed value.</param>
+ /// <param name="max">The maximum allowed value.</param>
+ /// <returns>The vector with all components clamped.</returns>
+ public readonly Vector4I Clamp(int min, int max)
+ {
+ return new Vector4I
+ (
+ Mathf.Clamp(X, min, max),
+ Mathf.Clamp(Y, min, max),
+ Mathf.Clamp(Z, min, max),
+ Mathf.Clamp(W, min, max)
+ );
+ }
+
+ /// <summary>
/// Returns the squared distance between this vector and <paramref name="to"/>.
/// This method runs faster than <see cref="DistanceTo"/>, so prefer it if
/// you need to compare vectors or need the squared distance for some formula.
@@ -204,6 +223,78 @@ namespace Godot
}
/// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector4I(Mathf.Max(X, with.X), Mathf.Max(Y, with.Y), Mathf.Max(Z, with.Z), Mathf.Max(W, with.W))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ 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)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise maximum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector4I(Mathf.Max(X, with), Mathf.Max(Y, with), Mathf.Max(Z, with), Mathf.Max(W, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting maximum vector.</returns>
+ 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)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector4I(Mathf.Min(X, with.X), Mathf.Min(Y, with.Y), Mathf.Min(Z, with.Z), Mathf.Min(W, with.W))</c>.
+ /// </summary>
+ /// <param name="with">The other vector to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ 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)
+ );
+ }
+
+ /// <summary>
+ /// Returns the result of the component-wise minimum between
+ /// this vector and <paramref name="with"/>.
+ /// Equivalent to <c>new Vector4I(Mathf.Min(X, with), Mathf.Min(Y, with), Mathf.Min(Z, with), Mathf.Min(W, with))</c>.
+ /// </summary>
+ /// <param name="with">The other value to use.</param>
+ /// <returns>The resulting minimum vector.</returns>
+ 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)
+ );
+ }
+
+ /// <summary>
/// Returns the axis of the vector's highest value. See <see cref="Axis"/>.
/// If all components are equal, this method returns <see cref="Axis.X"/>.
/// </summary>
@@ -254,6 +345,36 @@ namespace Godot
return new Vector4I(Mathf.Sign(X), Mathf.Sign(Y), Mathf.Sign(Z), Mathf.Sign(W));
}
+ /// <summary>
+ /// Returns a new vector with each component snapped to the closest multiple of the corresponding component in <paramref name="step"/>.
+ /// </summary>
+ /// <param name="step">A vector value representing the step size to snap to.</param>
+ /// <returns>The snapped vector.</returns>
+ public readonly Vector4I Snapped(Vector4I step)
+ {
+ return new Vector4I(
+ (int)Mathf.Snapped((double)X, (double)step.X),
+ (int)Mathf.Snapped((double)Y, (double)step.Y),
+ (int)Mathf.Snapped((double)Z, (double)step.Z),
+ (int)Mathf.Snapped((double)W, (double)step.W)
+ );
+ }
+
+ /// <summary>
+ /// Returns a new vector with each component snapped to the closest multiple of <paramref name="step"/>.
+ /// </summary>
+ /// <param name="step">The step size to snap to.</param>
+ /// <returns>The snapped vector.</returns>
+ public readonly Vector4I Snapped(int step)
+ {
+ return new Vector4I(
+ (int)Mathf.Snapped((double)X, (double)step),
+ (int)Mathf.Snapped((double)Y, (double)step),
+ (int)Mathf.Snapped((double)Z, (double)step),
+ (int)Mathf.Snapped((double)W, (double)step)
+ );
+ }
+
// Constants
private static readonly Vector4I _minValue = new Vector4I(int.MinValue, int.MinValue, int.MinValue, int.MinValue);
private static readonly Vector4I _maxValue = new Vector4I(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue);
diff --git a/modules/openxr/action_map/openxr_action_map.cpp b/modules/openxr/action_map/openxr_action_map.cpp
index bbcb63a7e6..ba0e4f6cdd 100644
--- a/modules/openxr/action_map/openxr_action_map.cpp
+++ b/modules/openxr/action_map/openxr_action_map.cpp
@@ -167,11 +167,11 @@ void OpenXRActionMap::create_default_action_sets() {
// we still want it to be part of our action map as we may deploy the same game to platforms that do and don't support it.
// - the same applies for interaction profiles that are only supported if the relevant extension is supported.
- // Create our Godot action set
+ // Create our Godot action set.
Ref<OpenXRActionSet> action_set = OpenXRActionSet::new_action_set("godot", "Godot action set");
add_action_set(action_set);
- // Create our actions
+ // Create our actions.
Ref<OpenXRAction> trigger = action_set->add_new_action("trigger", "Trigger", OpenXRAction::OPENXR_ACTION_FLOAT, "/user/hand/left,/user/hand/right");
Ref<OpenXRAction> trigger_click = action_set->add_new_action("trigger_click", "Trigger click", OpenXRAction::OPENXR_ACTION_BOOL, "/user/hand/left,/user/hand/right");
Ref<OpenXRAction> trigger_touch = action_set->add_new_action("trigger_touch", "Trigger touching", OpenXRAction::OPENXR_ACTION_BOOL, "/user/hand/left,/user/hand/right");
@@ -193,7 +193,7 @@ void OpenXRActionMap::create_default_action_sets() {
Ref<OpenXRAction> default_pose = action_set->add_new_action("default_pose", "Default pose", OpenXRAction::OPENXR_ACTION_POSE,
"/user/hand/left,"
"/user/hand/right,"
- // "/user/vive_tracker_htcx/role/handheld_object," <-- getting errors on this one
+ // "/user/vive_tracker_htcx/role/handheld_object," <-- getting errors on this one.
"/user/vive_tracker_htcx/role/left_foot,"
"/user/vive_tracker_htcx/role/right_foot,"
"/user/vive_tracker_htcx/role/left_shoulder,"
@@ -213,7 +213,7 @@ void OpenXRActionMap::create_default_action_sets() {
Ref<OpenXRAction> haptic = action_set->add_new_action("haptic", "Haptic", OpenXRAction::OPENXR_ACTION_HAPTIC,
"/user/hand/left,"
"/user/hand/right,"
- // "/user/vive_tracker_htcx/role/handheld_object," <-- getting errors on this one
+ // "/user/vive_tracker_htcx/role/handheld_object," <-- getting errors on this one.
"/user/vive_tracker_htcx/role/left_foot,"
"/user/vive_tracker_htcx/role/right_foot,"
"/user/vive_tracker_htcx/role/left_shoulder,"
@@ -227,7 +227,7 @@ void OpenXRActionMap::create_default_action_sets() {
"/user/vive_tracker_htcx/role/camera,"
"/user/vive_tracker_htcx/role/keyboard");
- // Create our interaction profiles
+ // Create our interaction profiles.
Ref<OpenXRInteractionProfile> profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/khr/simple_controller");
profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
@@ -235,11 +235,11 @@ void OpenXRActionMap::create_default_action_sets() {
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click");
profile->add_new_binding(select_button, "/user/hand/left/input/select/click,/user/hand/right/input/select/click");
- // generic has no support for triggers, grip, A/B buttons, nor joystick/trackpad inputs
+ // generic has no support for triggers, grip, A/B buttons, nor joystick/trackpad inputs.
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
- // Create our Vive controller profile
+ // Create our Vive controller profile.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_controller");
profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
@@ -247,64 +247,64 @@ void OpenXRActionMap::create_default_action_sets() {
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click");
profile->add_new_binding(select_button, "/user/hand/left/input/system/click,/user/hand/right/input/system/click");
- // wmr controller has no a/b/x/y buttons
+ // wmr controller has no a/b/x/y buttons.
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click");
- profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); // OpenXR will convert bool to float
+ profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); // OpenXR will convert bool to float.
profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click");
- // primary on our vive controller is our trackpad
+ // primary on our vive controller is our trackpad.
profile->add_new_binding(primary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad");
profile->add_new_binding(primary_click, "/user/hand/left/input/trackpad/click,/user/hand/right/input/trackpad/click");
profile->add_new_binding(primary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch");
- // vive controllers have no secondary input
+ // vive controllers have no secondary input.
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
- // Create our WMR controller profile
+ // Create our WMR controller profile.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/microsoft/motion_controller");
profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose");
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
- // wmr controllers have no select button we can use
+ // wmr controllers have no select button we can use.
profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click");
- // wmr controller has no a/b/x/y buttons
+ // wmr controller has no a/b/x/y buttons.
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
- profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // OpenXR will convert float to bool
- profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); // OpenXR will convert bool to float
+ profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // OpenXR will convert float to bool.
+ profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click"); // OpenXR will convert bool to float.
profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click");
- // primary on our wmr controller is our thumbstick, no touch
+ // primary on our wmr controller is our thumbstick, no touch.
profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick");
profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click");
- // secondary on our wmr controller is our trackpad
+ // secondary on our wmr controller is our trackpad.
profile->add_new_binding(secondary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad");
profile->add_new_binding(secondary_click, "/user/hand/left/input/trackpad/click,/user/hand/right/input/trackpad/click");
profile->add_new_binding(secondary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch");
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
- // Create our Meta touch controller profile
+ // Create our Meta touch controller profile.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/oculus/touch_controller");
profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose");
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
- // touch controllers have no select button we can use
- profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/system/click"); // right hand system click may not be available
- profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand
+ // touch controllers have no select button we can use.
+ profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/system/click"); // right hand system click may not be available.
+ profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand.
profile->add_new_binding(ax_touch, "/user/hand/left/input/x/touch,/user/hand/right/input/a/touch");
- profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand
+ profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand.
profile->add_new_binding(by_touch, "/user/hand/left/input/y/touch,/user/hand/right/input/b/touch");
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
- profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // should be converted to boolean
+ profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // should be converted to boolean.
profile->add_new_binding(trigger_touch, "/user/hand/left/input/trigger/touch,/user/hand/right/input/trigger/touch");
- profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // should be converted to boolean
+ profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // should be converted to boolean.
profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value");
- // primary on our touch controller is our thumbstick
+ // primary on our touch controller is our thumbstick.
profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick");
profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click");
profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch");
- // touch controller has no secondary input
+ // touch controller has no secondary input.
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
@@ -314,73 +314,73 @@ void OpenXRActionMap::create_default_action_sets() {
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose");
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
- profile->add_new_binding(select_button, "/user/hand/left/input/system/click,/user/hand/right/input/system/click"); // system click may not be available
+ profile->add_new_binding(select_button, "/user/hand/left/input/system/click,/user/hand/right/input/system/click"); // system click may not be available.
profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click");
- profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand
+ profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand.
profile->add_new_binding(ax_touch, "/user/hand/left/input/x/touch,/user/hand/right/input/a/touch");
- profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand
+ profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand.
profile->add_new_binding(by_touch, "/user/hand/left/input/y/touch,/user/hand/right/input/b/touch");
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
- profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // should be converted to boolean
+ profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value"); // should be converted to boolean.
profile->add_new_binding(trigger_touch, "/user/hand/left/input/trigger/touch,/user/hand/right/input/trigger/touch");
- profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // should be converted to boolean
+ profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // should be converted to boolean.
profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value");
- // primary on our pico controller is our thumbstick
+ // primary on our pico controller is our thumbstick.
profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick");
profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click");
profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch");
- // pico controller has no secondary input
+ // pico controller has no secondary input.
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
- // Create our Valve index controller profile
+ // Create our Valve index controller profile.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/valve/index_controller");
profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose");
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
- // index controllers have no select button we can use
+ // index controllers have no select button we can use.
profile->add_new_binding(menu_button, "/user/hand/left/input/system/click,/user/hand/right/input/system/click");
- profile->add_new_binding(ax_button, "/user/hand/left/input/a/click,/user/hand/right/input/a/click"); // a on both controllers
+ profile->add_new_binding(ax_button, "/user/hand/left/input/a/click,/user/hand/right/input/a/click"); // a on both controllers.
profile->add_new_binding(ax_touch, "/user/hand/left/input/a/touch,/user/hand/right/input/a/touch");
- profile->add_new_binding(by_button, "/user/hand/left/input/b/click,/user/hand/right/input/b/click"); // b on both controllers
+ profile->add_new_binding(by_button, "/user/hand/left/input/b/click,/user/hand/right/input/b/click"); // b on both controllers.
profile->add_new_binding(by_touch, "/user/hand/left/input/b/touch,/user/hand/right/input/b/touch");
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click");
profile->add_new_binding(trigger_touch, "/user/hand/left/input/trigger/touch,/user/hand/right/input/trigger/touch");
profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value");
- profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // this should do a float to bool conversion
- profile->add_new_binding(grip_force, "/user/hand/left/input/squeeze/force,/user/hand/right/input/squeeze/force"); // grip force seems to be unique to the Valve Index
- // primary on our index controller is our thumbstick
+ profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value"); // this should do a float to bool conversion.
+ profile->add_new_binding(grip_force, "/user/hand/left/input/squeeze/force,/user/hand/right/input/squeeze/force"); // grip force seems to be unique to the Valve Index.
+ // primary on our index controller is our thumbstick.
profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick");
profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click");
profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch");
- // secondary on our index controller is our trackpad
+ // secondary on our index controller is our trackpad.
profile->add_new_binding(secondary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad");
profile->add_new_binding(secondary_click, "/user/hand/left/input/trackpad/force,/user/hand/right/input/trackpad/force"); // not sure if this will work but doesn't seem to support click...
profile->add_new_binding(secondary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch");
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
- // Create our HP MR controller profile
+ // Create our HP MR controller profile.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/hp/mixed_reality_controller");
profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose");
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
- // hpmr controllers have no select button we can use
+ // hpmr controllers have no select button we can use.
profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click");
- // hpmr controllers only register click, not touch, on our a/b/x/y buttons
- profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand
- profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand
+ // hpmr controllers only register click, not touch, on our a/b/x/y buttons.
+ profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand.
+ profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand.
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
profile->add_new_binding(grip, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value");
profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/value,/user/hand/right/input/squeeze/value");
- // primary on our hpmr controller is our thumbstick
+ // primary on our hpmr controller is our thumbstick.
profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick");
profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click");
- // No secondary on our hpmr controller
+ // No secondary on our hpmr controller.
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
@@ -391,72 +391,72 @@ void OpenXRActionMap::create_default_action_sets() {
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose");
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
- // Odyssey controllers have no select button we can use
+ // Odyssey controllers have no select button we can use.
profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click,/user/hand/right/input/menu/click");
- // Odyssey controller has no a/b/x/y buttons
+ // Odyssey controller has no a/b/x/y buttons.
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click");
profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click");
- // primary on our Odyssey controller is our thumbstick, no touch
+ // primary on our Odyssey controller is our thumbstick, no touch.
profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick");
profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click");
- // secondary on our Odyssey controller is our trackpad
+ // secondary on our Odyssey controller is our trackpad.
profile->add_new_binding(secondary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad");
profile->add_new_binding(secondary_click, "/user/hand/left/input/trackpad/click,/user/hand/right/input/trackpad/click");
profile->add_new_binding(secondary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch");
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
- // Create our Vive Cosmos controller
+ // Create our Vive Cosmos controller.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_cosmos_controller");
profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose");
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click");
- profile->add_new_binding(select_button, "/user/hand/right/input/system/click"); // we'll map system to select
- profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand
- profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand
+ profile->add_new_binding(select_button, "/user/hand/right/input/system/click"); // we'll map system to select.
+ profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand.
+ profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand.
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click");
profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click");
profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click");
- // primary on our Cosmos controller is our thumbstick
+ // primary on our Cosmos controller is our thumbstick.
profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick");
profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click");
profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch");
- // No secondary on our cosmos controller
+ // No secondary on our cosmos controller.
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
- // Create our Vive Focus 3 controller
+ // Create our Vive Focus 3 controller.
// Note, Vive Focus 3 currently is not yet supported as a stand alone device
- // however HTC currently has a beta OpenXR runtime in testing we may support in the near future
+ // however HTC currently has a beta OpenXR runtime in testing we may support in the near future.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_focus3_controller");
profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose");
profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
profile->add_new_binding(menu_button, "/user/hand/left/input/menu/click");
- profile->add_new_binding(select_button, "/user/hand/right/input/system/click"); // we'll map system to select
- profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand
- profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand
+ profile->add_new_binding(select_button, "/user/hand/right/input/system/click"); // we'll map system to select.
+ profile->add_new_binding(ax_button, "/user/hand/left/input/x/click,/user/hand/right/input/a/click"); // x on left hand, a on right hand.
+ profile->add_new_binding(by_button, "/user/hand/left/input/y/click,/user/hand/right/input/b/click"); // y on left hand, b on right hand.
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click");
profile->add_new_binding(trigger_touch, "/user/hand/left/input/trigger/touch,/user/hand/right/input/trigger/touch");
profile->add_new_binding(grip, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click");
profile->add_new_binding(grip_click, "/user/hand/left/input/squeeze/click,/user/hand/right/input/squeeze/click");
- // primary on our Focus 3 controller is our thumbstick
+ // primary on our Focus 3 controller is our thumbstick.
profile->add_new_binding(primary, "/user/hand/left/input/thumbstick,/user/hand/right/input/thumbstick");
profile->add_new_binding(primary_click, "/user/hand/left/input/thumbstick/click,/user/hand/right/input/thumbstick/click");
profile->add_new_binding(primary_touch, "/user/hand/left/input/thumbstick/touch,/user/hand/right/input/thumbstick/touch");
- // We only have a thumb rest
+ // We only have a thumb rest.
profile->add_new_binding(secondary_touch, "/user/hand/left/input/thumbrest/touch,/user/hand/right/input/thumbrest/touch");
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
- // Create our Huawei controller
+ // Create our Huawei controller.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/huawei/controller");
profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
@@ -465,17 +465,17 @@ void OpenXRActionMap::create_default_action_sets() {
profile->add_new_binding(menu_button, "/user/hand/left/input/home/click,/user/hand/right/input/home/click");
profile->add_new_binding(trigger, "/user/hand/left/input/trigger/value,/user/hand/right/input/trigger/value");
profile->add_new_binding(trigger_click, "/user/hand/left/input/trigger/click,/user/hand/right/input/trigger/click");
- // primary on our Huawei controller is our trackpad
+ // primary on our Huawei controller is our trackpad.
profile->add_new_binding(primary, "/user/hand/left/input/trackpad,/user/hand/right/input/trackpad");
profile->add_new_binding(primary_click, "/user/hand/left/input/trackpad/click,/user/hand/right/input/trackpad/click");
profile->add_new_binding(primary_touch, "/user/hand/left/input/trackpad/touch,/user/hand/right/input/trackpad/touch");
profile->add_new_binding(haptic, "/user/hand/left/output/haptic,/user/hand/right/output/haptic");
add_interaction_profile(profile);
- // Create our HTC Vive tracker profile
+ // Create our HTC Vive tracker profile.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_tracker_htcx");
profile->add_new_binding(default_pose,
- // "/user/vive_tracker_htcx/role/handheld_object/input/grip/pose," <-- getting errors on this one
+ // "/user/vive_tracker_htcx/role/handheld_object/input/grip/pose," <-- getting errors on this one.
"/user/vive_tracker_htcx/role/left_foot/input/grip/pose,"
"/user/vive_tracker_htcx/role/right_foot/input/grip/pose,"
"/user/vive_tracker_htcx/role/left_shoulder/input/grip/pose,"
@@ -489,7 +489,7 @@ void OpenXRActionMap::create_default_action_sets() {
"/user/vive_tracker_htcx/role/camera/input/grip/pose,"
"/user/vive_tracker_htcx/role/keyboard/input/grip/pose");
profile->add_new_binding(haptic,
- // "/user/vive_tracker_htcx/role/handheld_object/output/haptic," <-- getting errors on this one
+ // "/user/vive_tracker_htcx/role/handheld_object/output/haptic," <-- getting errors on this one.
"/user/vive_tracker_htcx/role/left_foot/output/haptic,"
"/user/vive_tracker_htcx/role/right_foot/output/haptic,"
"/user/vive_tracker_htcx/role/left_shoulder/output/haptic,"
@@ -504,10 +504,30 @@ void OpenXRActionMap::create_default_action_sets() {
"/user/vive_tracker_htcx/role/keyboard/output/haptic");
add_interaction_profile(profile);
- // Create our eye gaze interaction profile
+ // Create our eye gaze interaction profile.
profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/ext/eye_gaze_interaction");
profile->add_new_binding(default_pose, "/user/eyes_ext/input/gaze_ext/pose");
add_interaction_profile(profile);
+
+ // Create our hand interaction profile.
+ profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/ext/hand_interaction_ext");
+ profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
+ profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose");
+ profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose");
+ profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose");
+
+ // Use pinch as primary.
+ profile->add_new_binding(primary, "/user/hand/left/input/pinch_ext/value,/user/hand/right/input/pinch_ext/value");
+ profile->add_new_binding(primary_click, "/user/hand/left/input/pinch_ext/ready_ext,/user/hand/right/input/pinch_ext/ready_ext");
+
+ // Use activation as secondary.
+ profile->add_new_binding(secondary, "/user/hand/left/input/aim_activate_ext/value,/user/hand/right/input/aim_activate_ext/value");
+ profile->add_new_binding(secondary_click, "/user/hand/left/input/aim_activate_ext/ready_ext,/user/hand/right/input/aim_activate_ext/ready_ext");
+
+ // We link grasp to our grip.
+ profile->add_new_binding(grip, "/user/hand/left/input/grasp_ext/value,/user/hand/right/input/grasp_ext/value");
+ profile->add_new_binding(grip_click, "/user/hand/left/input/grasp_ext/ready_ext,/user/hand/right/input/grasp_ext/ready_ext");
+ add_interaction_profile(profile);
}
void OpenXRActionMap::create_editor_action_sets() {
diff --git a/modules/openxr/doc_classes/OpenXRAPIExtension.xml b/modules/openxr/doc_classes/OpenXRAPIExtension.xml
index f737f3b642..4419d24dd3 100644
--- a/modules/openxr/doc_classes/OpenXRAPIExtension.xml
+++ b/modules/openxr/doc_classes/OpenXRAPIExtension.xml
@@ -54,7 +54,7 @@
<method name="get_next_frame_time">
<return type="int" />
<description>
- Returns the timing for the next frame.
+ Returns the predicted display timing for the next frame.
</description>
</method>
<method name="get_play_space">
@@ -63,6 +63,12 @@
Returns the play space, which is an [url=https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrSpace.html]XrSpace[/url] cast to an integer.
</description>
</method>
+ <method name="get_predicted_display_time">
+ <return type="int" />
+ <description>
+ Returns the predicted display timing for the current frame.
+ </description>
+ </method>
<method name="get_session">
<return type="int" />
<description>
diff --git a/modules/openxr/doc_classes/OpenXRInterface.xml b/modules/openxr/doc_classes/OpenXRInterface.xml
index 05dff7d6ae..86ba1416c8 100644
--- a/modules/openxr/doc_classes/OpenXRInterface.xml
+++ b/modules/openxr/doc_classes/OpenXRInterface.xml
@@ -106,6 +106,13 @@
[b]Note:[/b] This feature is only available on the compatibility renderer and currently only available on some stand alone headsets. For Vulkan set [member Viewport.vrs_mode] to [code]VRS_XR[/code] on desktop.
</description>
</method>
+ <method name="is_hand_interaction_supported" qualifiers="const">
+ <return type="bool" />
+ <description>
+ Returns [code]true[/code] if OpenXR's hand interaction profile is supported and enabled.
+ [b]Note:[/b] This only returns a valid value after OpenXR has been initialized.
+ </description>
+ </method>
<method name="is_hand_tracking_supported">
<return type="bool" />
<description>
@@ -147,6 +154,11 @@
</member>
</members>
<signals>
+ <signal name="instance_exiting">
+ <description>
+ Informs our OpenXR instance is exiting.
+ </description>
+ </signal>
<signal name="pose_recentered">
<description>
Informs the user queued a recenter of the player position.
@@ -169,6 +181,11 @@
Informs our OpenXR session now has focus.
</description>
</signal>
+ <signal name="session_loss_pending">
+ <description>
+ Informs our OpenXR session is in the process of being lost.
+ </description>
+ </signal>
<signal name="session_stopping">
<description>
Informs our OpenXR session is stopping.
diff --git a/modules/openxr/extensions/openxr_composition_layer_extension.cpp b/modules/openxr/extensions/openxr_composition_layer_extension.cpp
index 1fba8e5f8b..51f4a03d52 100644
--- a/modules/openxr/extensions/openxr_composition_layer_extension.cpp
+++ b/modules/openxr/extensions/openxr_composition_layer_extension.cpp
@@ -274,7 +274,7 @@ bool OpenXRViewportCompositionLayerProvider::update_and_acquire_swapchain(bool p
if (swapchain_size == viewport_size && !p_static_image && !static_image) {
// We're all good! Just acquire it.
// We can ignore should_render here, return will be false.
- XrBool32 should_render = true;
+ bool should_render = true;
return swapchain_info.acquire(should_render);
}
@@ -296,7 +296,7 @@ bool OpenXRViewportCompositionLayerProvider::update_and_acquire_swapchain(bool p
// Acquire our image so we can start rendering into it,
// we can ignore should_render here, ret will be false.
- XrBool32 should_render = true;
+ bool should_render = true;
bool ret = swapchain_info.acquire(should_render);
swapchain_size = viewport_size;
diff --git a/modules/openxr/extensions/openxr_hand_interaction_extension.cpp b/modules/openxr/extensions/openxr_hand_interaction_extension.cpp
new file mode 100644
index 0000000000..65de4b23c4
--- /dev/null
+++ b/modules/openxr/extensions/openxr_hand_interaction_extension.cpp
@@ -0,0 +1,97 @@
+/**************************************************************************/
+/* openxr_hand_interaction_extension.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "openxr_hand_interaction_extension.h"
+
+#include "../action_map/openxr_interaction_profile_metadata.h"
+#include "core/config/project_settings.h"
+
+OpenXRHandInteractionExtension *OpenXRHandInteractionExtension::singleton = nullptr;
+
+OpenXRHandInteractionExtension *OpenXRHandInteractionExtension::get_singleton() {
+ return singleton;
+}
+
+OpenXRHandInteractionExtension::OpenXRHandInteractionExtension() {
+ singleton = this;
+}
+
+OpenXRHandInteractionExtension::~OpenXRHandInteractionExtension() {
+ singleton = nullptr;
+}
+
+HashMap<String, bool *> OpenXRHandInteractionExtension::get_requested_extensions() {
+ HashMap<String, bool *> request_extensions;
+
+ // Only enable this extension when requested.
+ // We still register our meta data or the action map editor will fail.
+ if (GLOBAL_GET("xr/openxr/extensions/hand_interaction_profile")) {
+ request_extensions[XR_EXT_HAND_INTERACTION_EXTENSION_NAME] = &available;
+ }
+
+ return request_extensions;
+}
+
+bool OpenXRHandInteractionExtension::is_available() {
+ return available;
+}
+
+void OpenXRHandInteractionExtension::on_register_metadata() {
+ OpenXRInteractionProfileMetadata *metadata = OpenXRInteractionProfileMetadata::get_singleton();
+ ERR_FAIL_NULL(metadata);
+
+ // Hand interaction profile
+ metadata->register_interaction_profile("Hand interaction", "/interaction_profiles/ext/hand_interaction_ext", XR_EXT_HAND_INTERACTION_EXTENSION_NAME);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grip pose", "/user/hand/left", "/user/hand/left/input/grip/pose", "", OpenXRAction::OPENXR_ACTION_POSE);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grip pose", "/user/hand/right", "/user/hand/right/input/grip/pose", "", OpenXRAction::OPENXR_ACTION_POSE);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim pose", "/user/hand/left", "/user/hand/left/input/aim/pose", "", OpenXRAction::OPENXR_ACTION_POSE);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim pose", "/user/hand/right", "/user/hand/right/input/aim/pose", "", OpenXRAction::OPENXR_ACTION_POSE);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch pose", "/user/hand/left", "/user/hand/left/input/pinch_ext/pose", "", OpenXRAction::OPENXR_ACTION_POSE);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch pose", "/user/hand/right", "/user/hand/right/input/pinch_ext/pose", "", OpenXRAction::OPENXR_ACTION_POSE);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Poke pose", "/user/hand/left", "/user/hand/left/input/poke_ext/pose", "", OpenXRAction::OPENXR_ACTION_POSE);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Poke pose", "/user/hand/right", "/user/hand/right/input/poke_ext/pose", "", OpenXRAction::OPENXR_ACTION_POSE);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Palm pose", "/user/hand/left", "/user/hand/left/input/palm_ext/pose", XR_EXT_PALM_POSE_EXTENSION_NAME, OpenXRAction::OPENXR_ACTION_POSE);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Palm pose", "/user/hand/right", "/user/hand/right/input/palm_ext/pose", XR_EXT_PALM_POSE_EXTENSION_NAME, OpenXRAction::OPENXR_ACTION_POSE);
+
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch", "/user/hand/left", "/user/hand/left/input/pinch_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch", "/user/hand/right", "/user/hand/right/input/pinch_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch ready", "/user/hand/left", "/user/hand/left/input/pinch_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Pinch ready", "/user/hand/right", "/user/hand/right/input/pinch_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL);
+
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim activate", "/user/hand/left", "/user/hand/left/input/aim_activate_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim activate", "/user/hand/right", "/user/hand/right/input/aim_activate_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim activate ready", "/user/hand/left", "/user/hand/left/input/aim_activate_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Aim activate ready", "/user/hand/right", "/user/hand/right/input/aim_activate_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL);
+
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grasp", "/user/hand/left", "/user/hand/left/input/grasp_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grasp", "/user/hand/right", "/user/hand/right/input/grasp_ext/value", "", OpenXRAction::OPENXR_ACTION_FLOAT);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grasp ready", "/user/hand/left", "/user/hand/left/input/grasp_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL);
+ metadata->register_io_path("/interaction_profiles/ext/hand_interaction_ext", "Grasp ready", "/user/hand/right", "/user/hand/right/input/grasp_ext/ready_ext", "", OpenXRAction::OPENXR_ACTION_BOOL);
+}
diff --git a/modules/openxr/extensions/openxr_hand_interaction_extension.h b/modules/openxr/extensions/openxr_hand_interaction_extension.h
new file mode 100644
index 0000000000..789e300c0b
--- /dev/null
+++ b/modules/openxr/extensions/openxr_hand_interaction_extension.h
@@ -0,0 +1,72 @@
+/**************************************************************************/
+/* openxr_hand_interaction_extension.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef OPENXR_HAND_INTERACTION_EXTENSION_H
+#define OPENXR_HAND_INTERACTION_EXTENSION_H
+
+#include "openxr_extension_wrapper.h"
+
+// When supported the hand interaction extension introduces an interaction
+// profile that becomes active when the user either lets go of their
+// controllers or isn't using controllers at all.
+//
+// The OpenXR specification states that all XR runtimes that support this
+// interaction profile should also allow it's controller to use this
+// interaction profile.
+// This means that if you only supply this interaction profile in your
+// action map, it should work both when the player is holding a controller
+// or using visual hand tracking.
+//
+// This allows easier portability between games that use controller
+// tracking or hand tracking.
+//
+// See: https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XR_EXT_hand_interaction
+// for more information.
+
+class OpenXRHandInteractionExtension : public OpenXRExtensionWrapper {
+public:
+ static OpenXRHandInteractionExtension *get_singleton();
+
+ OpenXRHandInteractionExtension();
+ virtual ~OpenXRHandInteractionExtension() override;
+
+ virtual HashMap<String, bool *> get_requested_extensions() override;
+
+ bool is_available();
+
+ virtual void on_register_metadata() override;
+
+private:
+ static OpenXRHandInteractionExtension *singleton;
+
+ bool available = false;
+};
+
+#endif // OPENXR_HAND_INTERACTION_EXTENSION_H
diff --git a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp
index 12fa3bed7e..b8a2f58935 100644
--- a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp
+++ b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp
@@ -128,7 +128,7 @@ void OpenXRHandTrackingExtension::on_process() {
}
// process our hands
- const XrTime time = OpenXRAPI::get_singleton()->get_next_frame_time(); // This data will be used for the next frame we render
+ const XrTime time = OpenXRAPI::get_singleton()->get_predicted_display_time();
if (time == 0) {
// we don't have timing info yet, or we're skipping a frame...
return;
diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp
index 1fe402341b..40e3ecfefc 100644
--- a/modules/openxr/openxr_api.cpp
+++ b/modules/openxr/openxr_api.cpp
@@ -160,7 +160,7 @@ void OpenXRAPI::OpenXRSwapChainInfo::free() {
}
}
-bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) {
+bool OpenXRAPI::OpenXRSwapChainInfo::acquire(bool &p_should_render) {
ERR_FAIL_COND_V(image_acquired, true); // This was not released when it should be, error out and reuse...
OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
@@ -193,10 +193,18 @@ bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) {
XrSwapchainImageWaitInfo swapchain_image_wait_info = {
XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, // type
nullptr, // next
- 17000000 // timeout in nanoseconds
+ 1000000000 // 1s timeout in nanoseconds
};
- result = openxr_api->xrWaitSwapchainImage(swapchain, &swapchain_image_wait_info);
+ // Wait for a maximum of 10 seconds before calling it a critical failure...
+ for (int retry = 0; retry < 10; retry++) {
+ result = openxr_api->xrWaitSwapchainImage(swapchain, &swapchain_image_wait_info);
+ if (result != XR_TIMEOUT_EXPIRED) {
+ break;
+ }
+ WARN_PRINT("OpenXR: timed out waiting for swapchain image.");
+ }
+
if (!XR_UNQUALIFIED_SUCCESS(result)) {
// Make sure end_frame knows we need to submit an empty frame
p_should_render = false;
@@ -206,6 +214,8 @@ bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) {
print_line("OpenXR: failed to wait for swapchain image [", openxr_api->get_error_string(result), "]");
return false;
} else {
+ WARN_PRINT("OpenXR: couldn't to wait for swapchain but not a complete error [" + openxr_api->get_error_string(result) + "]");
+
// Make sure to skip trying to acquire the swapchain image in the next frame
skip_acquire_swapchain = true;
return false;
@@ -760,21 +770,6 @@ bool OpenXRAPI::load_supported_view_configuration_views(XrViewConfigurationType
print_verbose(String(" - recommended render sample count: ") + itos(view_configuration_views[i].recommendedSwapchainSampleCount));
}
- // Allocate buffers we'll be populating with view information.
- views = (XrView *)memalloc(sizeof(XrView) * view_count);
- ERR_FAIL_NULL_V_MSG(views, false, "OpenXR Couldn't allocate memory for views");
- memset(views, 0, sizeof(XrView) * view_count);
-
- projection_views = (XrCompositionLayerProjectionView *)memalloc(sizeof(XrCompositionLayerProjectionView) * view_count);
- ERR_FAIL_NULL_V_MSG(projection_views, false, "OpenXR Couldn't allocate memory for projection views");
- memset(projection_views, 0, sizeof(XrCompositionLayerProjectionView) * view_count);
-
- if (submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) {
- depth_views = (XrCompositionLayerDepthInfoKHR *)memalloc(sizeof(XrCompositionLayerDepthInfoKHR) * view_count);
- ERR_FAIL_NULL_V_MSG(depth_views, false, "OpenXR Couldn't allocate memory for depth views");
- memset(depth_views, 0, sizeof(XrCompositionLayerDepthInfoKHR) * view_count);
- }
-
return true;
}
@@ -927,6 +922,9 @@ bool OpenXRAPI::setup_play_space() {
// If we've previously created a play space, clean it up first.
if (play_space != XR_NULL_HANDLE) {
+ // TODO Investigate if destroying our play space here is safe,
+ // it may still be used in the rendering thread.
+
xrDestroySpace(play_space);
}
play_space = new_play_space;
@@ -936,7 +934,11 @@ bool OpenXRAPI::setup_play_space() {
if (emulating_local_floor) {
// We'll use the STAGE space to get the floor height, but we can't do that until
// after xrWaitFrame(), so just set this flag for now.
+ // Render state will be updated then.
should_reset_emulated_floor_height = true;
+ } else {
+ // Update render state so this play space is used rendering the upcoming frame.
+ set_render_play_space(play_space);
}
return true;
@@ -1016,7 +1018,7 @@ bool OpenXRAPI::reset_emulated_floor_height() {
identityPose, // pose
};
- result = xrLocateSpace(stage_space, local_space, get_next_frame_time(), &stage_location);
+ result = xrLocateSpace(stage_space, local_space, get_predicted_display_time(), &stage_location);
xrDestroySpace(local_space);
xrDestroySpace(stage_space);
@@ -1042,6 +1044,9 @@ bool OpenXRAPI::reset_emulated_floor_height() {
// report that as the reference space to the outside world.
reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR_EXT;
+ // Update render state so this play space is used rendering the upcoming frame.
+ set_render_play_space(play_space);
+
return true;
}
@@ -1136,6 +1141,7 @@ bool OpenXRAPI::obtain_swapchain_formats() {
}
bool OpenXRAPI::create_main_swapchains(Size2i p_size) {
+ ERR_NOT_ON_RENDER_THREAD_V(false);
ERR_FAIL_NULL_V(graphics_extension, false);
ERR_FAIL_COND_V(session == XR_NULL_HANDLE, false);
@@ -1154,12 +1160,12 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) {
as we render 3D content into internal buffers that are copied into the swapchain, we do now have (basic) VRS support
*/
- main_swapchain_size = p_size;
+ render_state.main_swapchain_size = p_size;
uint32_t sample_count = 1;
// We start with our color swapchain...
if (color_swapchain_format != 0) {
- if (!main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, main_swapchain_size.width, main_swapchain_size.height, sample_count, view_count)) {
+ if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_count)) {
return false;
}
}
@@ -1169,7 +1175,7 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) {
// - we support our depth layer extension
// - we have our spacewarp extension (not yet implemented)
if (depth_swapchain_format != 0 && submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) {
- if (!main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, main_swapchain_size.width, main_swapchain_size.height, sample_count, view_count)) {
+ if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_count)) {
return false;
}
}
@@ -1180,36 +1186,36 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) {
// TBD
}
- for (uint32_t i = 0; i < view_count; i++) {
- views[i].type = XR_TYPE_VIEW;
- views[i].next = nullptr;
-
- projection_views[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW;
- projection_views[i].next = nullptr;
- projection_views[i].subImage.swapchain = main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain();
- projection_views[i].subImage.imageArrayIndex = i;
- projection_views[i].subImage.imageRect.offset.x = 0;
- projection_views[i].subImage.imageRect.offset.y = 0;
- projection_views[i].subImage.imageRect.extent.width = main_swapchain_size.width;
- projection_views[i].subImage.imageRect.extent.height = main_swapchain_size.height;
-
- if (submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available() && depth_views) {
- projection_views[i].next = &depth_views[i];
-
- depth_views[i].type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR;
- depth_views[i].next = nullptr;
- depth_views[i].subImage.swapchain = main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_swapchain();
- depth_views[i].subImage.imageArrayIndex = i;
- depth_views[i].subImage.imageRect.offset.x = 0;
- depth_views[i].subImage.imageRect.offset.y = 0;
- depth_views[i].subImage.imageRect.extent.width = main_swapchain_size.width;
- depth_views[i].subImage.imageRect.extent.height = main_swapchain_size.height;
+ for (uint32_t i = 0; i < render_state.view_count; i++) {
+ render_state.views[i].type = XR_TYPE_VIEW;
+ render_state.views[i].next = nullptr;
+
+ render_state.projection_views[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW;
+ render_state.projection_views[i].next = nullptr;
+ render_state.projection_views[i].subImage.swapchain = render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain();
+ render_state.projection_views[i].subImage.imageArrayIndex = i;
+ render_state.projection_views[i].subImage.imageRect.offset.x = 0;
+ render_state.projection_views[i].subImage.imageRect.offset.y = 0;
+ render_state.projection_views[i].subImage.imageRect.extent.width = render_state.main_swapchain_size.width;
+ render_state.projection_views[i].subImage.imageRect.extent.height = render_state.main_swapchain_size.height;
+
+ if (render_state.submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available() && render_state.depth_views) {
+ render_state.projection_views[i].next = &render_state.depth_views[i];
+
+ render_state.depth_views[i].type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR;
+ render_state.depth_views[i].next = nullptr;
+ render_state.depth_views[i].subImage.swapchain = render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_swapchain();
+ render_state.depth_views[i].subImage.imageArrayIndex = i;
+ render_state.depth_views[i].subImage.imageRect.offset.x = 0;
+ render_state.depth_views[i].subImage.imageRect.offset.y = 0;
+ render_state.depth_views[i].subImage.imageRect.extent.width = render_state.main_swapchain_size.width;
+ render_state.depth_views[i].subImage.imageRect.extent.height = render_state.main_swapchain_size.height;
// OpenXR spec says that: minDepth < maxDepth.
- depth_views[i].minDepth = 0.0;
- depth_views[i].maxDepth = 1.0;
+ render_state.depth_views[i].minDepth = 0.0;
+ render_state.depth_views[i].maxDepth = 1.0;
// But we can reverse near and far for reverse-Z.
- depth_views[i].nearZ = 100.0; // Near and far Z will be set to the correct values in fill_projection_matrix
- depth_views[i].farZ = 0.01;
+ render_state.depth_views[i].nearZ = 100.0; // Near and far Z will be set to the correct values in fill_projection_matrix
+ render_state.depth_views[i].farZ = 0.01;
}
};
@@ -1217,23 +1223,33 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) {
};
void OpenXRAPI::destroy_session() {
- if (running && session != XR_NULL_HANDLE) {
- xrEndSession(session);
+ // TODO need to figure out if we're still rendering our current frame
+ // in a separate rendering thread and if so,
+ // if we need to wait for completion.
+ // We could be pulling the rug from underneath rendering...
+
+ if (running) {
+ if (session != XR_NULL_HANDLE) {
+ xrEndSession(session);
+ }
+
+ running = false;
+ render_state.running = false;
}
- if (views != nullptr) {
- memfree(views);
- views = nullptr;
+ if (render_state.views != nullptr) {
+ memfree(render_state.views);
+ render_state.views = nullptr;
}
- if (projection_views != nullptr) {
- memfree(projection_views);
- projection_views = nullptr;
+ if (render_state.projection_views != nullptr) {
+ memfree(render_state.projection_views);
+ render_state.projection_views = nullptr;
}
- if (depth_views != nullptr) {
- memfree(depth_views);
- depth_views = nullptr;
+ if (render_state.depth_views != nullptr) {
+ memfree(render_state.depth_views);
+ render_state.depth_views = nullptr;
}
free_main_swapchains();
@@ -1248,6 +1264,7 @@ void OpenXRAPI::destroy_session() {
if (play_space != XR_NULL_HANDLE) {
xrDestroySpace(play_space);
play_space = XR_NULL_HANDLE;
+ render_state.play_space = XR_NULL_HANDLE;
}
if (view_space != XR_NULL_HANDLE) {
xrDestroySpace(view_space);
@@ -1298,6 +1315,7 @@ bool OpenXRAPI::on_state_ready() {
// we're running
running = true;
+ set_render_session_running(true);
for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) {
wrapper->on_state_ready();
@@ -1374,34 +1392,37 @@ bool OpenXRAPI::on_state_stopping() {
}
running = false;
+ set_render_session_running(false);
}
- // TODO further cleanup
-
return true;
}
bool OpenXRAPI::on_state_loss_pending() {
print_verbose("On state loss pending");
+ if (xr_interface) {
+ xr_interface->on_state_loss_pending();
+ }
+
for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) {
wrapper->on_state_loss_pending();
}
- // TODO need to look into the correct action here, read up on the spec but we may need to signal Godot to exit (if it's not already exiting)
-
return true;
}
bool OpenXRAPI::on_state_exiting() {
print_verbose("On state existing");
+ if (xr_interface) {
+ xr_interface->on_state_exiting();
+ }
+
for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) {
wrapper->on_state_exiting();
}
- // TODO need to look into the correct action here, read up on the spec but we may need to signal Godot to exit (if it's not already exiting)
-
return true;
}
@@ -1419,10 +1440,7 @@ void OpenXRAPI::set_view_configuration(XrViewConfigurationType p_view_configurat
bool OpenXRAPI::set_requested_reference_space(XrReferenceSpaceType p_requested_reference_space) {
requested_reference_space = p_requested_reference_space;
-
- if (is_initialized()) {
- return setup_play_space();
- }
+ play_space_is_dirty = true;
return true;
}
@@ -1625,11 +1643,6 @@ bool OpenXRAPI::initialize_session() {
return false;
}
- if (!setup_play_space()) {
- destroy_session();
- return false;
- }
-
if (!setup_view_space()) {
destroy_session();
return false;
@@ -1645,6 +1658,8 @@ bool OpenXRAPI::initialize_session() {
return false;
}
+ allocate_view_buffers(view_count, submit_depth_buffer);
+
return true;
}
@@ -1696,12 +1711,18 @@ XrHandTrackerEXT OpenXRAPI::get_hand_tracker(int p_hand_index) {
}
Size2 OpenXRAPI::get_recommended_target_size() {
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
ERR_FAIL_NULL_V(view_configuration_views, Size2());
Size2 target_size;
- target_size.width = view_configuration_views[0].recommendedImageRectWidth * render_target_size_multiplier;
- target_size.height = view_configuration_views[0].recommendedImageRectHeight * render_target_size_multiplier;
+ if (rendering_server && rendering_server->is_on_render_thread()) {
+ target_size.width = view_configuration_views[0].recommendedImageRectWidth * render_state.render_target_size_multiplier;
+ target_size.height = view_configuration_views[0].recommendedImageRectHeight * render_state.render_target_size_multiplier;
+ } else {
+ target_size.width = view_configuration_views[0].recommendedImageRectWidth * render_target_size_multiplier;
+ target_size.height = view_configuration_views[0].recommendedImageRectHeight * render_target_size_multiplier;
+ }
return target_size;
}
@@ -1713,14 +1734,12 @@ XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform,
return XRPose::XR_TRACKING_CONFIDENCE_NONE;
}
- // xrWaitFrame not run yet
- if (frame_state.predictedDisplayTime == 0) {
+ // Get display time
+ XrTime display_time = get_predicted_display_time();
+ if (display_time == 0) {
return XRPose::XR_TRACKING_CONFIDENCE_NONE;
}
- // Get timing for the next frame, as that is the current frame we're processing
- XrTime display_time = get_next_frame_time();
-
XrSpaceVelocity velocity = {
XR_TYPE_SPACE_VELOCITY, // type
nullptr, // next
@@ -1764,54 +1783,47 @@ XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform,
}
bool OpenXRAPI::get_view_transform(uint32_t p_view, Transform3D &r_transform) {
- if (!running) {
- return false;
- }
+ ERR_NOT_ON_RENDER_THREAD_V(false);
- // xrWaitFrame not run yet
- if (frame_state.predictedDisplayTime == 0) {
+ if (!render_state.running) {
return false;
}
// we don't have valid view info
- if (views == nullptr || !view_pose_valid) {
+ if (render_state.views == nullptr || !render_state.view_pose_valid) {
return false;
}
// Note, the timing of this is set right before rendering, which is what we need here.
- r_transform = transform_from_pose(views[p_view].pose);
+ r_transform = transform_from_pose(render_state.views[p_view].pose);
return true;
}
bool OpenXRAPI::get_view_projection(uint32_t p_view, double p_z_near, double p_z_far, Projection &p_camera_matrix) {
+ ERR_NOT_ON_RENDER_THREAD_V(false);
ERR_FAIL_NULL_V(graphics_extension, false);
- if (!running) {
- return false;
- }
-
- // xrWaitFrame not run yet
- if (frame_state.predictedDisplayTime == 0) {
+ if (!render_state.running) {
return false;
}
// we don't have valid view info
- if (views == nullptr || !view_pose_valid) {
+ if (render_state.views == nullptr || !render_state.view_pose_valid) {
return false;
}
// if we're using depth views, make sure we update our near and far there...
- if (depth_views != nullptr) {
- for (uint32_t i = 0; i < view_count; i++) {
+ if (render_state.depth_views != nullptr) {
+ for (uint32_t i = 0; i < render_state.view_count; i++) {
// As we are using reverse-Z these need to be flipped.
- depth_views[i].nearZ = p_z_far;
- depth_views[i].farZ = p_z_near;
+ render_state.depth_views[i].nearZ = p_z_far;
+ render_state.depth_views[i].farZ = p_z_near;
}
}
// now update our projection
- return graphics_extension->create_projection_fov(views[p_view].fov, p_z_near, p_z_far, p_camera_matrix);
+ return graphics_extension->create_projection_fov(render_state.views[p_view].fov, p_z_near, p_z_far, p_camera_matrix);
}
bool OpenXRAPI::poll_events() {
@@ -1934,53 +1946,85 @@ bool OpenXRAPI::poll_events() {
}
}
-bool OpenXRAPI::process() {
- ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, false);
+void OpenXRAPI::_allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
- if (!poll_events()) {
- return false;
- }
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+ ERR_FAIL_NULL(openxr_api);
- if (!running) {
- return false;
- }
+ openxr_api->render_state.view_count = p_view_count;
+ openxr_api->render_state.submit_depth_buffer = p_submit_depth_buffer;
- for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) {
- wrapper->on_process();
+ // Allocate buffers we'll be populating with view information.
+ openxr_api->render_state.views = (XrView *)memalloc(sizeof(XrView) * p_view_count);
+ ERR_FAIL_NULL_MSG(openxr_api->render_state.views, "OpenXR Couldn't allocate memory for views");
+ memset(openxr_api->render_state.views, 0, sizeof(XrView) * p_view_count);
+
+ openxr_api->render_state.projection_views = (XrCompositionLayerProjectionView *)memalloc(sizeof(XrCompositionLayerProjectionView) * p_view_count);
+ ERR_FAIL_NULL_MSG(openxr_api->render_state.projection_views, "OpenXR Couldn't allocate memory for projection views");
+ memset(openxr_api->render_state.projection_views, 0, sizeof(XrCompositionLayerProjectionView) * p_view_count);
+
+ if (p_submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) {
+ openxr_api->render_state.depth_views = (XrCompositionLayerDepthInfoKHR *)memalloc(sizeof(XrCompositionLayerDepthInfoKHR) * p_view_count);
+ ERR_FAIL_NULL_MSG(openxr_api->render_state.depth_views, "OpenXR Couldn't allocate memory for depth views");
+ memset(openxr_api->render_state.depth_views, 0, sizeof(XrCompositionLayerDepthInfoKHR) * p_view_count);
}
+}
- return true;
+void OpenXRAPI::_set_render_session_running(bool p_is_running) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
+
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+ ERR_FAIL_NULL(openxr_api);
+ openxr_api->render_state.running = p_is_running;
}
-void OpenXRAPI::free_main_swapchains() {
- for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) {
- main_swapchains[i].queue_free();
- }
+void OpenXRAPI::_set_render_display_info(XrTime p_predicted_display_time, bool p_should_render) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
+
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+ ERR_FAIL_NULL(openxr_api);
+ openxr_api->render_state.predicted_display_time = p_predicted_display_time;
+ openxr_api->render_state.should_render = p_should_render;
}
-void OpenXRAPI::pre_render() {
- ERR_FAIL_COND(instance == XR_NULL_HANDLE);
+void OpenXRAPI::_set_render_play_space(uint64_t p_play_space) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
- if (!running) {
- return;
- }
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+ ERR_FAIL_NULL(openxr_api);
+ openxr_api->render_state.play_space = XrSpace(p_play_space);
+}
- // Process any swapchains that were queued to be freed
- OpenXRSwapChainInfo::free_queued();
+void OpenXRAPI::_set_render_state_multiplier(double p_render_target_size_multiplier) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
- Size2i swapchain_size = get_recommended_target_size();
- if (swapchain_size != main_swapchain_size) {
- // Out with the old.
- free_main_swapchains();
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+ ERR_FAIL_NULL(openxr_api);
+ openxr_api->render_state.render_target_size_multiplier = p_render_target_size_multiplier;
+}
- // In with the new.
- create_main_swapchains(swapchain_size);
+bool OpenXRAPI::process() {
+ ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, false);
+
+ if (!poll_events()) {
+ return false;
}
- // Waitframe does 2 important things in our process:
- // 1) It provides us with predictive timing, telling us when OpenXR expects to display the frame we're about to commit
- // 2) It will use the previous timing to pause our thread so that rendering starts as close to displaying as possible
- // This must thus be called as close to when we start rendering as possible
+ if (!running) {
+ return false;
+ }
+
+ // We call xrWaitFrame as early as possible, this will allow OpenXR to get
+ // proper timing info between this point, and when we're ready to start rendering.
+ // As the name suggests, OpenXR can pause the thread to minimize the time between
+ // retrieving tracking data and using that tracking data to render.
+ // OpenXR thus works best if rendering is performed on a separate thread.
XrFrameWaitInfo frame_wait_info = { XR_TYPE_FRAME_WAIT_INFO, nullptr };
frame_state.predictedDisplayTime = 0;
frame_state.predictedDisplayPeriod = 0;
@@ -1995,7 +2039,9 @@ void OpenXRAPI::pre_render() {
frame_state.predictedDisplayPeriod = 0;
frame_state.shouldRender = false;
- return;
+ set_render_display_info(0, false);
+
+ return false;
}
if (frame_state.predictedDisplayPeriod > 500000000) {
@@ -2004,12 +2050,54 @@ void OpenXRAPI::pre_render() {
frame_state.predictedDisplayPeriod = 0;
}
+ set_render_display_info(frame_state.predictedDisplayTime, frame_state.shouldRender);
+
+ if (unlikely(play_space_is_dirty)) {
+ setup_play_space();
+ play_space_is_dirty = false;
+ }
+
if (unlikely(should_reset_emulated_floor_height)) {
reset_emulated_floor_height();
should_reset_emulated_floor_height = false;
}
for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) {
+ wrapper->on_process();
+ }
+
+ return true;
+}
+
+void OpenXRAPI::free_main_swapchains() {
+ for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) {
+ render_state.main_swapchains[i].queue_free();
+ }
+}
+
+void OpenXRAPI::pre_render() {
+ ERR_FAIL_COND(session == XR_NULL_HANDLE);
+
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
+
+ if (!render_state.running) {
+ return;
+ }
+
+ // Process any swapchains that were queued to be freed
+ OpenXRSwapChainInfo::free_queued();
+
+ Size2i swapchain_size = get_recommended_target_size();
+ if (swapchain_size != render_state.main_swapchain_size) {
+ // Out with the old.
+ free_main_swapchains();
+
+ // In with the new.
+ create_main_swapchains(swapchain_size);
+ }
+
+ for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) {
wrapper->on_pre_render();
}
@@ -2028,8 +2116,8 @@ void OpenXRAPI::pre_render() {
XR_TYPE_VIEW_LOCATE_INFO, // type
nullptr, // next
view_configuration, // viewConfigurationType
- frame_state.predictedDisplayTime, // displayTime
- play_space // space
+ render_state.predicted_display_time, // displayTime
+ render_state.play_space // space
};
XrViewState view_state = {
XR_TYPE_VIEW_STATE, // type
@@ -2037,7 +2125,7 @@ void OpenXRAPI::pre_render() {
0 // viewStateFlags
};
uint32_t view_count_output;
- result = xrLocateViews(session, &view_locate_info, &view_state, view_count, &view_count_output, views);
+ XrResult result = xrLocateViews(session, &view_locate_info, &view_state, render_state.view_count, &view_count_output, render_state.views);
if (XR_FAILED(result)) {
print_line("OpenXR: Couldn't locate views [", get_error_string(result), "]");
return;
@@ -2050,9 +2138,9 @@ void OpenXRAPI::pre_render() {
pose_valid = false;
}
}
- if (view_pose_valid != pose_valid) {
- view_pose_valid = pose_valid;
- if (!view_pose_valid) {
+ if (render_state.view_pose_valid != pose_valid) {
+ render_state.view_pose_valid = pose_valid;
+ if (!render_state.view_pose_valid) {
print_verbose("OpenXR View pose became invalid");
} else {
print_verbose("OpenXR View pose became valid");
@@ -2071,23 +2159,24 @@ void OpenXRAPI::pre_render() {
}
// Reset this, we haven't found a viewport for output yet
- has_xr_viewport = false;
+ render_state.has_xr_viewport = false;
}
bool OpenXRAPI::pre_draw_viewport(RID p_render_target) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD_V(false);
+
// We found an XR viewport!
- has_xr_viewport = true;
+ render_state.has_xr_viewport = true;
- if (!can_render()) {
+ if (instance == XR_NULL_HANDLE || session == XR_NULL_HANDLE || !render_state.running || !render_state.view_pose_valid || !render_state.should_render) {
return false;
}
- // TODO: at some point in time we may support multiple viewports in which case we need to handle that...
-
// Acquire our images
for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) {
- if (!main_swapchains[i].is_image_acquired() && main_swapchains[i].get_swapchain() != XR_NULL_HANDLE) {
- if (!main_swapchains[i].acquire(frame_state.shouldRender)) {
+ if (!render_state.main_swapchains[i].is_image_acquired() && render_state.main_swapchains[i].get_swapchain() != XR_NULL_HANDLE) {
+ if (!render_state.main_swapchains[i].acquire(render_state.should_render)) {
return false;
}
}
@@ -2101,24 +2190,33 @@ bool OpenXRAPI::pre_draw_viewport(RID p_render_target) {
}
XrSwapchain OpenXRAPI::get_color_swapchain() {
- return main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain();
+ ERR_NOT_ON_RENDER_THREAD_V(XR_NULL_HANDLE);
+
+ return render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain();
}
RID OpenXRAPI::get_color_texture() {
- return main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_image();
+ ERR_NOT_ON_RENDER_THREAD_V(RID());
+
+ return render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_image();
}
RID OpenXRAPI::get_depth_texture() {
+ ERR_NOT_ON_RENDER_THREAD_V(RID());
+
// Note, image will not be acquired if we didn't have a suitable swap chain format.
- if (submit_depth_buffer) {
- return main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_image();
+ if (render_state.submit_depth_buffer && render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].is_image_acquired()) {
+ return render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_image();
} else {
return RID();
}
}
void OpenXRAPI::post_draw_viewport(RID p_render_target) {
- if (!can_render()) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
+
+ if (instance == XR_NULL_HANDLE || session == XR_NULL_HANDLE || !render_state.running || !render_state.view_pose_valid || !render_state.should_render) {
return;
}
@@ -2130,30 +2228,33 @@ void OpenXRAPI::post_draw_viewport(RID p_render_target) {
void OpenXRAPI::end_frame() {
XrResult result;
- ERR_FAIL_COND(instance == XR_NULL_HANDLE);
+ ERR_FAIL_COND(session == XR_NULL_HANDLE);
- if (!running) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
+
+ if (!render_state.running) {
return;
}
- if (frame_state.shouldRender && view_pose_valid) {
- if (!has_xr_viewport) {
+ if (render_state.should_render && render_state.view_pose_valid) {
+ if (!render_state.has_xr_viewport) {
print_line("OpenXR: No viewport was marked with use_xr, there is no rendered output!");
- } else if (!main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) {
+ } else if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) {
print_line("OpenXR: No swapchain could be acquired to render to!");
}
}
// must have:
- // - shouldRender set to true
+ // - should_render set to true
// - a valid view pose for projection_views[eye].pose to submit layer
// - an image to render
- if (!frame_state.shouldRender || !view_pose_valid || !main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) {
+ if (!render_state.should_render || !render_state.view_pose_valid || !render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) {
// submit 0 layers when we shouldn't render
XrFrameEndInfo frame_end_info = {
XR_TYPE_FRAME_END_INFO, // type
nullptr, // next
- frame_state.predictedDisplayTime, // displayTime
+ render_state.predicted_display_time, // displayTime
environment_blend_mode, // environmentBlendMode
0, // layerCount
nullptr // layers
@@ -2170,14 +2271,14 @@ void OpenXRAPI::end_frame() {
// release our swapchain image if we acquired it
for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) {
- if (main_swapchains[i].is_image_acquired()) {
- main_swapchains[i].release();
+ if (render_state.main_swapchains[i].is_image_acquired()) {
+ render_state.main_swapchains[i].release();
}
}
- for (uint32_t eye = 0; eye < view_count; eye++) {
- projection_views[eye].fov = views[eye].fov;
- projection_views[eye].pose = views[eye].pose;
+ for (uint32_t eye = 0; eye < render_state.view_count; eye++) {
+ render_state.projection_views[eye].fov = render_state.views[eye].fov;
+ render_state.projection_views[eye].pose = render_state.views[eye].pose;
}
Vector<OrderedCompositionLayer> ordered_layers_list;
@@ -2210,9 +2311,9 @@ void OpenXRAPI::end_frame() {
XR_TYPE_COMPOSITION_LAYER_PROJECTION, // type
nullptr, // next
layer_flags, // layerFlags
- play_space, // space
- view_count, // viewCount
- projection_views, // views
+ render_state.play_space, // space
+ render_state.view_count, // viewCount
+ render_state.projection_views, // views
};
ordered_layers_list.push_back({ (const XrCompositionLayerBaseHeader *)&projection_layer, 0 });
@@ -2228,7 +2329,7 @@ void OpenXRAPI::end_frame() {
XrFrameEndInfo frame_end_info = {
XR_TYPE_FRAME_END_INFO, // type
nullptr, // next
- frame_state.predictedDisplayTime, // displayTime
+ render_state.predicted_display_time, // displayTime
environment_blend_mode, // environmentBlendMode
static_cast<uint32_t>(layers_list.size()), // layerCount
layers_list.ptr() // layers
@@ -2271,6 +2372,7 @@ double OpenXRAPI::get_render_target_size_multiplier() const {
void OpenXRAPI::set_render_target_size_multiplier(double multiplier) {
render_target_size_multiplier = multiplier;
+ set_render_state_multiplier(multiplier);
}
bool OpenXRAPI::is_foveation_supported() const {
@@ -2414,10 +2516,6 @@ OpenXRAPI::OpenXRAPI() {
submit_depth_buffer = GLOBAL_GET("xr/openxr/submit_depth_buffer");
}
-
- // Reset a few things that can't be done in our class definition.
- frame_state.predictedDisplayTime = 0;
- frame_state.predictedDisplayPeriod = 0;
}
OpenXRAPI::~OpenXRAPI() {
@@ -3132,7 +3230,7 @@ XRPose::TrackingConfidence OpenXRAPI::get_action_pose(RID p_action, RID p_tracke
return XRPose::XR_TRACKING_CONFIDENCE_NONE;
}
- XrTime display_time = get_next_frame_time();
+ XrTime display_time = get_predicted_display_time();
if (display_time == 0) {
return XRPose::XR_TRACKING_CONFIDENCE_NONE;
}
diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h
index e835366200..c95867810c 100644
--- a/modules/openxr/openxr_api.h
+++ b/modules/openxr/openxr_api.h
@@ -46,13 +46,11 @@
#include "core/templates/rb_map.h"
#include "core/templates/rid_owner.h"
#include "core/templates/vector.h"
+#include "servers/rendering_server.h"
#include "servers/xr/xr_pose.h"
#include <openxr/openxr.h>
-// Note, OpenXR code that we wrote for our plugin makes use of C++20 notation for initializing structs which ensures zeroing out unspecified members.
-// Godot is currently restricted to C++17 which doesn't allow this notation. Make sure critical fields are set.
-
// forward declarations, we don't want to include these fully
class OpenXRInterface;
@@ -77,7 +75,7 @@ public:
static void free_queued();
void free();
- bool acquire(XrBool32 &p_should_render);
+ bool acquire(bool &p_should_render);
bool release();
RID get_image();
};
@@ -151,9 +149,6 @@ private:
uint32_t view_count = 0;
XrViewConfigurationView *view_configuration_views = nullptr;
- XrView *views = nullptr;
- XrCompositionLayerProjectionView *projection_views = nullptr;
- XrCompositionLayerDepthInfoKHR *depth_views = nullptr; // Only used by Composition Layer Depth Extension if available
enum OpenXRSwapChainTypes {
OPENXR_SWAPCHAIN_COLOR,
@@ -164,14 +159,11 @@ private:
int64_t color_swapchain_format = 0;
int64_t depth_swapchain_format = 0;
- Size2i main_swapchain_size = { 0, 0 };
- OpenXRSwapChainInfo main_swapchains[OPENXR_SWAPCHAIN_MAX];
+ bool play_space_is_dirty = true;
XrSpace play_space = XR_NULL_HANDLE;
XrSpace view_space = XR_NULL_HANDLE;
- bool view_pose_valid = false;
XRPose::TrackingConfidence head_pose_confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE;
- bool has_xr_viewport = false;
bool emulating_local_floor = false;
bool should_reset_emulated_floor_height = false;
@@ -328,6 +320,72 @@ private:
// convenience
void copy_string_to_char_buffer(const String p_string, char *p_buffer, int p_buffer_len);
+ // Render state, Only accessible in rendering thread
+ struct RenderState {
+ bool running = false;
+ bool should_render = false;
+ bool has_xr_viewport = false;
+ XrTime predicted_display_time = 0;
+ XrSpace play_space = XR_NULL_HANDLE;
+ double render_target_size_multiplier = 1.0;
+
+ uint32_t view_count = 0;
+ XrView *views = nullptr;
+ XrCompositionLayerProjectionView *projection_views = nullptr;
+ XrCompositionLayerDepthInfoKHR *depth_views = nullptr; // Only used by Composition Layer Depth Extension if available
+ bool submit_depth_buffer = false; // if set to true we submit depth buffers to OpenXR if a suitable extension is enabled.
+ bool view_pose_valid = false;
+
+ Size2i main_swapchain_size;
+ OpenXRSwapChainInfo main_swapchains[OPENXR_SWAPCHAIN_MAX];
+ } render_state;
+
+ static void _allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer);
+ static void _set_render_session_running(bool p_is_running);
+ static void _set_render_display_info(XrTime p_predicted_display_time, bool p_should_render);
+ static void _set_render_play_space(uint64_t p_play_space);
+ static void _set_render_state_multiplier(double p_render_target_size_multiplier);
+
+ _FORCE_INLINE_ void allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer) {
+ // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready...
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rendering_server);
+
+ rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_allocate_view_buffers).bind(p_view_count, p_submit_depth_buffer));
+ }
+
+ _FORCE_INLINE_ void set_render_session_running(bool p_is_running) {
+ // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready...
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rendering_server);
+
+ rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_session_running).bind(p_is_running));
+ }
+
+ _FORCE_INLINE_ void set_render_display_info(XrTime p_predicted_display_time, bool p_should_render) {
+ // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready...
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rendering_server);
+
+ rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_display_info).bind(p_predicted_display_time, p_should_render));
+ }
+
+ _FORCE_INLINE_ void set_render_play_space(XrSpace p_play_space) {
+ // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready...
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rendering_server);
+
+ rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_play_space).bind(uint64_t(p_play_space)));
+ }
+
+ _FORCE_INLINE_ void set_render_state_multiplier(double p_render_target_size_multiplier) {
+ // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready...
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rendering_server);
+
+ rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_state_multiplier).bind(p_render_target_size_multiplier));
+ }
+
public:
XrInstance get_instance() const { return instance; };
XrSystemId get_system_id() const { return system_id; };
@@ -384,9 +442,13 @@ public:
bool initialize_session();
void finish();
- XrSpace get_play_space() const { return play_space; }
- XrTime get_next_frame_time() { return frame_state.predictedDisplayTime + frame_state.predictedDisplayPeriod; }
- bool can_render() { return instance != XR_NULL_HANDLE && session != XR_NULL_HANDLE && running && view_pose_valid && frame_state.shouldRender; }
+ _FORCE_INLINE_ XrSpace get_play_space() const { return play_space; }
+ _FORCE_INLINE_ XrTime get_predicted_display_time() { return frame_state.predictedDisplayTime; }
+ _FORCE_INLINE_ XrTime get_next_frame_time() { return frame_state.predictedDisplayTime + frame_state.predictedDisplayPeriod; }
+ _FORCE_INLINE_ bool can_render() {
+ ERR_ON_RENDER_THREAD_V(false);
+ return instance != XR_NULL_HANDLE && session != XR_NULL_HANDLE && running && frame_state.shouldRender;
+ }
XrHandTrackerEXT get_hand_tracker(int p_hand_index);
diff --git a/modules/openxr/openxr_api_extension.cpp b/modules/openxr/openxr_api_extension.cpp
index fae0fc13d3..a1744fa1db 100644
--- a/modules/openxr/openxr_api_extension.cpp
+++ b/modules/openxr/openxr_api_extension.cpp
@@ -48,6 +48,7 @@ void OpenXRAPIExtension::_bind_methods() {
ClassDB::bind_method(D_METHOD("is_running"), &OpenXRAPIExtension::is_running);
ClassDB::bind_method(D_METHOD("get_play_space"), &OpenXRAPIExtension::get_play_space);
+ ClassDB::bind_method(D_METHOD("get_predicted_display_time"), &OpenXRAPIExtension::get_predicted_display_time);
ClassDB::bind_method(D_METHOD("get_next_frame_time"), &OpenXRAPIExtension::get_next_frame_time);
ClassDB::bind_method(D_METHOD("can_render"), &OpenXRAPIExtension::can_render);
@@ -130,8 +131,17 @@ uint64_t OpenXRAPIExtension::get_play_space() {
return (uint64_t)OpenXRAPI::get_singleton()->get_play_space();
}
+int64_t OpenXRAPIExtension::get_predicted_display_time() {
+ ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), 0);
+ return (XrTime)OpenXRAPI::get_singleton()->get_predicted_display_time();
+}
+
int64_t OpenXRAPIExtension::get_next_frame_time() {
ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), 0);
+
+ // In the past we needed to look a frame ahead, may be calling this unintentionally so lets warn the dev.
+ WARN_PRINT_ONCE("OpenXR: Next frame timing called, verify this is intended.");
+
return (XrTime)OpenXRAPI::get_singleton()->get_next_frame_time();
}
diff --git a/modules/openxr/openxr_api_extension.h b/modules/openxr/openxr_api_extension.h
index 576e497798..cff2c4738e 100644
--- a/modules/openxr/openxr_api_extension.h
+++ b/modules/openxr/openxr_api_extension.h
@@ -69,6 +69,7 @@ public:
bool is_running();
uint64_t get_play_space();
+ int64_t get_predicted_display_time();
int64_t get_next_frame_time();
bool can_render();
diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp
index aa68441f03..39a61d1b4d 100644
--- a/modules/openxr/openxr_interface.cpp
+++ b/modules/openxr/openxr_interface.cpp
@@ -35,6 +35,7 @@
#include "servers/rendering/rendering_server_globals.h"
#include "extensions/openxr_eye_gaze_interaction.h"
+#include "extensions/openxr_hand_interaction_extension.h"
#include "thirdparty/openxr/include/openxr/openxr.h"
void OpenXRInterface::_bind_methods() {
@@ -43,6 +44,8 @@ void OpenXRInterface::_bind_methods() {
ADD_SIGNAL(MethodInfo("session_stopping"));
ADD_SIGNAL(MethodInfo("session_focussed"));
ADD_SIGNAL(MethodInfo("session_visible"));
+ ADD_SIGNAL(MethodInfo("session_loss_pending"));
+ ADD_SIGNAL(MethodInfo("instance_exiting"));
ADD_SIGNAL(MethodInfo("pose_recentered"));
ADD_SIGNAL(MethodInfo("refresh_rate_changed", PropertyInfo(Variant::FLOAT, "refresh_rate")));
@@ -91,6 +94,7 @@ void OpenXRInterface::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_hand_joint_angular_velocity", "hand", "joint"), &OpenXRInterface::get_hand_joint_angular_velocity);
ClassDB::bind_method(D_METHOD("is_hand_tracking_supported"), &OpenXRInterface::is_hand_tracking_supported);
+ ClassDB::bind_method(D_METHOD("is_hand_interaction_supported"), &OpenXRInterface::is_hand_interaction_supported);
ClassDB::bind_method(D_METHOD("is_eye_gaze_interaction_supported"), &OpenXRInterface::is_eye_gaze_interaction_supported);
BIND_ENUM_CONSTANT(HAND_LEFT);
@@ -806,6 +810,21 @@ bool OpenXRInterface::is_hand_tracking_supported() {
}
}
+bool OpenXRInterface::is_hand_interaction_supported() const {
+ if (openxr_api == nullptr) {
+ return false;
+ } else if (!openxr_api->is_initialized()) {
+ return false;
+ } else {
+ OpenXRHandInteractionExtension *hand_interaction_ext = OpenXRHandInteractionExtension::get_singleton();
+ if (hand_interaction_ext == nullptr) {
+ return false;
+ } else {
+ return hand_interaction_ext->is_available();
+ }
+ }
+}
+
bool OpenXRInterface::is_eye_gaze_interaction_supported() {
if (openxr_api == nullptr) {
return false;
@@ -1258,6 +1277,14 @@ void OpenXRInterface::on_state_stopping() {
emit_signal(SNAME("session_stopping"));
}
+void OpenXRInterface::on_state_loss_pending() {
+ emit_signal(SNAME("session_loss_pending"));
+}
+
+void OpenXRInterface::on_state_exiting() {
+ emit_signal(SNAME("instance_exiting"));
+}
+
void OpenXRInterface::on_pose_recentered() {
emit_signal(SNAME("pose_recentered"));
}
diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h
index e916c7dac2..ac33304757 100644
--- a/modules/openxr/openxr_interface.h
+++ b/modules/openxr/openxr_interface.h
@@ -31,6 +31,29 @@
#ifndef OPENXR_INTERFACE_H
#define OPENXR_INTERFACE_H
+// A note on multithreading and thread safety in OpenXR.
+//
+// Most entry points will be called from the main thread in Godot
+// however a number of entry points will be called from the
+// rendering thread, potentially while we're already processing
+// the next frame on the main thread.
+//
+// OpenXR itself has been designed with threading in mind including
+// a high likelihood that the XR runtime runs in separate threads
+// as well.
+// Hence all the frame timing information, use of swapchains and
+// sync functions.
+// Do note that repeated calls to tracking APIs will provide
+// increasingly more accurate data for the same timestamp as
+// tracking data is continuously updated.
+//
+// For our code we mostly implement this in our OpenXRAPI class.
+// We store data accessed from the rendering thread in a separate
+// struct, setting values through our renderer command queue.
+//
+// As some data is setup before we start rendering, and cleaned up
+// after we've stopped, that is accessed directly from both threads.
+
#include "action_map/openxr_action_map.h"
#include "extensions/openxr_hand_tracking_extension.h"
#include "openxr_api.h"
@@ -110,6 +133,7 @@ public:
virtual TrackingStatus get_tracking_status() const override;
bool is_hand_tracking_supported();
+ bool is_hand_interaction_supported() const;
bool is_eye_gaze_interaction_supported();
bool initialize_on_startup() const;
@@ -173,6 +197,8 @@ public:
void on_state_visible();
void on_state_focused();
void on_state_stopping();
+ void on_state_loss_pending();
+ void on_state_exiting();
void on_pose_recentered();
void on_refresh_rate_changes(float p_new_rate);
void tracker_profile_changed(RID p_tracker, RID p_interaction_profile);
diff --git a/modules/openxr/register_types.cpp b/modules/openxr/register_types.cpp
index eb0527f07c..85514737f2 100644
--- a/modules/openxr/register_types.cpp
+++ b/modules/openxr/register_types.cpp
@@ -49,6 +49,7 @@
#include "extensions/openxr_composition_layer_extension.h"
#include "extensions/openxr_eye_gaze_interaction.h"
#include "extensions/openxr_fb_display_refresh_rate_extension.h"
+#include "extensions/openxr_hand_interaction_extension.h"
#include "extensions/openxr_hand_tracking_extension.h"
#include "extensions/openxr_htc_controller_extension.h"
#include "extensions/openxr_htc_vive_tracker_extension.h"
@@ -124,6 +125,7 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
OpenXRAPI::register_extension_wrapper(memnew(OpenXRML2ControllerExtension));
OpenXRAPI::register_extension_wrapper(memnew(OpenXRMetaControllerExtension));
OpenXRAPI::register_extension_wrapper(memnew(OpenXREyeGazeInteractionExtension));
+ OpenXRAPI::register_extension_wrapper(memnew(OpenXRHandInteractionExtension));
// register gated extensions
if (GLOBAL_GET("xr/openxr/extensions/hand_tracking")) {
diff --git a/modules/raycast/config.py b/modules/raycast/config.py
index 26329d813a..0fd35af528 100644
--- a/modules/raycast/config.py
+++ b/modules/raycast/config.py
@@ -1,8 +1,9 @@
def can_build(env, platform):
- # Supported architectures depend on the Embree library.
+ # Supported architectures and platforms depend on the Embree library.
+ if env["arch"] == "arm64" and platform == "windows":
+ return False
if env["arch"] in ["x86_64", "arm64", "wasm32"]:
return True
- # x86_32 only seems supported on Windows for now.
if env["arch"] == "x86_32" and platform == "windows":
return True
return False
diff --git a/modules/text_server_adv/gdextension_build/methods.py b/modules/text_server_adv/gdextension_build/methods.py
index 1c43759c55..3453c3e8f0 100644
--- a/modules/text_server_adv/gdextension_build/methods.py
+++ b/modules/text_server_adv/gdextension_build/methods.py
@@ -15,59 +15,61 @@ class ANSI(Enum):
internal value, or an empty string in a non-colorized scope.
"""
- GRAY = "\x1b[0;30m"
- RED = "\x1b[0;31m"
- GREEN = "\x1b[0;32m"
- YELLOW = "\x1b[0;33m"
- BLUE = "\x1b[0;34m"
- PURPLE = "\x1b[0;35m"
- CYAN = "\x1b[0;36m"
- WHITE = "\x1b[0;37m"
-
- BOLD_GRAY = "\x1b[1;90m"
- BOLD_RED = "\x1b[1;91m"
- BOLD_GREEN = "\x1b[1;92m"
- BOLD_YELLOW = "\x1b[1;93m"
- BOLD_BLUE = "\x1b[1;94m"
- BOLD_PURPLE = "\x1b[1;95m"
- BOLD_CYAN = "\x1b[1;96m"
- BOLD_WHITE = "\x1b[1;97m"
-
RESET = "\x1b[0m"
- def __str__(self):
+ 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 self.value if _colorize else ""
+ return str(self.value) if _colorize else ""
def no_verbose(env):
- colors = [ANSI.BLUE, ANSI.BOLD_BLUE, ANSI.RESET]
+ 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 = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors)
- java_compile_source_message = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors)
- compile_shared_source_message = "{0}Compiling shared {1}$SOURCE{0} ...{2}".format(*colors)
- link_program_message = "{0}Linking Program {1}$TARGET{0} ...{2}".format(*colors)
- link_library_message = "{0}Linking Static Library {1}$TARGET{0} ...{2}".format(*colors)
- ranlib_library_message = "{0}Ranlib Library {1}$TARGET{0} ...{2}".format(*colors)
- link_shared_library_message = "{0}Linking Shared Library {1}$TARGET{0} ...{2}".format(*colors)
- java_library_message = "{0}Creating Java Archive {1}$TARGET{0} ...{2}".format(*colors)
- compiled_resource_message = "{0}Creating Compiled Resource {1}$TARGET{0} ...{2}".format(*colors)
- generated_file_message = "{0}Generating {1}$TARGET{0} ...{2}".format(*colors)
-
- env.Append(CXXCOMSTR=compile_source_message)
- env.Append(CCCOMSTR=compile_source_message)
- env.Append(SHCCCOMSTR=compile_shared_source_message)
- env.Append(SHCXXCOMSTR=compile_shared_source_message)
- env.Append(ARCOMSTR=link_library_message)
- env.Append(RANLIBCOMSTR=ranlib_library_message)
- env.Append(SHLINKCOMSTR=link_shared_library_message)
- env.Append(LINKCOMSTR=link_program_message)
- env.Append(JARCOMSTR=java_library_message)
- env.Append(JAVACCOMSTR=java_compile_source_message)
- env.Append(RCCOMSTR=compiled_resource_message)
- env.Append(GENCOMSTR=generated_file_message)
+ compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors)
+ java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors)
+ compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors)
+ link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors)
+ link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors)
+ ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors)
+ link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors)
+ java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors)
+ compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors)
+ generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors)
+
+ env["CXXCOMSTR"] = compile_source_message
+ env["CCCOMSTR"] = compile_source_message
+ env["SHCCCOMSTR"] = compile_shared_source_message
+ env["SHCXXCOMSTR"] = compile_shared_source_message
+ env["ARCOMSTR"] = link_library_message
+ env["RANLIBCOMSTR"] = ranlib_library_message
+ env["SHLINKCOMSTR"] = link_shared_library_message
+ env["LINKCOMSTR"] = link_program_message
+ env["JARCOMSTR"] = java_library_message
+ env["JAVACCOMSTR"] = java_compile_source_message
+ env["RCCOMSTR"] = compiled_resource_message
+ env["GENCOMSTR"] = generated_file_message
def disable_warnings(self):
diff --git a/modules/text_server_fb/gdextension_build/methods.py b/modules/text_server_fb/gdextension_build/methods.py
index 1c43759c55..3453c3e8f0 100644
--- a/modules/text_server_fb/gdextension_build/methods.py
+++ b/modules/text_server_fb/gdextension_build/methods.py
@@ -15,59 +15,61 @@ class ANSI(Enum):
internal value, or an empty string in a non-colorized scope.
"""
- GRAY = "\x1b[0;30m"
- RED = "\x1b[0;31m"
- GREEN = "\x1b[0;32m"
- YELLOW = "\x1b[0;33m"
- BLUE = "\x1b[0;34m"
- PURPLE = "\x1b[0;35m"
- CYAN = "\x1b[0;36m"
- WHITE = "\x1b[0;37m"
-
- BOLD_GRAY = "\x1b[1;90m"
- BOLD_RED = "\x1b[1;91m"
- BOLD_GREEN = "\x1b[1;92m"
- BOLD_YELLOW = "\x1b[1;93m"
- BOLD_BLUE = "\x1b[1;94m"
- BOLD_PURPLE = "\x1b[1;95m"
- BOLD_CYAN = "\x1b[1;96m"
- BOLD_WHITE = "\x1b[1;97m"
-
RESET = "\x1b[0m"
- def __str__(self):
+ 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 self.value if _colorize else ""
+ return str(self.value) if _colorize else ""
def no_verbose(env):
- colors = [ANSI.BLUE, ANSI.BOLD_BLUE, ANSI.RESET]
+ 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 = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors)
- java_compile_source_message = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors)
- compile_shared_source_message = "{0}Compiling shared {1}$SOURCE{0} ...{2}".format(*colors)
- link_program_message = "{0}Linking Program {1}$TARGET{0} ...{2}".format(*colors)
- link_library_message = "{0}Linking Static Library {1}$TARGET{0} ...{2}".format(*colors)
- ranlib_library_message = "{0}Ranlib Library {1}$TARGET{0} ...{2}".format(*colors)
- link_shared_library_message = "{0}Linking Shared Library {1}$TARGET{0} ...{2}".format(*colors)
- java_library_message = "{0}Creating Java Archive {1}$TARGET{0} ...{2}".format(*colors)
- compiled_resource_message = "{0}Creating Compiled Resource {1}$TARGET{0} ...{2}".format(*colors)
- generated_file_message = "{0}Generating {1}$TARGET{0} ...{2}".format(*colors)
-
- 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/platform/android/SCsub b/platform/android/SCsub
index 7380511d6d..4d76ffb180 100644
--- a/platform/android/SCsub
+++ b/platform/android/SCsub
@@ -1,5 +1,6 @@
#!/usr/bin/env python
+import sys
import subprocess
from methods import print_warning
@@ -82,10 +83,21 @@ if lib_arch_dir != "":
env_android.Command(out_dir + "/libc++_shared.so", stl_lib_path, Copy("$TARGET", "$SOURCE"))
def generate_apk(target, source, env):
+ gradle_process = []
+
+ if sys.platform.startswith("win"):
+ gradle_process = [
+ "cmd",
+ "/c",
+ "gradlew.bat",
+ ]
+ else:
+ gradle_process = ["./gradlew"]
+
if env["target"] != "editor" and env["dev_build"]:
subprocess.run(
- [
- "./gradlew",
+ gradle_process
+ + [
"generateDevTemplate",
"--quiet",
],
@@ -94,8 +106,8 @@ if lib_arch_dir != "":
else:
# Android editor with `dev_build=yes` is handled by the `generateGodotEditor` task.
subprocess.run(
- [
- "./gradlew",
+ gradle_process
+ + [
"generateGodotEditor" if env["target"] == "editor" else "generateGodotTemplates",
"--quiet",
],
diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp
index 3a63b87c7a..1701aa650d 100644
--- a/platform/linuxbsd/wayland/wayland_thread.cpp
+++ b/platform/linuxbsd/wayland/wayland_thread.cpp
@@ -2461,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);
diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp
index e854fe9274..b76cbc126f 100644
--- a/platform/linuxbsd/x11/display_server_x11.cpp
+++ b/platform/linuxbsd/x11/display_server_x11.cpp
@@ -2225,7 +2225,7 @@ void DisplayServerX11::window_set_size(const Size2i p_size, WindowID p_window) {
ERR_FAIL_COND(!windows.has(p_window));
Size2i size = p_size;
- size = size.max(Size2i(1, 1));
+ size = size.maxi(1);
WindowData &wd = windows[p_window];
diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h
index 5d38bf55ea..db76b7d78a 100644
--- a/platform/macos/display_server_macos.h
+++ b/platform/macos/display_server_macos.h
@@ -192,7 +192,7 @@ private:
HashMap<WindowID, WindowData> windows;
struct IndicatorData {
- id view;
+ id delegate;
id item;
};
@@ -431,10 +431,12 @@ public:
virtual void set_native_icon(const String &p_filename) override;
virtual void set_icon(const Ref<Image> &p_icon) override;
- virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) override;
- virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) override;
+ virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) override;
+ virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) override;
virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) override;
+ virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) override;
virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) override;
+ virtual 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 6461f50818..0041848c78 100644
--- a/platform/macos/display_server_macos.mm
+++ b/platform/macos/display_server_macos.mm
@@ -2321,7 +2321,7 @@ void DisplayServerMacOS::window_set_window_buttons_offset(const Vector2i &p_offs
WindowData &wd = windows[p_window];
float scale = screen_get_max_scale();
wd.wb_offset = p_offset / scale;
- wd.wb_offset = wd.wb_offset.max(Vector2i(12, 12));
+ wd.wb_offset = wd.wb_offset.maxi(12);
if (wd.window_button_view) {
[wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)];
}
@@ -3151,10 +3151,11 @@ void DisplayServerMacOS::set_icon(const Ref<Image> &p_icon) {
}
}
-DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) {
+DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) {
NSImage *nsimg = nullptr;
if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) {
- Ref<Image> img = p_icon->duplicate();
+ Ref<Image> img = p_icon->get_image();
+ img = img->duplicate();
img->convert(Image::FORMAT_RGBA8);
NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc]
@@ -3192,13 +3193,18 @@ DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref
IndicatorData idat;
- idat.item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
- idat.view = [[GodotStatusItemView alloc] init];
+ NSStatusItem *item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
+ idat.item = item;
+ idat.delegate = [[GodotStatusItemDelegate alloc] init];
+ [idat.delegate setCallback:p_callback];
- [idat.view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]];
- [idat.view setImage:nsimg];
- [idat.view setCallback:p_callback];
- [idat.item setView:idat.view];
+ item.button.image = nsimg;
+ item.button.imagePosition = NSImageOnly;
+ item.button.imageScaling = NSImageScaleProportionallyUpOrDown;
+ item.button.target = idat.delegate;
+ item.button.action = @selector(click:);
+ [item.button sendActionOn:(NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown)];
+ item.button.toolTip = [NSString stringWithUTF8String:p_tooltip.utf8().get_data()];
IndicatorID iid = indicator_id_counter++;
indicators[iid] = idat;
@@ -3206,12 +3212,13 @@ DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref
return iid;
}
-void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) {
+void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) {
ERR_FAIL_COND(!indicators.has(p_id));
NSImage *nsimg = nullptr;
if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) {
- Ref<Image> img = p_icon->duplicate();
+ Ref<Image> img = p_icon->get_image();
+ img = img->duplicate();
img->convert(Image::FORMAT_RGBA8);
NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc]
@@ -3247,19 +3254,57 @@ void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<I
}
}
- [indicators[p_id].view setImage:nsimg];
+ NSStatusItem *item = indicators[p_id].item;
+ item.button.image = nsimg;
}
void DisplayServerMacOS::status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) {
ERR_FAIL_COND(!indicators.has(p_id));
- [indicators[p_id].view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]];
+ NSStatusItem *item = indicators[p_id].item;
+ item.button.toolTip = [NSString stringWithUTF8String:p_tooltip.utf8().get_data()];
+}
+
+void DisplayServerMacOS::status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) {
+ ERR_FAIL_COND(!indicators.has(p_id));
+
+ NSStatusItem *item = indicators[p_id].item;
+ if (p_menu_rid.is_valid() && native_menu->has_menu(p_menu_rid)) {
+ NSMenu *menu = native_menu->get_native_menu_handle(p_menu_rid);
+ item.menu = menu;
+ } else {
+ item.menu = nullptr;
+ }
}
void DisplayServerMacOS::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) {
ERR_FAIL_COND(!indicators.has(p_id));
- [indicators[p_id].view setCallback:p_callback];
+ [indicators[p_id].delegate setCallback:p_callback];
+}
+
+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/godot_status_item.h b/platform/macos/godot_status_item.h
index 1827baa9bd..5bc790956e 100644
--- a/platform/macos/godot_status_item.h
+++ b/platform/macos/godot_status_item.h
@@ -37,13 +37,12 @@
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
-@interface GodotStatusItemView : NSView {
- NSImage *image;
+@interface GodotStatusItemDelegate : NSObject {
Callable cb;
}
-- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index;
-- (void)setImage:(NSImage *)image;
+- (IBAction)click:(id)sender;
+
- (void)setCallback:(const Callable &)callback;
@end
diff --git a/platform/macos/godot_status_item.mm b/platform/macos/godot_status_item.mm
index 71ed0a0f71..0990a16b2b 100644
--- a/platform/macos/godot_status_item.mm
+++ b/platform/macos/godot_status_item.mm
@@ -32,30 +32,32 @@
#include "display_server_macos.h"
-@implementation GodotStatusItemView
+@implementation GodotStatusItemDelegate
- (id)init {
self = [super init];
- image = nullptr;
return self;
}
-- (void)setImage:(NSImage *)newImage {
- image = newImage;
- [self setNeedsDisplayInRect:self.frame];
-}
-
-- (void)setCallback:(const Callable &)callback {
- cb = callback;
-}
-
-- (void)drawRect:(NSRect)rect {
- if (image) {
- [image drawInRect:rect];
+- (IBAction)click:(id)sender {
+ NSEvent *current_event = [NSApp currentEvent];
+ MouseButton index = MouseButton::LEFT;
+ if (current_event) {
+ if (current_event.type == NSEventTypeLeftMouseDown) {
+ index = MouseButton::LEFT;
+ } else if (current_event.type == NSEventTypeRightMouseDown) {
+ index = MouseButton::RIGHT;
+ } else if (current_event.type == NSEventTypeOtherMouseDown) {
+ if ((int)[current_event buttonNumber] == 2) {
+ index = MouseButton::MIDDLE;
+ } else if ((int)[current_event buttonNumber] == 3) {
+ index = MouseButton::MB_XBUTTON1;
+ } else if ((int)[current_event buttonNumber] == 4) {
+ index = MouseButton::MB_XBUTTON2;
+ }
+ }
}
-}
-- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index {
DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton();
if (!ds) {
return;
@@ -71,31 +73,8 @@
}
}
-- (void)mouseDown:(NSEvent *)event {
- [super mouseDown:event];
- if (([event modifierFlags] & NSEventModifierFlagControl)) {
- [self processMouseEvent:event index:MouseButton::RIGHT];
- } else {
- [self processMouseEvent:event index:MouseButton::LEFT];
- }
-}
-
-- (void)rightMouseDown:(NSEvent *)event {
- [super rightMouseDown:event];
-
- [self processMouseEvent:event index:MouseButton::RIGHT];
-}
-
-- (void)otherMouseDown:(NSEvent *)event {
- [super otherMouseDown:event];
-
- if ((int)[event buttonNumber] == 2) {
- [self processMouseEvent:event index:MouseButton::MIDDLE];
- } else if ((int)[event buttonNumber] == 3) {
- [self processMouseEvent:event index:MouseButton::MB_XBUTTON1];
- } else if ((int)[event buttonNumber] == 4) {
- [self processMouseEvent:event index:MouseButton::MB_XBUTTON2];
- }
+- (void)setCallback:(const Callable &)callback {
+ cb = callback;
}
@end
diff --git a/platform/macos/native_menu_macos.h b/platform/macos/native_menu_macos.h
index 1d9feb64a7..b5dbb8b9b0 100644
--- a/platform/macos/native_menu_macos.h
+++ b/platform/macos/native_menu_macos.h
@@ -85,6 +85,8 @@ public:
virtual bool has_menu(const RID &p_rid) const override;
virtual void free_menu(const RID &p_rid) override;
+ NSMenu *get_native_menu_handle(const RID &p_rid);
+
virtual Size2 get_size(const RID &p_rid) const override;
virtual void popup(const RID &p_rid, const Vector2i &p_position) override;
diff --git a/platform/macos/native_menu_macos.mm b/platform/macos/native_menu_macos.mm
index 8c2dd98862..1cf13a2d69 100644
--- a/platform/macos/native_menu_macos.mm
+++ b/platform/macos/native_menu_macos.mm
@@ -248,6 +248,13 @@ void NativeMenuMacOS::free_menu(const RID &p_rid) {
}
}
+NSMenu *NativeMenuMacOS::get_native_menu_handle(const RID &p_rid) {
+ MenuData *md = menus.get_or_null(p_rid);
+ ERR_FAIL_NULL_V(md, nullptr);
+
+ return md->menu;
+}
+
Size2 NativeMenuMacOS::get_size(const RID &p_rid) const {
const MenuData *md = menus.get_or_null(p_rid);
ERR_FAIL_NULL_V(md, Size2());
diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp
index 41c969b5f4..d42303ad25 100644
--- a/platform/web/export/export_plugin.cpp
+++ b/platform/web/export/export_plugin.cpp
@@ -170,6 +170,7 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito
replaces["$GODOT_PROJECT_NAME"] = GLOBAL_GET("application/config/name");
replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;
replaces["$GODOT_CONFIG"] = str_config;
+ replaces["$GODOT_SPLASH"] = p_name + ".png";
if (p_preset->get("variant/thread_support")) {
replaces["$GODOT_THREADS_ENABLED"] = "true";
@@ -584,32 +585,176 @@ bool EditorExportPlatformWeb::poll_export() {
}
}
- int prev = menu_options;
- menu_options = preset.is_valid();
+ HTTPServerState prev_server_state = server_state;
+ server_state = HTTP_SERVER_STATE_OFF;
if (server->is_listening()) {
- if (menu_options == 0) {
+ if (preset.is_null()) {
server->stop();
} else {
- menu_options += 1;
+ server_state = HTTP_SERVER_STATE_ON;
}
}
- return menu_options != prev;
+
+ return server_state != prev_server_state;
}
Ref<ImageTexture> EditorExportPlatformWeb::get_option_icon(int p_index) const {
- return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
+ Ref<ImageTexture> play_icon = EditorExportPlatform::get_option_icon(p_index);
+
+ switch (server_state) {
+ case HTTP_SERVER_STATE_OFF: {
+ switch (p_index) {
+ case 0:
+ case 1:
+ return play_icon;
+ }
+ } break;
+
+ case HTTP_SERVER_STATE_ON: {
+ switch (p_index) {
+ case 0:
+ return play_icon;
+ case 1:
+ return restart_icon;
+ case 2:
+ return stop_icon;
+ }
+ } break;
+ }
+
+ ERR_FAIL_V_MSG(nullptr, vformat(R"(EditorExportPlatformWeb option icon index "%s" is invalid.)", p_index));
}
int EditorExportPlatformWeb::get_options_count() const {
- return menu_options;
+ if (server_state == HTTP_SERVER_STATE_ON) {
+ return 3;
+ }
+ return 2;
+}
+
+String EditorExportPlatformWeb::get_option_label(int p_index) const {
+ String run_in_browser = TTR("Run in Browser");
+ String start_http_server = TTR("Start HTTP Server");
+ String reexport_project = TTR("Re-export Project");
+ String stop_http_server = TTR("Stop HTTP Server");
+
+ switch (server_state) {
+ case HTTP_SERVER_STATE_OFF: {
+ switch (p_index) {
+ case 0:
+ return run_in_browser;
+ case 1:
+ return start_http_server;
+ }
+ } break;
+
+ case HTTP_SERVER_STATE_ON: {
+ switch (p_index) {
+ case 0:
+ return run_in_browser;
+ case 1:
+ return reexport_project;
+ case 2:
+ return stop_http_server;
+ }
+ } break;
+ }
+
+ ERR_FAIL_V_MSG("", vformat(R"(EditorExportPlatformWeb option label index "%s" is invalid.)", p_index));
+}
+
+String EditorExportPlatformWeb::get_option_tooltip(int p_index) const {
+ String run_in_browser = TTR("Run exported HTML in the system's default browser.");
+ String start_http_server = TTR("Start the HTTP server.");
+ String reexport_project = TTR("Export project again to account for updates.");
+ String stop_http_server = TTR("Stop the HTTP server.");
+
+ switch (server_state) {
+ case HTTP_SERVER_STATE_OFF: {
+ switch (p_index) {
+ case 0:
+ return run_in_browser;
+ case 1:
+ return start_http_server;
+ }
+ } break;
+
+ case HTTP_SERVER_STATE_ON: {
+ switch (p_index) {
+ case 0:
+ return run_in_browser;
+ case 1:
+ return reexport_project;
+ case 2:
+ return stop_http_server;
+ }
+ } break;
+ }
+
+ ERR_FAIL_V_MSG("", vformat(R"(EditorExportPlatformWeb option tooltip index "%s" is invalid.)", p_index));
}
Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) {
- if (p_option == 1) {
- server->stop();
- return OK;
+ const uint16_t bind_port = EDITOR_GET("export/web/http_port");
+ // Resolve host if needed.
+ const String bind_host = EDITOR_GET("export/web/http_host");
+ const bool use_tls = EDITOR_GET("export/web/use_tls");
+
+ switch (server_state) {
+ case HTTP_SERVER_STATE_OFF: {
+ switch (p_option) {
+ // Run in Browser.
+ case 0: {
+ Error err = _export_project(p_preset, p_debug_flags);
+ if (err != OK) {
+ return err;
+ }
+ err = _start_server(bind_host, bind_port, use_tls);
+ if (err != OK) {
+ return err;
+ }
+ return _launch_browser(bind_host, bind_port, use_tls);
+ } break;
+
+ // Start HTTP Server.
+ case 1: {
+ Error err = _export_project(p_preset, p_debug_flags);
+ if (err != OK) {
+ return err;
+ }
+ return _start_server(bind_host, bind_port, use_tls);
+ } break;
+ }
+ } break;
+
+ case HTTP_SERVER_STATE_ON: {
+ switch (p_option) {
+ // Run in Browser.
+ case 0: {
+ Error err = _export_project(p_preset, p_debug_flags);
+ if (err != OK) {
+ return err;
+ }
+ return _launch_browser(bind_host, bind_port, use_tls);
+ } break;
+
+ // Re-export Project.
+ case 1: {
+ return _export_project(p_preset, p_debug_flags);
+ } break;
+
+ // Stop HTTP Server.
+ case 2: {
+ return _stop_server();
+ } break;
+ }
+ } break;
}
+ ERR_FAIL_V_MSG(ERR_INVALID_PARAMETER, vformat(R"(Trying to run EditorExportPlatformWeb, but option "%s" isn't known.)", p_option));
+}
+
+Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags) {
const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("web");
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
if (!da->dir_exists(dest)) {
@@ -636,35 +781,40 @@ Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int
DirAccess::remove_file_or_error(basepath + ".wasm");
DirAccess::remove_file_or_error(basepath + ".icon.png");
DirAccess::remove_file_or_error(basepath + ".apple-touch-icon.png");
- return err;
}
+ return err;
+}
- const uint16_t bind_port = EDITOR_GET("export/web/http_port");
- // Resolve host if needed.
- const String bind_host = EDITOR_GET("export/web/http_host");
+Error EditorExportPlatformWeb::_launch_browser(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {
+ OS::get_singleton()->shell_open(String((p_use_tls ? "https://" : "http://") + p_bind_host + ":" + itos(p_bind_port) + "/tmp_js_export.html"));
+ // FIXME: Find out how to clean up export files after running the successfully
+ // exported game. Might not be trivial.
+ return OK;
+}
+
+Error EditorExportPlatformWeb::_start_server(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {
IPAddress bind_ip;
- if (bind_host.is_valid_ip_address()) {
- bind_ip = bind_host;
+ if (p_bind_host.is_valid_ip_address()) {
+ bind_ip = p_bind_host;
} else {
- bind_ip = IP::get_singleton()->resolve_hostname(bind_host);
+ bind_ip = IP::get_singleton()->resolve_hostname(p_bind_host);
}
- ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + bind_host + "'. Try using '127.0.0.1'.");
+ ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + p_bind_host + "'. Try using '127.0.0.1'.");
- const bool use_tls = EDITOR_GET("export/web/use_tls");
const String tls_key = EDITOR_GET("export/web/tls_key");
const String tls_cert = EDITOR_GET("export/web/tls_certificate");
// Restart server.
server->stop();
- err = server->listen(bind_port, bind_ip, use_tls, tls_key, tls_cert);
+ Error err = server->listen(p_bind_port, bind_ip, p_use_tls, tls_key, tls_cert);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err));
- return err;
}
+ return err;
+}
- OS::get_singleton()->shell_open(String((use_tls ? "https://" : "http://") + bind_host + ":" + itos(bind_port) + "/tmp_js_export.html"));
- // FIXME: Find out how to clean up export files after running the successfully
- // exported game. Might not be trivial.
+Error EditorExportPlatformWeb::_stop_server() {
+ server->stop();
return OK;
}
@@ -690,8 +840,10 @@ EditorExportPlatformWeb::EditorExportPlatformWeb() {
Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
if (theme.is_valid()) {
stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons));
+ restart_icon = theme->get_icon(SNAME("Reload"), EditorStringName(EditorIcons));
} else {
stop_icon.instantiate();
+ restart_icon.instantiate();
}
}
}
diff --git a/platform/web/export/export_plugin.h b/platform/web/export/export_plugin.h
index 952d03cdb4..9d3a1a7861 100644
--- a/platform/web/export/export_plugin.h
+++ b/platform/web/export/export_plugin.h
@@ -46,10 +46,16 @@
class EditorExportPlatformWeb : public EditorExportPlatform {
GDCLASS(EditorExportPlatformWeb, EditorExportPlatform);
+ enum HTTPServerState {
+ HTTP_SERVER_STATE_OFF,
+ HTTP_SERVER_STATE_ON,
+ };
+
Ref<ImageTexture> logo;
Ref<ImageTexture> run_icon;
Ref<ImageTexture> stop_icon;
- int menu_options = 0;
+ Ref<ImageTexture> restart_icon;
+ HTTPServerState server_state = HTTP_SERVER_STATE_OFF;
Ref<EditorHTTPServer> server;
@@ -96,6 +102,11 @@ class EditorExportPlatformWeb : public EditorExportPlatform {
Error _build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects);
Error _write_or_error(const uint8_t *p_content, int p_len, String p_path);
+ Error _export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags);
+ Error _launch_browser(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls);
+ Error _start_server(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls);
+ Error _stop_server();
+
public:
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
@@ -112,8 +123,8 @@ public:
virtual bool poll_export() override;
virtual int get_options_count() const override;
- virtual String get_option_label(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); }
- virtual String get_option_tooltip(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); }
+ virtual String get_option_label(int p_index) const override;
+ virtual String get_option_tooltip(int p_index) const override;
virtual Ref<ImageTexture> get_option_icon(int p_index) const override;
virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) override;
virtual Ref<Texture2D> get_run_icon() const override;
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index ebae00017b..f101d85d58 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -3171,14 +3171,12 @@ void DisplayServerWindows::set_icon(const Ref<Image> &p_icon) {
}
}
-DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) {
+DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) {
HICON hicon = nullptr;
if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) {
- Ref<Image> img = p_icon;
- if (img != icon) {
- img = img->duplicate();
- img->convert(Image::FORMAT_RGBA8);
- }
+ Ref<Image> img = p_icon->get_image();
+ img = img->duplicate();
+ img->convert(Image::FORMAT_RGBA8);
int w = img->get_width();
int h = img->get_height();
@@ -3241,16 +3239,14 @@ DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const R
return iid;
}
-void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) {
+void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) {
ERR_FAIL_COND(!indicators.has(p_id));
HICON hicon = nullptr;
if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) {
- Ref<Image> img = p_icon;
- if (img != icon) {
- img = img->duplicate();
- img->convert(Image::FORMAT_RGBA8);
- }
+ Ref<Image> img = p_icon->get_image();
+ img = img->duplicate();
+ img->convert(Image::FORMAT_RGBA8);
int w = img->get_width();
int h = img->get_height();
@@ -3317,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));
@@ -3838,7 +3864,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 };
diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h
index 2fe1b0733d..80f6061348 100644
--- a/platform/windows/display_server_windows.h
+++ b/platform/windows/display_server_windows.h
@@ -464,6 +464,7 @@ class DisplayServerWindows : public DisplayServer {
WNDPROC user_proc = nullptr;
struct IndicatorData {
+ RID menu_rid;
Callable callback;
};
@@ -684,10 +685,12 @@ public:
virtual void set_native_icon(const String &p_filename) override;
virtual void set_icon(const Ref<Image> &p_icon) override;
- virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) override;
- virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) override;
+ virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) override;
+ virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) override;
virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) override;
+ virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_rid) override;
virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) override;
+ virtual 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/scene/2d/parallax_2d.cpp b/scene/2d/parallax_2d.cpp
index 555f3b031c..aacab3213d 100644
--- a/scene/2d/parallax_2d.cpp
+++ b/scene/2d/parallax_2d.cpp
@@ -144,7 +144,7 @@ void Parallax2D::set_repeat_size(const Size2 &p_repeat_size) {
return;
}
- repeat_size = p_repeat_size.max(Vector2(0, 0));
+ repeat_size = p_repeat_size.maxf(0);
_update_process();
_update_repeat();
diff --git a/scene/3d/cpu_particles_3d.cpp b/scene/3d/cpu_particles_3d.cpp
index 0dc9834539..db7b80683c 100644
--- a/scene/3d/cpu_particles_3d.cpp
+++ b/scene/3d/cpu_particles_3d.cpp
@@ -880,7 +880,7 @@ void CPUParticles3D::_particles_process(double p_delta) {
} break;
case EMISSION_SHAPE_RING: {
real_t ring_random_angle = Math::randf() * Math_TAU;
- real_t ring_random_radius = Math::randf() * (emission_ring_radius - emission_ring_inner_radius) + emission_ring_inner_radius;
+ real_t ring_random_radius = Math::sqrt(Math::randf() * (emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius);
Vector3 axis = emission_ring_axis == Vector3(0.0, 0.0, 0.0) ? Vector3(0.0, 0.0, 1.0) : emission_ring_axis.normalized();
Vector3 ortho_axis;
if (axis.abs() == Vector3(1.0, 0.0, 0.0)) {
diff --git a/scene/3d/decal.cpp b/scene/3d/decal.cpp
index 8415fb38cb..485599d0fb 100644
--- a/scene/3d/decal.cpp
+++ b/scene/3d/decal.cpp
@@ -31,7 +31,7 @@
#include "decal.h"
void Decal::set_size(const Vector3 &p_size) {
- size = p_size.max(Vector3(0.001, 0.001, 0.001));
+ size = p_size.maxf(0.001);
RS::get_singleton()->decal_set_size(decal, size);
update_gizmos();
}
diff --git a/scene/3d/fog_volume.cpp b/scene/3d/fog_volume.cpp
index 8af386f282..54631a8dff 100644
--- a/scene/3d/fog_volume.cpp
+++ b/scene/3d/fog_volume.cpp
@@ -73,7 +73,7 @@ bool FogVolume::_get(const StringName &p_name, Variant &r_property) const {
void FogVolume::set_size(const Vector3 &p_size) {
size = p_size;
- size = size.max(Vector3());
+ size = size.maxf(0);
RS::get_singleton()->fog_volume_set_size(_get_volume(), size);
update_gizmos();
}
diff --git a/scene/3d/gpu_particles_collision_3d.cpp b/scene/3d/gpu_particles_collision_3d.cpp
index 8fd5f25749..3a05ec9c9e 100644
--- a/scene/3d/gpu_particles_collision_3d.cpp
+++ b/scene/3d/gpu_particles_collision_3d.cpp
@@ -382,7 +382,7 @@ Vector3i GPUParticlesCollisionSDF3D::get_estimated_cell_size() const {
float cell_size = aabb.get_longest_axis_size() / float(subdiv);
Vector3i sdf_size = Vector3i(aabb.size / cell_size);
- sdf_size = sdf_size.max(Vector3i(1, 1, 1));
+ sdf_size = sdf_size.maxi(1);
return sdf_size;
}
@@ -395,7 +395,7 @@ Ref<Image> GPUParticlesCollisionSDF3D::bake() {
float cell_size = aabb.get_longest_axis_size() / float(subdiv);
Vector3i sdf_size = Vector3i(aabb.size / cell_size);
- sdf_size = sdf_size.max(Vector3i(1, 1, 1));
+ sdf_size = sdf_size.maxi(1);
if (bake_begin_function) {
bake_begin_function(100);
diff --git a/scene/3d/occluder_instance_3d.cpp b/scene/3d/occluder_instance_3d.cpp
index 2f77185d0d..150771545b 100644
--- a/scene/3d/occluder_instance_3d.cpp
+++ b/scene/3d/occluder_instance_3d.cpp
@@ -192,7 +192,7 @@ void QuadOccluder3D::set_size(const Size2 &p_size) {
return;
}
- size = p_size.max(Size2());
+ size = p_size.maxf(0);
_update();
}
@@ -236,7 +236,7 @@ void BoxOccluder3D::set_size(const Vector3 &p_size) {
return;
}
- size = p_size.max(Vector3());
+ size = p_size.maxf(0);
_update();
}
diff --git a/scene/3d/voxel_gi.cpp b/scene/3d/voxel_gi.cpp
index 938d6e5699..fbdda67526 100644
--- a/scene/3d/voxel_gi.cpp
+++ b/scene/3d/voxel_gi.cpp
@@ -294,7 +294,7 @@ VoxelGI::Subdiv VoxelGI::get_subdiv() const {
void VoxelGI::set_size(const Vector3 &p_size) {
// Prevent very small size dimensions as these breaks baking if other size dimensions are set very high.
- size = p_size.max(Vector3(1.0, 1.0, 1.0));
+ size = p_size.maxf(1.0);
update_gizmos();
}
diff --git a/scene/3d/xr_hand_modifier_3d.cpp b/scene/3d/xr_hand_modifier_3d.cpp
index 1e78a4630f..baaa9eee48 100644
--- a/scene/3d/xr_hand_modifier_3d.cpp
+++ b/scene/3d/xr_hand_modifier_3d.cpp
@@ -70,6 +70,11 @@ void XRHandModifier3D::_get_joint_data() {
return;
}
+ if (has_stored_previous_transforms) {
+ previous_relative_transforms.clear();
+ has_stored_previous_transforms = false;
+ }
+
// Table of bone names for different rig types.
static const String bone_names[XRHandTracker::HAND_JOINT_MAX] = {
"Palm",
@@ -196,6 +201,18 @@ void XRHandModifier3D::_process_modification() {
// Skip if no tracking data
if (!tracker->get_has_tracking_data()) {
+ if (!has_stored_previous_transforms) {
+ return;
+ }
+
+ // Apply previous relative transforms if they are stored.
+ for (int joint = 0; joint < XRHandTracker::HAND_JOINT_MAX; joint++) {
+ if (bone_update == BONE_UPDATE_FULL) {
+ skeleton->set_bone_pose_position(joints[joint].bone, previous_relative_transforms[joint].origin);
+ }
+
+ skeleton->set_bone_pose_rotation(joints[joint].bone, Quaternion(previous_relative_transforms[joint].basis));
+ }
return;
}
@@ -223,6 +240,12 @@ void XRHandModifier3D::_process_modification() {
return;
}
+ if (!has_stored_previous_transforms) {
+ previous_relative_transforms.resize(XRHandTracker::HAND_JOINT_MAX);
+ has_stored_previous_transforms = true;
+ }
+ Transform3D *previous_relative_transforms_ptr = previous_relative_transforms.ptrw();
+
for (int joint = 0; joint < XRHandTracker::HAND_JOINT_MAX; joint++) {
// Get the skeleton bone (skip if none).
const int bone = joints[joint].bone;
@@ -233,6 +256,7 @@ void XRHandModifier3D::_process_modification() {
// Calculate the relative relationship to the parent bone joint.
const int parent_joint = joints[joint].parent_joint;
const Transform3D relative_transform = inv_transforms[parent_joint] * transforms[joint];
+ previous_relative_transforms_ptr[joint] = relative_transform;
// Update the bone position if enabled by update mode.
if (bone_update == BONE_UPDATE_FULL) {
diff --git a/scene/3d/xr_hand_modifier_3d.h b/scene/3d/xr_hand_modifier_3d.h
index 67d1694d41..3d78f32b64 100644
--- a/scene/3d/xr_hand_modifier_3d.h
+++ b/scene/3d/xr_hand_modifier_3d.h
@@ -73,6 +73,9 @@ private:
BoneUpdate bone_update = BONE_UPDATE_FULL;
JointData joints[XRHandTracker::HAND_JOINT_MAX];
+ bool has_stored_previous_transforms = false;
+ Vector<Transform3D> previous_relative_transforms;
+
void _get_joint_data();
void _tracker_changed(StringName p_tracker_name, XRServer::TrackerType p_tracker_type);
};
diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp
index 5a3a5f9bc0..d22b58346f 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<AnimatedValuesBackup> 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<Animation> reference_animation = get_animation(p_name);
if (!cache_valid) {
diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp
index 7140161eca..e4808a0ecc 100644
--- a/scene/animation/animation_player.cpp
+++ b/scene/animation/animation_player.cpp
@@ -370,73 +370,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<Animation> 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, -1.0, p_custom_blend, p_custom_scale, p_from_end);
+ } 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<Animation> 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 +473,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<Animation> 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 +714,14 @@ 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;
+}
+
+bool AnimationPlayer::is_auto_capture() const {
+ return auto_capture;
+}
+
#ifdef TOOLS_ENABLED
void AnimationPlayer::get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const {
const String pf = p_function;
@@ -815,9 +812,12 @@ 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("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 +855,7 @@ 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_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 13e1e37ca9..3b229e7699 100644
--- a/scene/animation/animation_player.h
+++ b/scene/animation/animation_player.h
@@ -56,6 +56,7 @@ private:
float speed_scale = 1.0;
double default_blend_time = 0.0;
+ bool auto_capture = true;
bool is_stopping = false;
struct PlaybackData {
@@ -108,6 +109,8 @@ private:
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 +161,12 @@ public:
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 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<String> get_queue();
void clear_queue();
diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp
index 4f90504e35..8131fe7aaa 100644
--- a/scene/gui/code_edit.cpp
+++ b/scene/gui/code_edit.cpp
@@ -624,16 +624,31 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const {
return TextEdit::get_cursor_shape(p_pos);
}
+void CodeEdit::_unhide_carets() {
+ // Unfold caret and selection origin.
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (_is_line_hidden(get_caret_line(i))) {
+ unfold_line(get_caret_line(i));
+ }
+ if (has_selection(i) && _is_line_hidden(get_selection_origin_line(i))) {
+ unfold_line(get_selection_origin_line(i));
+ }
+ }
+}
+
/* Text manipulation */
// Overridable actions
void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) {
start_action(EditAction::ACTION_TYPING);
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
+ begin_multicaret_edit();
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_caret != -1 && p_caret != i) {
continue;
}
+ if (p_caret == -1 && multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
bool had_selection = has_selection(i);
String selection_text = (had_selection ? get_selected_text(i) : "");
@@ -691,6 +706,7 @@ void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca
insert_text_at_caret(chr, i);
}
}
+ end_multicaret_edit();
end_action();
}
@@ -705,66 +721,80 @@ void CodeEdit::_backspace_internal(int p_caret) {
}
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
+ begin_multicaret_edit();
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_caret != -1 && p_caret != i) {
continue;
}
+ if (p_caret == -1 && multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
- int cc = get_caret_column(i);
- int cl = get_caret_line(i);
+ int to_line = get_caret_line(i);
+ int to_column = get_caret_column(i);
- if (cc == 0 && cl == 0) {
+ if (to_column == 0 && to_line == 0) {
continue;
}
- if (cl > 0 && _is_line_hidden(cl - 1)) {
- unfold_line(get_caret_line(i) - 1);
+ if (to_line > 0 && _is_line_hidden(to_line - 1)) {
+ unfold_line(to_line - 1);
}
- int prev_line = cc ? cl : cl - 1;
- int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length());
+ int from_line = to_column > 0 ? to_line : to_line - 1;
+ int from_column = to_column > 0 ? (to_column - 1) : (get_line(to_line - 1).length());
- merge_gutters(prev_line, cl);
+ merge_gutters(from_line, to_line);
- if (auto_brace_completion_enabled && cc > 0) {
- int idx = _get_auto_brace_pair_open_at_pos(cl, cc);
+ if (auto_brace_completion_enabled && to_column > 0) {
+ int idx = _get_auto_brace_pair_open_at_pos(to_line, to_column);
if (idx != -1) {
- prev_column = cc - auto_brace_completion_pairs[idx].open_key.length();
+ from_column = to_column - auto_brace_completion_pairs[idx].open_key.length();
- if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) {
- cc += auto_brace_completion_pairs[idx].close_key.length();
+ if (_get_auto_brace_pair_close_at_pos(to_line, to_column) == idx) {
+ to_column += auto_brace_completion_pairs[idx].close_key.length();
}
-
- remove_text(prev_line, prev_column, cl, cc);
-
- set_caret_line(prev_line, false, true, 0, i);
- set_caret_column(prev_column, i == 0, i);
-
- adjust_carets_after_edit(i, prev_line, prev_column, cl, cc);
- continue;
}
}
// For space indentation we need to do a basic unindent if there are no chars to the left, acting the same way as tabs.
- if (indent_using_spaces && cc != 0) {
- if (get_first_non_whitespace_column(cl) >= cc) {
- prev_column = cc - _calculate_spaces_till_next_left_indent(cc);
- prev_line = cl;
+ if (indent_using_spaces && to_column != 0) {
+ if (get_first_non_whitespace_column(to_line) >= to_column) {
+ from_column = to_column - _calculate_spaces_till_next_left_indent(to_column);
+ from_line = to_line;
}
}
- remove_text(prev_line, prev_column, cl, cc);
-
- set_caret_line(prev_line, false, true, 0, i);
- set_caret_column(prev_column, i == 0, i);
+ remove_text(from_line, from_column, to_line, to_column);
- adjust_carets_after_edit(i, prev_line, prev_column, cl, cc);
+ set_caret_line(from_line, false, true, -1, i);
+ set_caret_column(from_column, i == 0, i);
}
- merge_overlapping_carets();
+
+ end_multicaret_edit();
end_complex_operation();
}
+void CodeEdit::_cut_internal(int p_caret) {
+ // Overridden to unfold lines.
+ _copy_internal(p_caret);
+
+ if (!is_editable()) {
+ return;
+ }
+
+ if (has_selection(p_caret)) {
+ delete_selection(p_caret);
+ return;
+ }
+ if (p_caret == -1) {
+ delete_lines();
+ } else {
+ unfold_line(get_caret_line(p_caret));
+ remove_line_at(get_caret_line(p_caret));
+ }
+}
+
/* Indent management */
void CodeEdit::set_indent_size(const int p_size) {
ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0.");
@@ -838,13 +868,17 @@ void CodeEdit::do_indent() {
}
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
+ begin_multicaret_edit();
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
int spaces_to_add = _calculate_spaces_till_next_right_indent(get_caret_column(i));
if (spaces_to_add > 0) {
insert_text_at_caret(String(" ").repeat(spaces_to_add), i);
}
}
+ end_multicaret_edit();
end_complex_operation();
}
@@ -854,51 +888,28 @@ void CodeEdit::indent_lines() {
}
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &c : caret_edit_order) {
- // This value informs us by how much we changed selection position by indenting right.
- // Default is 1 for tab indentation.
- int selection_offset = 1;
-
- int start_line = get_caret_line(c);
- int end_line = start_line;
- if (has_selection(c)) {
- start_line = get_selection_from_line(c);
- end_line = get_selection_to_line(c);
+ begin_multicaret_edit();
- // Ignore the last line if the selection is not past the first column.
- if (get_selection_to_column(c) == 0) {
- selection_offset = 0;
- end_line--;
- }
- }
-
- for (int i = start_line; i <= end_line; i++) {
+ Vector<Point2i> line_ranges = get_line_ranges_from_carets();
+ for (Point2i line_range : line_ranges) {
+ for (int i = line_range.x; i <= line_range.y; i++) {
const String line_text = get_line(i);
- if (line_text.size() == 0 && has_selection(c)) {
+ if (line_text.size() == 0) {
+ // Ignore empty lines.
continue;
}
- if (!indent_using_spaces) {
- set_line(i, '\t' + line_text);
- continue;
+ if (indent_using_spaces) {
+ int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i));
+ insert_text(String(" ").repeat(spaces_to_add), i, 0, false);
+ } else {
+ insert_text("\t", i, 0, false);
}
-
- // We don't really care where selection is - we just need to know indentation level at the beginning of the line.
- // Since we will add this many spaces, we want to move the whole selection and caret by this much.
- int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i));
- set_line(i, String(" ").repeat(spaces_to_add) + line_text);
- selection_offset = spaces_to_add;
}
-
- // Fix selection and caret being off after shifting selection right.
- if (has_selection(c)) {
- select(start_line, get_selection_from_column(c) + selection_offset, get_selection_to_line(c), get_selection_to_column(c) + selection_offset, c);
- }
- set_caret_column(get_caret_column(c) + selection_offset, false, c);
}
+
+ end_multicaret_edit();
end_complex_operation();
- queue_redraw();
}
void CodeEdit::unindent_lines() {
@@ -907,76 +918,25 @@ void CodeEdit::unindent_lines() {
}
begin_complex_operation();
+ begin_multicaret_edit();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &c : caret_edit_order) {
- // Moving caret and selection after unindenting can get tricky because
- // changing content of line can move caret and selection on its own (if new line ends before previous position of either)
- // therefore we just remember initial values and at the end of the operation offset them by number of removed characters.
- int removed_characters = 0;
- int initial_selection_end_column = 0;
- int initial_cursor_column = get_caret_column(c);
-
- int start_line = get_caret_line(c);
- int end_line = start_line;
- if (has_selection(c)) {
- start_line = get_selection_from_line(c);
- end_line = get_selection_to_line(c);
-
- // Ignore the last line if the selection is not past the first column.
- initial_selection_end_column = get_selection_to_column(c);
- if (initial_selection_end_column == 0) {
- end_line--;
- }
- }
-
- bool first_line_edited = false;
- bool last_line_edited = false;
-
- for (int i = start_line; i <= end_line; i++) {
- String line_text = get_line(i);
+ Vector<Point2i> line_ranges = get_line_ranges_from_carets();
+ for (Point2i line_range : line_ranges) {
+ for (int i = line_range.x; i <= line_range.y; i++) {
+ const String line_text = get_line(i);
if (line_text.begins_with("\t")) {
- line_text = line_text.substr(1, line_text.length());
-
- set_line(i, line_text);
- removed_characters = 1;
-
- first_line_edited = (i == start_line) ? true : first_line_edited;
- last_line_edited = (i == end_line) ? true : last_line_edited;
- continue;
- }
-
- if (line_text.begins_with(" ")) {
- // When unindenting we aim to remove spaces before line that has selection no matter what is selected.
- // Here we remove only enough spaces to align text to nearest full multiple of indentation_size.
- // In case where selection begins at the start of indentation_size multiple we remove whole indentation level.
+ remove_text(i, 0, i, 1);
+ } else if (line_text.begins_with(" ")) {
+ // Remove only enough spaces to align text to nearest full multiple of indentation_size.
int spaces_to_remove = _calculate_spaces_till_next_left_indent(get_first_non_whitespace_column(i));
- line_text = line_text.substr(spaces_to_remove, line_text.length());
-
- set_line(i, line_text);
- removed_characters = spaces_to_remove;
-
- first_line_edited = (i == start_line) ? true : first_line_edited;
- last_line_edited = (i == end_line) ? true : last_line_edited;
+ remove_text(i, 0, i, spaces_to_remove);
}
}
-
- if (has_selection(c)) {
- // Fix selection being off by one on the first line.
- if (first_line_edited) {
- select(get_selection_from_line(c), get_selection_from_column(c) - removed_characters, get_selection_to_line(c), initial_selection_end_column, c);
- }
-
- // Fix selection being off by one on the last line.
- if (last_line_edited) {
- select(get_selection_from_line(c), get_selection_from_column(c), get_selection_to_line(c), initial_selection_end_column - removed_characters, c);
- }
- }
- set_caret_column(initial_cursor_column - removed_characters, false, c);
}
+
+ end_multicaret_edit();
end_complex_operation();
- queue_redraw();
}
void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
@@ -992,27 +952,6 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
ERR_FAIL_COND(p_to_line >= get_line_count());
ERR_FAIL_COND(p_to_line < p_from_line);
- // Store caret states.
- Vector<int> caret_columns;
- Vector<Pair<int, int>> from_selections;
- Vector<Pair<int, int>> to_selections;
- caret_columns.resize(get_caret_count());
- from_selections.resize(get_caret_count());
- to_selections.resize(get_caret_count());
- for (int c = 0; c < get_caret_count(); c++) {
- caret_columns.write[c] = get_caret_column(c);
-
- // Set "selection_from_line" to -1 to allow checking if there was a selection later.
- if (!has_selection(c)) {
- from_selections.write[c].first = -1;
- continue;
- }
- from_selections.write[c].first = get_selection_from_line(c);
- from_selections.write[c].second = get_selection_from_column(c);
- to_selections.write[c].first = get_selection_to_line(c);
- to_selections.write[c].second = get_selection_to_column(c);
- }
-
// Check lines within range.
const char32_t from_indent_char = indent_using_spaces ? '\t' : ' ';
int size_diff = indent_using_spaces ? indent_size - 1 : -(indent_size - 1);
@@ -1044,23 +983,10 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
line_changed = true;
if (!changed_indentation) {
begin_complex_operation();
+ begin_multicaret_edit();
changed_indentation = true;
}
- // Calculate new caret state.
- for (int c = 0; c < get_caret_count(); c++) {
- if (get_caret_line(c) != i || caret_columns[c] <= j) {
- continue;
- }
- caret_columns.write[c] += size_diff;
-
- if (from_selections.write[c].first == -1) {
- continue;
- }
- from_selections.write[c].second = from_selections[c].first == i ? from_selections[c].second + size_diff : from_selections[c].second;
- to_selections.write[c].second = to_selections[c].first == i ? to_selections[c].second + size_diff : to_selections[c].second;
- }
-
// Calculate new line.
line = line.left(j + ((size_diff < 0) ? size_diff : 0)) + indent_text + line.substr(j + 1);
@@ -1069,6 +995,7 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
}
if (line_changed) {
+ // Use set line to preserve carets visual position.
set_line(i, line);
}
}
@@ -1077,16 +1004,9 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) {
return;
}
- // Restore caret states.
- for (int c = 0; c < get_caret_count(); c++) {
- set_caret_column(caret_columns[c], c == 0, c);
- if (from_selections.write[c].first != -1) {
- select(from_selections.write[c].first, from_selections.write[c].second, to_selections.write[c].first, to_selections.write[c].second, c);
- }
- }
merge_overlapping_carets();
+ end_multicaret_edit();
end_complex_operation();
- queue_redraw();
}
int CodeEdit::_calculate_spaces_till_next_left_indent(int p_column) const {
@@ -1107,15 +1027,22 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) {
}
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
+ begin_multicaret_edit();
+
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
// When not splitting the line, we need to factor in indentation from the end of the current line.
const int cc = p_split_current_line ? get_caret_column(i) : get_line(get_caret_line(i)).length();
const int cl = get_caret_line(i);
const String line = get_line(cl);
- String ins = "\n";
+ String ins = "";
+ if (!p_above) {
+ ins = "\n";
+ }
// Append current indentation.
int space_count = 0;
@@ -1138,6 +1065,9 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) {
}
break;
}
+ if (p_above) {
+ ins += "\n";
+ }
if (is_line_folded(cl)) {
unfold_line(cl);
@@ -1183,33 +1113,22 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) {
}
}
- bool first_line = false;
- if (!p_split_current_line) {
+ if (p_split_current_line) {
+ insert_text_at_caret(ins, i);
+ } else {
+ insert_text(ins, cl, p_above ? 0 : get_line(cl).length(), p_above, p_above);
deselect(i);
-
- if (p_above) {
- if (cl > 0) {
- set_caret_line(cl - 1, false, true, 0, i);
- set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i);
- } else {
- set_caret_column(0, i == 0, i);
- first_line = true;
- }
- } else {
- set_caret_column(line.length(), i == 0, i);
- }
+ set_caret_line(p_above ? cl : cl + 1, false, true, -1, i);
+ set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i);
}
-
- insert_text_at_caret(ins, i);
-
- if (first_line) {
- set_caret_line(0, i == 0, true, 0, i);
- } else if (brace_indent) {
+ if (brace_indent) {
+ // Move to inner indented line.
set_caret_line(get_caret_line(i) - 1, false, true, 0, i);
set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i);
}
}
+ end_multicaret_edit();
end_complex_operation();
}
@@ -1700,27 +1619,8 @@ void CodeEdit::fold_line(int p_line) {
_set_line_as_hidden(i, true);
}
- for (int i = 0; i < get_caret_count(); i++) {
- // Fix selection.
- if (has_selection(i)) {
- if (_is_line_hidden(get_selection_from_line(i)) && _is_line_hidden(get_selection_to_line(i))) {
- deselect(i);
- } else if (_is_line_hidden(get_selection_from_line(i))) {
- select(p_line, 9999, get_selection_to_line(i), get_selection_to_column(i), i);
- } else if (_is_line_hidden(get_selection_to_line(i))) {
- select(get_selection_from_line(i), get_selection_from_column(i), p_line, 9999, i);
- }
- }
-
- // Reset caret.
- if (_is_line_hidden(get_caret_line(i))) {
- set_caret_line(p_line, false, false, 0, i);
- set_caret_column(get_line(p_line).length(), false, i);
- }
- }
-
- merge_overlapping_carets();
- queue_redraw();
+ // Collapse any carets in the hidden area.
+ collapse_carets(p_line, get_line(p_line).length(), end_line, get_line(end_line).length(), true);
}
void CodeEdit::unfold_line(int p_line) {
@@ -1769,6 +1669,23 @@ void CodeEdit::toggle_foldable_line(int p_line) {
fold_line(p_line);
}
+void CodeEdit::toggle_foldable_lines_at_carets() {
+ begin_multicaret_edit();
+ int previous_line = -1;
+ Vector<int> sorted = get_sorted_carets();
+ for (int caret_idx : sorted) {
+ if (multicaret_edit_ignore_caret(caret_idx)) {
+ continue;
+ }
+ int line_idx = get_caret_line(caret_idx);
+ if (line_idx != previous_line) {
+ toggle_foldable_line(line_idx);
+ previous_line = line_idx;
+ }
+ }
+ end_multicaret_edit();
+}
+
bool CodeEdit::is_line_folded(int p_line) const {
ERR_FAIL_INDEX_V(p_line, get_line_count(), false);
return p_line + 1 < get_line_count() && !_is_line_hidden(p_line) && _is_line_hidden(p_line + 1);
@@ -1795,49 +1712,29 @@ void CodeEdit::create_code_region() {
WARN_PRINT_ONCE("Cannot create code region without any one line comment delimiters");
return;
}
+ String region_name = atr(ETR("New Code Region"));
+
begin_complex_operation();
- // Merge selections if selection starts on the same line the previous one ends.
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- Vector<int> carets_to_remove;
- for (int i = 1; i < caret_edit_order.size(); i++) {
- int current_caret = caret_edit_order[i - 1];
- int next_caret = caret_edit_order[i];
- if (get_selection_from_line(current_caret) == get_selection_to_line(next_caret)) {
- select(get_selection_from_line(next_caret), get_selection_from_column(next_caret), get_selection_to_line(current_caret), get_selection_to_column(current_caret), next_caret);
- carets_to_remove.append(current_caret);
- }
- }
- // Sort and remove backwards to preserve indices.
- carets_to_remove.sort();
- for (int i = carets_to_remove.size() - 1; i >= 0; i--) {
- remove_caret(carets_to_remove[i]);
- }
-
- // Adding start and end region tags.
- int first_region_start = -1;
- for (int caret_idx : get_caret_index_edit_order()) {
- if (!has_selection(caret_idx)) {
- continue;
- }
- int from_line = get_selection_from_line(caret_idx);
- if (first_region_start == -1 || from_line < first_region_start) {
- first_region_start = from_line;
- }
- int to_line = get_selection_to_line(caret_idx);
- set_line(to_line, get_line(to_line) + "\n" + code_region_end_string);
- insert_line_at(from_line, code_region_start_string + " " + atr(ETR("New Code Region")));
- fold_line(from_line);
+ begin_multicaret_edit();
+ Vector<Point2i> line_ranges = get_line_ranges_from_carets(true, false);
+
+ // Add start and end region tags.
+ int line_offset = 0;
+ for (Point2i line_range : line_ranges) {
+ insert_text("\n" + code_region_end_string, line_range.y + line_offset, get_line(line_range.y + line_offset).length());
+ insert_line_at(line_range.x + line_offset, code_region_start_string + " " + region_name);
+ fold_line(line_range.x + line_offset);
+ line_offset += 2;
}
+ int first_region_start = line_ranges[0].x;
// Select name of the first region to allow quick edit.
remove_secondary_carets();
- set_caret_line(first_region_start);
- int tag_length = code_region_start_string.length() + atr(ETR("New Code Region")).length() + 1;
- set_caret_column(tag_length);
+ int tag_length = code_region_start_string.length() + region_name.length() + 1;
select(first_region_start, code_region_start_string.length() + 1, first_region_start, tag_length);
+ end_multicaret_edit();
end_complex_operation();
- queue_redraw();
}
String CodeEdit::get_code_region_start_tag() const {
@@ -2236,8 +2133,12 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
char32_t caret_last_completion_char = 0;
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
+ begin_multicaret_edit();
+
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
int caret_line = get_caret_line(i);
const String &insert_text = code_completion_options[code_completion_current_selected].insert_text;
@@ -2270,8 +2171,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
// Replace.
remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_remove_line, caret_col);
- adjust_carets_after_edit(i, caret_line, caret_col - code_completion_base.length(), caret_remove_line, caret_col);
- set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i);
insert_text_at_caret(insert_text, i);
} else {
// Get first non-matching char.
@@ -2287,8 +2186,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
// Remove base completion text.
remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i));
- adjust_carets_after_edit(i, caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i));
- set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i);
// Merge with text.
insert_text_at_caret(insert_text.substr(0, code_completion_base.length()), i);
@@ -2313,12 +2210,10 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
if (has_string_delimiter(String::chr(last_completion_char))) {
if (post_brace_pair != -1 && last_char_matches) {
remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1);
- adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1);
}
} else {
if (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && last_char_matches) {
remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1);
- adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1);
} else if (auto_brace_completion_enabled && pre_brace_pair != -1) {
insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key, i);
set_caret_column(get_caret_column(i) - auto_brace_completion_pairs[pre_brace_pair].close_key.length(), i == 0, i);
@@ -2329,13 +2224,16 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
pre_brace_pair = _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column(i) + 1);
if (pre_brace_pair != -1 && pre_brace_pair == _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1)) {
remove_text(caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i));
- adjust_carets_after_edit(i, caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i));
- if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1) != pre_brace_pair) {
- set_caret_column(get_caret_column(i) - 1, i == 0, i);
+ if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) + 1) != pre_brace_pair) {
+ set_caret_column(get_caret_column(i) + 1, i == 0, i);
+ } else {
+ set_caret_column(get_caret_column(i) + 2, i == 0, i);
}
}
}
}
+
+ end_multicaret_edit();
end_complex_operation();
cancel_code_completion();
@@ -2418,65 +2316,154 @@ void CodeEdit::set_symbol_lookup_word_as_valid(bool p_valid) {
}
/* Text manipulation */
-void CodeEdit::duplicate_lines() {
+void CodeEdit::move_lines_up() {
begin_complex_operation();
+ begin_multicaret_edit();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &caret_index : caret_edit_order) {
- // The text that will be inserted. All lines in one string.
- String insert_text;
-
- // The new line position of the caret after the operation.
- int new_caret_line = get_caret_line(caret_index);
- // The new column position of the caret after the operation.
- int new_caret_column = get_caret_column(caret_index);
- // The caret positions of the selection. Stays -1 if there is no selection.
- int select_from_line = -1;
- int select_to_line = -1;
- int select_from_column = -1;
- int select_to_column = -1;
- // Number of lines of the selection.
- int select_num_lines = -1;
-
- if (has_selection(caret_index)) {
- select_from_line = get_selection_from_line(caret_index);
- select_to_line = get_selection_to_line(caret_index);
- select_from_column = get_selection_from_column(caret_index);
- select_to_column = get_selection_to_column(caret_index);
- select_num_lines = select_to_line - select_from_line + 1;
-
- for (int i = select_from_line; i <= select_to_line; i++) {
- insert_text += "\n" + get_line(i);
- unfold_line(i);
- }
- new_caret_line = select_to_line + select_num_lines;
- } else {
- insert_text = "\n" + get_line(new_caret_line);
- new_caret_line++;
+ // Move lines up by swapping each line with the one above it.
+ Vector<Point2i> line_ranges = get_line_ranges_from_carets();
+ for (Point2i line_range : line_ranges) {
+ if (line_range.x == 0) {
+ continue;
+ }
+ unfold_line(line_range.x - 1);
+ for (int line = line_range.x; line <= line_range.y; line++) {
+ unfold_line(line);
+ swap_lines(line - 1, line);
+ }
+ }
- unfold_line(get_caret_line(caret_index));
+ // Fix selection if it ended at column 0, since it wasn't moved.
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != 0) {
+ if (is_caret_after_selection_origin(i)) {
+ set_caret_line(get_caret_line(i) - 1, false, true, -1, i);
+ } else {
+ set_selection_origin_line(get_selection_origin_line(i) - 1, true, -1, i);
+ }
}
+ }
- // The text will be inserted at the end of the current line.
- set_caret_column(get_line(get_caret_line(caret_index)).length(), false, caret_index);
+ end_multicaret_edit();
+ end_complex_operation();
+}
- deselect(caret_index);
+void CodeEdit::move_lines_down() {
+ begin_complex_operation();
+ begin_multicaret_edit();
- insert_text_at_caret(insert_text, caret_index);
- set_caret_line(new_caret_line, false, true, 0, caret_index);
- set_caret_column(new_caret_column, true, caret_index);
+ Vector<Point2i> line_ranges = get_line_ranges_from_carets();
- if (select_from_line != -1) {
- // Advance the selection by the number of duplicated lines.
- select_from_line += select_num_lines;
- select_to_line += select_num_lines;
+ // Fix selection if it ended at column 0, since it won't be moved.
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != get_line_count() - 1) {
+ if (is_caret_after_selection_origin(i)) {
+ set_caret_line(get_caret_line(i) + 1, false, true, -1, i);
+ } else {
+ set_selection_origin_line(get_selection_origin_line(i) + 1, true, -1, i);
+ }
+ }
+ }
- select(select_from_line, select_from_column, select_to_line, select_to_column, caret_index);
+ // Move lines down by swapping each line with the one below it.
+ for (Point2i line_range : line_ranges) {
+ if (line_range.y == get_line_count() - 1) {
+ continue;
+ }
+ unfold_line(line_range.y + 1);
+ for (int line = line_range.y; line >= line_range.x; line--) {
+ unfold_line(line);
+ swap_lines(line + 1, line);
}
}
+ end_multicaret_edit();
+ end_complex_operation();
+}
+
+void CodeEdit::delete_lines() {
+ begin_complex_operation();
+ begin_multicaret_edit();
+
+ Vector<Point2i> line_ranges = get_line_ranges_from_carets();
+ int line_offset = 0;
+ for (Point2i line_range : line_ranges) {
+ // Remove last line of range separately to preserve carets.
+ unfold_line(line_range.y + line_offset);
+ remove_line_at(line_range.y + line_offset);
+ if (line_range.x != line_range.y) {
+ remove_text(line_range.x + line_offset, 0, line_range.y + line_offset, 0);
+ }
+ line_offset += line_range.x - line_range.y - 1;
+ }
+
+ // Deselect all.
+ deselect();
+
+ end_multicaret_edit();
+ end_complex_operation();
+}
+
+void CodeEdit::duplicate_selection() {
+ begin_complex_operation();
+ begin_multicaret_edit();
+
+ // Duplicate lines from carets without selections first.
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
+ for (int l = get_selection_from_line(i); l <= get_selection_to_line(i); l++) {
+ unfold_line(l);
+ }
+ if (has_selection(i)) {
+ continue;
+ }
+
+ String text_to_insert = get_line(get_caret_line(i)) + "\n";
+ // Insert new text before the line, so the caret is on the second one.
+ insert_text(text_to_insert, get_caret_line(i), 0);
+ }
+
+ // Duplicate selections.
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
+ if (!has_selection(i)) {
+ continue;
+ }
+
+ // Insert new text before the selection, so the caret is on the second one.
+ insert_text(get_selected_text(i), get_selection_from_line(i), get_selection_from_column(i));
+ }
+
+ end_multicaret_edit();
+ end_complex_operation();
+}
+
+void CodeEdit::duplicate_lines() {
+ begin_complex_operation();
+ begin_multicaret_edit();
+
+ Vector<Point2i> line_ranges = get_line_ranges_from_carets(false, false);
+ int line_offset = 0;
+ for (Point2i line_range : line_ranges) {
+ // The text that will be inserted. All lines in one string.
+ String text_to_insert;
+
+ for (int i = line_range.x + line_offset; i <= line_range.y + line_offset; i++) {
+ text_to_insert += get_line(i) + "\n";
+ unfold_line(i);
+ }
+
+ // Insert new text before the line.
+ insert_text(text_to_insert, line_range.x + line_offset, 0);
+ line_offset += line_range.y - line_range.x + 1;
+ }
+
+ end_multicaret_edit();
end_complex_operation();
- queue_redraw();
}
/* Visual */
@@ -2578,6 +2565,7 @@ void CodeEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("fold_all_lines"), &CodeEdit::fold_all_lines);
ClassDB::bind_method(D_METHOD("unfold_all_lines"), &CodeEdit::unfold_all_lines);
ClassDB::bind_method(D_METHOD("toggle_foldable_line", "line"), &CodeEdit::toggle_foldable_line);
+ ClassDB::bind_method(D_METHOD("toggle_foldable_lines_at_carets"), &CodeEdit::toggle_foldable_lines_at_carets);
ClassDB::bind_method(D_METHOD("is_line_folded", "line"), &CodeEdit::is_line_folded);
ClassDB::bind_method(D_METHOD("get_folded_lines"), &CodeEdit::get_folded_lines);
@@ -2679,6 +2667,10 @@ void CodeEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_symbol_lookup_word_as_valid", "valid"), &CodeEdit::set_symbol_lookup_word_as_valid);
/* Text manipulation */
+ ClassDB::bind_method(D_METHOD("move_lines_up"), &CodeEdit::move_lines_up);
+ ClassDB::bind_method(D_METHOD("move_lines_down"), &CodeEdit::move_lines_down);
+ ClassDB::bind_method(D_METHOD("delete_lines"), &CodeEdit::delete_lines);
+ ClassDB::bind_method(D_METHOD("duplicate_selection"), &CodeEdit::duplicate_selection);
ClassDB::bind_method(D_METHOD("duplicate_lines"), &CodeEdit::duplicate_lines);
/* Inspector */
@@ -2846,10 +2838,12 @@ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) {
if (p_gutter == line_number_gutter) {
remove_secondary_carets();
- set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE, p_line, 0);
- select(p_line, 0, p_line + 1, 0);
- set_caret_line(p_line + 1);
- set_caret_column(0);
+ set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE);
+ if (p_line == get_line_count() - 1) {
+ select(p_line, 0, p_line, INT_MAX);
+ } else {
+ select(p_line, 0, p_line + 1, 0);
+ }
return;
}
diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h
index 1770d4f4d8..56f8cce548 100644
--- a/scene/gui/code_edit.h
+++ b/scene/gui/code_edit.h
@@ -309,11 +309,14 @@ protected:
static void _bind_compatibility_methods();
#endif
+ virtual void _unhide_carets() override;
+
/* Text manipulation */
// Overridable actions
virtual void _handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) override;
virtual void _backspace_internal(int p_caret) override;
+ virtual void _cut_internal(int p_caret) override;
GDVIRTUAL1(_confirm_code_completion, bool)
GDVIRTUAL1(_request_code_completion, bool)
@@ -409,6 +412,7 @@ public:
void fold_all_lines();
void unfold_all_lines();
void toggle_foldable_line(int p_line);
+ void toggle_foldable_lines_at_carets();
bool is_line_folded(int p_line) const;
TypedArray<int> get_folded_lines() const;
@@ -489,6 +493,10 @@ public:
void set_symbol_lookup_word_as_valid(bool p_valid);
/* Text manipulation */
+ void move_lines_up();
+ void move_lines_down();
+ void delete_lines();
+ void duplicate_selection();
void duplicate_lines();
CodeEdit();
diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp
index d430fe9bfc..7ac7ceb6bc 100644
--- a/scene/gui/control.cpp
+++ b/scene/gui/control.cpp
@@ -142,8 +142,8 @@ Size2 Control::_edit_get_scale() const {
void Control::_edit_set_rect(const Rect2 &p_edit_rect) {
ERR_FAIL_COND_MSG(!Engine::get_singleton()->is_editor_hint(), "This function can only be used from editor plugins.");
- set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snapped(Vector2(1, 1)), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled());
- set_size(p_edit_rect.size.snapped(Vector2(1, 1)), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled());
+ set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snappedf(1), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled());
+ set_size(p_edit_rect.size.snappedf(1), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled());
}
Rect2 Control::_edit_get_rect() const {
diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp
index ef9c7e35ed..646e45b27a 100644
--- a/scene/gui/graph_edit.cpp
+++ b/scene/gui/graph_edit.cpp
@@ -501,7 +501,7 @@ void GraphEdit::_graph_element_resize_request(const Vector2 &p_new_minsize, Node
// Snap the new size to the grid if snapping is enabled.
Vector2 new_size = p_new_minsize;
if (snapping_enabled ^ Input::get_singleton()->is_key_pressed(Key::CTRL)) {
- new_size = new_size.snapped(Vector2(snapping_distance, snapping_distance));
+ new_size = new_size.snappedf(snapping_distance);
}
// Disallow resizing the frame to a size smaller than the minimum size of the attached nodes.
@@ -851,7 +851,7 @@ void GraphEdit::_set_position_of_frame_attached_nodes(GraphFrame *p_frame, const
Vector2 pos = (attached_node->get_drag_from() * zoom + drag_accum) / zoom;
if (snapping_enabled ^ Input::get_singleton()->is_key_pressed(Key::CTRL)) {
- pos = pos.snapped(Vector2(snapping_distance, snapping_distance));
+ pos = pos.snappedf(snapping_distance);
}
// Recursively move graph frames.
@@ -1678,7 +1678,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
// Snapping can be toggled temporarily by holding down Ctrl.
// This is done here as to not toggle the grid when holding down Ctrl.
if (snapping_enabled ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) {
- pos = pos.snapped(Vector2(snapping_distance, snapping_distance));
+ pos = pos.snappedf(snapping_distance);
}
graph_element->set_position_offset(pos);
diff --git a/scene/gui/graph_edit_arranger.cpp b/scene/gui/graph_edit_arranger.cpp
index 49998beb42..fa1059c667 100644
--- a/scene/gui/graph_edit_arranger.cpp
+++ b/scene/gui/graph_edit_arranger.cpp
@@ -180,7 +180,7 @@ void GraphEditArranger::arrange_nodes() {
if (graph_edit->is_snapping_enabled()) {
float snapping_distance = graph_edit->get_snapping_distance();
- pos = pos.snapped(Vector2(snapping_distance, snapping_distance));
+ pos = pos.snappedf(snapping_distance);
}
graph_node->set_position_offset(pos);
graph_node->set_drag(false);
diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp
index b2617e6fc7..90ce01e383 100644
--- a/scene/gui/progress_bar.cpp
+++ b/scene/gui/progress_bar.cpp
@@ -41,7 +41,7 @@ Size2 ProgressBar::get_minimum_size() const {
TextLine tl = TextLine(txt, theme_cache.font, theme_cache.font_size);
minimum_size.height = MAX(minimum_size.height, theme_cache.background_style->get_minimum_size().height + tl.get_size().y);
} else { // this is needed, else the progressbar will collapse
- minimum_size = minimum_size.max(Size2(1, 1));
+ minimum_size = minimum_size.maxf(1);
}
return minimum_size;
}
diff --git a/scene/gui/rich_text_label.compat.inc b/scene/gui/rich_text_label.compat.inc
index 626278a405..97739c4b79 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<Texture2D> &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 0773181239..19b02f33c6 100644
--- a/scene/gui/rich_text_label.cpp
+++ b/scene/gui/rich_text_label.cpp
@@ -3342,7 +3342,7 @@ void RichTextLabel::_remove_frame(HashSet<Item *> &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);
@@ -3391,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));
@@ -5851,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);
diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h
index 371f6724d7..189ee1da6e 100644
--- a/scene/gui/rich_text_label.h
+++ b/scene/gui/rich_text_label.h
@@ -134,6 +134,7 @@ protected:
#ifndef DISABLE_DEPRECATED
void _push_meta_bind_compat_89024(const Variant &p_meta);
void _add_image_bind_compat_80410(const Ref<Texture2D> &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
@@ -664,7 +665,8 @@ public:
void add_image(const Ref<Texture2D> &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<ImageUpdateMask> p_mask, const Ref<Texture2D> &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<Font> &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<Font> &p_font, int p_size = -1);
diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp
index e2feb59a8c..dc53cf82e6 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<Control *> 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 0000000000..bf73229868
--- /dev/null
+++ b/scene/gui/text_edit.compat.inc
@@ -0,0 +1,41 @@
+/**************************************************************************/
+/* text_edit.compat.inc */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef DISABLE_DEPRECATED
+
+void TextEdit::_set_selection_mode_compat_86978(SelectionMode p_mode, int p_line, int p_column, int p_caret) {
+ set_selection_mode(p_mode);
+}
+
+void TextEdit::_bind_compatibility_methods() {
+ ClassDB::bind_compatibility_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::_set_selection_mode_compat_86978, DEFVAL(-1), DEFVAL(-1), DEFVAL(0));
+}
+
+#endif
diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp
index 38b4ffc8ae..4fda49a877 100644
--- a/scene/gui/text_edit.cpp
+++ b/scene/gui/text_edit.cpp
@@ -29,6 +29,7 @@
/**************************************************************************/
#include "text_edit.h"
+#include "text_edit.compat.inc"
#include "core/config/project_settings.h"
#include "core/input/input.h"
@@ -451,7 +452,7 @@ void TextEdit::_notification(int p_what) {
callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred();
}
if (text_changed_dirty) {
- callable_mp(this, &TextEdit::_text_changed_emit).call_deferred();
+ callable_mp(this, &TextEdit::_emit_text_changed).call_deferred();
}
_update_wrap_at_column(true);
} break;
@@ -565,9 +566,9 @@ void TextEdit::_notification(int p_what) {
Vector<BraceMatchingData> brace_matching;
if (highlight_matching_braces_enabled) {
- brace_matching.resize(carets.size());
+ brace_matching.resize(get_caret_count());
- for (int caret = 0; caret < carets.size(); caret++) {
+ for (int caret = 0; caret < get_caret_count(); caret++) {
if (get_caret_line(caret) < 0 || get_caret_line(caret) >= text.size() || get_caret_column(caret) < 0) {
continue;
}
@@ -1104,7 +1105,7 @@ void TextEdit::_notification(int p_what) {
// Draw selections.
float char_w = theme_cache.font->get_char_size(' ', theme_cache.font_size).width;
- for (int c = 0; c < carets.size(); c++) {
+ for (int c = 0; c < get_caret_count(); c++) {
if (!clipped && has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) {
int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c);
int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c);
@@ -1257,7 +1258,7 @@ void TextEdit::_notification(int p_what) {
}
Color gl_color = current_color;
- for (int c = 0; c < carets.size(); c++) {
+ for (int c = 0; c < get_caret_count(); c++) {
if (has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { // Selection
int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c);
int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c);
@@ -1271,7 +1272,7 @@ void TextEdit::_notification(int p_what) {
float char_pos = char_ofs + char_margin + ofs_x;
if (char_pos >= xmargin_beg) {
if (highlight_matching_braces_enabled) {
- for (int c = 0; c < carets.size(); c++) {
+ for (int c = 0; c < get_caret_count(); c++) {
if ((brace_matching[c].open_match_line == line && brace_matching[c].open_match_column == glyphs[j].start) ||
(get_caret_column(c) == glyphs[j].start && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_matching[c].open_matching || brace_matching[c].open_mismatch))) {
if (brace_matching[c].open_mismatch) {
@@ -1562,10 +1563,15 @@ void TextEdit::_notification(int p_what) {
case MainLoop::NOTIFICATION_OS_IME_UPDATE: {
if (has_focus()) {
+ bool had_ime_text = has_ime_text();
ime_text = DisplayServer::get_singleton()->ime_get_text();
ime_selection = DisplayServer::get_singleton()->ime_get_selection();
- if (!ime_text.is_empty() && has_selection()) {
+ if (!had_ime_text && has_ime_text()) {
+ _cancel_drag_and_drop_text();
+ }
+
+ if (has_ime_text() && has_selection()) {
delete_selection();
}
@@ -1576,7 +1582,7 @@ void TextEdit::_notification(int p_what) {
} break;
case NOTIFICATION_DRAG_BEGIN: {
- selecting_mode = SelectionMode::SELECTION_MODE_NONE;
+ set_selection_mode(SelectionMode::SELECTION_MODE_NONE);
drag_action = true;
dragging_minimap = false;
dragging_selection = false;
@@ -1587,19 +1593,31 @@ void TextEdit::_notification(int p_what) {
case NOTIFICATION_DRAG_END: {
if (is_drag_successful()) {
if (selection_drag_attempt) {
- selection_drag_attempt = false;
+ // Dropped elsewhere.
if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) {
delete_selection();
} else if (deselect_on_focus_loss_enabled) {
deselect();
}
}
- } else {
- selection_drag_attempt = false;
}
+ if (drag_caret_index >= 0) {
+ if (drag_caret_index < carets.size()) {
+ remove_caret(drag_caret_index);
+ }
+ drag_caret_index = -1;
+ }
+ selection_drag_attempt = false;
drag_action = false;
drag_caret_force_displayed = false;
} break;
+
+ case NOTIFICATION_MOUSE_EXIT_SELF: {
+ if (drag_caret_force_displayed) {
+ drag_caret_force_displayed = false;
+ queue_redraw();
+ }
+ } break;
}
}
@@ -1702,15 +1720,17 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
if (mb->get_button_index() == MouseButton::WHEEL_RIGHT) {
h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor()));
}
+
if (mb->get_button_index() == MouseButton::LEFT) {
_reset_caret_blink_timer();
apply_ime();
Point2i pos = get_line_column_at_pos(mpos);
- int row = pos.y;
+ int line = pos.y;
int col = pos.x;
+ // Gutters.
int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT);
for (int i = 0; i < gutters.size(); i++) {
if (!gutters[i].draw || gutters[i].width <= 0) {
@@ -1718,14 +1738,14 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
}
if (mpos.x >= left_margin && mpos.x <= left_margin + gutters[i].width) {
- emit_signal(SNAME("gutter_clicked"), row, i);
+ emit_signal(SNAME("gutter_clicked"), line, i);
return;
}
left_margin += gutters[i].width;
}
- // Minimap
+ // Minimap.
if (draw_minimap) {
_update_minimap_click();
if (dragging_minimap) {
@@ -1733,121 +1753,86 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
}
}
+ // Update caret.
+
int caret = carets.size() - 1;
int prev_col = get_caret_column(caret);
int prev_line = get_caret_line(caret);
+ int mouse_over_selection_caret = get_selection_at_line_column(line, col, true);
+
const int triple_click_timeout = 600;
const int triple_click_tolerance = 5;
bool is_triple_click = (!mb->is_double_click() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < triple_click_timeout && mb->get_position().distance_to(last_dblclk_pos) < triple_click_tolerance);
if (!mb->is_double_click() && !is_triple_click) {
if (mb->is_alt_pressed()) {
- prev_line = row;
+ prev_line = line;
prev_col = col;
// Remove caret at clicked location.
- if (carets.size() > 1) {
- for (int i = 0; i < carets.size(); i++) {
- // Deselect if clicked on caret or its selection.
- if ((get_caret_column(i) == col && get_caret_line(i) == row) || is_mouse_over_selection(true, i)) {
- remove_caret(i);
- last_dblclk = 0;
- return;
- }
+ if (get_caret_count() > 1) {
+ // Deselect if clicked on caret or its selection.
+ int clicked_caret = get_selection_at_line_column(line, col, true, false);
+ if (clicked_caret != -1) {
+ remove_caret(clicked_caret);
+ last_dblclk = 0;
+ return;
}
}
- if (is_mouse_over_selection()) {
+ if (mouse_over_selection_caret >= 0) {
+ // Did not remove selection under mouse, don't add a new caret.
return;
}
- caret = add_caret(row, col);
+ // Create new caret at clicked location.
+ caret = add_caret(line, col);
if (caret == -1) {
return;
}
- carets.write[caret].selection.selecting_line = row;
- carets.write[caret].selection.selecting_column = col;
-
last_dblclk = 0;
- } else if (!mb->is_shift_pressed() && !is_mouse_over_selection()) {
- caret = 0;
- remove_secondary_carets();
- }
- }
-
- _push_current_op();
- set_caret_line(row, false, true, 0, caret);
- set_caret_column(col, false, caret);
- selection_drag_attempt = false;
-
- if (selecting_enabled && mb->is_shift_pressed() && (get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line)) {
- if (!has_selection(caret)) {
- carets.write[caret].selection.active = true;
- selecting_mode = SelectionMode::SELECTION_MODE_POINTER;
- carets.write[caret].selection.from_column = prev_col;
- carets.write[caret].selection.from_line = prev_line;
- carets.write[caret].selection.to_column = carets[caret].column;
- carets.write[caret].selection.to_line = carets[caret].line;
-
- if (get_selection_from_line(caret) > get_selection_to_line(caret) || (get_selection_from_line(caret) == get_selection_to_line(caret) && get_selection_from_column(caret) > get_selection_to_column(caret))) {
- SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column);
- SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line);
- carets.write[caret].selection.shiftclick_left = false;
- } else {
- carets.write[caret].selection.shiftclick_left = true;
- }
- carets.write[caret].selection.selecting_line = prev_line;
- carets.write[caret].selection.selecting_column = prev_col;
- caret_index_edit_dirty = true;
- merge_overlapping_carets();
- queue_redraw();
- } else {
- if (carets[caret].line < get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column < get_selection_column(caret))) {
- if (carets[caret].selection.shiftclick_left) {
- carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left;
- }
- carets.write[caret].selection.from_column = carets[caret].column;
- carets.write[caret].selection.from_line = carets[caret].line;
-
- } else if (carets[caret].line > get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column > get_selection_column(caret))) {
- if (!carets[caret].selection.shiftclick_left) {
- SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column);
- SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line);
- carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left;
- }
- carets.write[caret].selection.to_column = carets[caret].column;
- carets.write[caret].selection.to_line = carets[caret].line;
-
+ } else if (!mb->is_shift_pressed()) {
+ if (drag_and_drop_selection_enabled && mouse_over_selection_caret >= 0) {
+ // Try to drag and drop.
+ set_selection_mode(SelectionMode::SELECTION_MODE_NONE);
+ selection_drag_attempt = true;
+ drag_and_drop_origin_caret_index = mouse_over_selection_caret;
+ last_dblclk = 0;
+ // Don't update caret until we know if it is not drag and drop.
+ return;
} else {
- deselect(caret);
+ // A regular click clears all other carets.
+ caret = 0;
+ remove_secondary_carets();
+ deselect();
}
- caret_index_edit_dirty = true;
- merge_overlapping_carets();
- queue_redraw();
}
- } else if (drag_and_drop_selection_enabled && is_mouse_over_selection()) {
- set_selection_mode(SelectionMode::SELECTION_MODE_NONE, get_selection_line(caret), get_selection_column(caret), caret);
- // We use the main caret for dragging, so reset this one.
- set_caret_line(prev_line, false, true, 0, caret);
- set_caret_column(prev_col, false, caret);
- selection_drag_attempt = true;
- } else if (caret == 0) {
- deselect();
- set_selection_mode(SelectionMode::SELECTION_MODE_POINTER, row, col);
- }
- if (is_triple_click) {
- // Triple-click select line.
- selecting_mode = SelectionMode::SELECTION_MODE_LINE;
+ _push_current_op();
+ set_caret_line(line, false, true, -1, caret);
+ set_caret_column(col, false, caret);
selection_drag_attempt = false;
- _update_selection_mode_line();
+ bool caret_moved = get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line;
+
+ if (selecting_enabled && mb->is_shift_pressed() && !has_selection(caret) && caret_moved) {
+ // Select from the previous caret position.
+ select(prev_line, prev_col, line, col, caret);
+ }
+
+ // Start regular select mode.
+ set_selection_mode(SelectionMode::SELECTION_MODE_POINTER);
+ _update_selection_mode_pointer(true);
+ } else if (is_triple_click) {
+ // Start triple-click select line mode.
+ set_selection_mode(SelectionMode::SELECTION_MODE_LINE);
+ _update_selection_mode_line(true);
last_dblclk = 0;
- } else if (mb->is_double_click() && text[get_caret_line(caret)].length()) {
- // Double-click select word.
- selecting_mode = SelectionMode::SELECTION_MODE_WORD;
- _update_selection_mode_word();
+ } else if (mb->is_double_click()) {
+ // Start double-click select word mode.
+ set_selection_mode(SelectionMode::SELECTION_MODE_WORD);
+ _update_selection_mode_word(true);
last_dblclk = OS::get_singleton()->get_ticks_msec();
last_dblclk_pos = mb->get_position();
}
@@ -1863,34 +1848,20 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
_push_current_op();
_reset_caret_blink_timer();
apply_ime();
+ _cancel_drag_and_drop_text();
Point2i pos = get_line_column_at_pos(mpos);
- int row = pos.y;
- int col = pos.x;
+ int mouse_line = pos.y;
+ int mouse_column = pos.x;
- bool selection_clicked = false;
if (is_move_caret_on_right_click_enabled()) {
- if (has_selection()) {
- for (int i = 0; i < get_caret_count(); i++) {
- int from_line = get_selection_from_line(i);
- int to_line = get_selection_to_line(i);
- int from_column = get_selection_from_column(i);
- int to_column = get_selection_to_column(i);
-
- if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) {
- // Right click in one of the selected text
- selection_clicked = true;
- break;
- }
- }
- }
+ bool selection_clicked = get_selection_at_line_column(mouse_line, mouse_column, true) >= 0;
if (!selection_clicked) {
deselect();
remove_secondary_carets();
- set_caret_line(row, false, false);
- set_caret_column(col);
+ set_caret_line(mouse_line, false, false, -1);
+ set_caret_column(mouse_column);
}
- merge_overlapping_carets();
}
if (context_menu_enabled) {
@@ -1908,22 +1879,20 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
}
if (mb->get_button_index() == MouseButton::LEFT) {
- if (selection_drag_attempt && is_mouse_over_selection()) {
+ if (!drag_action && selection_drag_attempt && is_mouse_over_selection()) {
+ // This is not a drag and drop attempt, update the caret.
+ selection_drag_attempt = false;
remove_secondary_carets();
+ deselect();
Point2i pos = get_line_column_at_pos(get_local_mouse_pos());
- set_caret_line(pos.y, false, true, 0, 0);
+ set_caret_line(pos.y, false, true, -1, 0);
set_caret_column(pos.x, true, 0);
-
- deselect();
}
dragging_minimap = false;
dragging_selection = false;
can_drag_minimap = false;
click_select_held->stop();
- if (!drag_action) {
- selection_drag_attempt = false;
- }
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) {
DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text());
}
@@ -1958,7 +1927,8 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
mpos.x = get_size().x - mpos.x;
}
- if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging.
+ if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) {
+ // Update if not in drag and drop.
_reset_caret_blink_timer();
if (draw_minimap && !dragging_selection) {
@@ -2011,10 +1981,19 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
if (drag_action && can_drop_data(mpos, get_viewport()->gui_get_drag_data())) {
apply_ime();
+ // Update drag and drop caret.
drag_caret_force_displayed = true;
Point2i pos = get_line_column_at_pos(get_local_mouse_pos());
- set_caret_line(pos.y, false, true, 0, 0);
- set_caret_column(pos.x, true, 0);
+
+ if (drag_caret_index == -1) {
+ // Force create a new caret for drag and drop.
+ carets.push_back(Caret());
+ drag_caret_index = carets.size() - 1;
+ }
+
+ drag_caret_force_displayed = true;
+ set_caret_line(pos.y, false, true, -1, drag_caret_index);
+ set_caret_column(pos.x, true, drag_caret_index);
dragging_selection = true;
}
}
@@ -2043,6 +2022,8 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
return;
}
+ _cancel_drag_and_drop_text();
+
_reset_caret_blink_timer();
// Allow unicode handling if:
@@ -2321,42 +2302,36 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) {
}
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
- bool first_line = false;
- if (!p_split_current_line) {
- deselect(i);
- if (p_above) {
- if (get_caret_line(i) > 0) {
- set_caret_line(get_caret_line(i) - 1, false, true, 0, i);
- set_caret_column(text[get_caret_line(i)].length(), i == 0, i);
- } else {
- set_caret_column(0, i == 0, i);
- first_line = true;
- }
- } else {
- set_caret_column(text[get_caret_line(i)].length(), i == 0, i);
- }
- }
-
- insert_text_at_caret("\n", i);
+ begin_multicaret_edit();
- if (first_line) {
- set_caret_line(0, i == 0, true, 0, i);
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
+ if (p_split_current_line) {
+ insert_text_at_caret("\n", i);
+ } else {
+ int line = get_caret_line(i);
+ insert_text("\n", line, p_above ? 0 : text[line].length(), p_above, p_above);
+ deselect(i);
+ set_caret_line(p_above ? line : line + 1, false, true, -1, i);
+ set_caret_column(0, i == 0, i);
}
}
+
+ end_multicaret_edit();
end_complex_operation();
}
void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) {
_push_current_op();
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
// Handle selection.
if (p_select) {
_pre_shift_selection(i);
} else if (has_selection(i) && !p_move_by_word) {
// If a selection is active, move caret to start of selection.
- set_caret_line(get_selection_from_line(i), false, true, 0, i);
+ set_caret_line(get_selection_from_line(i), false, true, -1, i);
set_caret_column(get_selection_from_column(i), i == 0, i);
deselect(i);
continue;
@@ -2368,7 +2343,7 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) {
int cc = get_caret_column(i);
// If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line.
if (cc == 0 && get_caret_line(i) > 0) {
- set_caret_line(get_caret_line(i) - 1, false, true, 0, i);
+ set_caret_line(get_caret_line(i) - 1, false, true, -1, i);
set_caret_column(text[get_caret_line(i)].length(), i == 0, i);
} else {
PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid());
@@ -2389,7 +2364,8 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) {
// If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line.
if (get_caret_column(i) == 0) {
if (get_caret_line(i) > 0) {
- set_caret_line(get_caret_line(i) - get_next_visible_line_offset_from(CLAMP(get_caret_line(i) - 1, 0, text.size() - 1), -1), false, true, 0, i);
+ int new_caret_line = get_caret_line(i) - get_next_visible_line_offset_from(CLAMP(get_caret_line(i) - 1, 0, text.size() - 1), -1);
+ set_caret_line(new_caret_line, false, true, -1, i);
set_caret_column(text[get_caret_line(i)].length(), i == 0, i);
}
} else {
@@ -2400,23 +2376,19 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) {
}
}
}
-
- if (p_select) {
- _post_shift_selection(i);
- }
}
merge_overlapping_carets();
}
void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) {
_push_current_op();
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
// Handle selection.
if (p_select) {
_pre_shift_selection(i);
} else if (has_selection(i) && !p_move_by_word) {
// If a selection is active, move caret to end of selection.
- set_caret_line(get_selection_to_line(i), false, true, 0, i);
+ set_caret_line(get_selection_to_line(i), false, true, -1, i);
set_caret_column(get_selection_to_column(i), i == 0, i);
deselect(i);
continue;
@@ -2428,7 +2400,7 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) {
int cc = get_caret_column(i);
// If the caret is at the end of the line, and not on the last line, move it down to the beginning of the next line.
if (cc == text[get_caret_line(i)].length() && get_caret_line(i) < text.size() - 1) {
- set_caret_line(get_caret_line(i) + 1, false, true, 0, i);
+ set_caret_line(get_caret_line(i) + 1, false, true, -1, i);
set_caret_column(0, i == 0, i);
} else {
PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid());
@@ -2449,7 +2421,8 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) {
// If we are at the end of the line, move the caret to the next line down.
if (get_caret_column(i) == text[get_caret_line(i)].length()) {
if (get_caret_line(i) < text.size() - 1) {
- set_caret_line(get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1), false, false, 0, i);
+ int new_caret_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1);
+ set_caret_line(new_caret_line, false, false, -1, i);
set_caret_column(0, i == 0, i);
}
} else {
@@ -2460,17 +2433,13 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) {
}
}
}
-
- if (p_select) {
- _post_shift_selection(i);
- }
}
merge_overlapping_carets();
}
void TextEdit::_move_caret_up(bool p_select) {
_push_current_op();
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_select) {
_pre_shift_selection(i);
} else {
@@ -2490,17 +2459,13 @@ void TextEdit::_move_caret_up(bool p_select) {
set_caret_line(new_line, i == 0, false, 0, i);
}
}
-
- if (p_select) {
- _post_shift_selection(i);
- }
}
merge_overlapping_carets();
}
void TextEdit::_move_caret_down(bool p_select) {
_push_current_op();
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_select) {
_pre_shift_selection(i);
} else {
@@ -2516,17 +2481,13 @@ void TextEdit::_move_caret_down(bool p_select) {
int new_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1);
set_caret_line(new_line, i == 0, false, 0, i);
}
-
- if (p_select) {
- _post_shift_selection(i);
- }
}
merge_overlapping_carets();
}
void TextEdit::_move_caret_to_line_start(bool p_select) {
_push_current_op();
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_select) {
_pre_shift_selection(i);
} else {
@@ -2551,17 +2512,13 @@ void TextEdit::_move_caret_to_line_start(bool p_select) {
} else {
set_caret_column(row_start_col, i == 0, i);
}
-
- if (p_select) {
- _post_shift_selection(i);
- }
}
merge_overlapping_carets();
}
void TextEdit::_move_caret_to_line_end(bool p_select) {
_push_current_op();
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_select) {
_pre_shift_selection(i);
} else {
@@ -2580,17 +2537,13 @@ void TextEdit::_move_caret_to_line_end(bool p_select) {
} else {
set_caret_column(row_end_col, i == 0, i);
}
-
- if (p_select) {
- _post_shift_selection(i);
- }
}
merge_overlapping_carets();
}
void TextEdit::_move_caret_page_up(bool p_select) {
_push_current_op();
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_select) {
_pre_shift_selection(i);
} else {
@@ -2600,17 +2553,13 @@ void TextEdit::_move_caret_page_up(bool p_select) {
Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), -get_visible_line_count());
int n_line = get_caret_line(i) - next_line.x + 1;
set_caret_line(n_line, i == 0, false, next_line.y, i);
-
- if (p_select) {
- _post_shift_selection(i);
- }
}
merge_overlapping_carets();
}
void TextEdit::_move_caret_page_down(bool p_select) {
_push_current_op();
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_select) {
_pre_shift_selection(i);
} else {
@@ -2620,10 +2569,6 @@ void TextEdit::_move_caret_page_down(bool p_select) {
Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), get_visible_line_count());
int n_line = get_caret_line(i) + next_line.x - 1;
set_caret_line(n_line, i == 0, false, next_line.y, i);
-
- if (p_select) {
- _post_shift_selection(i);
- }
}
merge_overlapping_carets();
}
@@ -2634,58 +2579,47 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) {
}
start_action(EditAction::ACTION_BACKSPACE);
- Vector<int> carets_to_remove;
+ begin_multicaret_edit();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (int i = 0; i < caret_edit_order.size(); i++) {
- int caret_idx = caret_edit_order[i];
- if (get_caret_column(caret_idx) == 0 && get_caret_line(caret_idx) == 0 && !has_selection(caret_idx)) {
+ Vector<int> sorted_carets = get_sorted_carets();
+ sorted_carets.reverse();
+ for (int i = 0; i < sorted_carets.size(); i++) {
+ int caret_index = sorted_carets[i];
+ if (multicaret_edit_ignore_caret(caret_index)) {
continue;
}
- if (has_selection(caret_idx) || (!p_all_to_left && !p_word) || get_caret_column(caret_idx) == 0) {
- backspace(caret_idx);
+ if (get_caret_column(caret_index) == 0 && get_caret_line(caret_index) == 0 && !has_selection(caret_index)) {
continue;
}
- if (p_all_to_left) {
- int caret_current_column = get_caret_column(caret_idx);
- set_caret_column(0, caret_idx == 0, caret_idx);
- _remove_text(get_caret_line(caret_idx), 0, get_caret_line(caret_idx), caret_current_column);
- adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), caret_current_column, get_caret_line(caret_idx), get_caret_column(caret_idx));
-
- // Check for any overlapping carets since we removed the entire line.
- for (int j = i + 1; j < caret_edit_order.size(); j++) {
- // Selection only end on this line, only the one as carets cannot overlap.
- if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx) && get_selection_to_line(caret_edit_order[j]) == get_caret_line(caret_idx)) {
- carets.write[caret_edit_order[j]].selection.to_column = 0;
- break;
- }
-
- // Check for caret.
- if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) || (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx))) {
- break;
- }
+ if (has_selection(caret_index) || (!p_all_to_left && !p_word) || get_caret_column(caret_index) == 0) {
+ backspace(caret_index);
+ continue;
+ }
- deselect(caret_edit_order[j]);
- carets_to_remove.push_back(caret_edit_order[j]);
- set_caret_column(0, caret_idx == 0, caret_idx);
- i = j;
- }
+ if (p_all_to_left) {
+ // Remove everything to left of caret to the start of the line.
+ int caret_current_column = get_caret_column(caret_index);
+ _remove_text(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column);
+ collapse_carets(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column);
+ set_caret_column(0, caret_index == 0, caret_index);
+ _offset_carets_after(get_caret_line(caret_index), caret_current_column, get_caret_line(caret_index), 0);
continue;
}
if (p_word) {
- // Save here as the caret may change when resolving overlaps.
- int from_column = get_caret_column(caret_idx);
- int column = get_caret_column(caret_idx);
+ // Remove text to the start of the word left of the caret.
+ int from_column = get_caret_column(caret_index);
+ int column = get_caret_column(caret_index);
// Check for the case "<word><space><caret>" and ignore the space.
// No need to check for column being 0 since it is checked above.
- if (is_whitespace(text[get_caret_line(caret_idx)][get_caret_column(caret_idx) - 1])) {
+ if (is_whitespace(text[get_caret_line(caret_index)][get_caret_column(caret_index) - 1])) {
column -= 1;
}
+
// Get a list with the indices of the word bounds of the given text line.
- const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_idx))->get_rid());
+ const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_index))->get_rid());
if (words.is_empty() || column <= words[0]) {
// If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line.
column = 0;
@@ -2699,57 +2633,14 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) {
}
}
- // Check for any other carets in this range.
- int overlapping_caret_index = -1;
- for (int j = i + 1; j < caret_edit_order.size(); j++) {
- // Check caret and selection in on the right line.
- if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) && (!has_selection(caret_edit_order[j]) || get_selection_to_line(caret_edit_order[j]) != get_caret_line(caret_idx))) {
- break;
- }
-
- // If it has a selection, check it ends with in the range.
- if ((has_selection(caret_edit_order[j]) && get_selection_to_column(caret_edit_order[j]) < column)) {
- break;
- }
-
- // If it has a selection and it starts outside our word, we need to adjust the selection, and handle it later to prevent overlap.
- if ((has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) < column)) {
- carets.write[caret_edit_order[j]].selection.to_column = column;
- overlapping_caret_index = caret_edit_order[j];
- break;
- }
-
- // Otherwise we can remove it.
- if (get_caret_column(caret_edit_order[j]) > column || (has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) > column)) {
- deselect(caret_edit_order[j]);
- carets_to_remove.push_back(caret_edit_order[j]);
- set_caret_column(0, caret_idx == 0, caret_idx);
- i = j;
- }
- }
-
- _remove_text(get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column);
-
- set_caret_line(get_caret_line(caret_idx), false, true, 0, caret_idx);
- set_caret_column(column, caret_idx == 0, caret_idx);
- adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column);
-
- // Now we can clean up the overlapping caret.
- if (overlapping_caret_index != -1) {
- backspace(overlapping_caret_index);
- i++;
- carets_to_remove.push_back(overlapping_caret_index);
- set_caret_column(get_caret_column(overlapping_caret_index), caret_idx == 0, caret_idx);
- }
- continue;
+ _remove_text(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column);
+ collapse_carets(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column);
+ set_caret_column(column, caret_index == 0, caret_index);
+ _offset_carets_after(get_caret_line(caret_index), from_column, get_caret_line(caret_index), column);
}
}
- // Sort and remove backwards to preserve indexes.
- carets_to_remove.sort();
- for (int i = carets_to_remove.size() - 1; i >= 0; i--) {
- remove_caret(carets_to_remove[i]);
- }
+ end_multicaret_edit();
end_action();
}
@@ -2759,61 +2650,40 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) {
}
start_action(EditAction::ACTION_DELETE);
- Vector<int> carets_to_remove;
+ begin_multicaret_edit();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (int i = 0; i < caret_edit_order.size(); i++) {
- int caret_idx = caret_edit_order[i];
- if (has_selection(caret_idx)) {
- delete_selection(caret_idx);
+ Vector<int> sorted_carets = get_sorted_carets();
+ for (int i = 0; i < sorted_carets.size(); i++) {
+ int caret_index = sorted_carets[i];
+ if (multicaret_edit_ignore_caret(caret_index)) {
continue;
}
- int curline_len = text[get_caret_line(caret_idx)].length();
- if (get_caret_line(caret_idx) == text.size() - 1 && get_caret_column(caret_idx) == curline_len) {
+ if (has_selection(caret_index)) {
+ delete_selection(caret_index);
+ continue;
+ }
+
+ int curline_len = text[get_caret_line(caret_index)].length();
+ if (get_caret_line(caret_index) == text.size() - 1 && get_caret_column(caret_index) == curline_len) {
continue; // Last line, last column: Nothing to do.
}
- int next_line = get_caret_column(caret_idx) < curline_len ? get_caret_line(caret_idx) : get_caret_line(caret_idx) + 1;
+ int next_line = get_caret_column(caret_index) < curline_len ? get_caret_line(caret_index) : get_caret_line(caret_index) + 1;
int next_column;
if (p_all_to_right) {
- // Get caret furthest to the left.
- for (int j = i + 1; j < caret_edit_order.size(); j++) {
- if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) {
- break;
- }
-
- if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx)) {
- break;
- }
-
- if (!has_selection(caret_edit_order[j])) {
- i = j;
- caret_idx = caret_edit_order[i];
- }
- }
-
- if (get_caret_column(caret_idx) == curline_len) {
+ if (get_caret_column(caret_index) == curline_len) {
continue;
}
// Delete everything to right of caret.
next_column = curline_len;
- next_line = get_caret_line(caret_idx);
-
- // Remove overlapping carets.
- for (int j = i - 1; j >= 0; j--) {
- if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) {
- break;
- }
- carets_to_remove.push_back(caret_edit_order[j]);
- }
-
- } else if (p_word && get_caret_column(caret_idx) < curline_len - 1) {
+ next_line = get_caret_line(caret_index);
+ } else if (p_word && get_caret_column(caret_index) < curline_len - 1) {
// Delete next word to right of caret.
- int line = get_caret_line(caret_idx);
- int column = get_caret_column(caret_idx);
+ int line = get_caret_line(caret_index);
+ int column = get_caret_column(caret_index);
PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid());
for (int j = 1; j < words.size(); j = j + 2) {
@@ -2825,49 +2695,22 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) {
next_line = line;
next_column = column;
-
- // Remove overlapping carets.
- for (int j = i - 1; j >= 0; j--) {
- if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) {
- break;
- }
-
- if (get_caret_column(caret_edit_order[j]) > column) {
- break;
- }
- carets_to_remove.push_back(caret_edit_order[j]);
- }
} else {
// Delete one character.
if (caret_mid_grapheme_enabled) {
- next_column = get_caret_column(caret_idx) < curline_len ? (get_caret_column(caret_idx) + 1) : 0;
+ next_column = get_caret_column(caret_index) < curline_len ? (get_caret_column(caret_index) + 1) : 0;
} else {
- next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_idx))->get_rid(), (get_caret_column(caret_idx))) : 0;
- }
-
- // Remove overlapping carets.
- if (i > 0) {
- int prev_caret_idx = caret_edit_order[i - 1];
- if (get_caret_line(prev_caret_idx) == next_line && get_caret_column(prev_caret_idx) == next_column) {
- carets_to_remove.push_back(prev_caret_idx);
- }
+ next_column = get_caret_column(caret_index) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_index))->get_rid(), (get_caret_column(caret_index))) : 0;
}
}
- _remove_text(get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column);
- adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column);
- }
-
- // Sort and remove backwards to preserve indexes.
- carets_to_remove.sort();
- for (int i = carets_to_remove.size() - 1; i >= 0; i--) {
- remove_caret(carets_to_remove[i]);
+ _remove_text(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column);
+ collapse_carets(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column);
+ _offset_carets_after(next_line, next_column, get_caret_line(caret_index), get_caret_column(caret_index));
}
- // If we are deleting from the end of a line, due to column preservation we could still overlap with another caret.
- merge_overlapping_carets();
+ end_multicaret_edit();
end_action();
- queue_redraw();
}
void TextEdit::_move_caret_document_start(bool p_select) {
@@ -2878,12 +2721,8 @@ void TextEdit::_move_caret_document_start(bool p_select) {
deselect();
}
- set_caret_line(0, false);
+ set_caret_line(0, false, true, -1);
set_caret_column(0);
-
- if (p_select) {
- _post_shift_selection(0);
- }
}
void TextEdit::_move_caret_document_end(bool p_select) {
@@ -2894,12 +2733,8 @@ void TextEdit::_move_caret_document_end(bool p_select) {
deselect();
}
- set_caret_line(get_last_unhidden_line(), true, false, 9999);
+ set_caret_line(get_last_unhidden_line(), true, false, -1);
set_caret_column(text[get_caret_line()].length());
-
- if (p_select) {
- _post_shift_selection(0);
- }
}
bool TextEdit::_clear_carets_and_selection() {
@@ -2917,51 +2752,6 @@ bool TextEdit::_clear_carets_and_selection() {
return false;
}
-void TextEdit::_get_above_below_caret_line_column(int p_old_line, int p_old_wrap_index, int p_old_column, bool p_below, int &p_new_line, int &p_new_column, int p_last_fit_x) const {
- if (p_last_fit_x == -1) {
- p_last_fit_x = _get_column_x_offset_for_line(p_old_column, p_old_line, p_old_column);
- }
-
- // Calculate the new line and wrap index.
- p_new_line = p_old_line;
- int caret_wrap_index = p_old_wrap_index;
- if (p_below) {
- if (caret_wrap_index < get_line_wrap_count(p_new_line)) {
- caret_wrap_index++;
- } else {
- p_new_line++;
- caret_wrap_index = 0;
- }
- } else {
- if (caret_wrap_index == 0) {
- p_new_line--;
- caret_wrap_index = get_line_wrap_count(p_new_line);
- } else {
- caret_wrap_index--;
- }
- }
-
- // Boundary checks.
- if (p_new_line < 0) {
- p_new_line = 0;
- }
- if (p_new_line >= text.size()) {
- p_new_line = text.size() - 1;
- }
-
- p_new_column = _get_char_pos_for_line(p_last_fit_x, p_new_line, caret_wrap_index);
- if (p_new_column != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && caret_wrap_index < get_line_wrap_count(p_new_line)) {
- Vector<String> rows = get_line_wrapped_text(p_new_line);
- int row_end_col = 0;
- for (int i = 0; i < caret_wrap_index + 1; i++) {
- row_end_col += rows[i].length();
- }
- if (p_new_column >= row_end_col) {
- p_new_column -= 1;
- }
- }
-}
-
void TextEdit::_update_placeholder() {
if (theme_cache.font.is_null() || theme_cache.font_size <= 0) {
return; // Not in tree?
@@ -3127,53 +2917,48 @@ void TextEdit::drop_data(const Point2 &p_point, const Variant &p_data) {
if (p_data.get_type() == Variant::STRING && is_editable()) {
Point2i pos = get_line_column_at_pos(get_local_mouse_pos());
- int caret_row_tmp = pos.y;
- int caret_column_tmp = pos.x;
+ int drop_at_line = pos.y;
+ int drop_at_column = pos.x;
+ int selection_index = get_selection_at_line_column(drop_at_line, drop_at_column, !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL));
+
+ // Remove drag caret before the complex operation starts so it won't appear in undo.
+ remove_caret(drag_caret_index);
+
+ if (selection_drag_attempt && selection_index >= 0 && selection_index == drag_and_drop_origin_caret_index) {
+ // Dropped onto original selection, do nothing.
+ selection_drag_attempt = false;
+ return;
+ }
+
+ begin_complex_operation();
+ begin_multicaret_edit();
if (selection_drag_attempt) {
+ // Drop from self.
selection_drag_attempt = false;
- if (!is_mouse_over_selection(!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL))) {
- // Set caret back at selection for undo / redo.
- set_caret_line(get_selection_to_line(), false, false);
- set_caret_column(get_selection_to_column());
-
- begin_complex_operation();
- if (!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) {
- if (caret_row_tmp > get_selection_to_line()) {
- caret_row_tmp = caret_row_tmp - (get_selection_to_line() - get_selection_from_line());
- } else if (caret_row_tmp == get_selection_to_line() && caret_column_tmp >= get_selection_to_column()) {
- caret_column_tmp = caret_column_tmp - (get_selection_to_column() - get_selection_from_column());
- }
- delete_selection();
- } else {
- deselect();
- }
+ if (!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) {
+ // Delete all selections.
+ int temp_caret = add_caret(drop_at_line, drop_at_column);
- remove_secondary_carets();
- set_caret_line(caret_row_tmp, true, false);
- set_caret_column(caret_column_tmp);
- insert_text_at_caret(p_data);
- end_complex_operation();
- }
- } else if (is_mouse_over_selection()) {
- remove_secondary_carets();
- caret_row_tmp = get_selection_from_line();
- caret_column_tmp = get_selection_from_column();
- set_caret_line(caret_row_tmp, true, false);
- set_caret_column(caret_column_tmp);
- insert_text_at_caret(p_data);
- grab_focus();
- } else {
- remove_secondary_carets();
- deselect();
- set_caret_line(caret_row_tmp, true, false);
- set_caret_column(caret_column_tmp);
- insert_text_at_caret(p_data);
- grab_focus();
- }
+ delete_selection();
- if (caret_row_tmp != get_caret_line() || caret_column_tmp != get_caret_column()) {
- select(caret_row_tmp, caret_column_tmp, get_caret_line(), get_caret_column());
+ // Use a temporary caret to update the drop at position.
+ drop_at_line = get_caret_line(temp_caret);
+ drop_at_column = get_caret_column(temp_caret);
+ }
}
+ remove_secondary_carets();
+ deselect();
+
+ // Insert the dragged text.
+ set_caret_line(drop_at_line, true, false, -1);
+ set_caret_column(drop_at_column);
+ insert_text_at_caret(p_data);
+
+ select(drop_at_line, drop_at_column, get_caret_line(), get_caret_column());
+ grab_focus();
+ adjust_viewport_to_caret();
+ end_multicaret_edit();
+ end_complex_operation();
}
}
@@ -3459,7 +3244,7 @@ void TextEdit::_clear() {
clear_undo_history();
text.clear();
remove_secondary_carets();
- set_caret_line(0, false);
+ set_caret_line(0, false, true, -1);
set_caret_column(0);
first_visible_col = 0;
first_visible_line = 0;
@@ -3532,17 +3317,36 @@ void TextEdit::set_line(int p_line, const String &p_new_text) {
return;
}
begin_complex_operation();
- _remove_text(p_line, 0, p_line, text[p_line].length());
- _insert_text(p_line, 0, p_new_text);
- for (int i = 0; i < carets.size(); i++) {
- if (get_caret_line(i) == p_line && get_caret_column(i) > p_new_text.length()) {
- set_caret_column(p_new_text.length(), false, i);
+
+ int old_column = text[p_line].length();
+
+ // Set the affected carets column to update their last offset x.
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) {
+ set_caret_column(get_caret_column(i), false, i);
}
+ if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) {
+ set_selection_origin_column(get_selection_origin_column(i), i);
+ }
+ }
+
+ _remove_text(p_line, 0, p_line, old_column);
+ int new_line, new_column;
+ _insert_text(p_line, 0, p_new_text, &new_line, &new_column);
- if (has_selection(i) && p_line == get_selection_to_line(i) && get_selection_to_column(i) > text[p_line].length()) {
- carets.write[i].selection.to_column = text[p_line].length();
+ // Don't offset carets that were on the old line.
+ _offset_carets_after(p_line, old_column, new_line, new_column, false, false);
+
+ // Set the caret lines to update the column to match visually.
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) {
+ set_caret_line(get_caret_line(i), false, true, 0, i);
+ }
+ if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) {
+ set_selection_origin_line(get_selection_origin_line(i), true, 0, i);
}
}
+ merge_overlapping_carets();
end_complex_operation();
}
@@ -3596,71 +3400,163 @@ void TextEdit::swap_lines(int p_from_line, int p_to_line) {
ERR_FAIL_INDEX(p_from_line, text.size());
ERR_FAIL_INDEX(p_to_line, text.size());
- String tmp = get_line(p_from_line);
- String tmp2 = get_line(p_to_line);
+ if (p_from_line == p_to_line) {
+ return;
+ }
+
+ String from_line_text = get_line(p_from_line);
+ String to_line_text = get_line(p_to_line);
begin_complex_operation();
- set_line(p_to_line, tmp);
- set_line(p_from_line, tmp2);
+ begin_multicaret_edit();
+ // Don't use set_line to avoid clamping and updating carets.
+ _remove_text(p_to_line, 0, p_to_line, text[p_to_line].length());
+ _insert_text(p_to_line, 0, from_line_text);
+ _remove_text(p_from_line, 0, p_from_line, text[p_from_line].length());
+ _insert_text(p_from_line, 0, to_line_text);
+
+ // Swap carets.
+ for (int i = 0; i < get_caret_count(); i++) {
+ bool selected = has_selection(i);
+ if (get_caret_line(i) == p_from_line || get_caret_line(i) == p_to_line) {
+ int caret_new_line = get_caret_line(i) == p_from_line ? p_to_line : p_from_line;
+ int caret_column = get_caret_column(i);
+ set_caret_line(caret_new_line, false, true, -1, i);
+ set_caret_column(caret_column, false, i);
+ }
+ if (selected && (get_selection_origin_line(i) == p_from_line || get_selection_origin_line(i) == p_to_line)) {
+ int origin_new_line = get_selection_origin_line(i) == p_from_line ? p_to_line : p_from_line;
+ int origin_column = get_selection_origin_column(i);
+ select(origin_new_line, origin_column, get_caret_line(i), get_caret_column(i), i);
+ }
+ }
+ // If only part of a selection was changed, it may now overlap.
+ merge_overlapping_carets();
+
+ end_multicaret_edit();
end_complex_operation();
}
-void TextEdit::insert_line_at(int p_at, const String &p_text) {
- ERR_FAIL_INDEX(p_at, text.size());
+void TextEdit::insert_line_at(int p_line, const String &p_text) {
+ ERR_FAIL_INDEX(p_line, text.size());
- _insert_text(p_at, 0, p_text + "\n");
+ // Use a complex operation so subsequent calls aren't merged together.
+ begin_complex_operation();
- for (int i = 0; i < carets.size(); i++) {
- if (get_caret_line(i) >= p_at) {
- // Offset caret when located after inserted line.
- set_caret_line(get_caret_line(i) + 1, false, true, 0, i);
- }
- if (has_selection(i)) {
- if (get_selection_from_line(i) >= p_at) {
- // Offset selection when located after inserted line.
- select(get_selection_from_line(i) + 1, get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i);
- } else if (get_selection_to_line(i) >= p_at) {
- // Extend selection that includes inserted line.
- select(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i);
+ int new_line, new_column;
+ _insert_text(p_line, 0, p_text + "\n", &new_line, &new_column);
+ _offset_carets_after(p_line, 0, new_line, new_column);
+
+ end_complex_operation();
+}
+
+void TextEdit::remove_line_at(int p_line, bool p_move_carets_down) {
+ ERR_FAIL_INDEX(p_line, text.size());
+
+ if (get_line_count() == 1) {
+ // Only one line, just remove contents.
+ begin_complex_operation();
+ int line_length = get_line(p_line).length();
+ _remove_text(p_line, 0, p_line, line_length);
+ collapse_carets(p_line, 0, p_line, line_length, true);
+ end_complex_operation();
+ return;
+ }
+
+ begin_complex_operation();
+
+ bool is_last_line = p_line == get_line_count() - 1;
+ int from_line = is_last_line ? p_line - 1 : p_line;
+ int next_line = is_last_line ? p_line : p_line + 1;
+ int from_column = is_last_line ? get_line(from_line).length() : 0;
+ int next_column = is_last_line ? get_line(next_line).length() : 0;
+
+ if ((!is_last_line && p_move_carets_down) || (p_line != 0 && !p_move_carets_down)) {
+ // Set the carets column to update their last offset x.
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (get_caret_line(i) == p_line) {
+ set_caret_column(get_caret_column(i), false, i);
+ }
+ if (has_selection(i) && get_selection_origin_line(i) == p_line) {
+ set_selection_origin_column(get_selection_origin_column(i), i);
}
}
}
- // Need to apply the above adjustments to the undo / redo carets.
- current_op.end_carets = carets;
- queue_redraw();
+ // Remove line.
+ _remove_text(from_line, from_column, next_line, next_column);
+
+ begin_multicaret_edit();
+ if ((is_last_line && p_move_carets_down) || (p_line == 0 && !p_move_carets_down)) {
+ // Collapse carets.
+ collapse_carets(from_line, from_column, next_line, next_column, true);
+ } else {
+ // Move carets to visually line up.
+ int target_line = p_move_carets_down ? p_line : p_line - 1;
+ for (int i = 0; i < get_caret_count(); i++) {
+ bool selected = has_selection(i);
+ if (get_caret_line(i) == p_line) {
+ set_caret_line(target_line, i == 0, true, 0, i);
+ }
+ if (selected && get_selection_origin_line(i) == p_line) {
+ set_selection_origin_line(target_line, true, 0, i);
+ select(get_selection_origin_line(i), get_selection_origin_column(i), get_caret_line(i), get_caret_column(i), i);
+ }
+ }
+
+ merge_overlapping_carets();
+ }
+ _offset_carets_after(next_line, next_column, from_line, from_column);
+ end_multicaret_edit();
+ end_complex_operation();
}
void TextEdit::insert_text_at_caret(const String &p_text, int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
+ ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1);
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
+ begin_multicaret_edit();
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_caret != -1 && p_caret != i) {
continue;
}
+ if (p_caret == -1 && multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
delete_selection(i);
int from_line = get_caret_line(i);
int from_col = get_caret_column(i);
- int new_column, new_line;
+ int new_line, new_column;
_insert_text(from_line, from_col, p_text, &new_line, &new_column);
_update_scrollbars();
+ _offset_carets_after(from_line, from_col, new_line, new_column);
- set_caret_line(new_line, false, true, 0, i);
+ set_caret_line(new_line, false, true, -1, i);
set_caret_column(new_column, i == 0, i);
-
- adjust_carets_after_edit(i, new_line, new_column, from_line, from_col);
}
if (has_ime_text()) {
_update_ime_text();
}
+ end_multicaret_edit();
+ end_complex_operation();
+}
+
+void TextEdit::insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin, bool p_before_selection_end) {
+ ERR_FAIL_INDEX(p_line, text.size());
+ ERR_FAIL_INDEX(p_column, text[p_line].length() + 1);
+
+ begin_complex_operation();
+
+ int new_line, new_column;
+ _insert_text(p_line, p_column, p_text, &new_line, &new_column);
+
+ _offset_carets_after(p_line, p_column, new_line, new_column, p_before_selection_begin, p_before_selection_end);
+
end_complex_operation();
- queue_redraw();
}
void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) {
@@ -3671,7 +3567,13 @@ void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, in
ERR_FAIL_COND(p_to_line < p_from_line);
ERR_FAIL_COND(p_to_line == p_from_line && p_to_column < p_from_column);
+ begin_complex_operation();
+
_remove_text(p_from_line, p_from_column, p_to_line, p_to_column);
+ collapse_carets(p_from_line, p_from_column, p_to_line, p_to_column);
+ _offset_carets_after(p_to_line, p_to_column, p_from_line, p_from_column);
+
+ end_complex_operation();
}
int TextEdit::get_last_unhidden_line() const {
@@ -4040,7 +3942,7 @@ void TextEdit::undo() {
_push_current_op();
if (undo_stack_pos == nullptr) {
- if (!undo_stack.size()) {
+ if (undo_stack.is_empty()) {
return; // Nothing to undo.
}
@@ -4059,6 +3961,7 @@ void TextEdit::undo() {
current_op.version = op.prev_version;
if (undo_stack_pos->get().chain_backward) {
+ // This was part of a complex operation, undo until the chain forward at the start of the complex operation.
while (true) {
ERR_BREAK(!undo_stack_pos->prev());
undo_stack_pos = undo_stack_pos->prev();
@@ -4072,9 +3975,9 @@ void TextEdit::undo() {
}
_update_scrollbars();
- bool dirty_carets = carets.size() != undo_stack_pos->get().start_carets.size();
+ bool dirty_carets = get_caret_count() != undo_stack_pos->get().start_carets.size();
if (!dirty_carets) {
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
if (carets[i].line != undo_stack_pos->get().start_carets[i].line || carets[i].column != undo_stack_pos->get().start_carets[i].column) {
dirty_carets = true;
break;
@@ -4084,11 +3987,11 @@ void TextEdit::undo() {
carets = undo_stack_pos->get().start_carets;
- if (dirty_carets && !caret_pos_dirty) {
- if (is_inside_tree()) {
- callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred();
- }
- caret_pos_dirty = true;
+ _unhide_carets();
+
+ if (dirty_carets) {
+ _caret_changed();
+ _selection_changed();
}
adjust_viewport_to_caret();
}
@@ -4113,6 +4016,7 @@ void TextEdit::redo() {
_do_text_op(op, false);
current_op.version = op.version;
if (undo_stack_pos->get().chain_forward) {
+ // This was part of a complex operation, redo until the chain backward at the end of the complex operation.
while (true) {
ERR_BREAK(!undo_stack_pos->next());
undo_stack_pos = undo_stack_pos->next();
@@ -4126,9 +4030,9 @@ void TextEdit::redo() {
}
_update_scrollbars();
- bool dirty_carets = carets.size() != undo_stack_pos->get().end_carets.size();
+ bool dirty_carets = get_caret_count() != undo_stack_pos->get().end_carets.size();
if (!dirty_carets) {
- for (int i = 0; i < carets.size(); i++) {
+ for (int i = 0; i < get_caret_count(); i++) {
if (carets[i].line != undo_stack_pos->get().end_carets[i].line || carets[i].column != undo_stack_pos->get().end_carets[i].column) {
dirty_carets = true;
break;
@@ -4139,11 +4043,11 @@ void TextEdit::redo() {
carets = undo_stack_pos->get().end_carets;
undo_stack_pos = undo_stack_pos->next();
- if (dirty_carets && !caret_pos_dirty) {
- if (is_inside_tree()) {
- callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred();
- }
- caret_pos_dirty = true;
+ _unhide_carets();
+
+ if (dirty_carets) {
+ _caret_changed();
+ _selection_changed();
}
adjust_viewport_to_caret();
}
@@ -4358,13 +4262,7 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_
}
}
- if (row < 0) {
- row = 0;
- }
-
- if (row >= text.size()) {
- row = text.size() - 1;
- }
+ row = CLAMP(row, 0, text.size() - 1);
int visible_lines = get_visible_line_count_in_range(first_vis_line, row);
if (rows > visible_lines) {
@@ -4510,29 +4408,13 @@ bool TextEdit::is_dragging_cursor() const {
}
bool TextEdit::is_mouse_over_selection(bool p_edges, int p_caret) const {
- for (int i = 0; i < carets.size(); i++) {
- if (p_caret != -1 && p_caret != i) {
- continue;
- }
-
- if (!has_selection(i)) {
- continue;
- }
-
- Point2i pos = get_line_column_at_pos(get_local_mouse_pos());
- int row = pos.y;
- int col = pos.x;
- if (p_edges) {
- if ((row == get_selection_from_line(i) && col == get_selection_from_column(i)) || (row == get_selection_to_line(i) && col == get_selection_to_column(i))) {
- return true;
- }
- }
+ Point2i pos = get_line_column_at_pos(get_local_mouse_pos());
+ int line = pos.y;
+ int column = pos.x;
- if (row >= get_selection_from_line(i) && row <= get_selection_to_line(i) && (row > get_selection_from_line(i) || col > get_selection_from_column(i)) && (row < get_selection_to_line(i) || col < get_selection_to_column(i))) {
- return true;
- }
+ if ((p_caret == -1 && get_selection_at_line_column(line, column, p_edges) != -1) || (p_caret != -1 && _selection_contains(p_caret, line, column, p_edges))) {
+ return true;
}
-
return false;
}
@@ -4619,270 +4501,401 @@ bool TextEdit::is_multiple_carets_enabled() const {
return multi_carets_enabled;
}
-int TextEdit::add_caret(int p_line, int p_col) {
+int TextEdit::add_caret(int p_line, int p_column) {
if (!multi_carets_enabled) {
return -1;
}
+ _cancel_drag_and_drop_text();
p_line = CLAMP(p_line, 0, text.size() - 1);
- p_col = CLAMP(p_col, 0, get_line(p_line).length());
+ p_column = CLAMP(p_column, 0, get_line(p_line).length());
- for (int i = 0; i < carets.size(); i++) {
- if (get_caret_line(i) == p_line && get_caret_column(i) == p_col) {
+ if (!is_in_mulitcaret_edit()) {
+ // Carets cannot overlap.
+ if (get_selection_at_line_column(p_line, p_column, true, false) != -1) {
return -1;
}
-
- if (has_selection(i)) {
- if (p_line >= get_selection_from_line(i) && p_line <= get_selection_to_line(i) && (p_line > get_selection_from_line(i) || p_col >= get_selection_from_column(i)) && (p_line < get_selection_to_line(i) || p_col <= get_selection_to_column(i))) {
- return -1;
- }
- }
}
carets.push_back(Caret());
- set_caret_line(p_line, false, false, 0, carets.size() - 1);
- set_caret_column(p_col, false, carets.size() - 1);
- caret_index_edit_dirty = true;
- return carets.size() - 1;
+ int new_index = carets.size() - 1;
+ set_caret_line(p_line, false, false, -1, new_index);
+ set_caret_column(p_column, false, new_index);
+ _caret_changed(new_index);
+
+ if (is_in_mulitcaret_edit()) {
+ multicaret_edit_ignore_carets.insert(new_index);
+ merge_overlapping_carets();
+ }
+ return new_index;
}
void TextEdit::remove_caret(int p_caret) {
ERR_FAIL_COND_MSG(carets.size() <= 1, "The main caret should not be removed.");
ERR_FAIL_INDEX(p_caret, carets.size());
+
+ _caret_changed(p_caret);
carets.remove_at(p_caret);
- caret_index_edit_dirty = true;
+
+ if (drag_caret_index >= 0) {
+ if (p_caret == drag_caret_index) {
+ drag_caret_index = -1;
+ } else if (p_caret < drag_caret_index) {
+ drag_caret_index -= 1;
+ }
+ }
}
void TextEdit::remove_secondary_carets() {
+ if (carets.size() == 1) {
+ return;
+ }
+
+ _caret_changed();
carets.resize(1);
- caret_index_edit_dirty = true;
- queue_redraw();
+
+ if (drag_caret_index >= 0) {
+ drag_caret_index = -1;
+ }
}
-void TextEdit::merge_overlapping_carets() {
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (int i = 0; i < caret_edit_order.size() - 1; i++) {
- int first_caret = caret_edit_order[i];
- int second_caret = caret_edit_order[i + 1];
+int TextEdit::get_caret_count() const {
+ // Don't include drag caret.
+ if (drag_caret_index >= 0) {
+ return carets.size() - 1;
+ }
+ return carets.size();
+}
- // Both have selection.
- if (has_selection(first_caret) && has_selection(second_caret)) {
- bool should_merge = false;
- if (get_selection_from_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_from_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_from_line(first_caret) > get_selection_from_line(second_caret) || get_selection_from_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_from_line(first_caret) < get_selection_to_line(second_caret) || get_selection_from_column(first_caret) <= get_selection_to_column(second_caret))) {
- should_merge = true;
- }
+void TextEdit::add_caret_at_carets(bool p_below) {
+ const int last_line_max_wrap = get_line_wrap_count(text.size() - 1);
+
+ begin_multicaret_edit();
+ int view_target_caret = -1;
+ int view_line = p_below ? -1 : INT_MAX;
+ int num_carets = get_caret_count();
+ for (int i = 0; i < num_carets; i++) {
+ const int caret_line = get_caret_line(i);
+ const int caret_column = get_caret_column(i);
+ const bool is_selected = has_selection(i) || carets[i].last_fit_x != carets[i].selection.origin_last_fit_x;
+ const int selection_origin_line = get_selection_origin_line(i);
+ const int selection_origin_column = get_selection_origin_column(i);
+ const int caret_wrap_index = get_caret_wrap_index(i);
+ const int selection_origin_wrap_index = !is_selected ? -1 : get_line_wrap_index_at_column(selection_origin_line, selection_origin_column);
+
+ if (caret_line == 0 && !p_below && (caret_wrap_index == 0 || selection_origin_wrap_index == 0)) {
+ // Can't add above the first line.
+ continue;
+ }
+ if (caret_line == text.size() - 1 && p_below && (caret_wrap_index == last_line_max_wrap || selection_origin_wrap_index == last_line_max_wrap)) {
+ // Can't add below the last line.
+ continue;
+ }
- if (get_selection_to_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_to_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_to_line(first_caret) > get_selection_from_line(second_caret) || get_selection_to_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_to_line(first_caret) < get_selection_to_line(second_caret) || get_selection_to_column(first_caret) <= get_selection_to_column(second_caret))) {
- should_merge = true;
- }
+ // Add a new caret.
+ int new_caret_index = add_caret(caret_line, caret_column);
- if (!should_merge) {
- continue;
- }
+ // Copy the selection origin and last fit.
+ set_selection_origin_line(selection_origin_line, true, -1, new_caret_index);
+ set_selection_origin_column(selection_origin_column, new_caret_index);
+ carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x;
+ carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x;
- // Save the newest one for Click + Drag.
- int caret_to_save = first_caret;
- int caret_to_remove = second_caret;
- if (first_caret < second_caret) {
- caret_to_save = second_caret;
- caret_to_remove = first_caret;
+ // Move the caret up or down one visible line.
+ if (!p_below) {
+ // Move caret up.
+ if (caret_wrap_index > 0) {
+ set_caret_line(caret_line, false, false, caret_wrap_index - 1, new_caret_index);
+ } else {
+ int new_line = caret_line - get_next_visible_line_offset_from(caret_line - 1, -1);
+ if (is_line_wrapped(new_line)) {
+ set_caret_line(new_line, false, false, get_line_wrap_count(new_line), new_caret_index);
+ } else {
+ set_caret_line(new_line, false, false, 0, new_caret_index);
+ }
}
-
- int from_line = MIN(get_selection_from_line(caret_to_save), get_selection_from_line(caret_to_remove));
- int to_line = MAX(get_selection_to_line(caret_to_save), get_selection_to_line(caret_to_remove));
- int from_col = get_selection_from_column(caret_to_save);
- int to_col = get_selection_to_column(caret_to_save);
- int selection_line = get_selection_line(caret_to_save);
- int selection_col = get_selection_column(caret_to_save);
-
- bool at_from = (get_caret_line(caret_to_save) == get_selection_from_line(caret_to_save) && get_caret_column(caret_to_save) == get_selection_from_column(caret_to_save));
-
- if (at_from) {
- if (get_selection_line(caret_to_remove) > get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) >= get_selection_column(caret_to_save))) {
- selection_line = get_selection_line(caret_to_remove);
- selection_col = get_selection_column(caret_to_remove);
+ // Move selection origin up.
+ if (is_selected) {
+ if (selection_origin_wrap_index > 0) {
+ set_selection_origin_line(caret_line, false, selection_origin_wrap_index - 1, new_caret_index);
+ } else {
+ int new_line = selection_origin_line - get_next_visible_line_offset_from(selection_origin_line - 1, -1);
+ if (is_line_wrapped(new_line)) {
+ set_selection_origin_line(new_line, false, get_line_wrap_count(new_line), new_caret_index);
+ } else {
+ set_selection_origin_line(new_line, false, 0, new_caret_index);
+ }
}
- } else if (get_selection_line(caret_to_remove) < get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) <= get_selection_column(caret_to_save))) {
- selection_line = get_selection_line(caret_to_remove);
- selection_col = get_selection_column(caret_to_remove);
}
-
- if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save) || (get_selection_from_line(caret_to_remove) == get_selection_from_line(caret_to_save) && get_selection_from_column(caret_to_remove) <= get_selection_from_column(caret_to_save))) {
- from_col = get_selection_from_column(caret_to_remove);
+ if (get_caret_line(new_caret_index) < view_line) {
+ view_line = get_caret_line(new_caret_index);
+ view_target_caret = new_caret_index;
+ }
+ } else {
+ // Move caret down.
+ if (caret_wrap_index < get_line_wrap_count(caret_line)) {
+ set_caret_line(caret_line, false, false, caret_wrap_index + 1, new_caret_index);
} else {
- to_col = get_selection_to_column(caret_to_remove);
+ int new_line = caret_line + get_next_visible_line_offset_from(CLAMP(caret_line + 1, 0, text.size() - 1), 1);
+ set_caret_line(new_line, false, false, 0, new_caret_index);
+ }
+ // Move selection origin down.
+ if (is_selected) {
+ if (selection_origin_wrap_index < get_line_wrap_count(selection_origin_line)) {
+ set_selection_origin_line(selection_origin_line, false, selection_origin_wrap_index + 1, new_caret_index);
+ } else {
+ int new_line = selection_origin_line + get_next_visible_line_offset_from(CLAMP(selection_origin_line + 1, 0, text.size() - 1), 1);
+ set_selection_origin_line(new_line, false, 0, new_caret_index);
+ }
+ }
+ if (get_caret_line(new_caret_index) > view_line) {
+ view_line = get_caret_line(new_caret_index);
+ view_target_caret = new_caret_index;
}
+ }
+ if (is_selected) {
+ // Make sure selection is active.
+ select(get_selection_origin_line(new_caret_index), get_selection_origin_column(new_caret_index), get_caret_line(new_caret_index), get_caret_column(new_caret_index), new_caret_index);
+ carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x;
+ carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x;
+ }
- select(from_line, from_col, to_line, to_col, caret_to_save);
- set_selection_mode(selecting_mode, selection_line, selection_col, caret_to_save);
- set_caret_line((at_from ? from_line : to_line), caret_to_save == 0, true, 0, caret_to_save);
- set_caret_column((at_from ? from_col : to_col), caret_to_save == 0, caret_to_save);
- remove_caret(caret_to_remove);
- i--;
- caret_edit_order = get_caret_index_edit_order();
- continue;
+ bool check_edges = !has_selection(0) || !has_selection(new_caret_index);
+ bool will_merge_with_main_caret = _selection_contains(0, get_caret_line(new_caret_index), get_caret_column(new_caret_index), check_edges, false) || _selection_contains(new_caret_index, get_caret_line(0), get_caret_column(0), check_edges, false);
+ if (will_merge_with_main_caret) {
+ // Move next to the main caret so it stays the main caret after merging.
+ Caret new_caret = carets[new_caret_index];
+ carets.remove_at(new_caret_index);
+ carets.insert(0, new_caret);
+ i++;
}
+ }
- // Only first has selection.
- if (has_selection(first_caret)) {
- if (get_caret_line(second_caret) >= get_selection_from_line(first_caret) && get_caret_line(second_caret) <= get_selection_to_line(first_caret) && (get_caret_line(second_caret) > get_selection_from_line(first_caret) || get_caret_column(second_caret) >= get_selection_from_column(first_caret)) && (get_caret_line(second_caret) < get_selection_to_line(first_caret) || get_caret_column(second_caret) <= get_selection_to_column(first_caret))) {
- remove_caret(second_caret);
- caret_edit_order = get_caret_index_edit_order();
- i--;
- }
- continue;
+ // Show the topmost caret if added above or bottommost caret if added below.
+ if (view_target_caret >= 0 && view_target_caret < get_caret_count()) {
+ adjust_viewport_to_caret(view_target_caret);
+ }
+
+ merge_overlapping_carets();
+ end_multicaret_edit();
+}
+
+struct _CaretSortComparator {
+ _FORCE_INLINE_ bool operator()(const Vector3i &a, const Vector3i &b) const {
+ // x is column, y is line, z is caret index.
+ if (a.y == b.y) {
+ return a.x < b.x;
}
+ return a.y < b.y;
+ }
+};
- // Only second has selection.
- if (has_selection(second_caret)) {
- if (get_caret_line(first_caret) >= get_selection_from_line(second_caret) && get_caret_line(first_caret) <= get_selection_to_line(second_caret) && (get_caret_line(first_caret) > get_selection_from_line(second_caret) || get_caret_column(first_caret) >= get_selection_from_column(second_caret)) && (get_caret_line(first_caret) < get_selection_to_line(second_caret) || get_caret_column(first_caret) <= get_selection_to_column(second_caret))) {
- remove_caret(first_caret);
- caret_edit_order = get_caret_index_edit_order();
- i--;
- }
+Vector<int> TextEdit::get_sorted_carets(bool p_include_ignored_carets) const {
+ // Returns caret indexes sorted by selection start or caret position from top to bottom of text.
+ Vector<Vector3i> caret_line_col_indexes;
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (!p_include_ignored_carets && multicaret_edit_ignore_caret(i)) {
continue;
}
+ caret_line_col_indexes.push_back(Vector3i(get_selection_from_column(i), get_selection_from_line(i), i));
+ }
+ caret_line_col_indexes.sort_custom<_CaretSortComparator>();
+ Vector<int> sorted;
+ sorted.resize(caret_line_col_indexes.size());
+ for (int i = 0; i < caret_line_col_indexes.size(); i++) {
+ sorted.set(i, caret_line_col_indexes[i].z);
+ }
+ return sorted;
+}
- // Both have no selection.
- if (get_caret_line(first_caret) == get_caret_line(second_caret) && get_caret_column(first_caret) == get_caret_column(second_caret)) {
- // Save the newest one for Click + Drag.
- if (first_caret < second_caret) {
- remove_caret(first_caret);
- } else {
- remove_caret(second_caret);
+void TextEdit::collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive) {
+ // Collapse carets in the selected range to the from position.
+
+ // Clamp the collapse target position.
+ int collapse_line = CLAMP(p_from_line, 0, text.size() - 1);
+ int collapse_column = CLAMP(p_from_column, 0, text[collapse_line].length());
+
+ // Swap the lines if they are in the wrong order.
+ if (p_from_line > p_to_line) {
+ SWAP(p_from_line, p_to_line);
+ SWAP(p_from_column, p_to_column);
+ }
+ if (p_from_line == p_to_line && p_from_column > p_to_column) {
+ SWAP(p_from_column, p_to_column);
+ }
+ bool any_collapsed = false;
+
+ // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together.
+ for (int i = 0; i < get_caret_count(); i++) {
+ bool is_caret_in = _is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive);
+ if (!has_selection(i)) {
+ if (is_caret_in) {
+ // Caret was in the collapsed area.
+ set_caret_line(collapse_line, false, true, -1, i);
+ set_caret_column(collapse_column, false, i);
+ if (is_in_mulitcaret_edit()) {
+ multicaret_edit_ignore_carets.insert(i);
+ }
+ any_collapsed = true;
+ }
+ } else {
+ bool is_origin_in = _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive);
+
+ if (is_caret_in && is_origin_in) {
+ // Selection was completely encapsulated.
+ deselect(i);
+ set_caret_line(collapse_line, false, true, -1, i);
+ set_caret_column(collapse_column, false, i);
+ if (is_in_mulitcaret_edit()) {
+ multicaret_edit_ignore_carets.insert(i);
+ }
+ any_collapsed = true;
+ } else if (is_caret_in) {
+ // Only caret was inside.
+ set_caret_line(collapse_line, false, true, -1, i);
+ set_caret_column(collapse_column, false, i);
+ any_collapsed = true;
+ } else if (is_origin_in) {
+ // Only selection origin was inside.
+ set_selection_origin_line(collapse_line, true, -1, i);
+ set_selection_origin_column(collapse_column, i);
+ any_collapsed = true;
}
- i--;
- caret_edit_order = get_caret_index_edit_order();
- continue;
}
+ if (!p_inclusive && !any_collapsed) {
+ if ((get_caret_line(i) == collapse_line && get_caret_column(i) == collapse_column) || (get_selection_origin_line(i) == collapse_line && get_selection_origin_column(i) == collapse_column)) {
+ // Make sure to queue a merge, even if we didn't include it.
+ any_collapsed = true;
+ }
+ }
+ }
+ if (any_collapsed) {
+ merge_overlapping_carets();
}
}
-int TextEdit::get_caret_count() const {
- return carets.size();
-}
+void TextEdit::merge_overlapping_carets() {
+ if (is_in_mulitcaret_edit()) {
+ // Queue merge to be performed the end of the multicaret edit.
+ multicaret_edit_merge_queued = true;
+ return;
+ }
-void TextEdit::add_caret_at_carets(bool p_below) {
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &caret_index : caret_edit_order) {
- const int caret_line = get_caret_line(caret_index);
- const int caret_column = get_caret_column(caret_index);
-
- // The last fit x will be cleared if the caret has a selection,
- // but if it does not have a selection the last fit x will be
- // transferred to the new caret.
- int caret_from_column = 0, caret_to_column = 0, caret_last_fit_x = carets[caret_index].last_fit_x;
- if (has_selection(caret_index)) {
- // If the selection goes over multiple lines, deselect it.
- if (get_selection_from_line(caret_index) != get_selection_to_line(caret_index)) {
- deselect(caret_index);
+ multicaret_edit_merge_queued = false;
+ multicaret_edit_ignore_carets.clear();
+
+ if (get_caret_count() == 1) {
+ return;
+ }
+
+ Vector<int> sorted_carets = get_sorted_carets(true);
+ for (int i = 0; i < sorted_carets.size() - 1; i++) {
+ int first_caret = sorted_carets[i];
+ int second_caret = sorted_carets[i + 1];
+
+ bool merge_carets;
+ if (!has_selection(first_caret) || !has_selection(second_caret)) {
+ // Merge if touching.
+ merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) <= get_selection_to_column(first_caret));
+ } else {
+ // Merge two selections if overlapping.
+ merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) < get_selection_to_column(first_caret));
+ }
+
+ if (!merge_carets) {
+ continue;
+ }
+
+ // Save the newest one for Click + Drag.
+ int caret_to_save = first_caret;
+ int caret_to_remove = second_caret;
+ if (first_caret < second_caret) {
+ caret_to_save = second_caret;
+ caret_to_remove = first_caret;
+ }
+
+ if (get_selection_from_line(caret_to_save) != get_selection_from_line(caret_to_remove) || get_selection_to_line(caret_to_save) != get_selection_to_line(caret_to_remove) || get_selection_from_column(caret_to_save) != get_selection_from_column(caret_to_remove) || get_selection_to_column(caret_to_save) != get_selection_to_column(caret_to_remove)) {
+ // Selections are not the same, merge them into one bigger selection.
+ int new_from_line = MIN(get_selection_from_line(caret_to_remove), get_selection_from_line(caret_to_save));
+ int new_to_line = MAX(get_selection_to_line(caret_to_remove), get_selection_to_line(caret_to_save));
+ int new_from_col;
+ int new_to_col;
+ if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save)) {
+ new_from_col = get_selection_from_column(caret_to_remove);
+ } else if (get_selection_from_line(caret_to_remove) > get_selection_from_line(caret_to_save)) {
+ new_from_col = get_selection_from_column(caret_to_save);
} else {
- caret_from_column = get_selection_from_column(caret_index);
- caret_to_column = get_selection_to_column(caret_index);
- caret_last_fit_x = -1;
- carets.write[caret_index].last_fit_x = _get_column_x_offset_for_line(caret_column, caret_line, caret_column);
+ new_from_col = MIN(get_selection_from_column(caret_to_remove), get_selection_from_column(caret_to_save));
+ }
+ if (get_selection_to_line(caret_to_remove) < get_selection_to_line(caret_to_save)) {
+ new_to_col = get_selection_to_column(caret_to_save);
+ } else if (get_selection_to_line(caret_to_remove) > get_selection_to_line(caret_to_save)) {
+ new_to_col = get_selection_to_column(caret_to_remove);
+ } else {
+ new_to_col = MAX(get_selection_to_column(caret_to_remove), get_selection_to_column(caret_to_save));
}
- }
- // Get the line and column of the new caret as if you would move the caret by pressing the arrow keys.
- int new_caret_line, new_caret_column, new_caret_from_column = 0, new_caret_to_column = 0;
- _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_column, p_below, new_caret_line, new_caret_column, caret_last_fit_x);
+ // Use the direction from the last caret or the saved one.
+ int caret_dir_to_copy;
+ if (has_selection(caret_to_remove) && has_selection(caret_to_save)) {
+ caret_dir_to_copy = caret_to_remove == get_caret_count() - 1 ? caret_to_remove : caret_to_save;
+ } else {
+ caret_dir_to_copy = !has_selection(caret_to_remove) ? caret_to_save : caret_to_remove;
+ }
- // If the caret does have a selection calculate the new from and to columns.
- if (caret_from_column != caret_to_column) {
- // We only need to calculate the selection columns if the column of the caret changed.
- if (caret_column != new_caret_column) {
- int _; // Unused placeholder for p_new_line.
- _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_from_column, p_below, _, new_caret_from_column);
- _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_to_column, p_below, _, new_caret_to_column);
+ if (is_caret_after_selection_origin(caret_dir_to_copy)) {
+ select(new_from_line, new_from_col, new_to_line, new_to_col, caret_to_save);
} else {
- new_caret_from_column = caret_from_column;
- new_caret_to_column = caret_to_column;
+ select(new_to_line, new_to_col, new_from_line, new_from_col, caret_to_save);
}
}
- // Add the new caret.
- const int new_caret_index = add_caret(new_caret_line, new_caret_column);
-
- if (new_caret_index == -1) {
- continue;
+ if (caret_to_save == 0) {
+ adjust_viewport_to_caret(caret_to_save);
}
- // Also add the selection if there should be one.
- if (new_caret_from_column != new_caret_to_column) {
- select(new_caret_line, new_caret_from_column, new_caret_line, new_caret_to_column, new_caret_index);
- // Necessary to properly modify the selection after adding the new caret.
- carets.write[new_caret_index].selection.selecting_line = new_caret_line;
- carets.write[new_caret_index].selection.selecting_column = new_caret_column == new_caret_from_column ? new_caret_to_column : new_caret_from_column;
- continue;
+ remove_caret(caret_to_remove);
+
+ // Update the rest of the sorted list.
+ for (int j = i; j < sorted_carets.size(); j++) {
+ if (sorted_carets[j] > caret_to_remove) {
+ // Shift the index since a caret before it was removed.
+ sorted_carets.write[j] -= 1;
+ }
}
+ // Remove the caret from the sorted array.
+ sorted_carets.remove_at(caret_to_remove == first_caret ? i : i + 1);
- // Copy the last fit x over.
- carets.write[new_caret_index].last_fit_x = carets[caret_index].last_fit_x;
+ // Process the caret again, since it and the next caret might also overlap.
+ i--;
}
+}
- merge_overlapping_carets();
- queue_redraw();
+// Starts a multicaret edit operation. Call this before iterating over the carets and call [end_multicaret_edit] afterwards.
+void TextEdit::begin_multicaret_edit() {
+ multicaret_edit_count++;
}
-Vector<int> TextEdit::get_caret_index_edit_order() {
- if (!caret_index_edit_dirty) {
- return caret_index_edit_order;
+void TextEdit::end_multicaret_edit() {
+ if (multicaret_edit_count > 0) {
+ multicaret_edit_count--;
+ }
+ if (multicaret_edit_count != 0) {
+ return;
}
- caret_index_edit_order.clear();
- caret_index_edit_order.push_back(0);
- for (int i = 1; i < carets.size(); i++) {
- int j = 0;
-
- int line = has_selection(i) ? get_selection_to_line(i) : carets[i].line;
- int col = has_selection(i) ? get_selection_to_column(i) : carets[i].column;
-
- for (; j < caret_index_edit_order.size(); j++) {
- int idx = caret_index_edit_order[j];
- int other_line = has_selection(idx) ? get_selection_to_line(idx) : carets[idx].line;
- int other_col = has_selection(idx) ? get_selection_to_column(idx) : carets[idx].column;
- if (line > other_line || (line == other_line && col > other_col)) {
- break;
- }
- }
- caret_index_edit_order.insert(j, i);
+ // This was the last multicaret edit operation.
+ if (multicaret_edit_merge_queued) {
+ merge_overlapping_carets();
}
- caret_index_edit_dirty = false;
- return caret_index_edit_order;
+ multicaret_edit_ignore_carets.clear();
}
-void TextEdit::adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col) {
- int edit_height = p_from_line - p_to_line;
- int edit_size = ((edit_height == 0) ? p_from_col : 0) - p_to_col;
-
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (int j = 0; j < caret_edit_order.size(); j++) {
- if (caret_edit_order[j] == p_caret) {
- return;
- }
-
- // Adjust caret.
- // set_caret_line could adjust the column, so save here.
- int cc = get_caret_column(caret_edit_order[j]);
- if (edit_height != 0) {
- set_caret_line(get_caret_line(caret_edit_order[j]) + edit_height, false, true, 0, caret_edit_order[j]);
- }
- if (get_caret_line(p_caret) == get_caret_line(caret_edit_order[j])) {
- set_caret_column(cc + edit_size, false, caret_edit_order[j]);
- }
+bool TextEdit::is_in_mulitcaret_edit() const {
+ return multicaret_edit_count > 0;
+}
- // Adjust selection.
- if (!has_selection(caret_edit_order[j])) {
- continue;
- }
- if (edit_height != 0) {
- carets.write[caret_edit_order[j]].selection.from_line += edit_height;
- carets.write[caret_edit_order[j]].selection.to_line += edit_height;
- }
- if (get_caret_line(p_caret) == get_selection_from_line(caret_edit_order[j])) {
- carets.write[caret_edit_order[j]].selection.from_column += edit_size;
- }
- }
+bool TextEdit::multicaret_edit_ignore_caret(int p_caret) const {
+ return multicaret_edit_ignore_carets.has(p_caret);
}
bool TextEdit::is_caret_visible(int p_caret) const {
@@ -4902,16 +4915,10 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_
}
setting_caret_line = true;
- if (p_line < 0) {
- p_line = 0;
- }
-
- if (p_line >= text.size()) {
- p_line = text.size() - 1;
- }
+ p_line = CLAMP(p_line, 0, text.size() - 1);
if (!p_can_be_hidden) {
- if (_is_line_hidden(CLAMP(p_line, 0, text.size() - 1))) {
+ if (_is_line_hidden(p_line)) {
int move_down = get_next_visible_line_offset_from(p_line, 1) - 1;
if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) {
p_line += move_down;
@@ -4920,7 +4927,7 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_
if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) {
p_line -= move_up;
} else {
- WARN_PRINT(("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines."));
+ WARN_PRINT("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines.");
}
}
}
@@ -4928,31 +4935,36 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_
bool caret_moved = get_caret_line(p_caret) != p_line;
carets.write[p_caret].line = p_line;
- int n_col = _get_char_pos_for_line(carets[p_caret].last_fit_x, p_line, p_wrap_index);
- if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) {
- Vector<String> rows = get_line_wrapped_text(p_line);
- int row_end_col = 0;
- for (int i = 0; i < p_wrap_index + 1; i++) {
- row_end_col += rows[i].length();
- }
- if (n_col >= row_end_col) {
- n_col -= 1;
+ int n_col;
+ if (p_wrap_index >= 0) {
+ // Keep caret in same visual x position it was at previously.
+ n_col = _get_char_pos_for_line(carets[p_caret].last_fit_x, p_line, p_wrap_index);
+ if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) {
+ // Offset by one to not go past the end of the wrapped line.
+ if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) {
+ n_col -= 1;
+ }
}
+ } else {
+ // Clamp the column.
+ n_col = MIN(get_caret_column(p_caret), get_line(p_line).length());
}
caret_moved = (caret_moved || get_caret_column(p_caret) != n_col);
carets.write[p_caret].column = n_col;
+ // Unselect if the caret moved to the selection origin.
+ if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) {
+ deselect(p_caret);
+ }
+
if (is_inside_tree() && p_adjust_viewport) {
adjust_viewport_to_caret(p_caret);
}
setting_caret_line = false;
- if (caret_moved && !caret_pos_dirty) {
- if (is_inside_tree()) {
- callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred();
- }
- caret_pos_dirty = true;
+ if (caret_moved) {
+ _caret_changed(p_caret);
}
}
@@ -4961,29 +4973,32 @@ int TextEdit::get_caret_line(int p_caret) const {
return carets[p_caret].line;
}
-void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport, int p_caret) {
+void TextEdit::set_caret_column(int p_column, bool p_adjust_viewport, int p_caret) {
ERR_FAIL_INDEX(p_caret, carets.size());
- if (p_col < 0) {
- p_col = 0;
- }
- if (p_col > get_line(get_caret_line(p_caret)).length()) {
- p_col = get_line(get_caret_line(p_caret)).length();
- }
- bool caret_moved = get_caret_column(p_caret) != p_col;
- carets.write[p_caret].column = p_col;
+ p_column = CLAMP(p_column, 0, get_line(get_caret_line(p_caret)).length());
+
+ bool caret_moved = get_caret_column(p_caret) != p_column;
+ carets.write[p_caret].column = p_column;
carets.write[p_caret].last_fit_x = _get_column_x_offset_for_line(get_caret_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret));
+ if (!has_selection(p_caret)) {
+ // Set the selection origin last fit x to be the same, so we can tell if there was a selection.
+ carets.write[p_caret].selection.origin_last_fit_x = carets[p_caret].last_fit_x;
+ }
+
+ // Unselect if the caret moved to the selection origin.
+ if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) {
+ deselect(p_caret);
+ }
+
if (is_inside_tree() && p_adjust_viewport) {
adjust_viewport_to_caret(p_caret);
}
- if (caret_moved && !caret_pos_dirty) {
- if (is_inside_tree()) {
- callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred();
- }
- caret_pos_dirty = true;
+ if (caret_moved) {
+ _caret_changed(p_caret);
}
}
@@ -4998,7 +5013,7 @@ int TextEdit::get_caret_wrap_index(int p_caret) const {
}
String TextEdit::get_word_under_caret(int p_caret) const {
- ERR_FAIL_COND_V(p_caret > carets.size(), "");
+ ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, "");
StringBuilder selected_text;
for (int c = 0; c < carets.size(); c++) {
@@ -5059,20 +5074,8 @@ bool TextEdit::is_drag_and_drop_selection_enabled() const {
return drag_and_drop_selection_enabled;
}
-void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column, int p_caret) {
- ERR_FAIL_INDEX(p_caret, carets.size());
-
+void TextEdit::set_selection_mode(SelectionMode p_mode) {
selecting_mode = p_mode;
- if (p_line >= 0) {
- ERR_FAIL_INDEX(p_line, text.size());
- carets.write[p_caret].selection.selecting_line = p_line;
- carets.write[p_caret].selection.selecting_column = CLAMP(carets[p_caret].selection.selecting_column, 0, text[carets[p_caret].selection.selecting_line].length());
- }
- if (p_column >= 0) {
- ERR_FAIL_INDEX(carets[p_caret].selection.selecting_line, text.size());
- ERR_FAIL_INDEX(p_column, text[carets[p_caret].selection.selecting_line].length() + 1);
- carets.write[p_caret].selection.selecting_column = p_column;
- }
}
TextEdit::SelectionMode TextEdit::get_selection_mode() const {
@@ -5090,16 +5093,12 @@ void TextEdit::select_all() {
}
remove_secondary_carets();
+ set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT);
select(0, 0, text.size() - 1, text[text.size() - 1].length());
- set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, 0, 0);
- carets.write[0].selection.shiftclick_left = true;
- set_caret_line(get_selection_to_line(), false);
- set_caret_column(get_selection_to_column(), false);
- queue_redraw();
}
void TextEdit::select_word_under_caret(int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
+ ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1);
_push_current_op();
if (!selecting_enabled) {
@@ -5140,8 +5139,6 @@ void TextEdit::select_word_under_caret(int p_caret) {
}
select(get_caret_line(c), begin, get_caret_line(c), end, c);
- // Move the caret to the end of the word for easier editing.
- set_caret_column(end, false, c);
}
merge_overlapping_carets();
}
@@ -5234,53 +5231,37 @@ void TextEdit::skip_selection_for_next_occurrence() {
}
}
-void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret) {
- ERR_FAIL_INDEX(p_caret, carets.size());
+void TextEdit::select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret) {
+ ERR_FAIL_INDEX(p_caret, get_caret_count());
+
+ p_caret_line = CLAMP(p_caret_line, 0, text.size() - 1);
+ p_caret_column = CLAMP(p_caret_column, 0, text[p_caret_line].length());
+ set_caret_line(p_caret_line, false, true, -1, p_caret);
+ set_caret_column(p_caret_column, false, p_caret);
+
if (!selecting_enabled) {
return;
}
- p_from_line = CLAMP(p_from_line, 0, text.size() - 1);
- p_from_column = CLAMP(p_from_column, 0, text[p_from_line].length());
- p_to_line = CLAMP(p_to_line, 0, text.size() - 1);
- p_to_column = CLAMP(p_to_column, 0, text[p_to_line].length());
-
- carets.write[p_caret].selection.from_line = p_from_line;
- carets.write[p_caret].selection.from_column = p_from_column;
- carets.write[p_caret].selection.to_line = p_to_line;
- carets.write[p_caret].selection.to_column = p_to_column;
+ p_origin_line = CLAMP(p_origin_line, 0, text.size() - 1);
+ p_origin_column = CLAMP(p_origin_column, 0, text[p_origin_line].length());
+ set_selection_origin_line(p_origin_line, true, -1, p_caret);
+ set_selection_origin_column(p_origin_column, p_caret);
- carets.write[p_caret].selection.active = true;
-
- if (get_selection_from_line(p_caret) == get_selection_to_line(p_caret)) {
- if (get_selection_from_column(p_caret) == get_selection_to_column(p_caret)) {
- carets.write[p_caret].selection.active = false;
-
- } else if (get_selection_from_column(p_caret) > get_selection_to_column(p_caret)) {
- carets.write[p_caret].selection.shiftclick_left = false;
- SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column);
- } else {
- carets.write[p_caret].selection.shiftclick_left = true;
- }
- } else if (get_selection_from_line(p_caret) > get_selection_to_line(p_caret)) {
- carets.write[p_caret].selection.shiftclick_left = false;
- SWAP(carets.write[p_caret].selection.from_line, carets.write[p_caret].selection.to_line);
- SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column);
- } else {
- carets.write[p_caret].selection.shiftclick_left = true;
+ bool had_selection = has_selection(p_caret);
+ bool activate = p_origin_line != p_caret_line || p_origin_column != p_caret_column;
+ carets.write[p_caret].selection.active = activate;
+ if (had_selection != activate) {
+ _selection_changed(p_caret);
}
-
- caret_index_edit_dirty = true;
- queue_redraw();
}
bool TextEdit::has_selection(int p_caret) const {
- ERR_FAIL_COND_V(p_caret > carets.size(), false);
+ ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, false);
+ if (p_caret >= 0) {
+ return carets[p_caret].selection.active;
+ }
for (int i = 0; i < carets.size(); i++) {
- if (p_caret != -1 && p_caret != i) {
- continue;
- }
-
if (carets[i].selection.active) {
return true;
}
@@ -5289,100 +5270,268 @@ bool TextEdit::has_selection(int p_caret) const {
}
String TextEdit::get_selected_text(int p_caret) {
- ERR_FAIL_COND_V(p_caret > carets.size(), "");
+ ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, "");
- StringBuilder selected_text;
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (int i = caret_edit_order.size() - 1; i >= 0; i--) {
- int caret_idx = caret_edit_order[i];
- if (p_caret != -1 && p_caret != caret_idx) {
- continue;
+ if (p_caret >= 0) {
+ if (!has_selection(p_caret)) {
+ return "";
}
+ return _base_get_text(get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret));
+ }
+
+ StringBuilder selected_text;
+ Vector<int> sorted_carets = get_sorted_carets();
+ for (int i = 0; i < sorted_carets.size(); i++) {
+ int caret_index = sorted_carets[i];
- if (!has_selection(caret_idx)) {
+ if (!has_selection(caret_index)) {
continue;
}
- selected_text += _base_get_text(get_selection_from_line(caret_idx), get_selection_from_column(caret_idx), get_selection_to_line(caret_idx), get_selection_to_column(caret_idx));
- if (p_caret == -1 && i != 0) {
+ if (selected_text.get_string_length() != 0) {
selected_text += "\n";
}
+ selected_text += _base_get_text(get_selection_from_line(caret_index), get_selection_from_column(caret_index), get_selection_to_line(caret_index), get_selection_to_column(caret_index));
}
return selected_text.as_string();
}
-int TextEdit::get_selection_line(int p_caret) const {
+int TextEdit::get_selection_at_line_column(int p_line, int p_column, bool p_include_edges, bool p_only_selections) const {
+ // Return the caret index of the found selection, or -1.
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (_selection_contains(i, p_line, p_column, p_include_edges, p_only_selections)) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+Vector<Point2i> TextEdit::get_line_ranges_from_carets(bool p_only_selections, bool p_merge_adjacent) const {
+ // Get a series of line ranges that cover all lines that have a caret or selection.
+ // For each Point2i range, x is the first line and y is the last line.
+ Vector<Point2i> ret;
+ int last_to_line = INT_MIN;
+
+ Vector<int> sorted_carets = get_sorted_carets();
+ for (int i = 0; i < sorted_carets.size(); i++) {
+ int caret_index = sorted_carets[i];
+ if (p_only_selections && !has_selection(caret_index)) {
+ continue;
+ }
+ Point2i range = Point2i(get_selection_from_line(caret_index), get_selection_to_line(caret_index));
+ if (has_selection(caret_index) && get_selection_to_column(caret_index) == 0) {
+ // Dont include selection end line if it ends at column 0.
+ range.y--;
+ }
+ if (range.x == last_to_line || (p_merge_adjacent && range.x - 1 == last_to_line)) {
+ // Merge if starts on the same line or adjacent line.
+ ret.write[ret.size() - 1].y = range.y;
+ } else {
+ ret.append(range);
+ }
+ last_to_line = range.y;
+ }
+ return ret;
+}
+
+TypedArray<Vector2i> TextEdit::get_line_ranges_from_carets_typed_array(bool p_only_selections, bool p_merge_adjacent) const {
+ // Wrapper for `get_line_ranges_from_carets` to return a datatype that can be exposed.
+ TypedArray<Vector2i> ret;
+ Vector<Point2i> ranges = get_line_ranges_from_carets(p_only_selections, p_merge_adjacent);
+ for (const Point2i &range : ranges) {
+ ret.push_back(range);
+ }
+ return ret;
+}
+
+void TextEdit::set_selection_origin_line(int p_line, bool p_can_be_hidden, int p_wrap_index, int p_caret) {
+ if (!selecting_enabled) {
+ return;
+ }
+ ERR_FAIL_INDEX(p_caret, carets.size());
+ p_line = CLAMP(p_line, 0, text.size() - 1);
+
+ if (!p_can_be_hidden) {
+ if (_is_line_hidden(p_line)) {
+ int move_down = get_next_visible_line_offset_from(p_line, 1) - 1;
+ if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) {
+ p_line += move_down;
+ } else {
+ int move_up = get_next_visible_line_offset_from(p_line, -1) - 1;
+ if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) {
+ p_line -= move_up;
+ } else {
+ WARN_PRINT("Selection origin set to hidden line " + itos(p_line) + " and there are no nonhidden lines.");
+ }
+ }
+ }
+ }
+
+ bool selection_moved = get_selection_origin_line(p_caret) != p_line;
+ carets.write[p_caret].selection.origin_line = p_line;
+
+ int n_col;
+ if (p_wrap_index >= 0) {
+ // Keep selection origin in same visual x position it was at previously.
+ n_col = _get_char_pos_for_line(carets[p_caret].selection.origin_last_fit_x, p_line, p_wrap_index);
+ if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) {
+ // Offset by one to not go past the end of the wrapped line.
+ if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) {
+ n_col -= 1;
+ }
+ }
+ } else {
+ // Clamp the column.
+ n_col = MIN(get_selection_origin_column(p_caret), get_line(p_line).length());
+ }
+ selection_moved = (selection_moved || get_selection_origin_column(p_caret) != n_col);
+ carets.write[p_caret].selection.origin_column = n_col;
+
+ // Unselect if the selection origin moved to the caret.
+ if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) {
+ deselect(p_caret);
+ }
+
+ if (selection_moved && has_selection(p_caret)) {
+ _selection_changed(p_caret);
+ }
+}
+
+void TextEdit::set_selection_origin_column(int p_column, int p_caret) {
+ if (!selecting_enabled) {
+ return;
+ }
+ ERR_FAIL_INDEX(p_caret, carets.size());
+
+ p_column = CLAMP(p_column, 0, get_line(get_selection_origin_line(p_caret)).length());
+
+ bool selection_moved = get_selection_origin_column(p_caret) != p_column;
+
+ carets.write[p_caret].selection.origin_column = p_column;
+
+ carets.write[p_caret].selection.origin_last_fit_x = _get_column_x_offset_for_line(get_selection_origin_column(p_caret), get_selection_origin_line(p_caret), get_selection_origin_column(p_caret));
+
+ // Unselect if the selection origin moved to the caret.
+ if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) {
+ deselect(p_caret);
+ }
+
+ if (selection_moved && has_selection(p_caret)) {
+ _selection_changed(p_caret);
+ }
+}
+
+int TextEdit::get_selection_origin_line(int p_caret) const {
ERR_FAIL_INDEX_V(p_caret, carets.size(), -1);
- ERR_FAIL_COND_V(!has_selection(p_caret), -1);
- return carets[p_caret].selection.selecting_line;
+ return carets[p_caret].selection.origin_line;
}
-int TextEdit::get_selection_column(int p_caret) const {
+int TextEdit::get_selection_origin_column(int p_caret) const {
ERR_FAIL_INDEX_V(p_caret, carets.size(), -1);
- ERR_FAIL_COND_V(!has_selection(p_caret), -1);
- return carets[p_caret].selection.selecting_column;
+ return carets[p_caret].selection.origin_column;
}
int TextEdit::get_selection_from_line(int p_caret) const {
ERR_FAIL_INDEX_V(p_caret, carets.size(), -1);
- ERR_FAIL_COND_V(!has_selection(p_caret), -1);
- return carets[p_caret].selection.from_line;
+ if (!has_selection(p_caret)) {
+ return carets[p_caret].line;
+ }
+ return MIN(carets[p_caret].selection.origin_line, carets[p_caret].line);
}
int TextEdit::get_selection_from_column(int p_caret) const {
ERR_FAIL_INDEX_V(p_caret, carets.size(), -1);
- ERR_FAIL_COND_V(!has_selection(p_caret), -1);
- return carets[p_caret].selection.from_column;
+ if (!has_selection(p_caret)) {
+ return carets[p_caret].column;
+ }
+ if (carets[p_caret].selection.origin_line < carets[p_caret].line) {
+ return carets[p_caret].selection.origin_column;
+ } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) {
+ return carets[p_caret].column;
+ } else {
+ return MIN(carets[p_caret].selection.origin_column, carets[p_caret].column);
+ }
}
int TextEdit::get_selection_to_line(int p_caret) const {
ERR_FAIL_INDEX_V(p_caret, carets.size(), -1);
- ERR_FAIL_COND_V(!has_selection(p_caret), -1);
- return carets[p_caret].selection.to_line;
+ if (!has_selection(p_caret)) {
+ return carets[p_caret].line;
+ }
+ return MAX(carets[p_caret].selection.origin_line, carets[p_caret].line);
}
int TextEdit::get_selection_to_column(int p_caret) const {
ERR_FAIL_INDEX_V(p_caret, carets.size(), -1);
- ERR_FAIL_COND_V(!has_selection(p_caret), -1);
- return carets[p_caret].selection.to_column;
+ if (!has_selection(p_caret)) {
+ return carets[p_caret].column;
+ }
+ if (carets[p_caret].selection.origin_line < carets[p_caret].line) {
+ return carets[p_caret].column;
+ } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) {
+ return carets[p_caret].selection.origin_column;
+ } else {
+ return MAX(carets[p_caret].selection.origin_column, carets[p_caret].column);
+ }
+}
+
+bool TextEdit::is_caret_after_selection_origin(int p_caret) const {
+ ERR_FAIL_INDEX_V(p_caret, carets.size(), false);
+ if (!has_selection(p_caret)) {
+ return true;
+ }
+ return carets[p_caret].line > carets[p_caret].selection.origin_line || (carets[p_caret].line == carets[p_caret].selection.origin_line && carets[p_caret].column >= carets[p_caret].selection.origin_column);
}
void TextEdit::deselect(int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
- for (int i = 0; i < carets.size(); i++) {
- if (p_caret != -1 && p_caret != i) {
- continue;
+ ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1);
+ bool selection_changed = false;
+ if (p_caret >= 0) {
+ selection_changed = carets.write[p_caret].selection.active;
+ carets.write[p_caret].selection.active = false;
+ } else {
+ for (int i = 0; i < carets.size(); i++) {
+ selection_changed |= carets.write[i].selection.active;
+ carets.write[i].selection.active = false;
}
- carets.write[i].selection.active = false;
}
- caret_index_edit_dirty = true;
- queue_redraw();
+ if (selection_changed) {
+ _selection_changed(p_caret);
+ }
}
void TextEdit::delete_selection(int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
+ ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1);
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
+ begin_multicaret_edit();
+ for (int i = 0; i < get_caret_count(); i++) {
if (p_caret != -1 && p_caret != i) {
continue;
}
+ if (p_caret == -1 && multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
if (!has_selection(i)) {
continue;
}
- selecting_mode = SelectionMode::SELECTION_MODE_NONE;
- _remove_text(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i), get_selection_to_column(i));
- set_caret_line(get_selection_from_line(i), false, false, 0, i);
- set_caret_column(get_selection_from_column(i), i == 0, i);
- carets.write[i].selection.active = false;
+ int selection_from_line = get_selection_from_line(i);
+ int selection_from_column = get_selection_from_column(i);
+ int selection_to_line = get_selection_to_line(i);
+ int selection_to_column = get_selection_to_column(i);
+
+ _remove_text(selection_from_line, selection_from_column, selection_to_line, selection_to_column);
+ _offset_carets_after(selection_to_line, selection_to_column, selection_from_line, selection_from_column);
+ merge_overlapping_carets();
- adjust_carets_after_edit(i, carets[i].selection.from_line, carets[i].selection.from_column, carets[i].selection.to_line, carets[i].selection.to_column);
+ deselect(i);
+ set_caret_line(selection_from_line, false, false, -1, i);
+ set_caret_column(selection_from_column, i == 0, i);
}
+ end_multicaret_edit();
end_complex_operation();
- queue_redraw();
}
/* Line wrapping. */
@@ -6224,8 +6373,10 @@ void TextEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("swap_lines", "from_line", "to_line"), &TextEdit::swap_lines);
ClassDB::bind_method(D_METHOD("insert_line_at", "line", "text"), &TextEdit::insert_line_at);
+ ClassDB::bind_method(D_METHOD("remove_line_at", "line", "move_carets_down"), &TextEdit::remove_line_at, DEFVAL(true));
ClassDB::bind_method(D_METHOD("insert_text_at_caret", "text", "caret_index"), &TextEdit::insert_text_at_caret, DEFVAL(-1));
+ ClassDB::bind_method(D_METHOD("insert_text", "text", "line", "column", "before_selection_begin", "before_selection_end"), &TextEdit::insert_text, DEFVAL(true), DEFVAL(false));
ClassDB::bind_method(D_METHOD("remove_text", "from_line", "from_column", "to_line", "to_column"), &TextEdit::remove_text);
ClassDB::bind_method(D_METHOD("get_last_unhidden_line"), &TextEdit::get_last_unhidden_line);
@@ -6311,7 +6462,7 @@ void TextEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_search_text", "search_text"), &TextEdit::set_search_text);
ClassDB::bind_method(D_METHOD("set_search_flags", "flags"), &TextEdit::set_search_flags);
- ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_colum"), &TextEdit::search);
+ ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_column"), &TextEdit::search);
/* Tooltip */
ClassDB::bind_method(D_METHOD("set_tooltip_request_func", "callback"), &TextEdit::set_tooltip_request_func);
@@ -6355,15 +6506,20 @@ void TextEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_multiple_carets_enabled", "enabled"), &TextEdit::set_multiple_carets_enabled);
ClassDB::bind_method(D_METHOD("is_multiple_carets_enabled"), &TextEdit::is_multiple_carets_enabled);
- ClassDB::bind_method(D_METHOD("add_caret", "line", "col"), &TextEdit::add_caret);
+ ClassDB::bind_method(D_METHOD("add_caret", "line", "column"), &TextEdit::add_caret);
ClassDB::bind_method(D_METHOD("remove_caret", "caret"), &TextEdit::remove_caret);
ClassDB::bind_method(D_METHOD("remove_secondary_carets"), &TextEdit::remove_secondary_carets);
- ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets);
ClassDB::bind_method(D_METHOD("get_caret_count"), &TextEdit::get_caret_count);
ClassDB::bind_method(D_METHOD("add_caret_at_carets", "below"), &TextEdit::add_caret_at_carets);
- ClassDB::bind_method(D_METHOD("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order);
- ClassDB::bind_method(D_METHOD("adjust_carets_after_edit", "caret", "from_line", "from_col", "to_line", "to_col"), &TextEdit::adjust_carets_after_edit);
+ ClassDB::bind_method(D_METHOD("get_sorted_carets", "include_ignored_carets"), &TextEdit::get_sorted_carets, DEFVAL(false));
+ ClassDB::bind_method(D_METHOD("collapse_carets", "from_line", "from_column", "to_line", "to_column", "inclusive"), &TextEdit::collapse_carets, DEFVAL(false));
+
+ ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets);
+ ClassDB::bind_method(D_METHOD("begin_multicaret_edit"), &TextEdit::begin_multicaret_edit);
+ ClassDB::bind_method(D_METHOD("end_multicaret_edit"), &TextEdit::end_multicaret_edit);
+ ClassDB::bind_method(D_METHOD("is_in_mulitcaret_edit"), &TextEdit::is_in_mulitcaret_edit);
+ ClassDB::bind_method(D_METHOD("multicaret_edit_ignore_caret", "caret_index"), &TextEdit::multicaret_edit_ignore_caret);
ClassDB::bind_method(D_METHOD("is_caret_visible", "caret_index"), &TextEdit::is_caret_visible, DEFVAL(0));
ClassDB::bind_method(D_METHOD("get_caret_draw_pos", "caret_index"), &TextEdit::get_caret_draw_pos, DEFVAL(0));
@@ -6394,27 +6550,33 @@ void TextEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_drag_and_drop_selection_enabled", "enable"), &TextEdit::set_drag_and_drop_selection_enabled);
ClassDB::bind_method(D_METHOD("is_drag_and_drop_selection_enabled"), &TextEdit::is_drag_and_drop_selection_enabled);
- ClassDB::bind_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::set_selection_mode, DEFVAL(-1), DEFVAL(-1), DEFVAL(0));
+ ClassDB::bind_method(D_METHOD("set_selection_mode", "mode"), &TextEdit::set_selection_mode);
ClassDB::bind_method(D_METHOD("get_selection_mode"), &TextEdit::get_selection_mode);
ClassDB::bind_method(D_METHOD("select_all"), &TextEdit::select_all);
ClassDB::bind_method(D_METHOD("select_word_under_caret", "caret_index"), &TextEdit::select_word_under_caret, DEFVAL(-1));
ClassDB::bind_method(D_METHOD("add_selection_for_next_occurrence"), &TextEdit::add_selection_for_next_occurrence);
ClassDB::bind_method(D_METHOD("skip_selection_for_next_occurrence"), &TextEdit::skip_selection_for_next_occurrence);
- ClassDB::bind_method(D_METHOD("select", "from_line", "from_column", "to_line", "to_column", "caret_index"), &TextEdit::select, DEFVAL(0));
+ ClassDB::bind_method(D_METHOD("select", "origin_line", "origin_column", "caret_line", "caret_column", "caret_index"), &TextEdit::select, DEFVAL(0));
ClassDB::bind_method(D_METHOD("has_selection", "caret_index"), &TextEdit::has_selection, DEFVAL(-1));
ClassDB::bind_method(D_METHOD("get_selected_text", "caret_index"), &TextEdit::get_selected_text, DEFVAL(-1));
+ ClassDB::bind_method(D_METHOD("get_selection_at_line_column", "line", "column", "include_edges", "only_selections"), &TextEdit::get_selection_at_line_column, DEFVAL(true), DEFVAL(true));
+ ClassDB::bind_method(D_METHOD("get_line_ranges_from_carets", "only_selections", "merge_adjacent"), &TextEdit::get_line_ranges_from_carets_typed_array, DEFVAL(false), DEFVAL(true));
- ClassDB::bind_method(D_METHOD("get_selection_line", "caret_index"), &TextEdit::get_selection_line, DEFVAL(0));
- ClassDB::bind_method(D_METHOD("get_selection_column", "caret_index"), &TextEdit::get_selection_column, DEFVAL(0));
+ ClassDB::bind_method(D_METHOD("get_selection_origin_line", "caret_index"), &TextEdit::get_selection_origin_line, DEFVAL(0));
+ ClassDB::bind_method(D_METHOD("get_selection_origin_column", "caret_index"), &TextEdit::get_selection_origin_column, DEFVAL(0));
+ ClassDB::bind_method(D_METHOD("set_selection_origin_line", "line", "can_be_hidden", "wrap_index", "caret_index"), &TextEdit::set_selection_origin_line, DEFVAL(true), DEFVAL(-1), DEFVAL(0));
+ ClassDB::bind_method(D_METHOD("set_selection_origin_column", "column", "caret_index"), &TextEdit::set_selection_origin_column, DEFVAL(0));
ClassDB::bind_method(D_METHOD("get_selection_from_line", "caret_index"), &TextEdit::get_selection_from_line, DEFVAL(0));
ClassDB::bind_method(D_METHOD("get_selection_from_column", "caret_index"), &TextEdit::get_selection_from_column, DEFVAL(0));
ClassDB::bind_method(D_METHOD("get_selection_to_line", "caret_index"), &TextEdit::get_selection_to_line, DEFVAL(0));
ClassDB::bind_method(D_METHOD("get_selection_to_column", "caret_index"), &TextEdit::get_selection_to_column, DEFVAL(0));
+ ClassDB::bind_method(D_METHOD("is_caret_after_selection_origin", "caret_index"), &TextEdit::is_caret_after_selection_origin, DEFVAL(0));
+
ClassDB::bind_method(D_METHOD("deselect", "caret_index"), &TextEdit::deselect, DEFVAL(-1));
ClassDB::bind_method(D_METHOD("delete_selection", "caret_index"), &TextEdit::delete_selection, DEFVAL(-1));
@@ -6550,6 +6712,14 @@ void TextEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("is_menu_visible"), &TextEdit::is_menu_visible);
ClassDB::bind_method(D_METHOD("menu_option", "option"), &TextEdit::menu_option);
+ /* Deprecated */
+#ifndef DISABLE_DEPRECATED
+ ClassDB::bind_method(D_METHOD("adjust_carets_after_edit", "caret", "from_line", "from_col", "to_line", "to_col"), &TextEdit::adjust_carets_after_edit);
+ ClassDB::bind_method(D_METHOD("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order);
+ ClassDB::bind_method(D_METHOD("get_selection_line", "caret_index"), &TextEdit::get_selection_line, DEFVAL(0));
+ ClassDB::bind_method(D_METHOD("get_selection_column", "caret_index"), &TextEdit::get_selection_column, DEFVAL(0));
+#endif
+
/* Inspector */
ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text", PROPERTY_HINT_MULTILINE_TEXT), "set_placeholder", "get_placeholder");
@@ -6617,7 +6787,7 @@ void TextEdit::_bind_methods() {
ADD_SIGNAL(MethodInfo("gutter_added"));
ADD_SIGNAL(MethodInfo("gutter_removed"));
- /* Theme items */
+ // Theme items
/* Search */
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_color);
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_border_color);
@@ -6690,6 +6860,10 @@ void TextEdit::_unhide_all_lines() {
queue_redraw();
}
+void TextEdit::_unhide_carets() {
+ // Override for functionality.
+}
+
void TextEdit::_set_line_as_hidden(int p_line, bool p_hidden) {
ERR_FAIL_INDEX(p_line, text.size());
@@ -6717,14 +6891,17 @@ void TextEdit::_set_symbol_lookup_word(const String &p_symbol) {
// Overridable actions
void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
+ ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1);
if (!editable) {
return;
}
start_action(EditAction::ACTION_TYPING);
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
+ begin_multicaret_edit();
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (p_caret == -1 && multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
if (p_caret != -1 && p_caret != i) {
continue;
}
@@ -6742,11 +6919,12 @@ void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca
const char32_t chr[2] = { (char32_t)p_unicode, 0 };
insert_text_at_caret(chr, i);
}
+ end_multicaret_edit();
end_action();
}
void TextEdit::_backspace_internal(int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
+ ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1);
if (!editable) {
return;
}
@@ -6757,194 +6935,163 @@ void TextEdit::_backspace_internal(int p_caret) {
}
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (const int &i : caret_edit_order) {
+ begin_multicaret_edit();
+ for (int i = 0; i < get_caret_count(); i++) {
+ if (p_caret == -1 && multicaret_edit_ignore_caret(i)) {
+ continue;
+ }
if (p_caret != -1 && p_caret != i) {
continue;
}
- int cc = get_caret_column(i);
- int cl = get_caret_line(i);
+ int to_line = get_caret_line(i);
+ int to_column = get_caret_column(i);
- if (cc == 0 && cl == 0) {
+ if (to_column == 0 && to_line == 0) {
continue;
}
- int prev_line = cc ? cl : cl - 1;
- int prev_column = cc ? (cc - 1) : (text[cl - 1].length());
+ int from_line = to_column > 0 ? to_line : to_line - 1;
+ int from_column = to_column > 0 ? (to_column - 1) : (text[to_line - 1].length());
- merge_gutters(prev_line, cl);
+ merge_gutters(from_line, to_line);
- if (_is_line_hidden(cl)) {
- _set_line_as_hidden(prev_line, true);
- }
- _remove_text(prev_line, prev_column, cl, cc);
-
- set_caret_line(prev_line, false, true, 0, i);
- set_caret_column(prev_column, i == 0, i);
+ _remove_text(from_line, from_column, to_line, to_column);
+ collapse_carets(from_line, from_column, to_line, to_column);
+ _offset_carets_after(to_line, to_column, from_line, from_column);
- adjust_carets_after_edit(i, prev_line, prev_column, cl, cc);
+ set_caret_line(from_line, false, true, -1, i);
+ set_caret_column(from_column, i == 0, i);
}
- merge_overlapping_carets();
+ end_multicaret_edit();
end_complex_operation();
}
void TextEdit::_cut_internal(int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
+ ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1);
+
+ _copy_internal(p_caret);
+
if (!editable) {
return;
}
if (has_selection(p_caret)) {
- DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret));
delete_selection(p_caret);
- cut_copy_line = "";
return;
}
+ // Remove full lines.
begin_complex_operation();
- Vector<int> carets_to_remove;
-
- StringBuilder clipboard;
- // This is the exception and has to edit in reverse order else the string copied to the clipboard will be backwards.
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (int i = caret_edit_order.size() - 1; i >= 0; i--) {
- int caret_idx = caret_edit_order[i];
- if (p_caret != -1 && p_caret != caret_idx) {
- continue;
- }
-
- int cl = get_caret_line(caret_idx);
- int cc = get_caret_column(caret_idx);
- int indent_level = get_indent_level(cl);
- double hscroll = get_h_scroll();
-
- // Check for overlapping carets.
- // We don't need to worry about selections as that is caught before this entire section.
- for (int j = i - 1; j >= 0; j--) {
- if (get_caret_line(caret_edit_order[j]) == cl) {
- carets_to_remove.push_back(caret_edit_order[j]);
- i = j;
- }
- }
-
- clipboard += text[cl];
- if (p_caret == -1 && caret_idx != 0) {
- clipboard += "\n";
- }
-
- if (cl == 0 && get_line_count() > 1) {
- _remove_text(cl, 0, cl + 1, 0);
- adjust_carets_after_edit(caret_idx, cl, 0, cl + 1, text[cl].length());
- } else {
- _remove_text(cl, 0, cl, text[cl].length());
- set_caret_column(0, false, caret_idx);
- backspace(caret_idx);
- set_caret_line(get_caret_line(caret_idx) + 1, caret_idx == 0, 0, 0, caret_idx);
- }
-
- // Correct the visually perceived caret column taking care of indentation level of the lines.
- int diff_indent = indent_level - get_indent_level(get_caret_line(caret_idx));
- cc += diff_indent;
- if (diff_indent != 0) {
- cc += diff_indent > 0 ? -1 : 1;
- }
-
- // Restore horizontal scroll and caret column modified by the backspace() call.
- set_h_scroll(hscroll);
- set_caret_column(cc, caret_idx == 0, caret_idx);
+ begin_multicaret_edit();
+ Vector<Point2i> line_ranges;
+ if (p_caret == -1) {
+ line_ranges = get_line_ranges_from_carets();
+ } else {
+ line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret)));
}
-
- // Sort and remove backwards to preserve indexes.
- carets_to_remove.sort();
- for (int i = carets_to_remove.size() - 1; i >= 0; i--) {
- remove_caret(carets_to_remove[i]);
+ int line_offset = 0;
+ for (Point2i line_range : line_ranges) {
+ // Preserve carets on the last line.
+ remove_line_at(line_range.y + line_offset);
+ if (line_range.x != line_range.y) {
+ remove_text(line_range.x + line_offset, 0, line_range.y + line_offset, 0);
+ }
+ line_offset += line_range.x - line_range.y - 1;
}
+ end_multicaret_edit();
end_complex_operation();
-
- String clipboard_string = clipboard.as_string();
- DisplayServer::get_singleton()->clipboard_set(clipboard_string);
- cut_copy_line = clipboard_string;
}
void TextEdit::_copy_internal(int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
+ ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1);
if (has_selection(p_caret)) {
DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret));
cut_copy_line = "";
return;
}
+ // Copy full lines.
StringBuilder clipboard;
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- for (int i = caret_edit_order.size() - 1; i >= 0; i--) {
- int caret_idx = caret_edit_order[i];
- if (p_caret != -1 && p_caret != caret_idx) {
- continue;
- }
-
- int cl = get_caret_line(caret_idx);
- if (text[cl].length() != 0) {
- clipboard += _base_get_text(cl, 0, cl, text[cl].length());
- if (p_caret == -1 && i != 0) {
- clipboard += "\n";
+ Vector<Point2i> line_ranges;
+ if (p_caret == -1) {
+ // When there are multiple carets on a line, only copy it once.
+ line_ranges = get_line_ranges_from_carets(false, true);
+ } else {
+ line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret)));
+ }
+ for (Point2i line_range : line_ranges) {
+ for (int i = line_range.x; i <= line_range.y; i++) {
+ if (text[i].length() != 0) {
+ clipboard += _base_get_text(i, 0, i, text[i].length());
}
+ clipboard += "\n";
}
}
String clipboard_string = clipboard.as_string();
DisplayServer::get_singleton()->clipboard_set(clipboard_string);
- cut_copy_line = clipboard_string;
+ // Set the cut copy line so we know to paste as a line.
+ if (get_caret_count() == 1) {
+ cut_copy_line = clipboard_string;
+ } else {
+ cut_copy_line = "";
+ }
}
void TextEdit::_paste_internal(int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
+ ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1);
if (!editable) {
return;
}
String clipboard = DisplayServer::get_singleton()->clipboard_get();
+
+ // Paste a full line. Ignore '\r' characters that may have been added to the clipboard by the OS.
+ if (get_caret_count() == 1 && !has_selection(0) && !cut_copy_line.is_empty() && cut_copy_line == clipboard.replace("\r", "")) {
+ insert_text(clipboard, get_caret_line(), 0);
+ return;
+ }
+
+ // Paste text at each caret or one line per caret.
Vector<String> clipboad_lines = clipboard.split("\n");
- bool insert_line_per_caret = p_caret == -1 && carets.size() > 1 && clipboad_lines.size() == carets.size();
+ bool insert_line_per_caret = p_caret == -1 && get_caret_count() > 1 && clipboad_lines.size() == get_caret_count();
begin_complex_operation();
- Vector<int> caret_edit_order = get_caret_index_edit_order();
- int clipboad_line = clipboad_lines.size() - 1;
- for (const int &i : caret_edit_order) {
- if (p_caret != -1 && p_caret != i) {
+ begin_multicaret_edit();
+ Vector<int> sorted_carets = get_sorted_carets();
+ for (int i = 0; i < get_caret_count(); i++) {
+ int caret_index = sorted_carets[i];
+ if (p_caret != -1 && p_caret != caret_index) {
continue;
}
- if (has_selection(i)) {
- delete_selection(i);
- } else if (!cut_copy_line.is_empty() && cut_copy_line == clipboard) {
- set_caret_column(0, i == 0, i);
- String ins = "\n";
- clipboard += ins;
+ if (has_selection(caret_index)) {
+ delete_selection(caret_index);
}
if (insert_line_per_caret) {
- clipboard = clipboad_lines[clipboad_line];
+ clipboard = clipboad_lines[i];
}
- insert_text_at_caret(clipboard, i);
- clipboad_line--;
+ insert_text_at_caret(clipboard, caret_index);
}
+ end_multicaret_edit();
end_complex_operation();
}
void TextEdit::_paste_primary_clipboard_internal(int p_caret) {
- ERR_FAIL_COND(p_caret > carets.size());
+ ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1);
if (!is_editable() || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) {
return;
}
String paste_buffer = DisplayServer::get_singleton()->clipboard_get_primary();
- if (carets.size() == 1) {
+ if (get_caret_count() == 1) {
Point2i pos = get_line_column_at_pos(get_local_mouse_pos());
deselect();
- set_caret_line(pos.y, true, false);
+ set_caret_line(pos.y, true, false, -1);
set_caret_column(pos.x);
}
@@ -7203,10 +7350,26 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con
}
/* Caret */
+void TextEdit::_caret_changed(int p_caret) {
+ queue_redraw();
+
+ if (has_selection(p_caret)) {
+ _selection_changed(p_caret);
+ }
+
+ if (caret_pos_dirty) {
+ return;
+ }
+
+ if (is_inside_tree()) {
+ callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred();
+ }
+ caret_pos_dirty = true;
+}
+
void TextEdit::_emit_caret_changed() {
emit_signal(SNAME("caret_changed"));
caret_pos_dirty = false;
- caret_index_edit_dirty = true;
}
void TextEdit::_reset_caret_blink_timer() {
@@ -7251,60 +7414,152 @@ int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line, int p_column
}
}
-/* Selection */
-void TextEdit::_click_selection_held() {
- // Warning: is_mouse_button_pressed(MouseButton::LEFT) returns false for double+ clicks, so this doesn't work for MODE_WORD
- // and MODE_LINE. However, moving the mouse triggers _gui_input, which calls these functions too, so that's not a huge problem.
- // I'm unsure if there's an actual fix that doesn't have a ton of side effects.
- if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) && get_selection_mode() != SelectionMode::SELECTION_MODE_NONE) {
- switch (get_selection_mode()) {
- case SelectionMode::SELECTION_MODE_POINTER: {
- _update_selection_mode_pointer();
- } break;
- case SelectionMode::SELECTION_MODE_WORD: {
- _update_selection_mode_word();
- } break;
- case SelectionMode::SELECTION_MODE_LINE: {
- _update_selection_mode_line();
- } break;
- default: {
- break;
+bool TextEdit::_is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges) const {
+ if (p_line >= p_from_line && p_line <= p_to_line && (p_line > p_from_line || p_column > p_from_column) && (p_line < p_to_line || p_column < p_to_column)) {
+ return true;
+ }
+ if (p_include_edges) {
+ if ((p_line == p_from_line && p_column == p_from_column) || (p_line == p_to_line && p_column == p_to_column)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void TextEdit::_offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin, bool p_include_selection_end) {
+ // Moves all carets at or after old_line and old_column.
+ // Called after deleting or inserting text so that the carets stay with the text they are at.
+
+ int edit_height = p_new_line - p_old_line;
+ int edit_size = p_new_column - p_old_column;
+ if (edit_height == 0 && edit_size == 0) {
+ return;
+ }
+
+ // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together.
+ for (int i = 0; i < get_caret_count(); i++) {
+ bool selected = has_selection(i);
+ bool caret_at_end = selected && is_caret_after_selection_origin(i);
+ bool include_caret_at = caret_at_end ? p_include_selection_end : p_include_selection_begin;
+
+ // Move caret.
+ int caret_line = get_caret_line(i);
+ int caret_column = get_caret_column(i);
+ bool caret_after = caret_line > p_old_line || (caret_line == p_old_line && caret_column > p_old_column);
+ bool caret_at = caret_line == p_old_line && caret_column == p_old_column;
+ if (caret_after || (caret_at && include_caret_at)) {
+ caret_line += edit_height;
+ if (caret_line == p_new_line) {
+ caret_column += edit_size;
}
+
+ if (edit_height != 0) {
+ set_caret_line(caret_line, false, true, -1, i);
+ }
+ set_caret_column(caret_column, false, i);
}
- } else {
+
+ // Move selection origin.
+ if (!selected) {
+ continue;
+ }
+ bool include_selection_origin_at = !caret_at_end ? p_include_selection_end : p_include_selection_begin;
+
+ int selection_origin_line = get_selection_origin_line(i);
+ int selection_origin_column = get_selection_origin_column(i);
+ bool selection_origin_after = selection_origin_line > p_old_line || (selection_origin_line == p_old_line && selection_origin_column > p_old_column);
+ bool selection_origin_at = selection_origin_line == p_old_line && selection_origin_column == p_old_column;
+ if (selection_origin_after || (selection_origin_at && include_selection_origin_at)) {
+ selection_origin_line += edit_height;
+ if (selection_origin_line == p_new_line) {
+ selection_origin_column += edit_size;
+ }
+ select(selection_origin_line, selection_origin_column, caret_line, caret_column, i);
+ }
+ }
+ if (!p_include_selection_begin && p_include_selection_end && has_selection()) {
+ // It is possible that two adjacent selections now overlap.
+ merge_overlapping_carets();
+ }
+}
+
+void TextEdit::_cancel_drag_and_drop_text() {
+ // Cancel the drag operation if drag originated from here.
+ if (selection_drag_attempt && get_viewport()) {
+ get_viewport()->gui_cancel_drag();
+ }
+}
+
+/* Selection */
+void TextEdit::_selection_changed(int p_caret) {
+ if (!selecting_enabled) {
+ return;
+ }
+
+ _cancel_drag_and_drop_text();
+ queue_redraw();
+}
+
+void TextEdit::_click_selection_held() {
+ // Update the selection mode on a timer so it is updated when the view scrolls even if the mouse isn't moving.
+ if (!Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) {
click_select_held->stop();
+ return;
+ }
+ switch (get_selection_mode()) {
+ case SelectionMode::SELECTION_MODE_POINTER: {
+ _update_selection_mode_pointer();
+ } break;
+ case SelectionMode::SELECTION_MODE_WORD: {
+ _update_selection_mode_word();
+ } break;
+ case SelectionMode::SELECTION_MODE_LINE: {
+ _update_selection_mode_line();
+ } break;
+ default: {
+ break;
+ }
}
}
-void TextEdit::_update_selection_mode_pointer() {
- dragging_selection = true;
+void TextEdit::_update_selection_mode_pointer(bool p_initial) {
Point2 mp = get_local_mouse_pos();
Point2i pos = get_line_column_at_pos(mp);
int line = pos.y;
- int col = pos.x;
- int caret_idx = carets.size() - 1;
-
- select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx);
+ int column = pos.x;
+ int caret_index = get_caret_count() - 1;
+
+ if (p_initial && !has_selection(caret_index)) {
+ set_selection_origin_line(line, true, -1, caret_index);
+ set_selection_origin_column(column, caret_index);
+ // Set the word begin and end to the column in case the mode changes later.
+ carets.write[caret_index].selection.word_begin_column = column;
+ carets.write[caret_index].selection.word_end_column = column;
+ } else {
+ select(get_selection_origin_line(caret_index), get_selection_origin_column(caret_index), line, column, caret_index);
+ }
+ adjust_viewport_to_caret(caret_index);
- set_caret_line(line, false, true, 0, caret_idx);
- set_caret_column(col, true, caret_idx);
- queue_redraw();
+ if (has_selection(caret_index)) {
+ // Only set to true if any selection has been made.
+ dragging_selection = true;
+ }
click_select_held->start();
merge_overlapping_carets();
}
-void TextEdit::_update_selection_mode_word() {
+void TextEdit::_update_selection_mode_word(bool p_initial) {
dragging_selection = true;
Point2 mp = get_local_mouse_pos();
Point2i pos = get_line_column_at_pos(mp);
int line = pos.y;
- int col = pos.x;
- int caret_idx = carets.size() - 1;
+ int column = pos.x;
+ int caret_index = get_caret_count() - 1;
- int caret_pos = CLAMP(col, 0, text[line].length());
+ int caret_pos = CLAMP(column, 0, text[line].length());
int beg = caret_pos;
int end = beg;
PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid());
@@ -7316,70 +7571,57 @@ void TextEdit::_update_selection_mode_word() {
}
}
- /* Initial selection. */
- if (!has_selection(caret_idx)) {
- select(line, beg, line, end, caret_idx);
- carets.write[caret_idx].selection.selecting_column = beg;
- carets.write[caret_idx].selection.selected_word_beg = beg;
- carets.write[caret_idx].selection.selected_word_end = end;
- carets.write[caret_idx].selection.selected_word_origin = beg;
- set_caret_line(line, false, true, 0, caret_idx);
- set_caret_column(end, true, caret_idx);
+ if (p_initial && !has_selection(caret_index)) {
+ // Set the selection origin if there is no existing selection.
+ select(line, beg, line, end, caret_index);
+ carets.write[caret_index].selection.word_begin_column = beg;
+ carets.write[caret_index].selection.word_end_column = end;
} else {
- if ((col <= carets[caret_idx].selection.selected_word_origin && line == get_selection_line(caret_idx)) || line < get_selection_line(caret_idx)) {
- carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_end;
- select(line, beg, get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_end, caret_idx);
- set_caret_line(line, false, true, 0, caret_idx);
- set_caret_column(beg, true, caret_idx);
- } else {
- carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_beg;
- select(get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_beg, line, end, caret_idx);
- set_caret_line(get_selection_to_line(caret_idx), false, true, 0, caret_idx);
- set_caret_column(get_selection_to_column(caret_idx), true, caret_idx);
- }
+ // Expand the word selection to the mouse.
+ int origin_line = get_selection_origin_line(caret_index);
+ bool is_new_selection_dir_right = line > origin_line || (line == origin_line && column >= carets[caret_index].selection.word_begin_column);
+ int origin_col = is_new_selection_dir_right ? carets[caret_index].selection.word_begin_column : carets[caret_index].selection.word_end_column;
+ int caret_col = is_new_selection_dir_right ? end : beg;
+
+ select(origin_line, origin_col, line, caret_col, caret_index);
}
+ adjust_viewport_to_caret(caret_index);
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) {
DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text());
}
- queue_redraw();
-
click_select_held->start();
merge_overlapping_carets();
}
-void TextEdit::_update_selection_mode_line() {
+void TextEdit::_update_selection_mode_line(bool p_initial) {
dragging_selection = true;
Point2 mp = get_local_mouse_pos();
Point2i pos = get_line_column_at_pos(mp);
int line = pos.y;
- int col = pos.x;
- int caret_idx = carets.size() - 1;
-
- col = 0;
- if (line < carets[caret_idx].selection.selecting_line) {
- // Caret is above us.
- set_caret_line(line - 1, false, true, 0, caret_idx);
- carets.write[caret_idx].selection.selecting_column = has_selection(caret_idx)
- ? text[get_selection_line(caret_idx)].length()
- : 0;
- } else {
- // Caret is below us.
- set_caret_line(line + 1, false, true, 0, caret_idx);
- carets.write[caret_idx].selection.selecting_column = 0;
- col = text[line].length();
+ int caret_index = get_caret_count() - 1;
+
+ int origin_line = p_initial && !has_selection(caret_index) ? line : get_selection_origin_line();
+ bool line_below = line >= origin_line;
+ int origin_col = line_below ? 0 : get_line(origin_line).length();
+ int caret_line = line_below ? line + 1 : line;
+ int caret_col = caret_line < text.size() ? 0 : get_line(text.size() - 1).length();
+
+ select(origin_line, origin_col, caret_line, caret_col, caret_index);
+ adjust_viewport_to_caret(caret_index);
+
+ if (p_initial) {
+ // Set the word begin and end to the start and end of the origin line in case the mode changes later.
+ carets.write[caret_index].selection.word_begin_column = 0;
+ carets.write[caret_index].selection.word_end_column = get_line(origin_line).length();
}
- set_caret_column(0, false, caret_idx);
- select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx);
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) {
DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text());
}
- queue_redraw();
-
click_select_held->start();
merge_overlapping_carets();
}
@@ -7389,23 +7631,23 @@ void TextEdit::_pre_shift_selection(int p_caret) {
return;
}
- if (!has_selection(p_caret) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) {
- carets.write[p_caret].selection.active = true;
- set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_caret_line(p_caret), get_caret_column(p_caret), p_caret);
+ set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT);
+ if (has_selection(p_caret)) {
return;
}
-
- set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_selection_line(p_caret), get_selection_column(p_caret), p_caret);
+ // Prepare selection to start at current caret position.
+ set_selection_origin_line(get_caret_line(p_caret), true, -1, p_caret);
+ set_selection_origin_column(get_caret_column(p_caret), p_caret);
+ carets.write[p_caret].selection.active = true;
+ carets.write[p_caret].selection.word_begin_column = get_caret_column(p_caret);
+ carets.write[p_caret].selection.word_end_column = get_caret_column(p_caret);
}
-void TextEdit::_post_shift_selection(int p_caret) {
- if (!selecting_enabled) {
- return;
- }
-
- if (has_selection(p_caret) && get_selection_mode() == SelectionMode::SELECTION_MODE_SHIFT) {
- select(get_selection_line(p_caret), get_selection_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret), p_caret);
+bool TextEdit::_selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges, bool p_only_selections) const {
+ if (!has_selection(p_caret)) {
+ return !p_only_selections && p_line == get_caret_line(p_caret) && p_column == get_caret_column(p_caret);
}
+ return _is_line_col_in_range(p_line, p_column, get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret), p_include_edges);
}
/* Line Wrapping */
@@ -7780,9 +8022,43 @@ Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) {
return syntax_highlighter.is_null() && !setting_text ? Dictionary() : syntax_highlighter->get_line_syntax_highlighting(p_line);
}
+/* Deprecated. */
+#ifndef DISABLE_DEPRECATED
+Vector<int> TextEdit::get_caret_index_edit_order() {
+ Vector<int> carets_order = get_sorted_carets();
+ carets_order.reverse();
+ return carets_order;
+}
+
+void TextEdit::adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col) {
+}
+
+int TextEdit::get_selection_line(int p_caret) const {
+ return get_selection_origin_line(p_caret);
+}
+
+int TextEdit::get_selection_column(int p_caret) const {
+ return get_selection_origin_column(p_caret);
+}
+#endif
+
/*** Super internal Core API. Everything builds on it. ***/
-void TextEdit::_text_changed_emit() {
+void TextEdit::_text_changed() {
+ _cancel_drag_and_drop_text();
+ queue_redraw();
+
+ if (text_changed_dirty || setting_text) {
+ return;
+ }
+
+ if (is_inside_tree()) {
+ callable_mp(this, &TextEdit::_emit_text_changed).call_deferred();
+ }
+ text_changed_dirty = true;
+}
+
+void TextEdit::_emit_text_changed() {
emit_signal(SNAME("text_changed"));
text_changed_dirty = false;
}
@@ -7918,12 +8194,7 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i
input_direction = (TextDirection)dir;
}
- if (!text_changed_dirty && !setting_text) {
- if (is_inside_tree()) {
- callable_mp(this, &TextEdit::_text_changed_emit).call_deferred();
- }
- text_changed_dirty = true;
- }
+ _text_changed();
emit_signal(SNAME("lines_edited_from"), p_line, r_end_line);
}
@@ -7964,12 +8235,7 @@ void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_li
text.remove_range(p_from_line, p_to_line);
text.set(p_from_line, pre_text + post_text, structured_text_parser(st_parser, st_args, pre_text + post_text));
- if (!text_changed_dirty && !setting_text) {
- if (is_inside_tree()) {
- callable_mp(this, &TextEdit::_text_changed_emit).call_deferred();
- }
- text_changed_dirty = true;
- }
+ _text_changed();
emit_signal(SNAME("lines_edited_from"), p_to_line, p_from_line);
}
diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h
index 1099295d3b..efade39876 100644
--- a/scene/gui/text_edit.h
+++ b/scene/gui/text_edit.h
@@ -389,18 +389,12 @@ private:
/* Caret. */
struct Selection {
bool active = false;
- bool shiftclick_left = false;
- int selecting_line = 0;
- int selecting_column = 0;
- int selected_word_beg = 0;
- int selected_word_end = 0;
- int selected_word_origin = 0;
-
- int from_line = 0;
- int from_column = 0;
- int to_line = 0;
- int to_column = 0;
+ int origin_line = 0;
+ int origin_column = 0;
+ int origin_last_fit_x = 0;
+ int word_begin_column = 0;
+ int word_end_column = 0;
};
struct Caret {
@@ -415,11 +409,13 @@ private:
// Vector containing all the carets, index '0' is the "main caret" and should never be removed.
Vector<Caret> carets;
- Vector<int> caret_index_edit_order;
bool setting_caret_line = false;
bool caret_pos_dirty = false;
- bool caret_index_edit_dirty = true;
+
+ int multicaret_edit_count = 0;
+ bool multicaret_edit_merge_queued = false;
+ HashSet<int> multicaret_edit_ignore_carets;
CaretType caret_type = CaretType::CARET_TYPE_LINE;
@@ -438,12 +434,18 @@ private:
bool drag_action = false;
bool drag_caret_force_displayed = false;
+ void _caret_changed(int p_caret = -1);
void _emit_caret_changed();
void _reset_caret_blink_timer();
void _toggle_draw_caret();
int _get_column_x_offset_for_line(int p_char, int p_line, int p_column) const;
+ bool _is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges = true) const;
+
+ void _offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin = true, bool p_include_selection_end = true);
+
+ void _cancel_drag_and_drop_text();
/* Selection. */
SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE;
@@ -456,18 +458,23 @@ private:
bool selection_drag_attempt = false;
bool dragging_selection = false;
+ int drag_and_drop_origin_caret_index = -1;
+ int drag_caret_index = -1;
Timer *click_select_held = nullptr;
uint64_t last_dblclk = 0;
Vector2 last_dblclk_pos;
+
+ void _selection_changed(int p_caret = -1);
void _click_selection_held();
- void _update_selection_mode_pointer();
- void _update_selection_mode_word();
- void _update_selection_mode_line();
+ void _update_selection_mode_pointer(bool p_initial = false);
+ void _update_selection_mode_word(bool p_initial = false);
+ void _update_selection_mode_line(bool p_initial = false);
void _pre_shift_selection(int p_caret);
- void _post_shift_selection(int p_caret);
+
+ bool _selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const;
/* Line wrapping. */
LineWrappingMode line_wrapping_mode = LineWrappingMode::LINE_WRAPPING_NONE;
@@ -599,7 +606,8 @@ private:
/*** Super internal Core API. Everything builds on it. ***/
bool text_changed_dirty = false;
- void _text_changed_emit();
+ void _text_changed();
+ void _emit_text_changed();
void _insert_text(int p_line, int p_char, const String &p_text, int *r_end_line = nullptr, int *r_end_char = nullptr);
void _remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column);
@@ -625,13 +633,15 @@ private:
void _move_caret_document_end(bool p_select);
bool _clear_carets_and_selection();
- // Used in add_caret_at_carets
- void _get_above_below_caret_line_column(int p_old_line, int p_old_wrap_index, int p_old_column, bool p_below, int &p_new_line, int &p_new_column, int p_last_fit_x = -1) const;
-
protected:
void _notification(int p_what);
static void _bind_methods();
+#ifndef DISABLE_DEPRECATED
+ void _set_selection_mode_compat_86978(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0);
+ static void _bind_compatibility_methods();
+#endif // DISABLE_DEPRECATED
+
virtual void _update_theme_item_cache() override;
/* Internal API for CodeEdit, pending public API. */
@@ -659,6 +669,7 @@ protected:
bool _is_line_hidden(int p_line) const;
void _unhide_all_lines();
+ virtual void _unhide_carets();
// Symbol lookup.
String lookup_symbol_word;
@@ -765,9 +776,11 @@ public:
void swap_lines(int p_from_line, int p_to_line);
- void insert_line_at(int p_at, const String &p_text);
- void insert_text_at_caret(const String &p_text, int p_caret = -1);
+ void insert_line_at(int p_line, const String &p_text);
+ void remove_line_at(int p_line, bool p_move_carets_down = true);
+ void insert_text_at_caret(const String &p_text, int p_caret = -1);
+ void insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin = true, bool p_before_selection_end = false);
void remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column);
int get_last_unhidden_line() const;
@@ -851,15 +864,20 @@ public:
void set_multiple_carets_enabled(bool p_enabled);
bool is_multiple_carets_enabled() const;
- int add_caret(int p_line, int p_col);
+ int add_caret(int p_line, int p_column);
void remove_caret(int p_caret);
void remove_secondary_carets();
- void merge_overlapping_carets();
int get_caret_count() const;
void add_caret_at_carets(bool p_below);
- Vector<int> get_caret_index_edit_order();
- void adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col);
+ Vector<int> get_sorted_carets(bool p_include_ignored_carets = false) const;
+ void collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive = false);
+
+ void merge_overlapping_carets();
+ void begin_multicaret_edit();
+ void end_multicaret_edit();
+ bool is_in_mulitcaret_edit() const;
+ bool multicaret_edit_ignore_caret(int p_caret) const;
bool is_caret_visible(int p_caret = 0) const;
Point2 get_caret_draw_pos(int p_caret = 0) const;
@@ -867,7 +885,7 @@ public:
void set_caret_line(int p_line, bool p_adjust_viewport = true, bool p_can_be_hidden = true, int p_wrap_index = 0, int p_caret = 0);
int get_caret_line(int p_caret = 0) const;
- void set_caret_column(int p_col, bool p_adjust_viewport = true, int p_caret = 0);
+ void set_caret_column(int p_column, bool p_adjust_viewport = true, int p_caret = 0);
int get_caret_column(int p_caret = 0) const;
int get_caret_wrap_index(int p_caret = 0) const;
@@ -884,27 +902,34 @@ public:
void set_drag_and_drop_selection_enabled(const bool p_enabled);
bool is_drag_and_drop_selection_enabled() const;
- void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0);
+ void set_selection_mode(SelectionMode p_mode);
SelectionMode get_selection_mode() const;
void select_all();
void select_word_under_caret(int p_caret = -1);
void add_selection_for_next_occurrence();
void skip_selection_for_next_occurrence();
- void select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret = 0);
+ void select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret = 0);
bool has_selection(int p_caret = -1) const;
String get_selected_text(int p_caret = -1);
+ int get_selection_at_line_column(int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const;
+ Vector<Point2i> get_line_ranges_from_carets(bool p_only_selections = false, bool p_merge_adjacent = true) const;
+ TypedArray<Vector2i> get_line_ranges_from_carets_typed_array(bool p_only_selections = false, bool p_merge_adjacent = true) const;
- int get_selection_line(int p_caret = 0) const;
- int get_selection_column(int p_caret = 0) const;
+ void set_selection_origin_line(int p_line, bool p_can_be_hidden = true, int p_wrap_index = -1, int p_caret = 0);
+ void set_selection_origin_column(int p_column, int p_caret = 0);
+ int get_selection_origin_line(int p_caret = 0) const;
+ int get_selection_origin_column(int p_caret = 0) const;
int get_selection_from_line(int p_caret = 0) const;
int get_selection_from_column(int p_caret = 0) const;
int get_selection_to_line(int p_caret = 0) const;
int get_selection_to_column(int p_caret = 0) const;
+ bool is_caret_after_selection_origin(int p_caret = 0) const;
+
void deselect(int p_caret = -1);
void delete_selection(int p_caret = -1);
@@ -1043,6 +1068,15 @@ public:
Color get_font_color() const;
+ /* Deprecated. */
+#ifndef DISABLE_DEPRECATED
+ Vector<int> get_caret_index_edit_order();
+ void adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col);
+
+ int get_selection_line(int p_caret = 0) const;
+ int get_selection_column(int p_caret = 0) const;
+#endif
+
TextEdit(const String &p_placeholder = String());
};
diff --git a/scene/gui/texture_button.cpp b/scene/gui/texture_button.cpp
index 0b197c8c02..df90257e03 100644
--- a/scene/gui/texture_button.cpp
+++ b/scene/gui/texture_button.cpp
@@ -103,7 +103,7 @@ bool TextureButton::has_point(const Point2 &p_point) const {
point *= scale;
// finally, we need to check if the point is inside a rectangle with a position >= 0,0 and a size <= mask_size
- rect.position = Point2().max(_texture_region.position);
+ rect.position = _texture_region.position.maxf(0);
rect.size = mask_size.min(_texture_region.size);
}
diff --git a/scene/gui/texture_progress_bar.cpp b/scene/gui/texture_progress_bar.cpp
index fc9ac2ab18..bbe5ddf1c3 100644
--- a/scene/gui/texture_progress_bar.cpp
+++ b/scene/gui/texture_progress_bar.cpp
@@ -249,7 +249,7 @@ Point2 TextureProgressBar::get_relative_center() {
p += rad_center_off;
p.x /= progress->get_width();
p.y /= progress->get_height();
- p = p.clamp(Point2(), Point2(1, 1));
+ p = p.clampf(0, 1);
return p;
}
diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp
index b17d345f1f..376ace2fe2 100644
--- a/scene/gui/tree.cpp
+++ b/scene/gui/tree.cpp
@@ -3429,7 +3429,7 @@ Rect2 Tree::_get_content_rect() const {
const real_t v_size = v_scroll->is_visible() ? (v_scroll->get_combined_minimum_size().x + theme_cache.scrollbar_h_separation) : 0;
const real_t h_size = h_scroll->is_visible() ? (h_scroll->get_combined_minimum_size().y + theme_cache.scrollbar_v_separation) : 0;
const Point2 scroll_begin = _get_scrollbar_layout_rect().get_end() - Vector2(v_size, h_size);
- const Size2 offset = (content_rect.get_end() - scroll_begin).max(Vector2(0, 0));
+ const Size2 offset = (content_rect.get_end() - scroll_begin).maxf(0);
return content_rect.grow_individual(0, 0, -offset.x, -offset.y);
}
diff --git a/scene/main/canvas_item.compat.inc b/scene/main/canvas_item.compat.inc
new file mode 100644
index 0000000000..7136fded15
--- /dev/null
+++ b/scene/main/canvas_item.compat.inc
@@ -0,0 +1,41 @@
+/**************************************************************************/
+/* canvas_item.compat.inc */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef DISABLE_DEPRECATED
+
+void CanvasItem::_draw_circle_compat_84472(const Point2 &p_pos, real_t p_radius, const Color &p_color) {
+ draw_circle(p_pos, p_radius, p_color, true, -1.0, false);
+}
+
+void CanvasItem::_bind_compatibility_methods() {
+ ClassDB::bind_compatibility_method(D_METHOD("draw_circle", "position", "radius", "color"), &CanvasItem::_draw_circle_compat_84472);
+}
+
+#endif
diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp
index 56aa453407..cabba0f2ed 100644
--- a/scene/main/canvas_item.cpp
+++ b/scene/main/canvas_item.cpp
@@ -29,6 +29,7 @@
/**************************************************************************/
#include "canvas_item.h"
+#include "canvas_item.compat.inc"
#include "scene/2d/canvas_group.h"
#include "scene/main/canvas_layer.h"
@@ -726,11 +727,40 @@ void CanvasItem::draw_rect(const Rect2 &p_rect, const Color &p_color, bool p_fil
}
}
-void CanvasItem::draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color) {
+void CanvasItem::draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color, bool p_filled, real_t p_width, bool p_antialiased) {
ERR_THREAD_GUARD;
ERR_DRAW_GUARD;
- RenderingServer::get_singleton()->canvas_item_add_circle(canvas_item, p_pos, p_radius, p_color);
+ if (p_filled) {
+ if (p_width != -1.0) {
+ WARN_PRINT("The draw_circle() \"width\" argument has no effect when \"filled\" is \"true\".");
+ }
+
+ RenderingServer::get_singleton()->canvas_item_add_circle(canvas_item, p_pos, p_radius, p_color);
+ } else if (p_width >= 2.0 * p_radius) {
+ RenderingServer::get_singleton()->canvas_item_add_circle(canvas_item, p_pos, p_radius + 0.5 * p_width, p_color);
+ } else {
+ // Tessellation count is hardcoded. Keep in sync with the same variable in `RendererCanvasCull::canvas_item_add_circle()`.
+ const int circle_segments = 64;
+
+ Vector<Vector2> points;
+ points.resize(circle_segments + 1);
+
+ Vector2 *points_ptr = points.ptrw();
+ const real_t circle_point_step = Math_TAU / circle_segments;
+
+ for (int i = 0; i < circle_segments; i++) {
+ float angle = i * circle_point_step;
+ points_ptr[i].x = Math::cos(angle) * p_radius;
+ points_ptr[i].y = Math::sin(angle) * p_radius;
+ points_ptr[i] += p_pos;
+ }
+ points_ptr[circle_segments] = points_ptr[0];
+
+ Vector<Color> colors = { p_color };
+
+ RenderingServer::get_singleton()->canvas_item_add_polyline(canvas_item, points, colors, p_width, p_antialiased);
+ }
}
void CanvasItem::draw_texture(const Ref<Texture2D> &p_texture, const Point2 &p_pos, const Color &p_modulate) {
@@ -1163,7 +1193,7 @@ void CanvasItem::_bind_methods() {
ClassDB::bind_method(D_METHOD("draw_multiline", "points", "color", "width"), &CanvasItem::draw_multiline, DEFVAL(-1.0));
ClassDB::bind_method(D_METHOD("draw_multiline_colors", "points", "colors", "width"), &CanvasItem::draw_multiline_colors, DEFVAL(-1.0));
ClassDB::bind_method(D_METHOD("draw_rect", "rect", "color", "filled", "width"), &CanvasItem::draw_rect, DEFVAL(true), DEFVAL(-1.0));
- ClassDB::bind_method(D_METHOD("draw_circle", "position", "radius", "color"), &CanvasItem::draw_circle);
+ ClassDB::bind_method(D_METHOD("draw_circle", "position", "radius", "color", "filled", "width", "antialiased"), &CanvasItem::draw_circle, DEFVAL(true), DEFVAL(-1.0), DEFVAL(false));
ClassDB::bind_method(D_METHOD("draw_texture", "texture", "position", "modulate"), &CanvasItem::draw_texture, DEFVAL(Color(1, 1, 1, 1)));
ClassDB::bind_method(D_METHOD("draw_texture_rect", "texture", "rect", "tile", "modulate", "transpose"), &CanvasItem::draw_texture_rect, DEFVAL(Color(1, 1, 1, 1)), DEFVAL(false));
ClassDB::bind_method(D_METHOD("draw_texture_rect_region", "texture", "rect", "src_rect", "modulate", "transpose", "clip_uv"), &CanvasItem::draw_texture_rect_region, DEFVAL(Color(1, 1, 1, 1)), DEFVAL(false), DEFVAL(true));
diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h
index 8cec086ca6..ae7b195ead 100644
--- a/scene/main/canvas_item.h
+++ b/scene/main/canvas_item.h
@@ -166,6 +166,12 @@ protected:
void _notification(int p_what);
static void _bind_methods();
+
+#ifndef DISABLE_DEPRECATED
+ void _draw_circle_compat_84472(const Point2 &p_pos, real_t p_radius, const Color &p_color);
+ static void _bind_compatibility_methods();
+#endif
+
void _validate_property(PropertyInfo &p_property) const;
_FORCE_INLINE_ void set_hide_clip_children(bool p_value) { hide_clip_children = p_value; }
@@ -273,7 +279,7 @@ public:
void draw_multiline(const Vector<Point2> &p_points, const Color &p_color, real_t p_width = -1.0);
void draw_multiline_colors(const Vector<Point2> &p_points, const Vector<Color> &p_colors, real_t p_width = -1.0);
void draw_rect(const Rect2 &p_rect, const Color &p_color, bool p_filled = true, real_t p_width = -1.0);
- void draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color);
+ void draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color, bool p_filled = true, real_t p_width = -1.0, bool p_antialiased = false);
void draw_texture(const Ref<Texture2D> &p_texture, const Point2 &p_pos, const Color &p_modulate = Color(1, 1, 1, 1));
void draw_texture_rect(const Ref<Texture2D> &p_texture, const Rect2 &p_rect, bool p_tile = false, const Color &p_modulate = Color(1, 1, 1), bool p_transpose = false);
void draw_texture_rect_region(const Ref<Texture2D> &p_texture, const Rect2 &p_rect, const Rect2 &p_src_rect, const Color &p_modulate = Color(1, 1, 1), bool p_transpose = false, bool p_clip_uv = false);
diff --git a/scene/main/status_indicator.cpp b/scene/main/status_indicator.cpp
index 54b2ff75ca..891974f68f 100644
--- a/scene/main/status_indicator.cpp
+++ b/scene/main/status_indicator.cpp
@@ -30,6 +30,8 @@
#include "status_indicator.h"
+#include "scene/gui/popup_menu.h"
+
void StatusIndicator::_notification(int p_what) {
ERR_MAIN_THREAD_GUARD;
#ifdef TOOLS_ENABLED
@@ -43,12 +45,22 @@ void StatusIndicator::_notification(int p_what) {
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) {
if (visible && iid == DisplayServer::INVALID_INDICATOR_ID) {
iid = DisplayServer::get_singleton()->create_status_indicator(icon, tooltip, callable_mp(this, &StatusIndicator::_callback));
+ PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu));
+ if (pm) {
+ RID menu_rid = pm->bind_global_menu();
+ DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid);
+ }
}
}
} break;
case NOTIFICATION_EXIT_TREE: {
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) {
if (iid != DisplayServer::INVALID_INDICATOR_ID) {
+ PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu));
+ if (pm) {
+ pm->unbind_global_menu();
+ DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID());
+ }
DisplayServer::get_singleton()->delete_status_indicator(iid);
iid = DisplayServer::INVALID_INDICATOR_ID;
}
@@ -66,11 +78,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<Image> &p_icon) {
+void StatusIndicator::set_icon(const Ref<Texture2D> &p_icon) {
ERR_MAIN_THREAD_GUARD;
icon = p_icon;
if (iid != DisplayServer::INVALID_INDICATOR_ID) {
@@ -86,7 +102,7 @@ void StatusIndicator::set_icon(const Ref<Image> &p_icon) {
}
}
-Ref<Image> StatusIndicator::get_icon() const {
+Ref<Texture2D> StatusIndicator::get_icon() const {
return icon;
}
@@ -102,6 +118,30 @@ String StatusIndicator::get_tooltip() const {
return tooltip;
}
+void StatusIndicator::set_menu(const NodePath &p_menu) {
+ PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu));
+ if (pm) {
+ pm->unbind_global_menu();
+ if (iid != DisplayServer::INVALID_INDICATOR_ID) {
+ DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID());
+ }
+ }
+
+ menu = p_menu;
+
+ pm = Object::cast_to<PopupMenu>(get_node_or_null(menu));
+ if (pm) {
+ if (iid != DisplayServer::INVALID_INDICATOR_ID) {
+ RID menu_rid = pm->bind_global_menu();
+ DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid);
+ }
+ }
+}
+
+NodePath StatusIndicator::get_menu() const {
+ return menu;
+}
+
void StatusIndicator::set_visible(bool p_visible) {
ERR_MAIN_THREAD_GUARD;
if (visible == p_visible) {
@@ -122,8 +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<PopupMenu>(get_node_or_null(menu));
+ if (pm) {
+ RID menu_rid = pm->bind_global_menu();
+ DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid);
+ }
}
if (!visible && iid != DisplayServer::INVALID_INDICATOR_ID) {
+ PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu));
+ if (pm) {
+ pm->unbind_global_menu();
+ DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID());
+ }
DisplayServer::get_singleton()->delete_status_indicator(iid);
iid = DisplayServer::INVALID_INDICATOR_ID;
}
@@ -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 aa3aa68d78..cd38da6e6c 100644
--- a/scene/main/status_indicator.h
+++ b/scene/main/status_indicator.h
@@ -37,10 +37,11 @@
class StatusIndicator : public Node {
GDCLASS(StatusIndicator, Node);
- Ref<Image> icon;
+ Ref<Texture2D> icon;
String tooltip;
bool visible = true;
DisplayServer::IndicatorID iid = DisplayServer::INVALID_INDICATOR_ID;
+ NodePath menu;
protected:
void _notification(int p_what);
@@ -49,14 +50,19 @@ protected:
void _callback(MouseButton p_index, const Point2i &p_pos);
public:
- void set_icon(const Ref<Image> &p_icon);
- Ref<Image> get_icon() const;
+ void set_icon(const Ref<Texture2D> &p_icon);
+ Ref<Texture2D> get_icon() const;
void set_tooltip(const String &p_tooltip);
String get_tooltip() const;
+ void set_menu(const NodePath &p_menu);
+ NodePath get_menu() const;
+
void set_visible(bool p_visible);
bool is_visible() const;
+
+ Rect2 get_rect() const;
};
#endif // STATUS_INDICATOR_H
diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp
index 73ce166123..9dbe10f30b 100644
--- a/scene/main/viewport.cpp
+++ b/scene/main/viewport.cpp
@@ -688,6 +688,18 @@ void Viewport::_process_picking() {
physics_picking_events.clear();
return;
}
+#ifndef _3D_DISABLED
+ if (use_xr) {
+ if (XRServer::get_singleton() != nullptr) {
+ Ref<XRInterface> xr_interface = XRServer::get_singleton()->get_primary_interface();
+ if (xr_interface.is_valid() && xr_interface->is_initialized() && xr_interface->get_view_count() > 1) {
+ WARN_PRINT_ONCE("Object picking can't be used when stereo rendering, this will be turned off!");
+ physics_object_picking = false; // don't try again.
+ return;
+ }
+ }
+ }
+#endif
_drop_physics_mouseover(true);
@@ -856,9 +868,10 @@ void Viewport::_process_picking() {
if (send_event) {
co->_input_event_call(this, ev, res[i].shape);
- if (physics_object_picking_first_only) {
- break;
- }
+ }
+
+ if (physics_object_picking_first_only) {
+ break;
}
}
}
@@ -970,7 +983,7 @@ void Viewport::_set_size(const Size2i &p_size, const Size2i &p_size_2d_override,
stretch_transform_new.scale(scale);
}
- Size2i new_size = p_size.max(Size2i(2, 2));
+ Size2i new_size = p_size.maxi(2);
if (size == new_size && size_allocated == p_allocated && stretch_transform == stretch_transform_new && p_size_2d_override == size_2d_override) {
return;
}
@@ -1721,7 +1734,6 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) {
gui.mouse_focus_mask.set_flag(button_mask);
} else {
gui.mouse_focus = gui_find_control(mpos);
- gui.last_mouse_focus = gui.mouse_focus;
if (!gui.mouse_focus) {
return;
@@ -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);
@@ -2378,9 +2391,6 @@ void Viewport::_gui_remove_control(Control *p_control) {
gui.forced_mouse_focus = false;
gui.mouse_focus_mask.clear();
}
- if (gui.last_mouse_focus == p_control) {
- gui.last_mouse_focus = nullptr;
- }
if (gui.key_focus == p_control) {
gui.key_focus = nullptr;
}
@@ -2758,7 +2768,7 @@ bool Viewport::_sub_windows_forward_input(const Ref<InputEvent> &p_event) {
Size2i min_size = gui.currently_dragged_subwindow->get_min_size();
Size2i min_size_clamped = gui.currently_dragged_subwindow->get_clamped_minimum_size();
- min_size_clamped = min_size_clamped.max(Size2i(1, 1));
+ min_size_clamped = min_size_clamped.maxi(1);
Rect2i r = gui.subwindow_resize_from_rect;
@@ -2819,7 +2829,7 @@ bool Viewport::_sub_windows_forward_input(const Ref<InputEvent> &p_event) {
Size2i max_size = gui.currently_dragged_subwindow->get_max_size();
if ((max_size.x > 0 || max_size.y > 0) && (max_size.x >= min_size.x && max_size.y >= min_size.y)) {
- max_size = max_size.max(Size2i(1, 1));
+ max_size = max_size.maxi(1);
if (r.size.x > max_size.x) {
r.size.x = max_size.x;
@@ -3578,6 +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) {
diff --git a/scene/main/viewport.h b/scene/main/viewport.h
index 21832a454c..394d48143c 100644
--- a/scene/main/viewport.h
+++ b/scene/main/viewport.h
@@ -345,7 +345,6 @@ private:
bool key_event_accepted = false;
HashMap<int, ObjectID> touch_focus;
Control *mouse_focus = nullptr;
- Control *last_mouse_focus = nullptr;
Control *mouse_click_grabber = nullptr;
BitField<MouseButtonMask> mouse_focus_mask;
Control *key_focus = nullptr;
@@ -616,6 +615,7 @@ public:
bool gui_is_dragging() const;
bool gui_is_drag_successful() const;
+ void gui_cancel_drag();
Control *gui_find_control(const Point2 &p_global);
diff --git a/scene/main/window.cpp b/scene/main/window.cpp
index 0ccc056a8d..929720fcf4 100644
--- a/scene/main/window.cpp
+++ b/scene/main/window.cpp
@@ -415,7 +415,7 @@ Size2i Window::_clamp_limit_size(const Size2i &p_limit_size) {
if (max_window_size != Size2i()) {
return p_limit_size.clamp(Vector2i(), max_window_size);
} else {
- return p_limit_size.max(Vector2i());
+ return p_limit_size.maxi(0);
}
}
@@ -1036,7 +1036,7 @@ void Window::_update_window_size() {
}
if (embedder) {
- size = size.max(Size2i(1, 1));
+ size = size.maxi(1);
embedder->_sub_window_update(this);
} else if (window_id != DisplayServer::INVALID_WINDOW_ID) {
diff --git a/scene/resources/2d/tile_set.cpp b/scene/resources/2d/tile_set.cpp
index 57cc4ad602..6649cb9b82 100644
--- a/scene/resources/2d/tile_set.cpp
+++ b/scene/resources/2d/tile_set.cpp
@@ -4650,7 +4650,7 @@ Ref<Texture2D> TileSetAtlasSource::get_texture() const {
void TileSetAtlasSource::set_margins(Vector2i p_margins) {
if (p_margins.x < 0 || p_margins.y < 0) {
WARN_PRINT("Atlas source margins should be positive.");
- margins = p_margins.max(Vector2i());
+ margins = p_margins.maxi(0);
} else {
margins = p_margins;
}
@@ -4666,7 +4666,7 @@ Vector2i TileSetAtlasSource::get_margins() const {
void TileSetAtlasSource::set_separation(Vector2i p_separation) {
if (p_separation.x < 0 || p_separation.y < 0) {
WARN_PRINT("Atlas source separation should be positive.");
- separation = p_separation.max(Vector2i());
+ separation = p_separation.maxi(0);
} else {
separation = p_separation;
}
@@ -4682,7 +4682,7 @@ Vector2i TileSetAtlasSource::get_separation() const {
void TileSetAtlasSource::set_texture_region_size(Vector2i p_tile_size) {
if (p_tile_size.x <= 0 || p_tile_size.y <= 0) {
WARN_PRINT("Atlas source tile_size should be strictly positive.");
- texture_region_size = p_tile_size.max(Vector2i(1, 1));
+ texture_region_size = p_tile_size.maxi(1);
} else {
texture_region_size = p_tile_size;
}
diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp
index cd530f100e..8ffb629ba9 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<real_t> 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<ValueTrack *>(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<ValueTrack *>(t);
vt->update_mode = p_mode;
+
+ capture_included = capture_included || (p_mode == UPDATE_CAPTURE);
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_STORAGE | 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 6005172c11..cc7bbae8a3 100644
--- a/scene/resources/animation.h
+++ b/scene/resources/animation.h
@@ -268,6 +268,8 @@ private:
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 @@ public:
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 0185c6ef85..ba5dad088f 100644
--- a/scene/resources/audio_stream_wav.cpp
+++ b/scene/resources/audio_stream_wav.cpp
@@ -86,15 +86,15 @@ void AudioStreamPlaybackWAV::seek(double p_time) {
offset = uint64_t(p_time * base->mix_rate) << MIX_FRAC_BITS;
}
-template <typename Depth, bool is_stereo, bool is_ima_adpcm>
-void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm) {
+template <typename Depth, bool is_stereo, bool is_ima_adpcm, bool is_qoa>
+void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm, QOA_State *p_qoa) {
// this function will be compiled branchless by any decent compiler
- int32_t final, final_r, next, next_r;
+ int32_t final = 0, final_r = 0, next = 0, next_r = 0;
while (p_amount) {
p_amount--;
int64_t pos = p_offset >> MIX_FRAC_BITS;
- if (is_stereo && !is_ima_adpcm) {
+ if (is_stereo && !is_ima_adpcm && !is_qoa) {
pos <<= 1;
}
@@ -175,32 +175,77 @@ void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst,
}
} else {
- final = p_src[pos];
- if (is_stereo) {
- final_r = p_src[pos + 1];
- }
+ if (is_qoa) {
+ if (pos != p_qoa->cache_pos) { // Prevents triple decoding on lower mix rates.
+ for (int i = 0; i < 2; i++) {
+ // Sign operations prevent triple decoding on backward loops, maxing prevents pop.
+ uint32_t interp_pos = MIN(pos + (i * sign) + (sign < 0), p_qoa->desc->samples - 1);
+ uint32_t new_data_ofs = 8 + interp_pos / QOA_FRAME_LEN * p_qoa->frame_len;
+
+ if (p_qoa->data_ofs != new_data_ofs) {
+ p_qoa->data_ofs = new_data_ofs;
+ const uint8_t *src_ptr = (const uint8_t *)base->data;
+ src_ptr += p_qoa->data_ofs + AudioStreamWAV::DATA_PAD;
+ qoa_decode_frame(src_ptr, p_qoa->frame_len, p_qoa->desc, p_qoa->dec, &p_qoa->dec_len);
+ }
- if constexpr (sizeof(Depth) == 1) { /* conditions will not exist anymore when compiled! */
- final <<= 8;
+ uint32_t dec_idx = (interp_pos % QOA_FRAME_LEN) * p_qoa->desc->channels;
+
+ if ((sign > 0 && i == 0) || (sign < 0 && i == 1)) {
+ final = p_qoa->dec[dec_idx];
+ p_qoa->cache[0] = final;
+ if (is_stereo) {
+ final_r = p_qoa->dec[dec_idx + 1];
+ p_qoa->cache_r[0] = final_r;
+ }
+ } else {
+ next = p_qoa->dec[dec_idx];
+ p_qoa->cache[1] = next;
+ if (is_stereo) {
+ next_r = p_qoa->dec[dec_idx + 1];
+ p_qoa->cache_r[1] = next_r;
+ }
+ }
+ }
+ p_qoa->cache_pos = pos;
+ } else {
+ final = p_qoa->cache[0];
+ if (is_stereo) {
+ final_r = p_qoa->cache_r[0];
+ }
+
+ next = p_qoa->cache[1];
+ if (is_stereo) {
+ next_r = p_qoa->cache_r[1];
+ }
+ }
+ } else {
+ final = p_src[pos];
if (is_stereo) {
- final_r <<= 8;
+ final_r = p_src[pos + 1];
}
- }
- if (is_stereo) {
- next = p_src[pos + 2];
- next_r = p_src[pos + 3];
- } else {
- next = p_src[pos + 1];
- }
+ if constexpr (sizeof(Depth) == 1) { /* conditions will not exist anymore when compiled! */
+ final <<= 8;
+ if (is_stereo) {
+ final_r <<= 8;
+ }
+ }
- if constexpr (sizeof(Depth) == 1) {
- next <<= 8;
if (is_stereo) {
- next_r <<= 8;
+ next = p_src[pos + 2];
+ next_r = p_src[pos + 3];
+ } else {
+ next = p_src[pos + 1];
}
- }
+ if constexpr (sizeof(Depth) == 1) {
+ next <<= 8;
+ if (is_stereo) {
+ next_r <<= 8;
+ }
+ }
+ }
int32_t frac = int64_t(p_offset & MIX_FRAC_MASK);
final = final + ((next - final) * frac >> MIX_FRAC_BITS);
@@ -240,6 +285,9 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_
case AudioStreamWAV::FORMAT_IMA_ADPCM:
len *= 2;
break;
+ case AudioStreamWAV::FORMAT_QOA:
+ len = qoa.desc->samples * qoa.desc->channels;
+ break;
}
if (base->stereo) {
@@ -368,27 +416,34 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_
switch (base->format) {
case AudioStreamWAV::FORMAT_8_BITS: {
if (is_stereo) {
- do_resample<int8_t, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm);
+ do_resample<int8_t, true, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
} else {
- do_resample<int8_t, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm);
+ do_resample<int8_t, false, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
}
} break;
case AudioStreamWAV::FORMAT_16_BITS: {
if (is_stereo) {
- do_resample<int16_t, true, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm);
+ do_resample<int16_t, true, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
} else {
- do_resample<int16_t, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm);
+ do_resample<int16_t, false, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
}
} break;
case AudioStreamWAV::FORMAT_IMA_ADPCM: {
if (is_stereo) {
- do_resample<int8_t, true, true>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm);
+ do_resample<int8_t, true, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
} else {
- do_resample<int8_t, false, true>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm);
+ do_resample<int8_t, false, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
}
} break;
+ case AudioStreamWAV::FORMAT_QOA: {
+ if (is_stereo) {
+ do_resample<uint8_t, true, false, true>((uint8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
+ } else {
+ do_resample<uint8_t, false, false, true>((uint8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
+ }
+ } break;
}
dst_buff += target;
@@ -412,6 +467,16 @@ void AudioStreamPlaybackWAV::tag_used_streams() {
AudioStreamPlaybackWAV::AudioStreamPlaybackWAV() {}
+AudioStreamPlaybackWAV::~AudioStreamPlaybackWAV() {
+ if (qoa.desc) {
+ memfree(qoa.desc);
+ }
+
+ if (qoa.dec) {
+ memfree(qoa.dec);
+ }
+}
+
/////////////////////
void AudioStreamWAV::set_format(Format p_format) {
@@ -475,6 +540,10 @@ double AudioStreamWAV::get_length() const {
case AudioStreamWAV::FORMAT_IMA_ADPCM:
len *= 2;
break;
+ case AudioStreamWAV::FORMAT_QOA:
+ qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } };
+ qoa_decode_header((uint8_t *)data + DATA_PAD, QOA_MIN_FILESIZE, &desc);
+ len = desc.samples * desc.channels;
}
if (stereo) {
@@ -526,8 +595,8 @@ Vector<uint8_t> AudioStreamWAV::get_data() const {
}
Error AudioStreamWAV::save_to_wav(const String &p_path) {
- if (format == AudioStreamWAV::FORMAT_IMA_ADPCM) {
- WARN_PRINT("Saving IMA_ADPC samples are not supported yet");
+ if (format == AudioStreamWAV::FORMAT_IMA_ADPCM || format == AudioStreamWAV::FORMAT_QOA) {
+ WARN_PRINT("Saving IMA_ADPCM and QOA samples is not supported yet");
return ERR_UNAVAILABLE;
}
@@ -548,6 +617,7 @@ Error AudioStreamWAV::save_to_wav(const String &p_path) {
byte_pr_sample = 1;
break;
case AudioStreamWAV::FORMAT_16_BITS:
+ case AudioStreamWAV::FORMAT_QOA:
byte_pr_sample = 2;
break;
case AudioStreamWAV::FORMAT_IMA_ADPCM:
@@ -590,6 +660,7 @@ Error AudioStreamWAV::save_to_wav(const String &p_path) {
}
break;
case AudioStreamWAV::FORMAT_16_BITS:
+ case AudioStreamWAV::FORMAT_QOA:
for (unsigned int i = 0; i < data_bytes / 2; i++) {
uint16_t data_point = decode_uint16(&read_data[i * 2]);
file->store_16(data_point);
@@ -607,6 +678,16 @@ Ref<AudioStreamPlayback> AudioStreamWAV::instantiate_playback() {
Ref<AudioStreamPlaybackWAV> sample;
sample.instantiate();
sample->base = Ref<AudioStreamWAV>(this);
+
+ if (format == AudioStreamWAV::FORMAT_QOA) {
+ sample->qoa.desc = (qoa_desc *)memalloc(sizeof(qoa_desc));
+ qoa_decode_header((uint8_t *)data + DATA_PAD, QOA_MIN_FILESIZE, sample->qoa.desc);
+ sample->qoa.frame_len = qoa_max_frame_size(sample->qoa.desc);
+ int samples_len = (sample->qoa.desc->samples > QOA_FRAME_LEN ? QOA_FRAME_LEN : sample->qoa.desc->samples);
+ int alloc_len = sample->qoa.desc->channels * samples_len * sizeof(int16_t);
+ sample->qoa.dec = (int16_t *)memalloc(alloc_len);
+ }
+
return sample;
}
@@ -639,7 +720,7 @@ void AudioStreamWAV::_bind_methods() {
ClassDB::bind_method(D_METHOD("save_to_wav", "path"), &AudioStreamWAV::save_to_wav);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_data", "get_data");
- ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM"), "set_format", "get_format");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM,QOA"), "set_format", "get_format");
ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_mode", PROPERTY_HINT_ENUM, "Disabled,Forward,Ping-Pong,Backward"), "set_loop_mode", "get_loop_mode");
ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_begin"), "set_loop_begin", "get_loop_begin");
ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_end"), "set_loop_end", "get_loop_end");
@@ -649,6 +730,7 @@ void AudioStreamWAV::_bind_methods() {
BIND_ENUM_CONSTANT(FORMAT_8_BITS);
BIND_ENUM_CONSTANT(FORMAT_16_BITS);
BIND_ENUM_CONSTANT(FORMAT_IMA_ADPCM);
+ BIND_ENUM_CONSTANT(FORMAT_QOA);
BIND_ENUM_CONSTANT(LOOP_DISABLED);
BIND_ENUM_CONSTANT(LOOP_FORWARD);
diff --git a/scene/resources/audio_stream_wav.h b/scene/resources/audio_stream_wav.h
index 959d1ceca0..146142d8a4 100644
--- a/scene/resources/audio_stream_wav.h
+++ b/scene/resources/audio_stream_wav.h
@@ -31,7 +31,11 @@
#ifndef AUDIO_STREAM_WAV_H
#define AUDIO_STREAM_WAV_H
+#define QOA_IMPLEMENTATION
+#define QOA_NO_STDIO
+
#include "servers/audio/audio_stream.h"
+#include "thirdparty/misc/qoa.h"
class AudioStreamWAV;
@@ -54,14 +58,25 @@ class AudioStreamPlaybackWAV : public AudioStreamPlayback {
int32_t window_ofs = 0;
} ima_adpcm[2];
+ struct QOA_State {
+ qoa_desc *desc = nullptr;
+ uint32_t data_ofs = 0;
+ uint32_t frame_len = 0;
+ int16_t *dec = nullptr;
+ uint32_t dec_len = 0;
+ int64_t cache_pos = -1;
+ int16_t cache[2] = { 0, 0 };
+ int16_t cache_r[2] = { 0, 0 };
+ } qoa;
+
int64_t offset = 0;
int sign = 1;
bool active = false;
friend class AudioStreamWAV;
Ref<AudioStreamWAV> base;
- template <typename Depth, bool is_stereo, bool is_ima_adpcm>
- void do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm);
+ template <typename Depth, bool is_stereo, bool is_ima_adpcm, bool is_qoa>
+ void do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm, QOA_State *p_qoa);
public:
virtual void start(double p_from_pos = 0.0) override;
@@ -78,6 +93,7 @@ public:
virtual void tag_used_streams() override;
AudioStreamPlaybackWAV();
+ ~AudioStreamPlaybackWAV();
};
class AudioStreamWAV : public AudioStream {
@@ -88,7 +104,8 @@ public:
enum Format {
FORMAT_8_BITS,
FORMAT_16_BITS,
- FORMAT_IMA_ADPCM
+ FORMAT_IMA_ADPCM,
+ FORMAT_QOA,
};
// Keep the ResourceImporterWAV `edit/loop_mode` enum hint in sync with these options.
diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp
index b381096df8..15b40e776c 100644
--- a/scene/resources/material.cpp
+++ b/scene/resources/material.cpp
@@ -688,6 +688,9 @@ void BaseMaterial3D::_update_shader() {
case BLEND_MODE_MUL:
code += "blend_mul";
break;
+ case BLEND_MODE_PREMULT_ALPHA:
+ code += "blend_premul_alpha";
+ break;
case BLEND_MODE_MAX:
break; // Internal value, skip.
}
@@ -1819,6 +1822,11 @@ void fragment() {)";
vec3 detail = mix(ALBEDO.rgb, ALBEDO.rgb * detail_tex.rgb, detail_tex.a);
)";
} break;
+ case BLEND_MODE_PREMULT_ALPHA: {
+ // This is unlikely to ever be used for detail textures, and in order for it to function in the editor, another bit must be used in MaterialKey,
+ // but there are only 5 bits left, so I'm going to leave this disabled unless it's actually requested.
+ //code += "\tvec3 detail = (1.0-detail_tex.a)*ALBEDO.rgb+detail_tex.rgb;\n";
+ } break;
case BLEND_MODE_MAX:
break; // Internal value, skip.
}
@@ -3040,7 +3048,7 @@ void BaseMaterial3D::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "alpha_hash_scale", PROPERTY_HINT_RANGE, "0,2,0.01"), "set_alpha_hash_scale", "get_alpha_hash_scale");
ADD_PROPERTY(PropertyInfo(Variant::INT, "alpha_antialiasing_mode", PROPERTY_HINT_ENUM, "Disabled,Alpha Edge Blend,Alpha Edge Clip"), "set_alpha_antialiasing", "get_alpha_antialiasing");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "alpha_antialiasing_edge", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_alpha_antialiasing_edge", "get_alpha_antialiasing_edge");
- ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Mix,Add,Subtract,Multiply"), "set_blend_mode", "get_blend_mode");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Mix,Add,Subtract,Multiply,Premultiplied Alpha"), "set_blend_mode", "get_blend_mode");
ADD_PROPERTY(PropertyInfo(Variant::INT, "cull_mode", PROPERTY_HINT_ENUM, "Back,Front,Disabled"), "set_cull_mode", "get_cull_mode");
ADD_PROPERTY(PropertyInfo(Variant::INT, "depth_draw_mode", PROPERTY_HINT_ENUM, "Opaque Only,Always,Never"), "set_depth_draw_mode", "get_depth_draw_mode");
ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "no_depth_test"), "set_flag", "get_flag", FLAG_DISABLE_DEPTH_TEST);
@@ -3269,6 +3277,7 @@ void BaseMaterial3D::_bind_methods() {
BIND_ENUM_CONSTANT(BLEND_MODE_ADD);
BIND_ENUM_CONSTANT(BLEND_MODE_SUB);
BIND_ENUM_CONSTANT(BLEND_MODE_MUL);
+ BIND_ENUM_CONSTANT(BLEND_MODE_PREMULT_ALPHA);
BIND_ENUM_CONSTANT(ALPHA_ANTIALIASING_OFF);
BIND_ENUM_CONSTANT(ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE);
diff --git a/scene/resources/material.h b/scene/resources/material.h
index 073403f71e..ecf79c581b 100644
--- a/scene/resources/material.h
+++ b/scene/resources/material.h
@@ -219,6 +219,7 @@ public:
BLEND_MODE_ADD,
BLEND_MODE_SUB,
BLEND_MODE_MUL,
+ BLEND_MODE_PREMULT_ALPHA,
BLEND_MODE_MAX
};
diff --git a/scene/resources/particle_process_material.cpp b/scene/resources/particle_process_material.cpp
index 685625ab72..0b65b33240 100644
--- a/scene/resources/particle_process_material.cpp
+++ b/scene/resources/particle_process_material.cpp
@@ -634,7 +634,7 @@ void ParticleProcessMaterial::_update_shader() {
if (emission_shape == EMISSION_SHAPE_RING) {
code += " \n";
code += " float ring_spawn_angle = rand_from_seed(alt_seed) * 2.0 * pi;\n";
- code += " float ring_random_radius = rand_from_seed(alt_seed) * (emission_ring_radius - emission_ring_inner_radius) + emission_ring_inner_radius;\n";
+ code += " float ring_random_radius = sqrt(rand_from_seed(alt_seed) * (emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius);\n";
code += " vec3 axis = emission_ring_axis == vec3(0.0) ? vec3(0.0, 0.0, 1.0) : normalize(emission_ring_axis);\n";
code += " vec3 ortho_axis = vec3(0.0);\n";
code += " if (abs(axis) == vec3(1.0, 0.0, 0.0)) {\n";
@@ -1136,9 +1136,9 @@ void ParticleProcessMaterial::_update_shader() {
code += " if (COLLIDED) emit_count = sub_emitter_amount_at_collision;\n";
} break;
case SUB_EMITTER_AT_END: {
- code += " float unit_delta = DELTA/LIFETIME;\n";
- code += " float end_time = CUSTOM.w * 0.95;\n"; // if we do at the end we might miss it, as it can just get deactivated by emitter
- code += " if (CUSTOM.y < end_time && (CUSTOM.y + unit_delta) >= end_time) emit_count = sub_emitter_amount_at_end;\n";
+ code += " if ((CUSTOM.y / CUSTOM.w * LIFETIME) > (LIFETIME - DELTA)) {\n";
+ code += " emit_count = sub_emitter_amount_at_end;\n";
+ code += " }\n";
} break;
default: {
}
diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp
index 6f1aa5c850..4b51f6c471 100644
--- a/scene/resources/visual_shader.cpp
+++ b/scene/resources/visual_shader.cpp
@@ -4928,6 +4928,10 @@ String VisualShaderNodeExpression::generate_code(Shader::Mode p_mode, VisualShad
return code;
}
+bool VisualShaderNodeExpression::is_output_port_expandable(int p_port) const {
+ return false;
+}
+
void VisualShaderNodeExpression::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_expression", "expression"), &VisualShaderNodeExpression::set_expression);
ClassDB::bind_method(D_METHOD("get_expression"), &VisualShaderNodeExpression::get_expression);
diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h
index d7270f3ac6..d32e2465b9 100644
--- a/scene/resources/visual_shader.h
+++ b/scene/resources/visual_shader.h
@@ -878,6 +878,7 @@ public:
String get_expression() const;
virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override;
+ virtual bool is_output_port_expandable(int p_port) const override;
VisualShaderNodeExpression();
};
diff --git a/servers/display_server.cpp b/servers/display_server.cpp
index 9600caa214..fcbedbc1f8 100644
--- a/servers/display_server.cpp
+++ b/servers/display_server.cpp
@@ -709,12 +709,12 @@ void DisplayServer::set_icon(const Ref<Image> &p_icon) {
WARN_PRINT("Icon not supported by this display server.");
}
-DisplayServer::IndicatorID DisplayServer::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) {
+DisplayServer::IndicatorID DisplayServer::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) {
WARN_PRINT("Status indicator not supported by this display server.");
return INVALID_INDICATOR_ID;
}
-void DisplayServer::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) {
+void DisplayServer::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) {
WARN_PRINT("Status indicator not supported by this display server.");
}
@@ -722,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 aab51644c0..30f6ee5ccf 100644
--- a/servers/display_server.h
+++ b/servers/display_server.h
@@ -564,10 +564,12 @@ public:
virtual void set_native_icon(const String &p_filename);
virtual void set_icon(const Ref<Image> &p_icon);
- virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback);
- virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon);
+ virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback);
+ virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon);
virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip);
+ virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid);
virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback);
+ virtual Rect2 status_indicator_get_rect(IndicatorID p_id) const;
virtual void delete_status_indicator(IndicatorID p_id);
enum Context {
diff --git a/servers/rendering/renderer_rd/effects/ss_effects.cpp b/servers/rendering/renderer_rd/effects/ss_effects.cpp
index 3db82c8fbd..36a2470c7b 100644
--- a/servers/rendering/renderer_rd/effects/ss_effects.cpp
+++ b/servers/rendering/renderer_rd/effects/ss_effects.cpp
@@ -521,8 +521,7 @@ void SSEffects::downsample_depth(Ref<RenderSceneBuffersRD> p_render_buffers, uin
RD::get_singleton()->compute_list_set_push_constant(compute_list, &ss_effects.downsample_push_constant, sizeof(SSEffectsDownsamplePushConstant));
if (use_half_size) {
- size.x = MAX(1, size.x >> 1);
- size.y = MAX(1, size.y >> 1);
+ size = Size2i(size.x >> 1, size.y >> 1).maxi(1);
}
RD::get_singleton()->compute_list_dispatch_threads(compute_list, size.x, size.y, 1);
diff --git a/servers/rendering/renderer_rd/environment/fog.cpp b/servers/rendering/renderer_rd/environment/fog.cpp
index 48537a97d9..2dfcd67411 100644
--- a/servers/rendering/renderer_rd/environment/fog.cpp
+++ b/servers/rendering/renderer_rd/environment/fog.cpp
@@ -541,7 +541,7 @@ void Fog::volumetric_fog_update(const VolumetricFogSettings &p_settings, const P
if (p_cam_projection.is_orthogonal()) {
fog_near_size = fog_far_size;
} else {
- fog_near_size = frustum_near_size.max(Vector2(0.001, 0.001));
+ fog_near_size = frustum_near_size.maxf(0.001);
}
params.fog_frustum_size_begin[0] = fog_near_size.x;
@@ -1001,7 +1001,7 @@ void Fog::volumetric_fog_update(const VolumetricFogSettings &p_settings, const P
if (p_cam_projection.is_orthogonal()) {
fog_near_size = fog_far_size;
} else {
- fog_near_size = frustum_near_size.max(Vector2(0.001, 0.001));
+ fog_near_size = frustum_near_size.maxf(0.001);
}
params.fog_frustum_size_begin[0] = fog_near_size.x;
diff --git a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp
index c7ab7ea462..2df0331688 100644
--- a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp
+++ b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp
@@ -1706,7 +1706,7 @@ void RenderForwardClustered::_render_scene(RenderDataRD *p_render_data, const Co
}
if (p_render_data->environment.is_valid()) {
- if (environment_get_sdfgi_enabled(p_render_data->environment)) {
+ if (environment_get_sdfgi_enabled(p_render_data->environment) && get_debug_draw_mode() != RS::VIEWPORT_DEBUG_DRAW_UNSHADED) {
using_sdfgi = true;
}
if (environment_get_ssr_enabled(p_render_data->environment)) {
diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp
index 8d865ba440..9e0dacc1f2 100644
--- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp
+++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp
@@ -90,6 +90,7 @@ void SceneShaderForwardClustered::ShaderData::set_code(const String &p_code) {
actions.render_mode_values["blend_mix"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MIX);
actions.render_mode_values["blend_sub"] = Pair<int *, int>(&blend_mode, BLEND_MODE_SUB);
actions.render_mode_values["blend_mul"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MUL);
+ actions.render_mode_values["blend_premul_alpha"] = Pair<int *, int>(&blend_mode, BLEND_MODE_PREMULT_ALPHA);
actions.render_mode_values["alpha_to_coverage"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE);
actions.render_mode_values["alpha_to_coverage_and_one"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE_AND_TO_ONE);
@@ -244,7 +245,17 @@ void SceneShaderForwardClustered::ShaderData::set_code(const String &p_code) {
blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE;
blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ZERO;
- }
+ } break;
+ case BLEND_MODE_PREMULT_ALPHA: {
+ blend_attachment.enable_blend = true;
+ blend_attachment.alpha_blend_op = RD::BLEND_OP_ADD;
+ blend_attachment.color_blend_op = RD::BLEND_OP_ADD;
+ blend_attachment.src_color_blend_factor = RD::BLEND_FACTOR_ONE;
+ blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
+ blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE;
+ blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
+ uses_blend_alpha = true; // Force alpha used because of blend.
+ } break;
}
// Color pass -> attachment 0: Color/Diffuse, attachment 1: Separate Specular, attachment 2: Motion Vectors
@@ -593,6 +604,7 @@ void SceneShaderForwardClustered::init(const String p_defines) {
actions.renames["NORMAL_MAP_DEPTH"] = "normal_map_depth";
actions.renames["ALBEDO"] = "albedo";
actions.renames["ALPHA"] = "alpha";
+ actions.renames["PREMUL_ALPHA_FACTOR"] = "premul_alpha";
actions.renames["METALLIC"] = "metallic";
actions.renames["SPECULAR"] = "specular";
actions.renames["ROUGHNESS"] = "roughness";
@@ -672,6 +684,7 @@ void SceneShaderForwardClustered::init(const String p_defines) {
actions.usage_defines["INSTANCE_CUSTOM"] = "#define ENABLE_INSTANCE_CUSTOM\n";
actions.usage_defines["POSITION"] = "#define OVERRIDE_POSITION\n";
actions.usage_defines["LIGHT_VERTEX"] = "#define LIGHT_VERTEX_USED\n";
+ actions.usage_defines["PREMUL_ALPHA_FACTOR"] = "#define PREMUL_ALPHA_USED\n";
actions.usage_defines["ALPHA_SCISSOR_THRESHOLD"] = "#define ALPHA_SCISSOR_USED\n";
actions.usage_defines["ALPHA_HASH_SCALE"] = "#define ALPHA_HASH_USED\n";
diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h
index 3b83b2b582..d5332032f9 100644
--- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h
+++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h
@@ -106,7 +106,8 @@ public:
BLEND_MODE_ADD,
BLEND_MODE_SUB,
BLEND_MODE_MUL,
- BLEND_MODE_ALPHA_TO_COVERAGE
+ BLEND_MODE_ALPHA_TO_COVERAGE,
+ BLEND_MODE_PREMULT_ALPHA,
};
enum DepthDraw {
diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp
index 0810f567cb..733cbd1ff2 100644
--- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp
+++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp
@@ -91,6 +91,7 @@ void SceneShaderForwardMobile::ShaderData::set_code(const String &p_code) {
actions.render_mode_values["blend_mix"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MIX);
actions.render_mode_values["blend_sub"] = Pair<int *, int>(&blend_mode, BLEND_MODE_SUB);
actions.render_mode_values["blend_mul"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MUL);
+ actions.render_mode_values["blend_premul_alpha"] = Pair<int *, int>(&blend_mode, BLEND_MODE_PREMULT_ALPHA);
actions.render_mode_values["alpha_to_coverage"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE);
actions.render_mode_values["alpha_to_coverage_and_one"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE_AND_TO_ONE);
@@ -255,7 +256,17 @@ void SceneShaderForwardMobile::ShaderData::set_code(const String &p_code) {
blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE;
blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ZERO;
- }
+ } break;
+ case BLEND_MODE_PREMULT_ALPHA: {
+ blend_attachment.enable_blend = true;
+ blend_attachment.alpha_blend_op = RD::BLEND_OP_ADD;
+ blend_attachment.color_blend_op = RD::BLEND_OP_ADD;
+ blend_attachment.src_color_blend_factor = RD::BLEND_FACTOR_ONE;
+ blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
+ blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE;
+ blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
+ uses_blend_alpha = true; // Force alpha used because of blend.
+ } break;
}
RD::PipelineColorBlendState blend_state_blend;
@@ -497,6 +508,7 @@ void SceneShaderForwardMobile::init(const String p_defines) {
actions.renames["NORMAL_MAP_DEPTH"] = "normal_map_depth";
actions.renames["ALBEDO"] = "albedo";
actions.renames["ALPHA"] = "alpha";
+ actions.renames["PREMUL_ALPHA_FACTOR"] = "premul_alpha";
actions.renames["METALLIC"] = "metallic";
actions.renames["SPECULAR"] = "specular";
actions.renames["ROUGHNESS"] = "roughness";
@@ -581,6 +593,7 @@ void SceneShaderForwardMobile::init(const String p_defines) {
actions.usage_defines["ALPHA_HASH_SCALE"] = "#define ALPHA_HASH_USED\n";
actions.usage_defines["ALPHA_ANTIALIASING_EDGE"] = "#define ALPHA_ANTIALIASING_EDGE_USED\n";
actions.usage_defines["ALPHA_TEXTURE_COORDINATE"] = "@ALPHA_ANTIALIASING_EDGE";
+ actions.usage_defines["PREMUL_ALPHA_FACTOR"] = "#define PREMUL_ALPHA_USED";
actions.usage_defines["SSS_STRENGTH"] = "#define ENABLE_SSS\n";
actions.usage_defines["SSS_TRANSMITTANCE_DEPTH"] = "#define ENABLE_TRANSMITTANCE\n";
diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h
index da189c6f67..833b06c1e3 100644
--- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h
+++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h
@@ -61,6 +61,7 @@ public:
BLEND_MODE_ADD,
BLEND_MODE_SUB,
BLEND_MODE_MUL,
+ BLEND_MODE_PREMULT_ALPHA,
BLEND_MODE_ALPHA_TO_COVERAGE
};
diff --git a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl
index cb07579c4b..20b080da4d 100644
--- a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl
+++ b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl
@@ -914,6 +914,9 @@ vec3 encode24(vec3 v) {
void fragment_shader(in SceneData scene_data) {
uint instance_index = instance_index_interp;
+#ifdef PREMUL_ALPHA_USED
+ float premul_alpha = 1.0;
+#endif // PREMUL_ALPHA_USED
//lay out everything, whatever is unused is optimized away anyway
vec3 vertex = vertex_interp;
#ifdef USE_MULTIVIEW
@@ -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 b98ea5a27f..1637326b48 100644
--- a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl
+++ b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl
@@ -749,6 +749,9 @@ void main() {
float clearcoat_roughness = 0.0;
float anisotropy = 0.0;
vec2 anisotropy_flow = vec2(1.0, 0.0);
+#ifdef PREMUL_ALPHA_USED
+ float premul_alpha = 1.0;
+#endif
#ifndef FOG_DISABLED
vec4 fog = vec4(0.0);
#endif // !FOG_DISABLED
@@ -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/storage_rd/material_storage.cpp b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp
index 1c3076b128..a10c672379 100644
--- a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp
+++ b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp
@@ -1656,13 +1656,9 @@ void MaterialStorage::global_shader_parameters_load_settings(bool p_load_texture
Variant value = d["value"];
if (gvtype >= RS::GLOBAL_VAR_TYPE_SAMPLER2D) {
- //textire
- if (!p_load_textures) {
- continue;
- }
-
String path = value;
- if (path.is_empty()) {
+ // Don't load the textures, but still add the parameter so shaders compile correctly while loading.
+ if (!p_load_textures || path.is_empty()) {
value = RID();
} else {
Ref<Resource> resource = ResourceLoader::load(path);
diff --git a/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp b/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp
index b7934cb3de..b5fdf8bebb 100644
--- a/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp
+++ b/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp
@@ -83,8 +83,7 @@ void RenderSceneBuffersRD::update_sizes(NamedTexture &p_named_texture) {
for (uint32_t mipmap = 0; mipmap < p_named_texture.format.mipmaps; mipmap++) {
p_named_texture.sizes.ptrw()[mipmap] = mipmap_size;
- mipmap_size.width = MAX(1, mipmap_size.width >> 1);
- mipmap_size.height = MAX(1, mipmap_size.height >> 1);
+ mipmap_size = Size2i(mipmap_size.width >> 1, mipmap_size.height >> 1).maxi(1);
}
}
diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp
index af30a32866..76ff566b18 100644
--- a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp
+++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp
@@ -2681,8 +2681,7 @@ void TextureStorage::update_decal_atlas() {
mm.size = s;
decal_atlas.texture_mipmaps.push_back(mm);
- s.width = MAX(1, s.width >> 1);
- s.height = MAX(1, s.height >> 1);
+ s = Vector2i(s.width >> 1, s.height >> 1).maxi(1);
}
{
//create the SRGB variant
@@ -3637,7 +3636,7 @@ void TextureStorage::_render_target_allocate_sdf(RenderTarget *rt) {
}
rt->process_size = size * scale / 100;
- rt->process_size = rt->process_size.max(Size2i(1, 1));
+ rt->process_size = rt->process_size.maxi(1);
tformat.format = RD::DATA_FORMAT_R16G16_SINT;
tformat.width = rt->process_size.width;
@@ -3838,10 +3837,8 @@ void TextureStorage::render_target_copy_to_back_buffer(RID p_render_target, cons
for (int i = 0; i < rt->backbuffer_mipmaps.size(); i++) {
region.position.x >>= 1;
region.position.y >>= 1;
- region.size.x = MAX(1, region.size.x >> 1);
- region.size.y = MAX(1, region.size.y >> 1);
- texture_size.x = MAX(1, texture_size.x >> 1);
- texture_size.y = MAX(1, texture_size.y >> 1);
+ region.size = Size2i(region.size.x >> 1, region.size.y >> 1).maxi(1);
+ texture_size = Size2i(texture_size.x >> 1, texture_size.y >> 1).maxi(1);
RID mipmap = rt->backbuffer_mipmaps[i];
if (RendererSceneRenderRD::get_singleton()->_render_buffers_can_be_storage()) {
@@ -3911,10 +3908,8 @@ void TextureStorage::render_target_gen_back_buffer_mipmaps(RID p_render_target,
for (int i = 0; i < rt->backbuffer_mipmaps.size(); i++) {
region.position.x >>= 1;
region.position.y >>= 1;
- region.size.x = MAX(1, region.size.x >> 1);
- region.size.y = MAX(1, region.size.y >> 1);
- texture_size.x = MAX(1, texture_size.x >> 1);
- texture_size.y = MAX(1, texture_size.y >> 1);
+ region.size = Size2i(region.size.x >> 1, region.size.y >> 1).maxi(1);
+ texture_size = Size2i(texture_size.x >> 1, texture_size.y >> 1).maxi(1);
RID mipmap = rt->backbuffer_mipmaps[i];
diff --git a/servers/rendering/renderer_scene_occlusion_cull.h b/servers/rendering/renderer_scene_occlusion_cull.h
index 5adba5dc6a..a848c86bd2 100644
--- a/servers/rendering/renderer_scene_occlusion_cull.h
+++ b/servers/rendering/renderer_scene_occlusion_cull.h
@@ -98,8 +98,8 @@ public:
rect_max = rect_max.max(normalized);
}
- rect_max = rect_max.min(Vector2(1, 1));
- rect_min = rect_min.max(Vector2(0, 0));
+ rect_max = rect_max.minf(1);
+ rect_min = rect_min.maxf(0);
int mip_count = mips.size();
diff --git a/servers/rendering/rendering_device_graph.cpp b/servers/rendering/rendering_device_graph.cpp
index adac7ee3eb..b04f2ebbaa 100644
--- a/servers/rendering/rendering_device_graph.cpp
+++ b/servers/rendering/rendering_device_graph.cpp
@@ -495,18 +495,19 @@ void RenderingDeviceGraph::_add_command_to_graph(ResourceTracker **p_resource_tr
// We add this command to the adjacency list of all commands that were reading from the entire resource.
int32_t read_full_command_list_index = search_tracker->read_full_command_list_index;
while (read_full_command_list_index >= 0) {
- const RecordedCommandListNode &command_list_node = command_list_nodes[read_full_command_list_index];
- if (command_list_node.command_index == p_command_index) {
+ int32_t read_full_command_index = command_list_nodes[read_full_command_list_index].command_index;
+ int32_t read_full_next_index = command_list_nodes[read_full_command_list_index].next_list_index;
+ if (read_full_command_index == p_command_index) {
if (!resource_has_parent) {
// Only slices are allowed to be in different usages in the same command as they are guaranteed to have no overlap in the same command.
ERR_FAIL_MSG("Command can't have itself as a dependency.");
}
} else {
// Add this command to the adjacency list of each command that was reading this resource.
- _add_adjacent_command(command_list_node.command_index, p_command_index, r_command);
+ _add_adjacent_command(read_full_command_index, p_command_index, r_command);
}
- read_full_command_list_index = command_list_node.next_list_index;
+ read_full_command_list_index = read_full_next_index;
}
if (!resource_has_parent) {
diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h
index 8de76c7dbc..e0049e3fa4 100644
--- a/servers/rendering/rendering_server_default.h
+++ b/servers/rendering/rendering_server_default.h
@@ -1048,6 +1048,10 @@ public:
virtual void init() override;
virtual void finish() override;
+ virtual bool is_on_render_thread() override {
+ return Thread::get_caller_id() == server_thread;
+ }
+
virtual void call_on_render_thread(const Callable &p_callable) override {
if (Thread::get_caller_id() == server_thread) {
command_queue.flush_if_pending();
diff --git a/servers/rendering/shader_types.cpp b/servers/rendering/shader_types.cpp
index af51083dc3..b2fa0ea9d2 100644
--- a/servers/rendering/shader_types.cpp
+++ b/servers/rendering/shader_types.cpp
@@ -124,6 +124,7 @@ ShaderTypes::ShaderTypes() {
shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["UV2"] = constt(ShaderLanguage::TYPE_VEC2);
shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["COLOR"] = constt(ShaderLanguage::TYPE_VEC4);
shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["ALBEDO"] = ShaderLanguage::TYPE_VEC3;
+ shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["PREMUL_ALPHA_FACTOR"] = ShaderLanguage::TYPE_FLOAT;
shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["ALPHA"] = ShaderLanguage::TYPE_FLOAT;
shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["METALLIC"] = ShaderLanguage::TYPE_FLOAT;
shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["SPECULAR"] = ShaderLanguage::TYPE_FLOAT;
@@ -208,7 +209,7 @@ ShaderTypes::ShaderTypes() {
// spatial render modes
{
- shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("blend"), "mix", "add", "sub", "mul" });
+ shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("blend"), "mix", "add", "sub", "mul", "premul_alpha" });
shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("depth_draw"), "opaque", "always", "never" });
shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("depth_prepass_alpha") });
shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("depth_test_disabled") });
diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp
index bbe6b1ad0d..60e8f18c19 100644
--- a/servers/rendering_server.cpp
+++ b/servers/rendering_server.cpp
@@ -1226,6 +1226,10 @@ Error RenderingServer::mesh_create_surface_data_from_arrays(SurfaceData *r_surfa
bsformat |= (1 << j);
}
}
+ if (bsformat & RS::ARRAY_FORMAT_NORMAL) {
+ // We must use tangents if using normals.
+ bsformat |= RS::ARRAY_FORMAT_TANGENT;
+ }
ERR_FAIL_COND_V_MSG(bsformat != (format & RS::ARRAY_FORMAT_BLEND_SHAPE_MASK), ERR_INVALID_PARAMETER, "Blend shape format must match the main array format for Vertex, Normal and Tangent arrays.");
}
@@ -3426,6 +3430,7 @@ void RenderingServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_rendering_device"), &RenderingServer::get_rendering_device);
ClassDB::bind_method(D_METHOD("create_local_rendering_device"), &RenderingServer::create_local_rendering_device);
+ ClassDB::bind_method(D_METHOD("is_on_render_thread"), &RenderingServer::is_on_render_thread);
ClassDB::bind_method(D_METHOD("call_on_render_thread", "callable"), &RenderingServer::call_on_render_thread);
#ifndef DISABLE_DEPRECATED
diff --git a/servers/rendering_server.h b/servers/rendering_server.h
index 8f0150f180..240d82c90b 100644
--- a/servers/rendering_server.h
+++ b/servers/rendering_server.h
@@ -41,6 +41,32 @@
#include "servers/display_server.h"
#include "servers/rendering/rendering_device.h"
+// Helper macros for code outside of the rendering server, but that is
+// called by the rendering server.
+#ifdef DEBUG_ENABLED
+#define ERR_ON_RENDER_THREAD \
+ RenderingServer *rendering_server = RenderingServer::get_singleton(); \
+ ERR_FAIL_NULL(rendering_server); \
+ ERR_FAIL_COND(rendering_server->is_on_render_thread());
+#define ERR_ON_RENDER_THREAD_V(m_ret) \
+ RenderingServer *rendering_server = RenderingServer::get_singleton(); \
+ ERR_FAIL_NULL_V(rendering_server, m_ret); \
+ ERR_FAIL_COND_V(rendering_server->is_on_render_thread(), m_ret);
+#define ERR_NOT_ON_RENDER_THREAD \
+ RenderingServer *rendering_server = RenderingServer::get_singleton(); \
+ ERR_FAIL_NULL(rendering_server); \
+ ERR_FAIL_COND(!rendering_server->is_on_render_thread());
+#define ERR_NOT_ON_RENDER_THREAD_V(m_ret) \
+ RenderingServer *rendering_server = RenderingServer::get_singleton(); \
+ ERR_FAIL_NULL_V(rendering_server, m_ret); \
+ ERR_FAIL_COND_V(!rendering_server->is_on_render_thread(), m_ret);
+#else
+#define ERR_ON_RENDER_THREAD
+#define ERR_ON_RENDER_THREAD_V(m_ret)
+#define ERR_NOT_ON_RENDER_THREAD
+#define ERR_NOT_ON_RENDER_THREAD_V(m_ret)
+#endif
+
template <typename T>
class TypedArray;
@@ -1684,7 +1710,7 @@ public:
#ifndef DISABLE_DEPRECATED
// Never actually used, should be removed when we can break compatibility.
- enum Features {
+ enum Features{
FEATURE_SHADERS,
FEATURE_MULTITHREADED,
};
@@ -1708,6 +1734,7 @@ public:
bool is_render_loop_enabled() const;
void set_render_loop_enabled(bool p_enabled);
+ virtual bool is_on_render_thread() = 0;
virtual void call_on_render_thread(const Callable &p_callable) = 0;
#ifdef TOOLS_ENABLED
diff --git a/servers/xr/xr_interface.h b/servers/xr/xr_interface.h
index d7bd212449..809800d8b9 100644
--- a/servers/xr/xr_interface.h
+++ b/servers/xr/xr_interface.h
@@ -122,17 +122,21 @@ public:
/** rendering and internal **/
+ // These methods are called from the main thread.
+ virtual Transform3D get_camera_transform() = 0; /* returns the position of our camera, only used for updating reference frame. For monoscopic this is equal to the views transform, for stereoscopic this should be an average */
+ virtual void process() = 0;
+
+ // These methods can be called from both main and render thread.
virtual Size2 get_render_target_size() = 0; /* returns the recommended render target size per eye for this device */
virtual uint32_t get_view_count() = 0; /* returns the view count we need (1 is monoscopic, 2 is stereoscopic but can be more) */
- virtual Transform3D get_camera_transform() = 0; /* returns the position of our camera for updating our camera node. For monoscopic this is equal to the views transform, for stereoscopic this should be an average */
+
+ // These methods are called from the rendering thread.
virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) = 0; /* get each views transform */
virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) = 0; /* get each view projection matrix */
virtual RID get_vrs_texture(); /* obtain VRS texture */
virtual RID get_color_texture(); /* obtain color output texture (if applicable) */
virtual RID get_depth_texture(); /* obtain depth output texture (if applicable, used for reprojection) */
virtual RID get_velocity_texture(); /* obtain velocity output texture (if applicable, used for spacewarp) */
-
- virtual void process() = 0;
virtual void pre_render(){};
virtual bool pre_draw_viewport(RID p_render_target) { return true; }; /* inform XR interface we are about to start our viewport draw process */
virtual Vector<BlitToScreen> post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) = 0; /* inform XR interface we finished our viewport draw process */
diff --git a/servers/xr_server.cpp b/servers/xr_server.cpp
index f1105a650d..2cfe98ea1e 100644
--- a/servers/xr_server.cpp
+++ b/servers/xr_server.cpp
@@ -51,7 +51,7 @@ XRServer *XRServer::singleton = nullptr;
XRServer *XRServer::get_singleton() {
return singleton;
-};
+}
void XRServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_world_scale"), &XRServer::get_world_scale);
@@ -59,7 +59,7 @@ void XRServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_world_origin"), &XRServer::get_world_origin);
ClassDB::bind_method(D_METHOD("set_world_origin", "world_origin"), &XRServer::set_world_origin);
ClassDB::bind_method(D_METHOD("get_reference_frame"), &XRServer::get_reference_frame);
- ClassDB::bind_method(D_METHOD("clear_reference_frame"), &XRServer::get_reference_frame);
+ ClassDB::bind_method(D_METHOD("clear_reference_frame"), &XRServer::clear_reference_frame);
ClassDB::bind_method(D_METHOD("center_on_hmd", "rotation_mode", "keep_height"), &XRServer::center_on_hmd);
ClassDB::bind_method(D_METHOD("get_hmd_transform"), &XRServer::get_hmd_transform);
@@ -104,11 +104,20 @@ void XRServer::_bind_methods() {
ADD_SIGNAL(MethodInfo("tracker_added", PropertyInfo(Variant::STRING_NAME, "tracker_name"), PropertyInfo(Variant::INT, "type")));
ADD_SIGNAL(MethodInfo("tracker_updated", PropertyInfo(Variant::STRING_NAME, "tracker_name"), PropertyInfo(Variant::INT, "type")));
ADD_SIGNAL(MethodInfo("tracker_removed", PropertyInfo(Variant::STRING_NAME, "tracker_name"), PropertyInfo(Variant::INT, "type")));
-};
+}
double XRServer::get_world_scale() const {
- return world_scale;
-};
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+
+ if (rendering_server && rendering_server->is_on_render_thread()) {
+ // Return the value with which we're currently rendering,
+ // if we're on the render thread
+ return render_state.world_scale;
+ } else {
+ // Return our current value
+ return world_scale;
+ }
+}
void XRServer::set_world_scale(double p_world_scale) {
if (p_world_scale < 0.01) {
@@ -118,19 +127,58 @@ void XRServer::set_world_scale(double p_world_scale) {
}
world_scale = p_world_scale;
-};
+ set_render_world_scale(world_scale);
+}
+
+void XRServer::_set_render_world_scale(double p_world_scale) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
+
+ XRServer *xr_server = XRServer::get_singleton();
+ ERR_FAIL_NULL(xr_server);
+ xr_server->render_state.world_scale = p_world_scale;
+}
Transform3D XRServer::get_world_origin() const {
- return world_origin;
-};
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+
+ if (rendering_server && rendering_server->is_on_render_thread()) {
+ // Return the value with which we're currently rendering,
+ // if we're on the render thread
+ return render_state.world_origin;
+ } else {
+ // Return our current value
+ return world_origin;
+ }
+}
void XRServer::set_world_origin(const Transform3D &p_world_origin) {
world_origin = p_world_origin;
-};
+ set_render_world_origin(world_origin);
+}
+
+void XRServer::_set_render_world_origin(const Transform3D &p_world_origin) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
+
+ XRServer *xr_server = XRServer::get_singleton();
+ ERR_FAIL_NULL(xr_server);
+ xr_server->render_state.world_origin = p_world_origin;
+}
Transform3D XRServer::get_reference_frame() const {
- return reference_frame;
-};
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+ ERR_FAIL_NULL_V(rendering_server, reference_frame);
+
+ if (rendering_server->is_on_render_thread()) {
+ // Return the value with which we're currently rendering,
+ // if we're on the render thread
+ return render_state.reference_frame;
+ } else {
+ // Return our current value
+ return reference_frame;
+ }
+}
void XRServer::center_on_hmd(RotationMode p_rotation_mode, bool p_keep_height) {
if (primary_interface == nullptr) {
@@ -156,27 +204,38 @@ void XRServer::center_on_hmd(RotationMode p_rotation_mode, bool p_keep_height) {
} else if (p_rotation_mode == 2) {
// remove our rotation, we're only interesting in centering on position
new_reference_frame.basis = Basis();
- };
+ }
// don't negate our height
if (p_keep_height) {
new_reference_frame.origin.y = 0.0;
- };
+ }
reference_frame = new_reference_frame.inverse();
-};
+ set_render_reference_frame(reference_frame);
+}
void XRServer::clear_reference_frame() {
reference_frame = Transform3D();
+ set_render_reference_frame(reference_frame);
+}
+
+void XRServer::_set_render_reference_frame(const Transform3D &p_reference_frame) {
+ // Must be called from rendering thread!
+ ERR_NOT_ON_RENDER_THREAD;
+
+ XRServer *xr_server = XRServer::get_singleton();
+ ERR_FAIL_NULL(xr_server);
+ xr_server->render_state.reference_frame = p_reference_frame;
}
Transform3D XRServer::get_hmd_transform() {
Transform3D hmd_transform;
if (primary_interface != nullptr) {
hmd_transform = primary_interface->get_camera_transform();
- };
+ }
return hmd_transform;
-};
+}
void XRServer::add_interface(const Ref<XRInterface> &p_interface) {
ERR_FAIL_COND(p_interface.is_null());
@@ -185,12 +244,12 @@ void XRServer::add_interface(const Ref<XRInterface> &p_interface) {
if (interfaces[i] == p_interface) {
ERR_PRINT("Interface was already added");
return;
- };
- };
+ }
+ }
interfaces.push_back(p_interface);
emit_signal(SNAME("interface_added"), p_interface->get_name());
-};
+}
void XRServer::remove_interface(const Ref<XRInterface> &p_interface) {
ERR_FAIL_COND(p_interface.is_null());
@@ -200,33 +259,33 @@ void XRServer::remove_interface(const Ref<XRInterface> &p_interface) {
if (interfaces[i] == p_interface) {
idx = i;
break;
- };
- };
+ }
+ }
ERR_FAIL_COND_MSG(idx == -1, "Interface not found.");
print_verbose("XR: Removed interface \"" + p_interface->get_name() + "\"");
emit_signal(SNAME("interface_removed"), p_interface->get_name());
interfaces.remove_at(idx);
-};
+}
int XRServer::get_interface_count() const {
return interfaces.size();
-};
+}
Ref<XRInterface> XRServer::get_interface(int p_index) const {
ERR_FAIL_INDEX_V(p_index, interfaces.size(), nullptr);
return interfaces[p_index];
-};
+}
Ref<XRInterface> XRServer::find_interface(const String &p_name) const {
for (int i = 0; i < interfaces.size(); i++) {
if (interfaces[i]->get_name() == p_name) {
return interfaces[i];
- };
- };
+ }
+ }
return Ref<XRInterface>();
-};
+}
TypedArray<Dictionary> XRServer::get_interfaces() const {
Array ret;
@@ -238,14 +297,14 @@ TypedArray<Dictionary> XRServer::get_interfaces() const {
iface_info["name"] = interfaces[i]->get_name();
ret.push_back(iface_info);
- };
+ }
return ret;
-};
+}
Ref<XRInterface> XRServer::get_primary_interface() const {
return primary_interface;
-};
+}
void XRServer::set_primary_interface(const Ref<XRInterface> &p_primary_interface) {
if (p_primary_interface.is_null()) {
@@ -256,7 +315,7 @@ void XRServer::set_primary_interface(const Ref<XRInterface> &p_primary_interface
print_verbose("XR: Primary interface set to: " + primary_interface->get_name());
}
-};
+}
void XRServer::add_tracker(const Ref<XRTracker> &p_tracker) {
ERR_FAIL_COND(p_tracker.is_null());
@@ -272,7 +331,7 @@ void XRServer::add_tracker(const Ref<XRTracker> &p_tracker) {
trackers[tracker_name] = p_tracker;
emit_signal(SNAME("tracker_added"), tracker_name, p_tracker->get_tracker_type());
}
-};
+}
void XRServer::remove_tracker(const Ref<XRTracker> &p_tracker) {
ERR_FAIL_COND(p_tracker.is_null());
@@ -285,7 +344,7 @@ void XRServer::remove_tracker(const Ref<XRTracker> &p_tracker) {
// and remove it
trackers.erase(tracker_name);
}
-};
+}
Dictionary XRServer::get_trackers(int p_tracker_types) {
Dictionary res;
@@ -307,7 +366,7 @@ Ref<XRTracker> XRServer::get_tracker(const StringName &p_name) const {
// tracker hasn't been registered yet, which is fine, no need to spam the error log...
return Ref<XRTracker>();
}
-};
+}
PackedStringArray XRServer::get_suggested_tracker_names() const {
PackedStringArray arr;
@@ -369,9 +428,9 @@ void XRServer::_process() {
// ignore, not a valid reference
} else if (interfaces[i]->is_initialized()) {
interfaces.write[i]->process();
- };
- };
-};
+ }
+ }
+}
void XRServer::pre_render() {
// called from RendererViewport.draw_viewports right before we start drawing our viewports
@@ -383,8 +442,8 @@ void XRServer::pre_render() {
// ignore, not a valid reference
} else if (interfaces[i]->is_initialized()) {
interfaces.write[i]->pre_render();
- };
- };
+ }
+ }
}
void XRServer::end_frame() {
@@ -396,14 +455,13 @@ void XRServer::end_frame() {
// ignore, not a valid reference
} else if (interfaces[i]->is_initialized()) {
interfaces.write[i]->end_frame();
- };
- };
+ }
+ }
}
XRServer::XRServer() {
singleton = this;
- world_scale = 1.0;
-};
+}
XRServer::~XRServer() {
primary_interface.unref();
@@ -412,4 +470,4 @@ XRServer::~XRServer() {
trackers.clear();
singleton = nullptr;
-};
+}
diff --git a/servers/xr_server.h b/servers/xr_server.h
index 717728171a..cd9c241bb0 100644
--- a/servers/xr_server.h
+++ b/servers/xr_server.h
@@ -36,6 +36,7 @@
#include "core/os/thread_safe.h"
#include "core/templates/rid.h"
#include "core/variant/variant.h"
+#include "rendering_server.h"
class XRInterface;
class XRTracker;
@@ -92,10 +93,46 @@ private:
Ref<XRInterface> primary_interface; /* we'll identify one interface as primary, this will be used by our viewports */
- double world_scale; /* scale by which we multiply our tracker positions */
+ double world_scale = 1.0; /* scale by which we multiply our tracker positions */
Transform3D world_origin; /* our world origin point, maps a location in our virtual world to the origin point in our real world tracking volume */
Transform3D reference_frame; /* our reference frame */
+ // As we may be updating our main state for our next frame while we're still rendering our previous frame,
+ // we need to keep copies around.
+ struct RenderState {
+ double world_scale = 1.0; /* scale by which we multiply our tracker positions */
+ Transform3D world_origin; /* our world origin point, maps a location in our virtual world to the origin point in our real world tracking volume */
+ Transform3D reference_frame; /* our reference frame */
+ } render_state;
+
+ static void _set_render_world_scale(double p_world_scale);
+ static void _set_render_world_origin(const Transform3D &p_world_origin);
+ static void _set_render_reference_frame(const Transform3D &p_reference_frame);
+
+ _FORCE_INLINE_ void set_render_world_scale(double p_world_scale) {
+ // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready...
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rendering_server);
+
+ rendering_server->call_on_render_thread(callable_mp_static(&XRServer::_set_render_world_scale).bind(p_world_scale));
+ }
+
+ _FORCE_INLINE_ void set_render_world_origin(const Transform3D &p_world_origin) {
+ // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready...
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rendering_server);
+
+ rendering_server->call_on_render_thread(callable_mp_static(&XRServer::_set_render_world_origin).bind(p_world_origin));
+ }
+
+ _FORCE_INLINE_ void set_render_reference_frame(const Transform3D &p_reference_frame) {
+ // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready...
+ RenderingServer *rendering_server = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rendering_server);
+
+ rendering_server->call_on_render_thread(callable_mp_static(&XRServer::_set_render_reference_frame).bind(p_reference_frame));
+ }
+
protected:
static XRServer *singleton;
diff --git a/tests/display_server_mock.h b/tests/display_server_mock.h
index ee7433fcbd..fd79a46c5c 100644
--- a/tests/display_server_mock.h
+++ b/tests/display_server_mock.h
@@ -47,6 +47,9 @@ private:
Callable event_callback;
Callable input_event_callback;
+ String clipboard_text;
+ String primary_clipboard_text;
+
static Vector<String> get_rendering_drivers_func() {
Vector<String> drivers;
drivers.push_back("dummy");
@@ -97,6 +100,8 @@ public:
switch (p_feature) {
case FEATURE_MOUSE:
case FEATURE_CURSOR_SHAPE:
+ case FEATURE_CLIPBOARD:
+ case FEATURE_CLIPBOARD_PRIMARY:
return true;
default: {
}
@@ -131,6 +136,11 @@ public:
virtual Point2i mouse_get_position() const override { return mouse_position; }
+ virtual void clipboard_set(const String &p_text) override { clipboard_text = p_text; }
+ virtual String clipboard_get() const override { return clipboard_text; }
+ virtual void clipboard_set_primary(const String &p_text) override { primary_clipboard_text = p_text; }
+ virtual String clipboard_get_primary() const override { return primary_clipboard_text; }
+
virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override {
return Size2i(1920, 1080);
}
diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h
index b0a46b8107..c02830b6df 100644
--- a/tests/scene/test_code_edit.h
+++ b/tests/scene/test_code_edit.h
@@ -36,6 +36,15 @@
#include "tests/test_macros.h"
namespace TestCodeEdit {
+static inline Array build_array() {
+ return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+ Array a = build_array(Fargs...);
+ a.push_front(item);
+ return a;
+}
TEST_CASE("[SceneTree][CodeEdit] line gutters") {
CodeEdit *code_edit = memnew(CodeEdit);
@@ -67,10 +76,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
ERR_PRINT_ON;
- Array arg1;
- arg1.push_back(0);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(0));
code_edit->set_line_as_breakpoint(0, true);
CHECK(code_edit->is_line_breakpointed(0));
@@ -86,10 +92,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
code_edit->clear_breakpointed_lines();
SIGNAL_CHECK_FALSE("breakpoint_toggled");
- Array arg1;
- arg1.push_back(0);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(0));
code_edit->set_line_as_breakpoint(0, true);
CHECK(code_edit->is_line_breakpointed(0));
@@ -101,10 +104,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
}
SUBCASE("[CodeEdit] breakpoints and set text") {
- Array arg1;
- arg1.push_back(0);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(0));
code_edit->set_text("test\nline");
code_edit->set_line_as_breakpoint(0, true);
@@ -121,7 +121,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
code_edit->clear_breakpointed_lines();
SIGNAL_DISCARD("breakpoint_toggled")
- ((Array)args[0])[0] = 1;
+ args = build_array(build_array(1));
code_edit->set_text("test\nline");
code_edit->set_line_as_breakpoint(1, true);
CHECK(code_edit->is_line_breakpointed(1));
@@ -137,10 +137,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
}
SUBCASE("[CodeEdit] breakpoints and clear") {
- Array arg1;
- arg1.push_back(0);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(0));
code_edit->set_text("test\nline");
code_edit->set_line_as_breakpoint(0, true);
@@ -157,7 +154,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
code_edit->clear_breakpointed_lines();
SIGNAL_DISCARD("breakpoint_toggled")
- ((Array)args[0])[0] = 1;
+ args = build_array(build_array(1));
code_edit->set_text("test\nline");
code_edit->set_line_as_breakpoint(1, true);
CHECK(code_edit->is_line_breakpointed(1));
@@ -173,21 +170,15 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
}
SUBCASE("[CodeEdit] breakpoints and new lines no text") {
- Array arg1;
- arg1.push_back(0);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(0));
/* No text moves breakpoint. */
code_edit->set_line_as_breakpoint(0, true);
CHECK(code_edit->is_line_breakpointed(0));
SIGNAL_CHECK("breakpoint_toggled", args);
- /* Normal. */
- ((Array)args[0])[0] = 0;
- Array arg2;
- arg2.push_back(1);
- args.push_back(arg2);
+ // Normal.
+ args = build_array(build_array(0), build_array(1));
SEND_GUI_ACTION("ui_text_newline");
CHECK(code_edit->get_line_count() == 2);
@@ -195,18 +186,16 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
CHECK(code_edit->is_line_breakpointed(1));
SIGNAL_CHECK("breakpoint_toggled", args);
- /* Non-Breaking. */
- ((Array)args[0])[0] = 1;
- ((Array)args[1])[0] = 2;
+ // Non-Breaking.
+ args = build_array(build_array(1), build_array(2));
SEND_GUI_ACTION("ui_text_newline_blank");
CHECK(code_edit->get_line_count() == 3);
CHECK_FALSE(code_edit->is_line_breakpointed(1));
CHECK(code_edit->is_line_breakpointed(2));
SIGNAL_CHECK("breakpoint_toggled", args);
- /* Above. */
- ((Array)args[0])[0] = 2;
- ((Array)args[1])[0] = 3;
+ // Above.
+ args = build_array(build_array(2), build_array(3));
SEND_GUI_ACTION("ui_text_newline_above");
CHECK(code_edit->get_line_count() == 4);
CHECK_FALSE(code_edit->is_line_breakpointed(2));
@@ -215,10 +204,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
}
SUBCASE("[CodeEdit] breakpoints and new lines with text") {
- Array arg1;
- arg1.push_back(0);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(0));
/* Having text does not move breakpoint. */
code_edit->insert_text_at_caret("text");
@@ -241,11 +227,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
CHECK_FALSE(code_edit->is_line_breakpointed(1));
SIGNAL_CHECK_FALSE("breakpoint_toggled");
- /* Above does move. */
- ((Array)args[0])[0] = 0;
- Array arg2;
- arg2.push_back(1);
- args.push_back(arg2);
+ // Above does move.
+ args = build_array(build_array(0), build_array(1));
code_edit->set_caret_line(0);
SEND_GUI_ACTION("ui_text_newline_above");
@@ -256,10 +239,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
}
SUBCASE("[CodeEdit] breakpoints and backspace") {
- Array arg1;
- arg1.push_back(1);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(1));
code_edit->set_text("\n\n");
code_edit->set_line_as_breakpoint(1, true);
@@ -281,8 +261,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
ERR_PRINT_ON;
SIGNAL_CHECK("breakpoint_toggled", args);
- /* Backspace above breakpointed line moves it. */
- ((Array)args[0])[0] = 2;
+ // Backspace above breakpointed line moves it.
+ args = build_array(build_array(2));
code_edit->set_text("\n\n");
code_edit->set_line_as_breakpoint(2, true);
@@ -291,9 +271,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
code_edit->set_caret_line(1);
- Array arg2;
- arg2.push_back(1);
- args.push_back(arg2);
+ args = build_array(build_array(2), build_array(1));
SEND_GUI_ACTION("ui_text_backspace");
ERR_PRINT_OFF;
CHECK_FALSE(code_edit->is_line_breakpointed(2));
@@ -303,10 +281,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
}
SUBCASE("[CodeEdit] breakpoints and delete") {
- Array arg1;
- arg1.push_back(1);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(1));
code_edit->set_text("\n\n");
code_edit->set_line_as_breakpoint(1, true);
@@ -329,8 +304,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
ERR_PRINT_ON;
SIGNAL_CHECK("breakpoint_toggled", args);
- /* Delete above breakpointed line moves it. */
- ((Array)args[0])[0] = 2;
+ // Delete above breakpointed line moves it.
+ args = build_array(build_array(2));
code_edit->set_text("\n\n");
code_edit->set_line_as_breakpoint(2, true);
@@ -339,9 +314,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
code_edit->set_caret_line(0);
- Array arg2;
- arg2.push_back(1);
- args.push_back(arg2);
+ args = build_array(build_array(2), build_array(1));
SEND_GUI_ACTION("ui_text_delete");
ERR_PRINT_OFF;
CHECK_FALSE(code_edit->is_line_breakpointed(2));
@@ -351,10 +324,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
}
SUBCASE("[CodeEdit] breakpoints and delete selection") {
- Array arg1;
- arg1.push_back(1);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(1));
code_edit->set_text("\n\n");
code_edit->set_line_as_breakpoint(1, true);
@@ -367,8 +337,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
CHECK_FALSE(code_edit->is_line_breakpointed(0));
SIGNAL_CHECK("breakpoint_toggled", args);
- /* Should handle breakpoint move when deleting selection by adding less text then removed. */
- ((Array)args[0])[0] = 9;
+ // Should handle breakpoint move when deleting selection by adding less text then removed.
+ args = build_array(build_array(9));
code_edit->set_text("\n\n\n\n\n\n\n\n\n");
code_edit->set_line_as_breakpoint(9, true);
@@ -377,9 +347,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
code_edit->select(0, 0, 6, 0);
- Array arg2;
- arg2.push_back(4);
- args.push_back(arg2);
+ args = build_array(build_array(9), build_array(4));
SEND_GUI_ACTION("ui_text_newline");
ERR_PRINT_OFF;
CHECK_FALSE(code_edit->is_line_breakpointed(9));
@@ -387,9 +355,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
CHECK(code_edit->is_line_breakpointed(4));
SIGNAL_CHECK("breakpoint_toggled", args);
- /* Should handle breakpoint move when deleting selection by adding more text then removed. */
- ((Array)args[0])[0] = 9;
- ((Array)args[1])[0] = 14;
+ // Should handle breakpoint move when deleting selection by adding more text then removed.
+ args = build_array(build_array(9), build_array(14));
code_edit->insert_text_at_caret("\n\n\n\n\n");
MessageQueue::get_singleton()->flush();
@@ -404,10 +371,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
}
SUBCASE("[CodeEdit] breakpoints and undo") {
- Array arg1;
- arg1.push_back(1);
- Array args;
- args.push_back(arg1);
+ Array args = build_array(build_array(1));
code_edit->set_text("\n\n");
code_edit->set_line_as_breakpoint(1, true);
@@ -1849,17 +1813,47 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->do_indent();
CHECK(code_edit->get_line(0) == "test\t");
- /* Indent lines does entire line and works without selection. */
+ // Insert in place with multiple carets.
+ code_edit->set_text("test text");
+ code_edit->set_caret_column(5);
+ code_edit->add_caret(0, 7);
+ code_edit->add_caret(0, 2);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "te\tst \tte\txt");
+ CHECK(code_edit->get_caret_count() == 3);
+ CHECK(code_edit->get_caret_column(0) == 7);
+ CHECK(code_edit->get_caret_column(1) == 10);
+ CHECK(code_edit->get_caret_column(2) == 3);
+ code_edit->remove_secondary_carets();
+
+ // Indent lines does entire line and works without selection.
code_edit->set_text("");
code_edit->insert_text_at_caret("test");
code_edit->indent_lines();
CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_caret_column() == 5);
/* Selection does entire line. */
code_edit->set_text("test");
code_edit->select_all();
code_edit->do_indent();
CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 5);
+
+ // Selection does entire line, right to left selection.
+ code_edit->set_text("test");
+ code_edit->select(0, 4, 0, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 5);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
/* Handles multiple lines. */
code_edit->set_text("test\ntext");
@@ -1867,6 +1861,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->do_indent();
CHECK(code_edit->get_line(0) == "\ttest");
CHECK(code_edit->get_line(1) == "\ttext");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 5);
/* Do not indent line if last col is zero. */
code_edit->set_text("test\ntext");
@@ -1874,6 +1873,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->do_indent();
CHECK(code_edit->get_line(0) == "\ttest");
CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
/* Indent even if last column of first line. */
code_edit->set_text("test\ntext");
@@ -1881,15 +1885,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->do_indent();
CHECK(code_edit->get_line(0) == "\ttest");
CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 5);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Indent even if last column of first line, reversed.
+ code_edit->set_text("test\ntext");
+ code_edit->select(1, 0, 0, 4);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 5);
/* Check selection is adjusted. */
code_edit->set_text("test");
code_edit->select(0, 1, 0, 2);
code_edit->do_indent();
- CHECK(code_edit->get_selection_from_column() == 2);
- CHECK(code_edit->get_selection_to_column() == 3);
CHECK(code_edit->get_line(0) == "\ttest");
- code_edit->undo();
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 3);
+
+ // Indent once with multiple selections.
+ code_edit->set_text("test");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->add_caret(0, 4);
+ code_edit->select(0, 4, 0, 3, 1);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 0);
+ CHECK(code_edit->get_selection_origin_column(0) == 2);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 3);
+ CHECK(code_edit->has_selection(1));
+ CHECK(code_edit->get_selection_origin_line(1) == 0);
+ CHECK(code_edit->get_selection_origin_column(1) == 5);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 4);
}
SUBCASE("[CodeEdit] indent spaces") {
@@ -1922,23 +1964,58 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->do_indent();
CHECK(code_edit->get_line(0) == "test ");
- /* Indent lines does entire line and works without selection. */
+ // Insert in place with multiple carets.
+ code_edit->set_text("test text");
+ code_edit->set_caret_column(5);
+ code_edit->add_caret(0, 7);
+ code_edit->add_caret(0, 2);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "te st te xt");
+ CHECK(code_edit->get_caret_count() == 3);
+ CHECK(code_edit->get_caret_column(0) == 10);
+ CHECK(code_edit->get_caret_column(1) == 14);
+ CHECK(code_edit->get_caret_column(2) == 4);
+ code_edit->remove_secondary_carets();
+
+ // Indent lines does entire line and works without selection.
code_edit->set_text("");
code_edit->insert_text_at_caret("test");
code_edit->indent_lines();
CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_caret_column() == 8);
/* Selection does entire line. */
code_edit->set_text("test");
code_edit->select_all();
code_edit->do_indent();
CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 8);
+
+ // Selection does entire line, right to left selection.
+ code_edit->set_text("test");
+ code_edit->select(0, 4, 0, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
/* single indent only add required spaces. */
code_edit->set_text(" test");
code_edit->select_all();
code_edit->do_indent();
CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 8);
/* Handles multiple lines. */
code_edit->set_text("test\ntext");
@@ -1946,6 +2023,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->do_indent();
CHECK(code_edit->get_line(0) == " test");
CHECK(code_edit->get_line(1) == " text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 8);
/* Do not indent line if last col is zero. */
code_edit->set_text("test\ntext");
@@ -1953,6 +2035,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->do_indent();
CHECK(code_edit->get_line(0) == " test");
CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
/* Indent even if last column of first line. */
code_edit->set_text("test\ntext");
@@ -1960,14 +2047,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->do_indent();
CHECK(code_edit->get_line(0) == " test");
CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Indent even if last column of first line, right to left selection.
+ code_edit->set_text("test\ntext");
+ code_edit->select(1, 0, 0, 4);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 8);
/* Check selection is adjusted. */
code_edit->set_text("test");
code_edit->select(0, 1, 0, 2);
code_edit->do_indent();
- CHECK(code_edit->get_selection_from_column() == 5);
- CHECK(code_edit->get_selection_to_column() == 6);
CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 5);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 6);
+
+ // Indent once with multiple selections.
+ code_edit->set_text("test");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->add_caret(0, 4);
+ code_edit->select(0, 4, 0, 3, 1);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 0);
+ CHECK(code_edit->get_selection_origin_column(0) == 5);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 6);
+ CHECK(code_edit->has_selection(1));
+ CHECK(code_edit->get_selection_origin_line(1) == 0);
+ CHECK(code_edit->get_selection_origin_column(1) == 8);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 7);
}
SUBCASE("[CodeEdit] unindent tabs") {
@@ -2003,11 +2129,28 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->insert_text_at_caret("\ttest");
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_caret_column() == 4);
+
+ // Unindent lines once with multiple carets.
+ code_edit->set_text("\t\ttest");
+ code_edit->set_caret_column(1);
+ code_edit->add_caret(0, 3);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 2);
+ code_edit->remove_secondary_carets();
/* Caret on col zero unindent line. */
code_edit->set_text("\t\ttest");
+ code_edit->set_caret_column(0);
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_caret_column() == 0);
/* Check input action. */
code_edit->set_text("\t\ttest");
@@ -2019,13 +2162,34 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->select_all();
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 5);
- /* Handles multiple lines. */
- code_edit->set_text("\ttest\n\ttext");
+ // Selection does entire line, right to left selection.
+ code_edit->set_text("\t\ttest");
+ code_edit->select(0, 6, 0, 0);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 5);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Handles multiple lines.
+ code_edit->set_text("\t\ttest\n\t\ttext");
code_edit->select_all();
code_edit->unindent_lines();
- CHECK(code_edit->get_line(0) == "test");
- CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_line(1) == "\ttext");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 5);
/* Do not unindent line if last col is zero. */
code_edit->set_text("\ttest\n\ttext");
@@ -2033,6 +2197,23 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == "test");
CHECK(code_edit->get_line(1) == "\ttext");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Do not unindent line if last col is zero, right to left selection.
+ code_edit->set_text("\ttest\n\ttext");
+ code_edit->select(1, 0, 0, 0);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "\ttext");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
/* Unindent even if last column of first line. */
code_edit->set_text("\ttest\n\ttext");
@@ -2040,14 +2221,50 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == "test");
CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 4);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
/* Check selection is adjusted. */
code_edit->set_text("\ttest");
code_edit->select(0, 1, 0, 2);
code_edit->unindent_lines();
- CHECK(code_edit->get_selection_from_column() == 0);
- CHECK(code_edit->get_selection_to_column() == 1);
CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Deselect if only the tab was selected.
+ code_edit->set_text("\ttest");
+ code_edit->select(0, 0, 0, 1);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Unindent once with multiple selections.
+ code_edit->set_text("\t\ttest");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->add_caret(0, 4);
+ code_edit->select(0, 4, 0, 3, 1);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 0);
+ CHECK(code_edit->get_selection_origin_column(0) == 0);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 1);
+ CHECK(code_edit->has_selection(1));
+ CHECK(code_edit->get_selection_origin_line(1) == 0);
+ CHECK(code_edit->get_selection_origin_column(1) == 3);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 2);
}
SUBCASE("[CodeEdit] unindent spaces") {
@@ -2089,11 +2306,28 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->insert_text_at_caret(" test");
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_caret_column() == 4);
+
+ // Unindent lines once with multiple carets.
+ code_edit->set_text(" test");
+ code_edit->set_caret_column(1);
+ code_edit->add_caret(0, 9);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 5);
+ code_edit->remove_secondary_carets();
/* Caret on col zero unindent line. */
code_edit->set_text(" test");
+ code_edit->set_caret_column(0);
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_caret_column() == 0);
/* Only as far as needed */
code_edit->set_text(" test");
@@ -2110,13 +2344,34 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->select_all();
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 8);
- /* Handles multiple lines. */
- code_edit->set_text(" test\n text");
+ // Selection does entire line, right to left selection.
+ code_edit->set_text(" test");
+ code_edit->select(0, 12, 0, 0);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Handles multiple lines.
+ code_edit->set_text(" test\n text");
code_edit->select_all();
code_edit->unindent_lines();
- CHECK(code_edit->get_line(0) == "test");
- CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_line(1) == " text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 8);
/* Do not unindent line if last col is zero. */
code_edit->set_text(" test\n text");
@@ -2124,6 +2379,23 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == "test");
CHECK(code_edit->get_line(1) == " text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Do not unindent line if last col is zero, right to left selection.
+ code_edit->set_text(" test\n text");
+ code_edit->select(1, 0, 0, 0);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == " text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
/* Unindent even if last column of first line. */
code_edit->set_text(" test\n text");
@@ -2131,14 +2403,48 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->unindent_lines();
CHECK(code_edit->get_line(0) == "test");
CHECK(code_edit->get_line(1) == "text");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 1);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
/* Check selection is adjusted. */
code_edit->set_text(" test");
code_edit->select(0, 4, 0, 5);
code_edit->unindent_lines();
- CHECK(code_edit->get_selection_from_column() == 0);
- CHECK(code_edit->get_selection_to_column() == 1);
CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Deselect if only the tab was selected.
+ code_edit->set_text(" test");
+ code_edit->select(0, 0, 0, 4);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Unindent once with multiple selections.
+ code_edit->set_text(" test");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->add_caret(0, 4);
+ code_edit->select(0, 12, 0, 10, 1);
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK_FALSE(code_edit->has_selection(0));
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->has_selection(1));
+ CHECK(code_edit->get_selection_origin_line(1) == 0);
+ CHECK(code_edit->get_selection_origin_column(1) == 8);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 6);
}
SUBCASE("[CodeEdit] auto indent") {
@@ -2153,6 +2459,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline");
CHECK(code_edit->get_line(0) == "test:");
CHECK(code_edit->get_line(1) == "\t");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
/* new blank line should still indent. */
code_edit->set_text("");
@@ -2160,6 +2468,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline_blank");
CHECK(code_edit->get_line(0) == "test:");
CHECK(code_edit->get_line(1) == "\t");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
/* new line above should not indent. */
code_edit->set_text("");
@@ -2167,6 +2477,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline_above");
CHECK(code_edit->get_line(0) == "");
CHECK(code_edit->get_line(1) == "test:");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
/* Whitespace between symbol and caret is okay. */
code_edit->set_text("");
@@ -2174,6 +2486,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline");
CHECK(code_edit->get_line(0) == "test: ");
CHECK(code_edit->get_line(1) == "\t");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
/* Comment between symbol and caret is okay. */
code_edit->add_comment_delimiter("#", "");
@@ -2183,6 +2497,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
CHECK(code_edit->get_line(0) == "test: # comment");
CHECK(code_edit->get_line(1) == "\t");
code_edit->remove_comment_delimiter("#");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
/* Strings between symbol and caret are not okay. */
code_edit->add_string_delimiter("#", "");
@@ -2192,6 +2508,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
CHECK(code_edit->get_line(0) == "test: # string");
CHECK(code_edit->get_line(1) == "");
code_edit->remove_string_delimiter("#");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
/* Non-whitespace prevents auto-indentation. */
code_edit->add_comment_delimiter("#", "");
@@ -2201,6 +2519,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
CHECK(code_edit->get_line(0) == "test := 0 # comment");
CHECK(code_edit->get_line(1) == "");
code_edit->remove_comment_delimiter("#");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
/* Even when there's no comments. */
code_edit->set_text("");
@@ -2208,6 +2528,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline");
CHECK(code_edit->get_line(0) == "test := 0");
CHECK(code_edit->get_line(1) == "");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Preserve current indentation.
+ code_edit->set_text("\ttest");
+ code_edit->set_caret_column(3);
+ SEND_GUI_ACTION("ui_text_newline");
+ CHECK(code_edit->get_line(0) == "\tte");
+ CHECK(code_edit->get_line(1) == "\tst");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Preserve current indentation blank.
+ code_edit->set_text("\ttest");
+ code_edit->set_caret_column(3);
+ SEND_GUI_ACTION("ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_line(1) == "\t");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Preserve current indentation above.
+ code_edit->set_text("\ttest");
+ code_edit->set_caret_column(3);
+ SEND_GUI_ACTION("ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "\t");
+ CHECK(code_edit->get_line(1) == "\ttest");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Increase existing indentation.
+ code_edit->set_text("\ttest:");
+ code_edit->set_caret_column(6);
+ SEND_GUI_ACTION("ui_text_newline");
+ CHECK(code_edit->get_line(0) == "\ttest:");
+ CHECK(code_edit->get_line(1) == "\t\t");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 2);
+
+ // Increase existing indentation blank.
+ code_edit->set_text("\ttest:");
+ code_edit->set_caret_column(3);
+ SEND_GUI_ACTION("ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "\ttest:");
+ CHECK(code_edit->get_line(1) == "\t\t");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 2);
/* If between brace pairs an extra line is added. */
code_edit->set_text("");
@@ -2217,6 +2584,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
CHECK(code_edit->get_line(0) == "test{");
CHECK(code_edit->get_line(1) == "\t");
CHECK(code_edit->get_line(2) == "}");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
/* Except when we are going above. */
code_edit->set_text("");
@@ -2225,6 +2594,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline_above");
CHECK(code_edit->get_line(0) == "");
CHECK(code_edit->get_line(1) == "test{}");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
/* or below. */
code_edit->set_text("");
@@ -2233,6 +2604,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline_blank");
CHECK(code_edit->get_line(0) == "test{}");
CHECK(code_edit->get_line(1) == "");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
}
SUBCASE("[CodeEdit] auto indent spaces") {
@@ -2246,6 +2619,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline");
CHECK(code_edit->get_line(0) == "test:");
CHECK(code_edit->get_line(1) == " ");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 4);
/* new blank line should still indent. */
code_edit->set_text("");
@@ -2253,6 +2628,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline_blank");
CHECK(code_edit->get_line(0) == "test:");
CHECK(code_edit->get_line(1) == " ");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 4);
/* new line above should not indent. */
code_edit->set_text("");
@@ -2260,6 +2637,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline_above");
CHECK(code_edit->get_line(0) == "");
CHECK(code_edit->get_line(1) == "test:");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
/* Whitespace between symbol and caret is okay. */
code_edit->set_text("");
@@ -2267,6 +2646,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline");
CHECK(code_edit->get_line(0) == "test: ");
CHECK(code_edit->get_line(1) == " ");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 4);
/* Comment between symbol and caret is okay. */
code_edit->add_comment_delimiter("#", "");
@@ -2276,6 +2657,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
CHECK(code_edit->get_line(0) == "test: # comment");
CHECK(code_edit->get_line(1) == " ");
code_edit->remove_comment_delimiter("#");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 4);
/* Strings between symbol and caret are not okay. */
code_edit->add_string_delimiter("#", "");
@@ -2285,6 +2668,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
CHECK(code_edit->get_line(0) == "test: # string");
CHECK(code_edit->get_line(1) == "");
code_edit->remove_string_delimiter("#");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
/* Non-whitespace prevents auto-indentation. */
code_edit->add_comment_delimiter("#", "");
@@ -2294,6 +2679,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
CHECK(code_edit->get_line(0) == "test := 0 # comment");
CHECK(code_edit->get_line(1) == "");
code_edit->remove_comment_delimiter("#");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
/* Even when there's no comments. */
code_edit->set_text("");
@@ -2301,6 +2688,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline");
CHECK(code_edit->get_line(0) == "test := 0");
CHECK(code_edit->get_line(1) == "");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Preserve current indentation.
+ code_edit->set_text(" test");
+ code_edit->set_caret_column(6);
+ SEND_GUI_ACTION("ui_text_newline");
+ CHECK(code_edit->get_line(0) == " te");
+ CHECK(code_edit->get_line(1) == " st");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 4);
+
+ // Preserve current indentation blank.
+ code_edit->set_text(" test");
+ code_edit->set_caret_column(6);
+ SEND_GUI_ACTION("ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_line(1) == " ");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 4);
+
+ // Preserve current indentation above.
+ code_edit->set_text(" test");
+ code_edit->set_caret_column(6);
+ SEND_GUI_ACTION("ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == " ");
+ CHECK(code_edit->get_line(1) == " test");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 4);
+
+ // Increase existing indentation.
+ code_edit->set_text(" test:");
+ code_edit->set_caret_column(9);
+ SEND_GUI_ACTION("ui_text_newline");
+ CHECK(code_edit->get_line(0) == " test:");
+ CHECK(code_edit->get_line(1) == " ");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 8);
+
+ // Increase existing indentation blank.
+ code_edit->set_text(" test:");
+ code_edit->set_caret_column(9);
+ SEND_GUI_ACTION("ui_text_newline");
+ CHECK(code_edit->get_line(0) == " test:");
+ CHECK(code_edit->get_line(1) == " ");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 8);
/* If between brace pairs an extra line is added. */
code_edit->set_text("");
@@ -2310,6 +2744,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
CHECK(code_edit->get_line(0) == "test{");
CHECK(code_edit->get_line(1) == " ");
CHECK(code_edit->get_line(2) == "}");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 4);
/* Except when we are going above. */
code_edit->set_text("");
@@ -2318,6 +2754,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline_above");
CHECK(code_edit->get_line(0) == "");
CHECK(code_edit->get_line(1) == "test{}");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
/* or below. */
code_edit->set_text("");
@@ -2326,6 +2764,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
SEND_GUI_ACTION("ui_text_newline_blank");
CHECK(code_edit->get_line(0) == "test{}");
CHECK(code_edit->get_line(1) == "");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
/* If there is something after a colon
and there is a colon in the comment it
@@ -2337,6 +2777,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
CHECK(code_edit->get_line(0) == "test:test#:");
CHECK(code_edit->get_line(1) == "");
code_edit->remove_comment_delimiter("#");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
}
}
@@ -2345,64 +2787,50 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->set_indent_using_spaces(false);
// Only line.
- code_edit->insert_text_at_caret(" test");
- code_edit->set_caret_line(0);
- code_edit->set_caret_column(8);
- code_edit->select(0, 8, 0, 9);
+ code_edit->set_text(" test");
+ code_edit->select(0, 9, 0, 8);
code_edit->convert_indent();
CHECK(code_edit->get_line(0) == "\t\ttest");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 3);
CHECK(code_edit->get_caret_column() == 2);
- CHECK(code_edit->get_selection_from_column() == 2);
- CHECK(code_edit->get_selection_to_column() == 3);
// First line.
- code_edit->set_text("");
- code_edit->insert_text_at_caret(" test\n");
- code_edit->set_caret_line(0);
- code_edit->set_caret_column(8);
+ code_edit->set_text(" test\n");
code_edit->select(0, 8, 0, 9);
code_edit->convert_indent();
CHECK(code_edit->get_line(0) == "\t\ttest");
- CHECK(code_edit->get_caret_column() == 2);
- CHECK(code_edit->get_selection_from_column() == 2);
- CHECK(code_edit->get_selection_to_column() == 3);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK(code_edit->get_caret_column() == 3);
// Middle line.
- code_edit->set_text("");
- code_edit->insert_text_at_caret("\n test\n");
- code_edit->set_caret_line(1);
- code_edit->set_caret_column(8);
+ code_edit->set_text("\n test\n");
code_edit->select(1, 8, 1, 9);
code_edit->convert_indent();
CHECK(code_edit->get_line(1) == "\t\ttest");
- CHECK(code_edit->get_caret_column() == 2);
- CHECK(code_edit->get_selection_from_column() == 2);
- CHECK(code_edit->get_selection_to_column() == 3);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK(code_edit->get_caret_column() == 3);
// End line.
- code_edit->set_text("");
- code_edit->insert_text_at_caret("\n test");
- code_edit->set_caret_line(1);
- code_edit->set_caret_column(8);
+ code_edit->set_text("\n test");
code_edit->select(1, 8, 1, 9);
code_edit->convert_indent();
CHECK(code_edit->get_line(1) == "\t\ttest");
- CHECK(code_edit->get_caret_column() == 2);
- CHECK(code_edit->get_selection_from_column() == 2);
- CHECK(code_edit->get_selection_to_column() == 3);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK(code_edit->get_caret_column() == 3);
// Within provided range.
- code_edit->set_text("");
- code_edit->insert_text_at_caret(" test\n test\n");
- code_edit->set_caret_line(1);
- code_edit->set_caret_column(8);
+ code_edit->set_text(" test\n test\n");
code_edit->select(1, 8, 1, 9);
code_edit->convert_indent(1, 1);
CHECK(code_edit->get_line(0) == " test");
CHECK(code_edit->get_line(1) == "\t\ttest");
- CHECK(code_edit->get_caret_column() == 2);
- CHECK(code_edit->get_selection_from_column() == 2);
- CHECK(code_edit->get_selection_to_column() == 3);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK(code_edit->get_caret_column() == 3);
}
SUBCASE("[CodeEdit] convert indent to spaces") {
@@ -2410,64 +2838,50 @@ TEST_CASE("[SceneTree][CodeEdit] indent") {
code_edit->set_indent_using_spaces(true);
// Only line.
- code_edit->insert_text_at_caret("\t\ttest");
- code_edit->set_caret_line(0);
- code_edit->set_caret_column(2);
- code_edit->select(0, 2, 0, 3);
+ code_edit->set_text("\t\ttest");
+ code_edit->select(0, 3, 0, 2);
code_edit->convert_indent();
CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 9);
CHECK(code_edit->get_caret_column() == 8);
- CHECK(code_edit->get_selection_from_column() == 8);
- CHECK(code_edit->get_selection_to_column() == 9);
// First line.
- code_edit->set_text("");
- code_edit->insert_text_at_caret("\t\ttest\n");
- code_edit->set_caret_line(0);
- code_edit->set_caret_column(2);
+ code_edit->set_text("\t\ttest\n");
code_edit->select(0, 2, 0, 3);
code_edit->convert_indent();
CHECK(code_edit->get_line(0) == " test");
- CHECK(code_edit->get_caret_column() == 8);
- CHECK(code_edit->get_selection_from_column() == 8);
- CHECK(code_edit->get_selection_to_column() == 9);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK(code_edit->get_caret_column() == 9);
// Middle line.
- code_edit->set_text("");
- code_edit->insert_text_at_caret("\n\t\ttest\n");
- code_edit->set_caret_line(1);
- code_edit->set_caret_column(2);
+ code_edit->set_text("\n\t\ttest\n");
code_edit->select(1, 2, 1, 3);
code_edit->convert_indent();
CHECK(code_edit->get_line(1) == " test");
- CHECK(code_edit->get_caret_column() == 8);
- CHECK(code_edit->get_selection_from_column() == 8);
- CHECK(code_edit->get_selection_to_column() == 9);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK(code_edit->get_caret_column() == 9);
// End line.
- code_edit->set_text("");
- code_edit->insert_text_at_caret("\n\t\ttest");
- code_edit->set_caret_line(1);
- code_edit->set_caret_column(2);
+ code_edit->set_text("\n\t\ttest");
code_edit->select(1, 2, 1, 3);
code_edit->convert_indent();
CHECK(code_edit->get_line(1) == " test");
- CHECK(code_edit->get_caret_column() == 8);
- CHECK(code_edit->get_selection_from_column() == 8);
- CHECK(code_edit->get_selection_to_column() == 9);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK(code_edit->get_caret_column() == 9);
// Within provided range.
- code_edit->set_text("");
- code_edit->insert_text_at_caret("\ttest\n\t\ttest\n");
- code_edit->set_caret_line(1);
- code_edit->set_caret_column(2);
+ code_edit->set_text("\ttest\n\t\ttest\n");
code_edit->select(1, 2, 1, 3);
code_edit->convert_indent(1, 1);
CHECK(code_edit->get_line(0) == "\ttest");
CHECK(code_edit->get_line(1) == " test");
- CHECK(code_edit->get_caret_column() == 8);
- CHECK(code_edit->get_selection_from_column() == 8);
- CHECK(code_edit->get_selection_to_column() == 9);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK(code_edit->get_caret_column() == 9);
// Outside of range.
ERR_PRINT_OFF;
@@ -2484,6 +2898,7 @@ TEST_CASE("[SceneTree][CodeEdit] folding") {
CodeEdit *code_edit = memnew(CodeEdit);
SceneTree::get_singleton()->get_root()->add_child(code_edit);
code_edit->grab_focus();
+ code_edit->set_line_folding_enabled(true);
SUBCASE("[CodeEdit] folding settings") {
code_edit->set_line_folding_enabled(true);
@@ -2494,8 +2909,6 @@ TEST_CASE("[SceneTree][CodeEdit] folding") {
}
SUBCASE("[CodeEdit] folding") {
- code_edit->set_line_folding_enabled(true);
-
// No indent.
code_edit->set_text("line1\nline2\nline3");
for (int i = 0; i < 2; i++) {
@@ -2862,6 +3275,100 @@ TEST_CASE("[SceneTree][CodeEdit] folding") {
CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
}
+ SUBCASE("[CodeEdit] folding carets") {
+ // Folding a line moves all carets that would be hidden.
+ code_edit->set_text("test\n\tline1\n\t\tline 2\n");
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(0);
+ code_edit->add_caret(1, 3);
+ code_edit->add_caret(2, 8);
+ code_edit->add_caret(2, 1);
+ code_edit->select(2, 0, 2, 1, 3);
+
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_caret_count() == 1);
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 4);
+
+ // Undoing an action that puts the caret on a folded line unfolds it.
+ code_edit->set_text("test\n\tline1");
+ code_edit->select(1, 1, 1, 2);
+ code_edit->duplicate_selection();
+ CHECK(code_edit->get_text() == "test\n\tlline1");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 3);
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 4);
+
+ code_edit->undo();
+ CHECK(code_edit->get_text() == "test\n\tline1");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 2);
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 1);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+
+ // Redoing doesn't refold.
+ code_edit->redo();
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 3);
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ }
+
+ SUBCASE("[CodeEdit] toggle folding carets") {
+ code_edit->set_text("test\n\tline1\ntest2\n\tline2");
+
+ // Fold lines with carets on them.
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(1);
+ code_edit->toggle_foldable_lines_at_carets();
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+
+ // Toggle fold on lines with carets.
+ code_edit->add_caret(2, 0);
+ code_edit->toggle_foldable_lines_at_carets();
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 1);
+ CHECK(code_edit->get_caret_line(1) == 2);
+ CHECK(code_edit->get_caret_column(1) == 0);
+
+ // Multiple carets as part of one fold.
+ code_edit->unfold_all_lines();
+ code_edit->remove_secondary_carets();
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(1);
+ code_edit->add_caret(0, 4);
+ code_edit->add_caret(1, 2);
+ code_edit->toggle_foldable_lines_at_carets();
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 1);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 4);
+ }
+
memdelete(code_edit);
}
@@ -2870,7 +3377,7 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") {
SceneTree::get_singleton()->get_root()->add_child(code_edit);
code_edit->grab_focus();
- SUBCASE("[CodeEdit] region folding") {
+ SUBCASE("[CodeEdit] region tags") {
code_edit->set_line_folding_enabled(true);
// Region tag detection.
@@ -2907,16 +3414,51 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") {
ERR_PRINT_ON;
CHECK(code_edit->get_code_region_start_tag() == "region");
CHECK(code_edit->get_code_region_end_tag() == "endregion");
+ }
- // Region creation with selection adds start / close region lines.
+ SUBCASE("[CodeEdit] create code region") {
+ code_edit->set_line_folding_enabled(true);
+
+ // Region creation with selection adds start and close region lines. Region name is selected and the region is folded.
code_edit->set_text("line1\nline2\nline3");
code_edit->clear_comment_delimiters();
code_edit->add_comment_delimiter("#", "");
code_edit->select(1, 0, 1, 4);
code_edit->create_code_region();
CHECK(code_edit->is_line_code_region_start(1));
- CHECK(code_edit->get_line(2).contains("line2"));
CHECK(code_edit->is_line_code_region_end(3));
+ CHECK(code_edit->get_text() == "line1\n#region New Code Region\nline2\n#endregion\nline3");
+ CHECK(code_edit->get_caret_count() == 1);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selected_text() == "New Code Region");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 23);
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK(code_edit->is_line_folded(1));
+
+ // Undo region creation. Line get unfolded.
+ code_edit->undo();
+ CHECK(code_edit->get_text() == "line1\nline2\nline3");
+ CHECK(code_edit->get_caret_count() == 1);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 4);
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+
+ // Redo region creation.
+ code_edit->redo();
+ CHECK(code_edit->get_text() == "line1\n#region New Code Region\nline2\n#endregion\nline3");
+ CHECK(code_edit->get_caret_count() == 1);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selected_text() == "New Code Region");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 23);
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK_FALSE(code_edit->is_line_folded(1));
// Region creation without any selection has no effect.
code_edit->set_text("line1\nline2\nline3");
@@ -2925,7 +3467,7 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") {
code_edit->create_code_region();
CHECK(code_edit->get_text() == "line1\nline2\nline3");
- // Region creation with multiple selections.
+ // Region creation with multiple selections. Secondary carets are removed and the first region name is selected.
code_edit->set_text("line1\nline2\nline3");
code_edit->clear_comment_delimiters();
code_edit->add_comment_delimiter("#", "");
@@ -2934,6 +3476,25 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") {
code_edit->select(2, 0, 2, 5, 1);
code_edit->create_code_region();
CHECK(code_edit->get_text() == "#region New Code Region\nline1\n#endregion\nline2\n#region New Code Region\nline3\n#endregion");
+ CHECK(code_edit->get_caret_count() == 1);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selected_text() == "New Code Region");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 23);
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 8);
+
+ // Region creation with mixed selection and non-selection carets. Regular carets are ignored.
+ code_edit->set_text("line1\nline2\nline3");
+ code_edit->clear_comment_delimiters();
+ code_edit->add_comment_delimiter("#", "");
+ code_edit->select(0, 0, 0, 4, 0);
+ code_edit->add_caret(2, 5);
+ code_edit->create_code_region();
+ CHECK(code_edit->get_text() == "#region New Code Region\nline1\n#endregion\nline2\nline3");
+ CHECK(code_edit->get_caret_count() == 1);
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selected_text() == "New Code Region");
// Two selections on the same line create only one region.
code_edit->set_text("test line1\ntest line2\ntest line3");
@@ -2960,6 +3521,10 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") {
code_edit->add_comment_delimiter("/*", "*/");
code_edit->create_code_region();
CHECK(code_edit->get_text() == "line1\nline2\nline3");
+ }
+
+ SUBCASE("[CodeEdit] region comment delimiters") {
+ code_edit->set_line_folding_enabled(true);
// Choose one line comment delimiter.
code_edit->set_text("//region region_name\nline2\n//endregion");
@@ -2993,6 +3558,10 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") {
code_edit->clear_comment_delimiters();
CHECK_FALSE(code_edit->is_line_code_region_start(0));
CHECK_FALSE(code_edit->is_line_code_region_end(2));
+ }
+
+ SUBCASE("[CodeEdit] fold region") {
+ code_edit->set_line_folding_enabled(true);
// Fold region.
code_edit->clear_comment_delimiters();
@@ -3895,10 +4464,7 @@ TEST_CASE("[SceneTree][CodeEdit] symbol lookup") {
SEND_GUI_KEY_EVENT(Key::CTRL);
#endif
- Array signal_args;
- Array arg;
- arg.push_back("some");
- signal_args.push_back(arg);
+ Array signal_args = build_array(build_array("some"));
SIGNAL_CHECK("symbol_validate", signal_args);
SIGNAL_UNWATCH(code_edit, "symbol_validate");
@@ -3928,178 +4494,980 @@ TEST_CASE("[SceneTree][CodeEdit] line length guidelines") {
memdelete(code_edit);
}
-TEST_CASE("[SceneTree][CodeEdit] Backspace delete") {
+TEST_CASE("[SceneTree][CodeEdit] text manipulation") {
CodeEdit *code_edit = memnew(CodeEdit);
SceneTree::get_singleton()->get_root()->add_child(code_edit);
code_edit->grab_focus();
- /* Backspace with selection on first line. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("test backspace");
- code_edit->select(0, 0, 0, 5);
- code_edit->backspace();
- CHECK(code_edit->get_line(0) == "backspace");
-
- /* Backspace with selection on first line and caret at the beginning of file. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("test backspace");
- code_edit->select(0, 0, 0, 5);
- code_edit->set_caret_column(0);
- code_edit->backspace();
- CHECK(code_edit->get_line(0) == "backspace");
-
- /* Move caret up to the previous line on backspace if caret is at the first column. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("line 1\nline 2");
- code_edit->set_caret_line(1);
- code_edit->set_caret_column(0);
- code_edit->backspace();
- CHECK(code_edit->get_line(0) == "line 1line 2");
- CHECK(code_edit->get_caret_line() == 0);
- CHECK(code_edit->get_caret_column() == 6);
-
- /* Backspace delete all text if all text is selected. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("line 1\nline 2\nline 3");
- code_edit->select_all();
- code_edit->backspace();
- CHECK(code_edit->get_text().is_empty());
-
- /* Backspace at the beginning without selection has no effect. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("line 1\nline 2\nline 3");
- code_edit->set_caret_line(0);
- code_edit->set_caret_column(0);
- code_edit->backspace();
- CHECK(code_edit->get_text() == "line 1\nline 2\nline 3");
+ SUBCASE("[SceneTree][CodeEdit] backspace") {
+ // Backspace with selection on first line.
+ code_edit->set_text("test backspace");
+ code_edit->select(0, 0, 0, 5);
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "backspace");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
- memdelete(code_edit);
-}
+ // Backspace with selection on first line and caret at the beginning of file.
+ code_edit->set_text("test backspace");
+ code_edit->select(0, 5, 0, 0);
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "backspace");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
-TEST_CASE("[SceneTree][CodeEdit] New Line") {
- CodeEdit *code_edit = memnew(CodeEdit);
- SceneTree::get_singleton()->get_root()->add_child(code_edit);
- code_edit->grab_focus();
+ // Move caret up to the previous line on backspace if caret is at the first column.
+ code_edit->set_text("line 1\nline 2");
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(0);
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "line 1line 2");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 6);
- /* Add a new line. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("test new line");
- code_edit->set_caret_line(0);
- code_edit->set_caret_column(13);
- SEND_GUI_ACTION("ui_text_newline");
- CHECK(code_edit->get_line(0) == "test new line");
- CHECK(code_edit->get_line(1) == "");
-
- /* Split line with new line. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("test new line");
- code_edit->set_caret_line(0);
- code_edit->set_caret_column(5);
- SEND_GUI_ACTION("ui_text_newline");
- CHECK(code_edit->get_line(0) == "test ");
- CHECK(code_edit->get_line(1) == "new line");
-
- /* Delete selection and split with new line. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("test new line");
- code_edit->select(0, 0, 0, 5);
- SEND_GUI_ACTION("ui_text_newline");
- CHECK(code_edit->get_line(0) == "");
- CHECK(code_edit->get_line(1) == "new line");
-
- /* Blank new line below with selection should not split. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("test new line");
- code_edit->select(0, 0, 0, 5);
- SEND_GUI_ACTION("ui_text_newline_blank");
- CHECK(code_edit->get_line(0) == "test new line");
- CHECK(code_edit->get_line(1) == "");
-
- /* Blank new line above with selection should not split. */
- code_edit->set_text("");
- code_edit->insert_text_at_caret("test new line");
- code_edit->select(0, 0, 0, 5);
- SEND_GUI_ACTION("ui_text_newline_above");
- CHECK(code_edit->get_line(0) == "");
- CHECK(code_edit->get_line(1) == "test new line");
+ // Multiple carets with a caret at the first column.
+ code_edit->set_text("line 1\nline 2");
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(2);
+ code_edit->add_caret(1, 0);
+ code_edit->add_caret(1, 5);
+ code_edit->backspace();
+ CHECK(code_edit->get_text() == "line 1lne2");
+ CHECK(code_edit->get_caret_count() == 3);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 7);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 6);
+ CHECK(code_edit->get_caret_line(2) == 0);
+ CHECK(code_edit->get_caret_column(2) == 9);
+ code_edit->remove_secondary_carets();
+
+ // Multiple carets close together.
+ code_edit->set_text("line 1\nline 2");
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(2);
+ code_edit->add_caret(1, 1);
+ code_edit->backspace();
+ CHECK(code_edit->get_text() == "line 1\nne 2");
+ CHECK(code_edit->get_caret_count() == 1);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
- memdelete(code_edit);
-}
+ // Backspace delete all text if all text is selected.
+ code_edit->set_text("line 1\nline 2\nline 3");
+ code_edit->select_all();
+ code_edit->backspace();
+ CHECK(code_edit->get_text().is_empty());
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
-TEST_CASE("[SceneTree][CodeEdit] Duplicate Lines") {
- CodeEdit *code_edit = memnew(CodeEdit);
- SceneTree::get_singleton()->get_root()->add_child(code_edit);
- code_edit->grab_focus();
+ // Backspace at the beginning without selection has no effect.
+ code_edit->set_text("line 1\nline 2\nline 3");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(0);
+ code_edit->backspace();
+ CHECK(code_edit->get_text() == "line 1\nline 2\nline 3");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+ }
+
+ SUBCASE("[TextEdit] cut") {
+ DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton());
+ code_edit->set_line_folding_enabled(true);
+
+ // Cut without a selection removes the entire line.
+ code_edit->set_text("this is\nsome\n");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(6);
+
+ code_edit->cut();
+ CHECK(DS->clipboard_get() == "this is\n");
+ CHECK(code_edit->get_text() == "some\n");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 3); // In the default font, this is the same position.
+
+ // Undo restores the cut text.
+ code_edit->undo();
+ CHECK(DS->clipboard_get() == "this is\n");
+ CHECK(code_edit->get_text() == "this is\nsome\n");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 6);
+
+ // Redo.
+ code_edit->redo();
+ CHECK(DS->clipboard_get() == "this is\n");
+ CHECK(code_edit->get_text() == "some\n");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 3);
+
+ // Cut unfolds the line.
+ code_edit->set_text("this is\n\tsome\n");
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+
+ code_edit->cut();
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK(DS->clipboard_get() == "this is\n");
+ CHECK(code_edit->get_text() == "\tsome\n");
+ CHECK(code_edit->get_caret_line() == 0);
+
+ // Cut with a selection removes just the selection.
+ code_edit->set_text("this is\nsome\n");
+ code_edit->select(0, 5, 0, 7);
+
+ SEND_GUI_ACTION("ui_cut");
+ CHECK(code_edit->get_viewport()->is_input_handled());
+ CHECK(DS->clipboard_get() == "is");
+ CHECK(code_edit->get_text() == "this \nsome\n");
+ CHECK_FALSE(code_edit->get_caret_line());
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 5);
+
+ // Cut does not change the text if not editable. Text is still added to clipboard.
+ code_edit->set_text("this is\nsome\n");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(5);
+
+ code_edit->set_editable(false);
+ code_edit->cut();
+ code_edit->set_editable(true);
+ CHECK(DS->clipboard_get() == "this is\n");
+ CHECK(code_edit->get_text() == "this is\nsome\n");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 5);
+
+ // Cut line with multiple carets.
+ code_edit->set_text("this is\nsome\n");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(3);
+ code_edit->add_caret(0, 2);
+ code_edit->add_caret(0, 4);
+ code_edit->add_caret(2, 0);
+
+ code_edit->cut();
+ CHECK(DS->clipboard_get() == "this is\n\n");
+ CHECK(code_edit->get_text() == "some");
+ CHECK(code_edit->get_caret_count() == 3);
+ CHECK_FALSE(code_edit->has_selection(0));
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 2); // In the default font, this is the same position.
+ // The previous caret at index 1 was merged.
+ CHECK_FALSE(code_edit->has_selection(1));
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 3); // In the default font, this is the same position.
+ CHECK_FALSE(code_edit->has_selection(2));
+ CHECK(code_edit->get_caret_line(2) == 0);
+ CHECK(code_edit->get_caret_column(2) == 4);
+ code_edit->remove_secondary_carets();
+
+ // Cut on the only line removes the contents.
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(2);
+
+ code_edit->cut();
+ CHECK(DS->clipboard_get() == "some\n");
+ CHECK(code_edit->get_text() == "");
+ CHECK(code_edit->get_line_count() == 1);
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Cut empty line.
+ code_edit->cut();
+ CHECK(DS->clipboard_get() == "\n");
+ CHECK(code_edit->get_text() == "");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Cut multiple lines, in order.
+ code_edit->set_text("this is\nsome\ntext to\nbe\n\ncut");
+ code_edit->set_caret_line(2);
+ code_edit->set_caret_column(7);
+ code_edit->add_caret(3, 0);
+ code_edit->add_caret(0, 2);
+
+ code_edit->cut();
+ CHECK(DS->clipboard_get() == "this is\ntext to\nbe\n");
+ CHECK(code_edit->get_text() == "some\n\ncut");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->get_caret_line(0) == 1);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 2);
+ code_edit->remove_secondary_carets();
+
+ // Cut multiple selections, in order. Ignores regular carets.
+ code_edit->set_text("this is\nsome\ntext to\nbe\n\ncut");
+ code_edit->add_caret(3, 0);
+ code_edit->add_caret(0, 2);
+ code_edit->add_caret(2, 0);
+ code_edit->select(1, 0, 1, 2, 0);
+ code_edit->select(3, 0, 4, 0, 1);
+ code_edit->select(0, 5, 0, 3, 2);
+
+ code_edit->cut();
+ CHECK(DS->clipboard_get() == "s \nso\nbe\n");
+ CHECK(code_edit->get_text() == "thiis\nme\ntext to\n\ncut");
+ CHECK(code_edit->get_caret_count() == 4);
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line(0) == 1);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->get_caret_line(1) == 3);
+ CHECK(code_edit->get_caret_column(1) == 0);
+ CHECK(code_edit->get_caret_line(2) == 0);
+ CHECK(code_edit->get_caret_column(2) == 3);
+ CHECK(code_edit->get_caret_line(3) == 2);
+ CHECK(code_edit->get_caret_column(3) == 0);
+ }
+
+ SUBCASE("[SceneTree][CodeEdit] new line") {
+ // Add a new line.
+ code_edit->set_text("test new line");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(13);
+ SEND_GUI_ACTION("ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test new line");
+ CHECK(code_edit->get_line(1) == "");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Split line with new line.
+ code_edit->set_text("test new line");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION("ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test ");
+ CHECK(code_edit->get_line(1) == "new line");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Delete selection and split with new line.
+ code_edit->set_text("test new line");
+ code_edit->select(0, 0, 0, 5);
+ SEND_GUI_ACTION("ui_text_newline");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "new line");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Blank new line below with selection should not split.
+ code_edit->set_text("test new line");
+ code_edit->select(0, 0, 0, 5);
+ SEND_GUI_ACTION("ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test new line");
+ CHECK(code_edit->get_line(1) == "");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Blank new line above with selection should not split.
+ code_edit->set_text("test new line");
+ code_edit->select(0, 0, 0, 5);
+ SEND_GUI_ACTION("ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test new line");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Multiple new lines with multiple carets.
+ code_edit->set_text("test new line");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(5);
+ code_edit->add_caret(0, 8);
+ SEND_GUI_ACTION("ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test ");
+ CHECK(code_edit->get_line(1) == "new");
+ CHECK(code_edit->get_line(2) == " line");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->get_caret_line(0) == 1);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->get_caret_line(1) == 2);
+ CHECK(code_edit->get_caret_column(1) == 0);
+
+ // Multiple blank new lines with multiple carets.
+ code_edit->set_text("test new line");
+ code_edit->remove_secondary_carets();
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(5);
+ code_edit->add_caret(0, 8);
+ SEND_GUI_ACTION("ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test new line");
+ CHECK(code_edit->get_line(1) == "");
+ CHECK(code_edit->get_line(2) == "");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->get_caret_line(0) == 2);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->get_caret_line(1) == 1);
+ CHECK(code_edit->get_caret_column(1) == 0);
+
+ // Multiple new lines above with multiple carets.
+ code_edit->set_text("test new line");
+ code_edit->remove_secondary_carets();
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(5);
+ code_edit->add_caret(0, 8);
+ SEND_GUI_ACTION("ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "");
+ CHECK(code_edit->get_line(2) == "test new line");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->get_caret_line(1) == 1);
+ CHECK(code_edit->get_caret_column(1) == 0);
+
+ // See '[CodeEdit] auto indent' tests for tests about new line with indentation.
+ }
+
+ SUBCASE("[SceneTree][CodeEdit] move lines up") {
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+
+ // Move line up with caret on it.
+ code_edit->set_caret_line(2);
+ code_edit->set_caret_column(1);
+ code_edit->move_lines_up();
+ CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Undo.
+ code_edit->undo();
+ CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround");
+ CHECK(code_edit->get_caret_line() == 2);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Redo.
+ code_edit->redo();
+ CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Does nothing at the first line.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(1);
+ code_edit->move_lines_up();
+ CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Works on empty line.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->set_caret_line(3);
+ code_edit->set_caret_column(0);
+ code_edit->move_lines_up();
+ CHECK(code_edit->get_text() == "test\nlines\n\nto\nmove\naround");
+ CHECK(code_edit->get_caret_line() == 2);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Move multiple lines up with selection.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(4, 0, 5, 1);
+ code_edit->move_lines_up();
+ CHECK(code_edit->get_text() == "test\nlines\nto\nmove\naround\n");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 3);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 4);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Does not affect line with selection end at column 0.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(4, 0, 5, 0);
+ code_edit->move_lines_up();
+ CHECK(code_edit->get_text() == "test\nlines\nto\nmove\n\naround");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 3);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 4);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Move multiple lines up with selection, right to left selection.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(5, 2, 4, 1);
+ code_edit->move_lines_up();
+ CHECK(code_edit->get_text() == "test\nlines\nto\nmove\naround\n");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 4);
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK(code_edit->get_caret_line() == 3);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Move multiple lines with multiple carets. A line with multiple carets is only moved once.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(5, 2, 5, 4);
+ code_edit->add_caret(4, 0);
+ code_edit->add_caret(4, 4);
+ code_edit->move_lines_up();
+ CHECK(code_edit->get_text() == "test\nlines\nto\nmove\naround\n");
+ CHECK(code_edit->get_caret_count() == 3);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 4);
+ CHECK(code_edit->get_selection_origin_column(0) == 2);
+ CHECK(code_edit->get_caret_line(0) == 4);
+ CHECK(code_edit->get_caret_column(0) == 4);
+ CHECK_FALSE(code_edit->has_selection(1));
+ CHECK(code_edit->get_caret_line(1) == 3);
+ CHECK(code_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(code_edit->has_selection(2));
+ CHECK(code_edit->get_caret_line(2) == 3);
+ CHECK(code_edit->get_caret_column(2) == 4);
+
+ // Move multiple separate lines with multiple selections.
+ code_edit->remove_secondary_carets();
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(2, 2, 1, 4);
+ code_edit->add_caret(5, 0);
+ code_edit->select(5, 0, 5, 1, 1);
+ code_edit->move_lines_up();
+ CHECK(code_edit->get_text() == "lines\nto\ntest\n\naround\nmove");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 1);
+ CHECK(code_edit->get_selection_origin_column(0) == 2);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 4);
+ CHECK(code_edit->has_selection(1));
+ CHECK(code_edit->get_selection_origin_line(1) == 4);
+ CHECK(code_edit->get_selection_origin_column(1) == 0);
+ CHECK(code_edit->get_caret_line(1) == 4);
+ CHECK(code_edit->get_caret_column(1) == 1);
+ }
+
+ SUBCASE("[SceneTree][CodeEdit] move lines down") {
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+
+ // Move line down with caret on it.
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(1);
+ code_edit->move_lines_down();
+ CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround");
+ CHECK(code_edit->get_caret_line() == 2);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Undo.
+ code_edit->undo();
+ CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Redo.
+ code_edit->redo();
+ CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround");
+ CHECK(code_edit->get_caret_line() == 2);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Does nothing at the last line.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->set_caret_line(5);
+ code_edit->set_caret_column(1);
+ code_edit->move_lines_down();
+ CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround");
+ CHECK(code_edit->get_caret_line() == 5);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Works on empty line.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->set_caret_line(3);
+ code_edit->set_caret_column(0);
+ code_edit->move_lines_down();
+ CHECK(code_edit->get_text() == "test\nlines\nto\nmove\n\naround");
+ CHECK(code_edit->get_caret_line() == 4);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Move multiple lines down with selection.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(1, 0, 2, 1);
+ code_edit->move_lines_down();
+ CHECK(code_edit->get_text() == "test\n\nlines\nto\nmove\naround");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 2);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 3);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Does not affect line with selection end at column 0.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(1, 0, 2, 0);
+ code_edit->move_lines_down();
+ CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 2);
+ CHECK(code_edit->get_selection_origin_column() == 0);
+ CHECK(code_edit->get_caret_line() == 3);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Move multiple lines down with selection, right to left selection.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(2, 2, 1, 1);
+ code_edit->move_lines_down();
+ CHECK(code_edit->get_text() == "test\n\nlines\nto\nmove\naround");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 3);
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK(code_edit->get_caret_line() == 2);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Move multiple lines with multiple carets. A line with multiple carets is only moved once.
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(1, 2, 1, 4);
+ code_edit->add_caret(0, 0);
+ code_edit->add_caret(0, 1);
+ code_edit->move_lines_down();
+ CHECK(code_edit->get_text() == "to\ntest\nlines\n\nmove\naround");
+ CHECK(code_edit->get_caret_count() == 3);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 2);
+ CHECK(code_edit->get_selection_origin_column(0) == 2);
+ CHECK(code_edit->get_caret_line(0) == 2);
+ CHECK(code_edit->get_caret_column(0) == 4);
+ CHECK_FALSE(code_edit->has_selection(1));
+ CHECK(code_edit->get_caret_line(1) == 1);
+ CHECK(code_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(code_edit->has_selection(2));
+ CHECK(code_edit->get_caret_line(2) == 1);
+ CHECK(code_edit->get_caret_column(2) == 1);
+
+ // Move multiple separate lines with multiple selections.
+ code_edit->remove_secondary_carets();
+ code_edit->set_text("test\nlines\nto\n\nmove\naround");
+ code_edit->select(0, 2, 1, 4);
+ code_edit->add_caret(4, 0);
+ code_edit->select(4, 0, 4, 2, 1);
+ code_edit->move_lines_down();
+ CHECK(code_edit->get_text() == "to\ntest\nlines\n\naround\nmove");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 1);
+ CHECK(code_edit->get_selection_origin_column(0) == 2);
+ CHECK(code_edit->get_caret_line(0) == 2);
+ CHECK(code_edit->get_caret_column(0) == 4);
+ CHECK(code_edit->has_selection(1));
+ CHECK(code_edit->get_selection_origin_line(1) == 5);
+ CHECK(code_edit->get_selection_origin_column(1) == 0);
+ CHECK(code_edit->get_caret_line(1) == 5);
+ CHECK(code_edit->get_caret_column(1) == 2);
+ }
+
+ SUBCASE("[SceneTree][CodeEdit] delete lines") {
+ code_edit->set_text("test\nlines\nto\n\ndelete");
+
+ // Delete line with caret on it.
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(1);
+ code_edit->delete_lines();
+ CHECK(code_edit->get_text() == "test\nto\n\ndelete");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Undo.
+ code_edit->undo();
+ CHECK(code_edit->get_text() == "test\nlines\nto\n\ndelete");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Redo.
+ code_edit->redo();
+ CHECK(code_edit->get_text() == "test\nto\n\ndelete");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Delete empty line.
+ code_edit->set_caret_line(2);
+ code_edit->set_caret_column(0);
+ code_edit->delete_lines();
+ CHECK(code_edit->get_text() == "test\nto\ndelete");
+ CHECK(code_edit->get_caret_line() == 2);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Deletes only one line when there are multiple carets on it. Carets move down and the column gets clamped.
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(0);
+ code_edit->add_caret(0, 1);
+ code_edit->add_caret(0, 4);
+ code_edit->delete_lines();
+ CHECK(code_edit->get_text() == "to\ndelete");
+ CHECK(code_edit->get_caret_count() == 3);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->get_caret_line(1) == 0);
+ CHECK(code_edit->get_caret_column(1) == 1);
+ CHECK(code_edit->get_caret_line(2) == 0);
+ CHECK(code_edit->get_caret_column(2) == 2);
+
+ // Delete multiple lines with selection.
+ code_edit->remove_secondary_carets();
+ code_edit->set_text("test\nlines\nto\n\ndelete");
+ code_edit->select(0, 1, 2, 1);
+ code_edit->delete_lines();
+ CHECK(code_edit->get_text() == "\ndelete");
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Does not affect line with selection end at column 0.
+ code_edit->set_text("test\nlines\nto\n\ndelete");
+ code_edit->select(0, 1, 1, 0);
+ code_edit->delete_lines();
+ CHECK(code_edit->get_text() == "lines\nto\n\ndelete");
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Delete multiple lines with multiple carets.
+ code_edit->set_text("test\nlines\nto\n\ndelete");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(2);
+ code_edit->add_caret(1, 0);
+ code_edit->add_caret(4, 5);
+ code_edit->delete_lines();
+ CHECK(code_edit->get_text() == "to\n");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 0);
+ CHECK(code_edit->get_caret_line(1) == 1);
+ CHECK(code_edit->get_caret_column(1) == 0);
+
+ // Delete multiple separate lines with multiple selections.
+ code_edit->remove_secondary_carets();
+ code_edit->set_text("test\nlines\nto\n\ndelete");
+ code_edit->add_caret(4, 5);
+ code_edit->select(0, 1, 1, 1);
+ code_edit->select(5, 5, 4, 0, 1);
+ code_edit->delete_lines();
+ CHECK(code_edit->get_text() == "to\n");
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->get_caret_line(0) == 0);
+ CHECK(code_edit->get_caret_column(0) == 1);
+ CHECK(code_edit->get_caret_line(1) == 1);
+ CHECK(code_edit->get_caret_column(1) == 0);
+
+ // Deletes contents when there is only one line.
+ code_edit->remove_secondary_carets();
+ code_edit->set_text("test");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(4);
+ code_edit->delete_lines();
+ CHECK(code_edit->get_text() == "");
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 0);
+ }
+
+ SUBCASE("[SceneTree][CodeEdit] duplicate selection") {
+ code_edit->set_text("test\nlines\nto\n\nduplicate");
+
+ // Duplicate selected text.
+ code_edit->select(0, 1, 1, 2);
+ code_edit->duplicate_selection();
+ CHECK(code_edit->get_text() == "test\nliest\nlines\nto\n\nduplicate");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK(code_edit->get_caret_line() == 2);
+ CHECK(code_edit->get_caret_column() == 2);
+
+ // Undo.
+ code_edit->undo();
+ CHECK(code_edit->get_text() == "test\nlines\nto\n\nduplicate");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 0);
+ CHECK(code_edit->get_selection_origin_column() == 1);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 2);
+
+ // Redo.
+ code_edit->redo();
+ CHECK(code_edit->get_text() == "test\nliest\nlines\nto\n\nduplicate");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 1);
+ CHECK(code_edit->get_selection_origin_column() == 2);
+ CHECK(code_edit->get_caret_line() == 2);
+ CHECK(code_edit->get_caret_column() == 2);
+
+ // Duplicate selected text, right to left selection.
+ code_edit->set_text("test\nlines\nto\n\nduplicate");
+ code_edit->select(1, 1, 0, 2);
+ code_edit->duplicate_selection();
+ CHECK(code_edit->get_text() == "test\nlst\nlines\nto\n\nduplicate");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 2);
+ CHECK(code_edit->get_selection_origin_column() == 1);
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 1);
+
+ // Duplicate line if there is no selection.
+ code_edit->deselect();
+ code_edit->set_text("test\nlines\nto\n\nduplicate");
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(2);
+ code_edit->duplicate_selection();
+ CHECK(code_edit->get_text() == "test\nlines\nlines\nto\n\nduplicate");
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line() == 2);
+ CHECK(code_edit->get_caret_column() == 2);
+
+ // Duplicate multiple lines.
+ code_edit->deselect();
+ code_edit->set_text("test\nlines\nto\n\nduplicate");
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(2);
+ code_edit->add_caret(5, 0);
+ code_edit->add_caret(0, 4);
+ code_edit->duplicate_selection();
+ CHECK(code_edit->get_text() == "test\ntest\nlines\nlines\nto\n\nduplicate\nduplicate");
+ CHECK(code_edit->get_caret_count() == 3);
+ CHECK_FALSE(code_edit->has_selection());
+ CHECK(code_edit->get_caret_line(0) == 3);
+ CHECK(code_edit->get_caret_column(0) == 2);
+ CHECK(code_edit->get_caret_line(1) == 7);
+ CHECK(code_edit->get_caret_column(1) == 0);
+ CHECK(code_edit->get_caret_line(2) == 1);
+ CHECK(code_edit->get_caret_column(2) == 4);
+
+ // Duplicate multiple separate selections.
+ code_edit->remove_secondary_carets();
+ code_edit->set_text("test\nlines\nto\n\nduplicate");
+ code_edit->add_caret(4, 4);
+ code_edit->add_caret(0, 1);
+ code_edit->add_caret(0, 4);
+ code_edit->select(2, 0, 2, 1, 0);
+ code_edit->select(3, 0, 4, 4, 1);
+ code_edit->select(0, 1, 0, 0, 2);
+ code_edit->select(0, 2, 0, 4, 3);
+ code_edit->duplicate_selection();
+ CHECK(code_edit->get_text() == "ttestst\nlines\ntto\n\ndupl\nduplicate");
+ CHECK(code_edit->get_caret_count() == 4);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 2);
+ CHECK(code_edit->get_selection_origin_column(0) == 1);
+ CHECK(code_edit->get_caret_line(0) == 2);
+ CHECK(code_edit->get_caret_column(0) == 2);
+ CHECK(code_edit->has_selection(1));
+ CHECK(code_edit->get_selection_origin_line(1) == 4);
+ CHECK(code_edit->get_selection_origin_column(1) == 4);
+ CHECK(code_edit->get_caret_line(1) == 5);
+ CHECK(code_edit->get_caret_column(1) == 4);
+ CHECK(code_edit->has_selection(2));
+ CHECK(code_edit->get_selection_origin_line(2) == 0);
+ CHECK(code_edit->get_selection_origin_column(2) == 2);
+ CHECK(code_edit->get_caret_line(2) == 0);
+ CHECK(code_edit->get_caret_column(2) == 1);
+ CHECK(code_edit->has_selection(3));
+ CHECK(code_edit->get_selection_origin_line(3) == 0);
+ CHECK(code_edit->get_selection_origin_column(3) == 5);
+ CHECK(code_edit->get_caret_line(3) == 0);
+ CHECK(code_edit->get_caret_column(3) == 7);
+
+ // Duplicate adjacent selections.
+ code_edit->remove_secondary_carets();
+ code_edit->set_text("test\nlines\nto\n\nduplicate");
+ code_edit->add_caret(1, 2);
+ code_edit->select(1, 0, 1, 1, 0);
+ code_edit->select(1, 1, 1, 4, 1);
+ code_edit->duplicate_selection();
+ CHECK(code_edit->get_text() == "test\nllineines\nto\n\nduplicate");
+ CHECK(code_edit->get_caret_count() == 2);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 1);
+ CHECK(code_edit->get_selection_origin_column(0) == 1);
+ CHECK(code_edit->get_caret_line(0) == 1);
+ CHECK(code_edit->get_caret_column(0) == 2);
+ CHECK(code_edit->has_selection(1));
+ CHECK(code_edit->get_selection_origin_line(1) == 1);
+ CHECK(code_edit->get_selection_origin_column(1) == 5);
+ CHECK(code_edit->get_caret_line(1) == 1);
+ CHECK(code_edit->get_caret_column(1) == 8);
+
+ // Duplicate lines then duplicate selections when there are both selections and non-selections.
+ code_edit->remove_secondary_carets();
+ code_edit->set_text("test duplicate");
+ code_edit->select(0, 14, 0, 13, 0);
+ code_edit->add_caret(0, 8);
+ code_edit->add_caret(0, 4);
+ code_edit->select(0, 2, 0, 4, 2);
+ code_edit->duplicate_selection();
+ CHECK(code_edit->get_text() == "test duplicate\ntestst duplicatee");
+ CHECK(code_edit->get_caret_count() == 3);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 1);
+ CHECK(code_edit->get_selection_origin_column(0) == 17);
+ CHECK(code_edit->get_caret_line(0) == 1);
+ CHECK(code_edit->get_caret_column(0) == 16);
+ CHECK_FALSE(code_edit->has_selection(1));
+ CHECK(code_edit->get_caret_line(1) == 1);
+ CHECK(code_edit->get_caret_column(1) == 10);
+ CHECK(code_edit->has_selection(2));
+ CHECK(code_edit->get_selection_origin_line(2) == 1);
+ CHECK(code_edit->get_selection_origin_column(2) == 4);
+ CHECK(code_edit->get_caret_line(2) == 1);
+ CHECK(code_edit->get_caret_column(2) == 6);
+ }
+
+ SUBCASE("[SceneTree][CodeEdit] duplicate lines") {
+ String reset_text = R"(extends Node
+
+func _ready():
+ var a := len(OS.get_cmdline_args())
+ var b := get_child_count()
+ var c := a + b
+ for i in range(c):
+ print("This is the solution: ", sin(i))
+ var pos = get_index() - 1
+ print("Make sure this exits: %b" % pos)
+)";
+
+ code_edit->set_text(reset_text);
- code_edit->set_text(R"(extends Node
+ // Duplicate a single line without selection.
+ code_edit->set_caret_line(0);
+ code_edit->duplicate_lines();
+ CHECK(code_edit->get_line(0) == "extends Node");
+ CHECK(code_edit->get_line(1) == "extends Node");
+ CHECK(code_edit->get_line(2) == "");
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 0);
+
+ // Duplicate multiple lines with selection.
+ code_edit->set_text(reset_text);
+ code_edit->select(4, 8, 6, 15);
+ code_edit->duplicate_lines();
+ CHECK(code_edit->get_text() == R"(extends Node
+
+func _ready():
+ var a := len(OS.get_cmdline_args())
+ var b := get_child_count()
+ var c := a + b
+ for i in range(c):
+ var b := get_child_count()
+ var c := a + b
+ for i in range(c):
+ print("This is the solution: ", sin(i))
+ var pos = get_index() - 1
+ print("Make sure this exits: %b" % pos)
+)");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 7);
+ CHECK(code_edit->get_selection_origin_column() == 8);
+ CHECK(code_edit->get_caret_line() == 9);
+ CHECK(code_edit->get_caret_column() == 15);
+
+ // Duplicate multiple lines with right to left selection.
+ code_edit->set_text(reset_text);
+ code_edit->select(6, 15, 4, 8);
+ code_edit->duplicate_lines();
+ CHECK(code_edit->get_text() == R"(extends Node
func _ready():
var a := len(OS.get_cmdline_args())
var b := get_child_count()
var c := a + b
for i in range(c):
+ var b := get_child_count()
+ var c := a + b
+ for i in range(c):
print("This is the solution: ", sin(i))
var pos = get_index() - 1
print("Make sure this exits: %b" % pos)
)");
+ CHECK(code_edit->has_selection());
+ CHECK(code_edit->get_selection_origin_line() == 9);
+ CHECK(code_edit->get_selection_origin_column() == 15);
+ CHECK(code_edit->get_caret_line() == 7);
+ CHECK(code_edit->get_caret_column() == 8);
+
+ // Duplicate single lines with multiple carets. Multiple carets on a single line only duplicate once.
+ code_edit->remove_secondary_carets();
+ code_edit->deselect();
+ code_edit->set_text(reset_text);
+ code_edit->set_caret_line(3);
+ code_edit->set_caret_column(1);
+ code_edit->add_caret(5, 1);
+ code_edit->add_caret(5, 5);
+ code_edit->add_caret(4, 2);
+ code_edit->duplicate_lines();
+ CHECK(code_edit->get_text() == R"(extends Node
- /* Duplicate a single line without selection. */
- code_edit->set_caret_line(0);
- code_edit->duplicate_lines();
- CHECK(code_edit->get_line(0) == "extends Node");
- CHECK(code_edit->get_line(1) == "extends Node");
- CHECK(code_edit->get_line(2) == "");
-
- /* Duplicate multiple lines with selection. */
- code_edit->set_caret_line(6);
- code_edit->set_caret_column(15);
- code_edit->select(4, 8, 6, 15);
- code_edit->duplicate_lines();
- CHECK(code_edit->get_line(6) == "\tvar c := a + b");
- CHECK(code_edit->get_line(7) == "\tvar a := len(OS.get_cmdline_args())");
- CHECK(code_edit->get_line(8) == "\tvar b := get_child_count()");
- CHECK(code_edit->get_line(9) == "\tvar c := a + b");
- CHECK(code_edit->get_line(10) == "\tfor i in range(c):");
-
- /* Duplicate single lines with multiple carets. */
- code_edit->deselect();
- code_edit->set_caret_line(10);
- code_edit->set_caret_column(1);
- code_edit->add_caret(11, 2);
- code_edit->add_caret(12, 1);
- code_edit->duplicate_lines();
- CHECK(code_edit->get_line(9) == "\tvar c := a + b");
- CHECK(code_edit->get_line(10) == "\tfor i in range(c):");
- CHECK(code_edit->get_line(11) == "\tfor i in range(c):");
- CHECK(code_edit->get_line(12) == "\t\tprint(\"This is the solution: \", sin(i))");
- CHECK(code_edit->get_line(13) == "\t\tprint(\"This is the solution: \", sin(i))");
- CHECK(code_edit->get_line(14) == "\tvar pos = get_index() - 1");
- CHECK(code_edit->get_line(15) == "\tvar pos = get_index() - 1");
- CHECK(code_edit->get_line(16) == "\tprint(\"Make sure this exits: %b\" % pos)");
-
- /* Duplicate multiple lines with multiple carets. */
- code_edit->select(0, 0, 1, 2, 0);
- code_edit->select(3, 0, 4, 2, 1);
- code_edit->select(16, 0, 17, 0, 2);
- code_edit->set_caret_line(1, false, true, 0, 0);
- code_edit->set_caret_column(2, false, 0);
- code_edit->set_caret_line(4, false, true, 0, 1);
- code_edit->set_caret_column(2, false, 1);
- code_edit->set_caret_line(17, false, true, 0, 2);
- code_edit->set_caret_column(0, false, 2);
- code_edit->duplicate_lines();
- CHECK(code_edit->get_line(1) == "extends Node");
- CHECK(code_edit->get_line(2) == "extends Node");
- CHECK(code_edit->get_line(3) == "extends Node");
- CHECK(code_edit->get_line(4) == "");
- CHECK(code_edit->get_line(6) == "\tvar a := len(OS.get_cmdline_args())");
- CHECK(code_edit->get_line(7) == "func _ready():");
- CHECK(code_edit->get_line(8) == "\tvar a := len(OS.get_cmdline_args())");
- CHECK(code_edit->get_line(9) == "\tvar b := get_child_count()");
- CHECK(code_edit->get_line(20) == "\tprint(\"Make sure this exits: %b\" % pos)");
- CHECK(code_edit->get_line(21) == "");
- CHECK(code_edit->get_line(22) == "\tprint(\"Make sure this exits: %b\" % pos)");
- CHECK(code_edit->get_line(23) == "");
+func _ready():
+ var a := len(OS.get_cmdline_args())
+ var a := len(OS.get_cmdline_args())
+ var b := get_child_count()
+ var b := get_child_count()
+ var c := a + b
+ var c := a + b
+ for i in range(c):
+ print("This is the solution: ", sin(i))
+ var pos = get_index() - 1
+ print("Make sure this exits: %b" % pos)
+)");
+ CHECK(code_edit->get_caret_count() == 4);
+ CHECK_FALSE(code_edit->has_selection(0));
+ CHECK(code_edit->get_caret_line(0) == 4);
+ CHECK(code_edit->get_caret_column(0) == 1);
+ CHECK_FALSE(code_edit->has_selection(1));
+ CHECK(code_edit->get_caret_line(1) == 8);
+ CHECK(code_edit->get_caret_column(1) == 1);
+ CHECK_FALSE(code_edit->has_selection(2));
+ CHECK(code_edit->get_caret_line(2) == 8);
+ CHECK(code_edit->get_caret_column(2) == 5);
+ CHECK_FALSE(code_edit->has_selection(3));
+ CHECK(code_edit->get_caret_line(3) == 6);
+ CHECK(code_edit->get_caret_column(3) == 2);
+
+ // Duplicate multiple lines with multiple selections.
+ code_edit->remove_secondary_carets();
+ code_edit->set_text(reset_text);
+ code_edit->add_caret(4, 2);
+ code_edit->add_caret(6, 0);
+ code_edit->add_caret(7, 8);
+ code_edit->select(0, 0, 2, 5, 0);
+ code_edit->select(3, 0, 4, 2, 1);
+ code_edit->select(7, 1, 6, 0, 2);
+ code_edit->select(7, 3, 7, 8, 3);
+ code_edit->duplicate_lines();
+ CHECK(code_edit->get_text() == R"(extends Node
+
+func _ready():
+extends Node
+
+func _ready():
+ var a := len(OS.get_cmdline_args())
+ var b := get_child_count()
+ var a := len(OS.get_cmdline_args())
+ var b := get_child_count()
+ var c := a + b
+ for i in range(c):
+ print("This is the solution: ", sin(i))
+ for i in range(c):
+ print("This is the solution: ", sin(i))
+ var pos = get_index() - 1
+ print("Make sure this exits: %b" % pos)
+)");
+ CHECK(code_edit->get_caret_count() == 4);
+ CHECK(code_edit->has_selection(0));
+ CHECK(code_edit->get_selection_origin_line(0) == 3);
+ CHECK(code_edit->get_selection_origin_column(0) == 0);
+ CHECK(code_edit->get_caret_line(0) == 5);
+ CHECK(code_edit->get_caret_column(0) == 5);
+
+ CHECK(code_edit->has_selection(1));
+ CHECK(code_edit->get_selection_origin_line(1) == 8);
+ CHECK(code_edit->get_selection_origin_column(1) == 0);
+ CHECK(code_edit->get_caret_line(1) == 9);
+ CHECK(code_edit->get_caret_column(1) == 2);
+
+ CHECK(code_edit->has_selection(2));
+ CHECK(code_edit->get_selection_origin_line(2) == 14);
+ CHECK(code_edit->get_selection_origin_column(2) == 1);
+ CHECK(code_edit->get_caret_line(2) == 13);
+ CHECK(code_edit->get_caret_column(2) == 0);
+
+ CHECK(code_edit->has_selection(3));
+ CHECK(code_edit->get_selection_origin_line(3) == 14);
+ CHECK(code_edit->get_selection_origin_column(3) == 3);
+ CHECK(code_edit->get_caret_line(3) == 14);
+ CHECK(code_edit->get_caret_column(3) == 8);
+ }
memdelete(code_edit);
}
diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h
index 8577dd7148..246d869687 100644
--- a/tests/scene/test_text_edit.h
+++ b/tests/scene/test_text_edit.h
@@ -36,6 +36,23 @@
#include "tests/test_macros.h"
namespace TestTextEdit {
+static inline Array build_array() {
+ return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+ Array a = build_array(Fargs...);
+ a.push_front(item);
+ return a;
+}
+static inline Array reverse_nested(Array array) {
+ Array reversed_array = array.duplicate(true);
+ reversed_array.reverse();
+ for (int i = 0; i < reversed_array.size(); i++) {
+ ((Array)reversed_array[i]).reverse();
+ }
+ return reversed_array;
+}
TEST_CASE("[SceneTree][TextEdit] text entry") {
SceneTree::get_singleton()->get_root()->set_physics_object_picking(false);
@@ -52,12 +69,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_WATCH(text_edit, "lines_edited_from");
SIGNAL_WATCH(text_edit, "caret_changed");
- Array args1;
- args1.push_back(0);
- args1.push_back(0);
- Array lines_edited_args;
- lines_edited_args.push_back(args1);
- lines_edited_args.push_back(args1.duplicate());
+ Array lines_edited_args = build_array(build_array(0, 0), build_array(0, 0));
SUBCASE("[TextEdit] clear and set text") {
// "text_changed" should not be emitted on clear / set.
@@ -119,13 +131,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_set");
- // Clear.
+ // Can clear even if not editable.
text_edit->set_editable(false);
- Array lines_edited_clear_args;
- Array new_args = args1.duplicate();
- new_args[0] = 1;
- lines_edited_clear_args.push_back(new_args);
+ Array lines_edited_clear_args = build_array(build_array(1, 0));
text_edit->clear();
MessageQueue::get_singleton()->flush();
@@ -210,6 +219,321 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK_FALSE("text_changed");
}
+ SUBCASE("[TextEdit] insert text") {
+ // insert_text is 0 indexed.
+ ERR_PRINT_OFF;
+ text_edit->insert_text("test", 1, 0);
+ ERR_PRINT_ON;
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Insert text when there is no text.
+ lines_edited_args = build_array(build_array(0, 0));
+
+ text_edit->insert_text("tes", 0, 0);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "tes");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 3);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Insert multiple lines.
+ lines_edited_args = build_array(build_array(0, 1));
+
+ text_edit->insert_text("t\ninserting text", 0, 3);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\ninserting text");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 14);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Can insert even if not editable.
+ lines_edited_args = build_array(build_array(1, 1));
+
+ text_edit->set_editable(false);
+ text_edit->insert_text("mid", 1, 2);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\ninmidserting text");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 17);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->set_editable(true);
+
+ // Undo insert.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\ninserting text");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 14);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Redo insert.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\ninmidserting text");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 17);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Insert offsets carets after the edit.
+ text_edit->add_caret(1, 1);
+ text_edit->add_caret(1, 4);
+ text_edit->select(1, 4, 1, 6, 2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 2));
+
+ text_edit->insert_text("\n ", 1, 2);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\nin\n midserting text");
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 2);
+ CHECK(text_edit->get_caret_column(0) == 16);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 1);
+ CHECK(text_edit->has_selection(2));
+ CHECK(text_edit->get_caret_line(2) == 2);
+ CHECK(text_edit->get_caret_column(2) == 5);
+ CHECK(text_edit->get_selection_origin_line(2) == 2);
+ CHECK(text_edit->get_selection_origin_column(2) == 3);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+
+ // Insert text outside of selections.
+ text_edit->set_text("test text");
+ text_edit->add_caret(0, 8);
+ text_edit->select(0, 1, 0, 4, 0);
+ text_edit->select(0, 4, 0, 8, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0));
+
+ text_edit->insert_text("a", 0, 4, true, false);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testa text");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 1);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 9);
+ CHECK(text_edit->get_selection_origin_line(1) == 0);
+ CHECK(text_edit->get_selection_origin_column(1) == 5);
+
+ // Insert text to beginning of selections.
+ text_edit->set_text("test text");
+ text_edit->add_caret(0, 8);
+ text_edit->select(0, 1, 0, 4, 0);
+ text_edit->select(0, 4, 0, 8, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0));
+
+ text_edit->insert_text("a", 0, 4, false, false);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testa text");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 1);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 9);
+ CHECK(text_edit->get_selection_origin_line(1) == 0);
+ CHECK(text_edit->get_selection_origin_column(1) == 4);
+
+ // Insert text to end of selections.
+ text_edit->set_text("test text");
+ text_edit->add_caret(0, 8);
+ text_edit->select(0, 1, 0, 4, 0);
+ text_edit->select(0, 4, 0, 8, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0));
+
+ text_edit->insert_text("a", 0, 4, true, true);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testa text");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 1);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 9);
+ CHECK(text_edit->get_selection_origin_line(1) == 0);
+ CHECK(text_edit->get_selection_origin_column(1) == 5);
+
+ // Insert text inside of selections.
+ text_edit->set_text("test text");
+ text_edit->add_caret(0, 8);
+ text_edit->select(0, 1, 0, 4, 0);
+ text_edit->select(0, 4, 0, 8, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0));
+
+ text_edit->insert_text("a", 0, 4, false, true);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testa text");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 9);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 1);
+ }
+
+ SUBCASE("[TextEdit] remove text") {
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 2));
+
+ text_edit->set_text("test\nremoveing text\nthird line");
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ // remove_text is 0 indexed.
+ ERR_PRINT_OFF;
+ text_edit->remove_text(3, 0, 3, 4);
+ ERR_PRINT_ON;
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\nremoveing text\nthird line");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Remove multiple lines.
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(10);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(2, 1));
+
+ text_edit->remove_text(1, 9, 2, 2);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\nremoveingird line");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 17);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Can remove even if not editable.
+ lines_edited_args = build_array(build_array(1, 1));
+
+ text_edit->set_editable(false);
+ text_edit->remove_text(1, 5, 1, 6);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\nremovingird line");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 16);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->set_editable(true);
+
+ // Undo remove.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\nremoveingird line");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 17);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Redo remove.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\nremovingird line");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 16);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Remove collapses carets and offsets carets after the edit.
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(9);
+ text_edit->add_caret(1, 10);
+ text_edit->select(1, 10, 1, 13, 1);
+ text_edit->add_caret(1, 14);
+ text_edit->add_caret(1, 2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+
+ text_edit->remove_text(1, 8, 1, 11);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test\nremoving line");
+ // Caret 0 was merged into the selection.
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 10);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 8);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 11);
+ CHECK(text_edit->get_caret_line(2) == 1);
+ CHECK(text_edit->get_caret_column(2) == 2);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->remove_secondary_carets();
+ }
+
SUBCASE("[TextEdit] set and get line") {
// Set / Get line is 0 indexed.
text_edit->set_line(1, "test");
@@ -225,6 +549,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_text() == "test");
CHECK(text_edit->get_line(0) == "test");
CHECK(text_edit->get_line(1) == "");
+ CHECK(text_edit->get_line_count() == 1);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_set");
@@ -233,14 +558,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
// Setting to a longer line, caret and selections should be preserved.
text_edit->select_all();
MessageQueue::get_singleton()->flush();
- CHECK(text_edit->has_selection());
- SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_DISCARD("caret_changed");
text_edit->set_line(0, "test text");
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_line(0) == "test text");
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "test");
+ CHECK(text_edit->get_selection_origin_column() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("caret_changed");
@@ -299,12 +625,84 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("text_set");
-
ERR_PRINT_ON;
+
+ // Both ends of selection are adjusted and deselects.
+ text_edit->set_text("test text");
+ text_edit->select(0, 8, 0, 6);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_line(0, "test");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_line(0) == "test");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_column() == 4);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Multiple carets adjust to keep visual position.
+ text_edit->set_text("test text");
+ text_edit->set_caret_column(2);
+ text_edit->add_caret(0, 0);
+ text_edit->add_caret(0, 1);
+ text_edit->add_caret(0, 6);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_line(0, "\tset line");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_line(0) == "\tset line");
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK_FALSE(text_edit->has_selection());
+ // In the default font, these are the same positions.
+ CHECK(text_edit->get_caret_column(0) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ // The previous caret at index 2 was merged.
+ CHECK(text_edit->get_caret_column(2) == 4);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->remove_secondary_carets();
+
+ // Insert multiple lines.
+ text_edit->set_text("test text\nsecond line");
+ text_edit->set_caret_column(5);
+ text_edit->add_caret(1, 6);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 1));
+
+ text_edit->set_line(0, "multiple\nlines");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "multiple\nlines\nsecond line");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 3); // In the default font, this is the same position.
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 6);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->remove_secondary_carets();
}
SUBCASE("[TextEdit] swap lines") {
- ((Array)lines_edited_args[1])[1] = 1;
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 1));
text_edit->set_text("testing\nswap");
MessageQueue::get_singleton()->flush();
@@ -317,15 +715,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
text_edit->set_caret_column(text_edit->get_line(0).length());
MessageQueue::get_singleton()->flush();
SIGNAL_CHECK("caret_changed", empty_signal_args);
+ // Emitted twice for each line.
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 0), build_array(1, 1), build_array(1, 1));
- ((Array)lines_edited_args[1])[1] = 0;
- Array swap_args;
- swap_args.push_back(1);
- swap_args.push_back(1);
- lines_edited_args.push_back(swap_args);
- lines_edited_args.push_back(swap_args);
-
- // Order does not matter. Should also work if not editable.
+ // Order does not matter. Works when not editable.
text_edit->set_editable(false);
text_edit->swap_lines(1, 0);
MessageQueue::get_singleton()->flush();
@@ -336,19 +729,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK_FALSE("text_set");
text_edit->set_editable(true);
- lines_edited_args.reverse();
-
- // Single undo/redo action
+ // Single undo/redo action.
text_edit->undo();
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "testing\nswap");
- SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_set");
- lines_edited_args.reverse();
-
text_edit->redo();
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "swap\ntesting");
@@ -361,36 +750,70 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
ERR_PRINT_OFF;
text_edit->swap_lines(-1, 0);
CHECK(text_edit->get_text() == "swap\ntesting");
- SIGNAL_CHECK_FALSE("lines_edited_from");
- SIGNAL_CHECK_FALSE("caret_changed");
- SIGNAL_CHECK_FALSE("text_changed");
- SIGNAL_CHECK_FALSE("text_set");
-
text_edit->swap_lines(0, -1);
CHECK(text_edit->get_text() == "swap\ntesting");
- SIGNAL_CHECK_FALSE("lines_edited_from");
- SIGNAL_CHECK_FALSE("caret_changed");
- SIGNAL_CHECK_FALSE("text_changed");
- SIGNAL_CHECK_FALSE("text_set");
-
text_edit->swap_lines(2, 0);
CHECK(text_edit->get_text() == "swap\ntesting");
+ text_edit->swap_lines(0, 2);
+ CHECK(text_edit->get_text() == "swap\ntesting");
+ MessageQueue::get_singleton()->flush();
SIGNAL_CHECK_FALSE("lines_edited_from");
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("text_set");
+ ERR_PRINT_ON;
+
+ // Carets are also swapped.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(2);
+ text_edit->select(0, 0, 0, 2);
+ text_edit->add_caret(1, 6);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 1), build_array(1, 1), build_array(0, 0), build_array(0, 0));
+
+ text_edit->swap_lines(0, 1);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testing\nswap");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 2);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 6);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->remove_secondary_carets();
+
+ // Swap non adjacent lines.
+ text_edit->insert_line_at(1, "new line");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(5);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testing\nnew line\nswap");
+ SIGNAL_DISCARD("caret_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("text_changed");
+ lines_edited_args = build_array(build_array(2, 2), build_array(2, 2), build_array(0, 0), build_array(0, 0));
text_edit->swap_lines(0, 2);
- CHECK(text_edit->get_text() == "swap\ntesting");
- SIGNAL_CHECK_FALSE("lines_edited_from");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "swap\nnew line\ntesting");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 5);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
SIGNAL_CHECK_FALSE("caret_changed");
- SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_set");
- ERR_PRINT_ON;
}
SUBCASE("[TextEdit] insert line at") {
- ((Array)lines_edited_args[1])[1] = 1;
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 1));
text_edit->set_text("testing\nswap");
MessageQueue::get_singleton()->flush();
@@ -407,9 +830,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_selection_to_line() == 1);
SIGNAL_CHECK("caret_changed", empty_signal_args);
- // Insert before should move caret and selection, and works when not editable.
+ // Insert line at inserts a line before and moves caret and selection. Works when not editable.
text_edit->set_editable(false);
- lines_edited_args.remove_at(0);
+ lines_edited_args = build_array(build_array(0, 1));
text_edit->insert_line_at(0, "new");
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "new\ntesting\nswap");
@@ -417,7 +840,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_caret_column() == text_edit->get_line(2).size() - 1);
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selection_from_line() == 1);
+ CHECK(text_edit->get_selection_from_column() == 0);
CHECK(text_edit->get_selection_to_line() == 2);
+ CHECK(text_edit->get_selection_to_column() == 4);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
@@ -425,19 +850,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
text_edit->set_editable(true);
// Can undo/redo as single action.
- ((Array)lines_edited_args[0])[0] = 1;
- ((Array)lines_edited_args[0])[1] = 0;
text_edit->undo();
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "testing\nswap");
CHECK(text_edit->has_selection());
- SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_set");
- ((Array)lines_edited_args[0])[0] = 0;
- ((Array)lines_edited_args[0])[1] = 1;
text_edit->redo();
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "new\ntesting\nswap");
@@ -454,9 +875,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_selection_from_line() == 0);
CHECK(text_edit->get_selection_to_line() == 2);
SIGNAL_CHECK_FALSE("caret_changed");
+ lines_edited_args = build_array(build_array(2, 3));
- ((Array)lines_edited_args[0])[0] = 2;
- ((Array)lines_edited_args[0])[1] = 3;
text_edit->insert_line_at(2, "after");
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap");
@@ -474,24 +894,222 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
ERR_PRINT_OFF;
text_edit->insert_line_at(-1, "after");
CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap");
+ text_edit->insert_line_at(4, "after");
+ CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap");
+ MessageQueue::get_singleton()->flush();
SIGNAL_CHECK_FALSE("lines_edited_from");
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("text_set");
+ ERR_PRINT_ON;
- text_edit->insert_line_at(4, "after");
- CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap");
+ // Can insert multiple lines.
+ text_edit->select(0, 1, 2, 2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(2, 4));
+
+ text_edit->insert_line_at(2, "multiple\nlines");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "new\ntesting\nmultiple\nlines\nafter\nswap");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 4);
+ CHECK(text_edit->get_caret_column() == 2);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 1);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ }
+
+ SUBCASE("[TextEdit] remove line at") {
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 5));
+ text_edit->set_text("testing\nremove line at\n\tremove\nlines\n\ntest");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testing\nremove line at\n\tremove\nlines\n\ntest");
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ // Remove line handles multiple carets.
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(0);
+ text_edit->add_caret(2, 7);
+ text_edit->select(2, 1, 2, 7, 1);
+ text_edit->add_caret(3, 1);
+ text_edit->add_caret(4, 5);
+ text_edit->add_caret(1, 5);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(3, 2));
+
+ text_edit->remove_line_at(2, true);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testing\nremove line at\nlines\n\ntest");
+ CHECK(text_edit->get_caret_count() == 5);
+ CHECK_FALSE(text_edit->has_selection(0)); // Same line.
+ CHECK(text_edit->get_caret_line(0) == 2);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->has_selection(1)); // Same line, clamped.
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 5);
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 3); // In the default font, this is the same position.
+ CHECK_FALSE(text_edit->has_selection(2)); // Moved up.
+ CHECK(text_edit->get_caret_line(2) == 2);
+ CHECK(text_edit->get_caret_column(2) == 1);
+ CHECK_FALSE(text_edit->has_selection(3)); // Moved up.
+ CHECK(text_edit->get_caret_line(3) == 3);
+ CHECK(text_edit->get_caret_column(3) == 0);
+ CHECK_FALSE(text_edit->has_selection(4)); // Didn't move.
+ CHECK(text_edit->get_caret_line(4) == 1);
+ CHECK(text_edit->get_caret_column(4) == 5);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->remove_secondary_carets();
+
+ // Remove first line.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(5);
+ text_edit->add_caret(4, 4);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 0));
+
+ text_edit->remove_line_at(0, false);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "remove line at\nlines\n\ntest");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->remove_secondary_carets();
+
+ // Remove empty line.
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(3, 2));
+
+ text_edit->remove_line_at(2, false);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "remove line at\nlines\ntest");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Remove last line.
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(2, 1));
+
+ text_edit->remove_line_at(2, true);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "remove line at\nlines");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Out of bounds.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ ERR_PRINT_OFF
+ text_edit->remove_line_at(2, true);
+ ERR_PRINT_ON
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "remove line at\nlines");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 2);
SIGNAL_CHECK_FALSE("lines_edited_from");
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("text_set");
- ERR_PRINT_ON;
+
+ // Remove regular line with move caret up and not editable.
+ text_edit->set_editable(false);
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 0));
+
+ text_edit->remove_line_at(1, false);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "remove line at");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 1); // In the default font, this is the same position.
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->set_editable(true);
+
+ // Undo.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "remove line at\nlines");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 2);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "remove line at");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 1);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Remove only line removes line content.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(10);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0));
+
+ text_edit->remove_line_at(0);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_line_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
}
- SUBCASE("[TextEdit] insert line at caret") {
- lines_edited_args.pop_back();
- ((Array)lines_edited_args[0])[1] = 1;
+ SUBCASE("[TextEdit] insert text at caret") {
+ lines_edited_args = build_array(build_array(0, 1));
+ // Insert text at caret can insert multiple lines.
text_edit->insert_text_at_caret("testing\nswap");
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "testing\nswap");
@@ -502,11 +1120,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_set");
+ // Text is inserted at caret.
text_edit->set_caret_line(0, false);
text_edit->set_caret_column(2);
+ MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("caret_changed");
- ((Array)lines_edited_args[0])[1] = 0;
+ lines_edited_args = build_array(build_array(0, 0));
text_edit->insert_text_at_caret("mid");
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "temidsting\nswap");
@@ -517,9 +1137,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_set");
+ // Selections are deleted then text is inserted. It also works even if not editable.
text_edit->select(0, 0, 0, text_edit->get_line(0).length());
CHECK(text_edit->has_selection());
- lines_edited_args.push_back(args1.duplicate());
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 0));
text_edit->set_editable(false);
text_edit->insert_text_at_caret("new line");
@@ -534,12 +1155,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK_FALSE("text_set");
text_edit->set_editable(true);
+ // Undo restores text and selection.
text_edit->undo();
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "temidsting\nswap");
CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 5);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 0);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("caret_changed", empty_signal_args);
@@ -589,24 +1213,19 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_WATCH(text_edit, "lines_edited_from");
SIGNAL_WATCH(text_edit, "caret_changed");
- Array args1;
- args1.push_back(0);
- args1.push_back(0);
- Array lines_edited_args;
- lines_edited_args.push_back(args1);
- lines_edited_args.push_back(args1.duplicate());
+ Array lines_edited_args = build_array(build_array(0, 0), build_array(0, 0));
SUBCASE("[TextEdit] select all") {
+ // Select when there is no text does not select.
text_edit->select_all();
CHECK_FALSE(text_edit->has_selection());
- ERR_PRINT_OFF;
- CHECK(text_edit->get_selection_from_line() == -1);
- CHECK(text_edit->get_selection_from_column() == -1);
- CHECK(text_edit->get_selection_to_line() == -1);
- CHECK(text_edit->get_selection_to_column() == -1);
+ CHECK(text_edit->get_selection_from_line() == 0);
+ CHECK(text_edit->get_selection_from_column() == 0);
+ CHECK(text_edit->get_selection_to_line() == 0);
+ CHECK(text_edit->get_selection_to_column() == 0);
CHECK(text_edit->get_selected_text() == "");
- ERR_PRINT_ON;
+ // Select all selects all text.
text_edit->set_text("test\nselection");
SEND_GUI_ACTION("ui_text_select_all");
CHECK(text_edit->get_viewport()->is_input_handled());
@@ -618,10 +1237,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_selection_to_line() == 1);
CHECK(text_edit->get_selection_to_column() == 9);
CHECK(text_edit->get_selection_mode() == TextEdit::SelectionMode::SELECTION_MODE_SHIFT);
+ CHECK(text_edit->is_caret_after_selection_origin());
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 9);
SIGNAL_CHECK("caret_changed", empty_signal_args);
+ // Cannot select when disabled.
text_edit->set_caret_line(0);
text_edit->set_caret_column(0);
text_edit->set_selecting_enabled(false);
@@ -654,6 +1275,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
+ // Select word under caret with multiple carets.
text_edit->select_word_under_caret();
CHECK(text_edit->has_selection(0));
CHECK(text_edit->get_selected_text(0) == "test");
@@ -675,6 +1297,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_caret_count() == 2);
+ // Select word under caret disables selection if there is already a selection.
text_edit->select_word_under_caret();
CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "");
@@ -703,6 +1326,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_selected_text() == "test\ntest");
SIGNAL_CHECK("caret_changed", empty_signal_args);
+ // Cannot select when disabled.
text_edit->set_selecting_enabled(false);
text_edit->select_word_under_caret();
CHECK_FALSE(text_edit->has_selection());
@@ -714,10 +1338,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK_FALSE("caret_changed");
text_edit->set_selecting_enabled(true);
- text_edit->set_caret_line(1, false, true, 0, 0);
+ // Select word under caret when there is no word does not select.
+ text_edit->set_caret_line(1, false, true, -1, 0);
text_edit->set_caret_column(5, false, 0);
-
- text_edit->set_caret_line(2, false, true, 0, 1);
+ text_edit->set_caret_line(2, false, true, -1, 1);
text_edit->set_caret_column(5, false, 1);
text_edit->select_word_under_caret();
@@ -739,7 +1363,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
text_edit->set_caret_column(0);
text_edit->set_caret_line(1);
- // First selection made by the implicit select_word_under_caret call
+ // First selection made by the implicit select_word_under_caret call.
text_edit->add_selection_for_next_occurrence();
CHECK(text_edit->get_caret_count() == 1);
CHECK(text_edit->get_selected_text(0) == "test");
@@ -780,7 +1404,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_caret_line(3) == 3);
CHECK(text_edit->get_caret_column(3) == 9);
- // A different word with a new manually added caret
+ // A different word with a new manually added caret.
text_edit->add_caret(2, 1);
text_edit->select(2, 0, 2, 4, 4);
CHECK(text_edit->get_selected_text(4) == "rand");
@@ -795,7 +1419,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_caret_line(5) == 3);
CHECK(text_edit->get_caret_column(5) == 22);
- // Make sure the previous selections are still active
+ // Make sure the previous selections are still active.
CHECK(text_edit->get_selected_text(0) == "test");
CHECK(text_edit->get_selected_text(1) == "test");
CHECK(text_edit->get_selected_text(2) == "test");
@@ -987,6 +1611,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT)
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "t");
+ CHECK(text_edit->is_caret_after_selection_origin());
#ifdef MACOS_ENABLED
SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT | KeyModifierMask::ALT)
@@ -995,10 +1620,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
#endif
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "test");
+ CHECK(text_edit->is_caret_after_selection_origin());
SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT)
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "tes");
+ CHECK(text_edit->is_caret_after_selection_origin());
#ifdef MACOS_ENABLED
SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT | KeyModifierMask::ALT)
@@ -1019,11 +1646,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT)
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "t");
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin());
SEND_GUI_KEY_EVENT(Key::LEFT)
CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "");
+ // Cannot select when disabled.
text_edit->set_selecting_enabled(false);
SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT)
CHECK_FALSE(text_edit->has_selection());
@@ -1032,46 +1661,120 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
}
SUBCASE("[TextEdit] mouse drag select") {
- /* Set size for mouse input. */
+ // Set size for mouse input.
text_edit->set_size(Size2(200, 200));
text_edit->set_text("this is some text\nfor selection");
text_edit->grab_focus();
MessageQueue::get_singleton()->flush();
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
- SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE);
+ // Click and drag to make a selection.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ // Add (2,0) to bring it past the center point of the grapheme and account for integer division flooring.
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "for s");
CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER);
- CHECK(text_edit->get_selection_from_line() == 1);
- CHECK(text_edit->get_selection_from_column() == 0);
- CHECK(text_edit->get_selection_to_line() == 1);
- CHECK(text_edit->get_selection_to_column() == 5);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 0);
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 5);
+ CHECK(text_edit->is_caret_after_selection_origin());
+ CHECK(text_edit->is_dragging_cursor());
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ // Releasing finishes.
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for s");
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for s");
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 5);
+ CHECK(text_edit->is_caret_after_selection_origin());
+
+ // Clicking clears selection.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 7);
+ // Cannot select when disabled, but caret still moves.
text_edit->set_selecting_enabled(false);
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
- SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 5);
text_edit->set_selecting_enabled(true);
+
+ // Only last caret is moved when adding a selection.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret(0, 15);
+ text_edit->select(0, 11, 0, 15, 1);
+ MessageQueue::get_singleton()->flush();
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selection_origin_line(1) == 0);
+ CHECK(text_edit->get_selection_origin_column(1) == 11);
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 15);
+
+ CHECK(text_edit->has_selection(2));
+ CHECK(text_edit->get_selected_text(2) == "for s");
+ CHECK(text_edit->get_selection_origin_line(2) == 1);
+ CHECK(text_edit->get_selection_origin_column(2) == 5);
+ CHECK(text_edit->get_caret_line(2) == 1);
+ CHECK(text_edit->get_caret_column(2) == 0);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(2));
+
+ // Overlapping carets and selections merges them.
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "s is some text\nfor s");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 5);
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 3);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin());
+
+ // Entering text stops selecting.
+ text_edit->insert_text_at_caret("a");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "thiaelection");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
}
SUBCASE("[TextEdit] mouse word select") {
- /* Set size for mouse input. */
+ // Set size for mouse input.
text_edit->set_size(Size2(200, 200));
- text_edit->set_text("this is some text\nfor selection");
+ text_edit->set_text("this is some text\nfor selection\n");
MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("caret_changed");
- SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE);
+ // Double click to select word.
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 2).get_center() + Point2i(2, 0), Key::NONE);
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "for");
CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
@@ -1081,9 +1784,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_selection_to_column() == 3);
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 3);
+ CHECK(text_edit->is_caret_after_selection_origin());
SIGNAL_CHECK("caret_changed", empty_signal_args);
- SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE);
+ // Moving mouse selects entire words at a time.
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 6).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "for selection");
CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
@@ -1093,15 +1798,116 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_selection_to_column() == 13);
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 13);
+ CHECK(text_edit->is_caret_after_selection_origin());
+ CHECK(text_edit->is_dragging_cursor());
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+
+ // Moving to a word before the initial selected word reverses selection direction and keeps the initial word selected.
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "some text\nfor");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
+ CHECK(text_edit->get_selection_from_line() == 0);
+ CHECK(text_edit->get_selection_from_column() == 8);
+ CHECK(text_edit->get_selection_to_line() == 1);
+ CHECK(text_edit->get_selection_to_column() == 3);
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 8);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin());
SIGNAL_CHECK("caret_changed", empty_signal_args);
- Point2i line_0 = text_edit->get_pos_at_line_column(0, 0);
- line_0.y /= 2;
- SEND_GUI_MOUSE_BUTTON_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ // Releasing finishes.
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "some text\nfor");
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 2).get_center(), MouseButtonMask::NONE, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "some text\nfor");
+ text_edit->deselect();
+
+ // Can start word select mode on an empty line.
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(2, 0).get_center() + Point2i(2, 0), Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 0);
+
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "selection\n");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 4);
+ CHECK(text_edit->get_selection_origin_line() == 2);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+
+ // Clicking clears selection.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+
+ // Can start word select mode when not on a word.
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(0, 12).get_center() + Point2i(2, 0), Key::NONE);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 12);
+
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == " text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 13);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 12);
+
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "some");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 8);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 12);
+
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 15).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+
+ // Add a new selection without affecting the old one.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 8).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER);
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "some");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 8);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 12);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "ele");
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 8);
+ CHECK(text_edit->get_selection_origin_line(1) == 1);
+ CHECK(text_edit->get_selection_origin_column(1) == 5);
+
+ // Shift + double click to extend selection and start word select mode.
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 8).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ text_edit->remove_secondary_carets();
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), Key::NONE | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == " text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 13);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 12);
+
+ // Cannot select when disabled, but caret still moves to end of word.
text_edit->set_selecting_enabled(false);
- SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE);
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 1).get_center() + Point2i(2, 0), Key::NONE);
CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 3);
@@ -1109,32 +1915,149 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
}
SUBCASE("[TextEdit] mouse line select") {
- /* Set size for mouse input. */
+ // Set size for mouse input.
text_edit->set_size(Size2(200, 200));
- text_edit->set_text("this is some text\nfor selection");
+ text_edit->set_text("this is some text\nfor selection\nwith 3 lines");
MessageQueue::get_singleton()->flush();
- SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE);
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ // Triple click to select line.
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 2).get_center(), Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 2).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK(text_edit->has_selection());
- CHECK(text_edit->get_selected_text() == "for selection");
+ CHECK(text_edit->get_selected_text() == "for selection\n");
CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE);
CHECK(text_edit->get_selection_from_line() == 1);
CHECK(text_edit->get_selection_from_column() == 0);
+ CHECK(text_edit->get_selection_to_line() == 2);
+ CHECK(text_edit->get_selection_to_column() == 0);
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->is_caret_after_selection_origin());
+
+ // Moving mouse selects entire lines at a time. Selecting above reverses the selection direction.
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "this is some text\nfor selection");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE);
+ CHECK(text_edit->get_selection_from_line() == 0);
+ CHECK(text_edit->get_selection_from_column() == 0);
CHECK(text_edit->get_selection_to_line() == 1);
CHECK(text_edit->get_selection_to_column() == 13);
- CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin());
+ CHECK(text_edit->is_dragging_cursor());
- Point2i line_0 = text_edit->get_pos_at_line_column(0, 0);
- line_0.y /= 2;
- SEND_GUI_MOUSE_BUTTON_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ // Selecting to the last line puts the caret at end of the line.
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for selection\nwith 3 lines");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE);
+ CHECK(text_edit->get_selection_from_line() == 1);
+ CHECK(text_edit->get_selection_from_column() == 0);
+ CHECK(text_edit->get_selection_to_line() == 2);
+ CHECK(text_edit->get_selection_to_column() == 12);
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 12);
+ CHECK(text_edit->is_caret_after_selection_origin());
+
+ // Releasing finishes.
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for selection\nwith 3 lines");
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 2).get_center(), MouseButtonMask::NONE, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for selection\nwith 3 lines");
+
+ // Clicking clears selection.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+
+ // Can start line select mode on an empty line.
+ text_edit->set_text("this is some text\n\nfor selection\nwith 4 lines");
+ MessageQueue::get_singleton()->flush();
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 0).get_center() + Point2i(2, 0), Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "\n");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE);
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(2, 9).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "\nfor selection\n");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE);
+ CHECK(text_edit->get_caret_line() == 3);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+
+ // Add a new selection without affecting the old one.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER);
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "\nfor selection\n");
+ CHECK(text_edit->get_caret_line(0) == 3);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
+
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "is");
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK(text_edit->get_selection_origin_line(1) == 0);
+ CHECK(text_edit->get_selection_origin_column(1) == 2);
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+
+ // Selecting the last line puts caret at the end.
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(3, 3).get_center(), Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(3, 3).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "with 4 lines");
+ CHECK(text_edit->get_caret_line() == 3);
+ CHECK(text_edit->get_caret_column() == 12);
+ CHECK(text_edit->get_selection_origin_line() == 3);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+
+ // Selecting above reverses direction.
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for selection\nwith 4 lines");
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_selection_origin_line() == 3);
+ CHECK(text_edit->get_selection_origin_column() == 12);
+
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ // Shift + triple click to extend selection and restart line select mode.
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(0, 9).get_center() + Point2i(2, 0), Key::NONE | KeyModifierMask::SHIFT);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 9).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "this is some text\n\nfor selection\nwith 4 lines");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_selection_origin_line() == 3);
+ CHECK(text_edit->get_selection_origin_column() == 12);
+
+ // Cannot select when disabled, but caret still moves to the start of the next line.
text_edit->set_selecting_enabled(false);
- SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE);
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(0, 2).get_center(), Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 0);
@@ -1142,30 +2065,47 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
}
SUBCASE("[TextEdit] mouse shift click select") {
- /* Set size for mouse input. */
+ // Set size for mouse input.
text_edit->set_size(Size2(200, 200));
text_edit->set_text("this is some text\nfor selection");
MessageQueue::get_singleton()->flush();
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT);
+ // Shift click to make a selection from the previous caret position.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 1).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT);
CHECK(text_edit->has_selection());
- CHECK(text_edit->get_selected_text() == "for s");
+ CHECK(text_edit->get_selected_text() == "or s");
CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER);
- CHECK(text_edit->get_selection_from_line() == 1);
- CHECK(text_edit->get_selection_from_column() == 0);
- CHECK(text_edit->get_selection_to_line() == 1);
- CHECK(text_edit->get_selection_to_column() == 5);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 1);
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 5);
+ CHECK(text_edit->is_caret_after_selection_origin());
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ // Shift click above to switch selection direction. Uses original selection position.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "s some text\nf");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 1);
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 6);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin());
+
+ // Clicking clears selection.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 7);
+ // Cannot select when disabled, but caret still moves.
text_edit->set_selecting_enabled(false);
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
- SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT);
CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 5);
@@ -1175,89 +2115,166 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SUBCASE("[TextEdit] select and deselect") {
text_edit->set_text("this is some text\nfor selection");
MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ // Select clamps input to full text.
text_edit->select(-1, -1, 500, 500);
+ MessageQueue::get_singleton()->flush();
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "this is some text\nfor selection");
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 13);
+ CHECK(text_edit->get_selection_from_line(0) == text_edit->get_selection_origin_line(0));
+ CHECK(text_edit->get_selection_from_column(0) == text_edit->get_selection_origin_column(0));
+ CHECK(text_edit->get_selection_to_line(0) == text_edit->get_caret_line(0));
+ CHECK(text_edit->get_selection_to_column(0) == text_edit->get_caret_column(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
text_edit->deselect();
+ MessageQueue::get_singleton()->flush();
CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK_FALSE("caret_changed");
+ // Select works in the other direction.
text_edit->select(500, 500, -1, -1);
+ MessageQueue::get_singleton()->flush();
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == "this is some text\nfor selection");
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 13);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->get_selection_from_line(0) == text_edit->get_caret_line(0));
+ CHECK(text_edit->get_selection_from_column(0) == text_edit->get_caret_column(0));
+ CHECK(text_edit->get_selection_to_line(0) == text_edit->get_selection_origin_line(0));
+ CHECK(text_edit->get_selection_to_column(0) == text_edit->get_selection_origin_column(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
text_edit->deselect();
+ MessageQueue::get_singleton()->flush();
CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK_FALSE("caret_changed");
+ // Select part of a line.
text_edit->select(0, 4, 0, 8);
+ MessageQueue::get_singleton()->flush();
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == " is ");
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 4);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 8);
+ CHECK(text_edit->get_selection_from_line(0) == text_edit->get_selection_origin_line(0));
+ CHECK(text_edit->get_selection_from_column(0) == text_edit->get_selection_origin_column(0));
+ CHECK(text_edit->get_selection_to_line(0) == text_edit->get_caret_line(0));
+ CHECK(text_edit->get_selection_to_column(0) == text_edit->get_caret_column(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
text_edit->deselect();
+ MessageQueue::get_singleton()->flush();
CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK_FALSE("caret_changed");
+ // Select part of a line in the other direction.
text_edit->select(0, 8, 0, 4);
+ MessageQueue::get_singleton()->flush();
CHECK(text_edit->has_selection());
CHECK(text_edit->get_selected_text() == " is ");
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 8);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_selection_from_line(0) == text_edit->get_caret_line(0));
+ CHECK(text_edit->get_selection_from_column(0) == text_edit->get_caret_column(0));
+ CHECK(text_edit->get_selection_to_line(0) == text_edit->get_selection_origin_line(0));
+ CHECK(text_edit->get_selection_to_column(0) == text_edit->get_selection_origin_column(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ // Cannot select when disabled.
text_edit->set_selecting_enabled(false);
CHECK_FALSE(text_edit->has_selection());
text_edit->select(0, 8, 0, 4);
+ MessageQueue::get_singleton()->flush();
CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK_FALSE("caret_changed");
text_edit->set_selecting_enabled(true);
+ }
- text_edit->select(0, 8, 0, 4);
- CHECK(text_edit->has_selection());
- SEND_GUI_ACTION("ui_text_caret_right");
+ SUBCASE("[TextEdit] delete selection") {
+ text_edit->set_text("this is some text\nfor selection");
+ MessageQueue::get_singleton()->flush();
+
+ // Delete selection does nothing if there is no selection.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(8);
CHECK_FALSE(text_edit->has_selection());
text_edit->delete_selection();
CHECK(text_edit->get_text() == "this is some text\nfor selection");
+ CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 8);
- text_edit->select(0, 8, 0, 4);
+ // Backspace removes selection.
+ text_edit->select(0, 4, 0, 8);
CHECK(text_edit->has_selection());
SEND_GUI_ACTION("ui_text_backspace");
+ CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_text() == "thissome text\nfor selection");
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 4);
+ // Undo restores previous selection.
text_edit->undo();
- CHECK(text_edit->has_selection());
CHECK(text_edit->get_text() == "this is some text\nfor selection");
+ CHECK(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 8);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 4);
+ // Redo restores caret.
text_edit->redo();
- CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_text() == "thissome text\nfor selection");
+ CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 4);
text_edit->undo();
- CHECK(text_edit->has_selection());
CHECK(text_edit->get_text() == "this is some text\nfor selection");
+ CHECK(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 8);
- text_edit->select(0, 8, 0, 4);
+ text_edit->select(0, 4, 0, 8);
CHECK(text_edit->has_selection());
+ // Delete selection removes text, deselects, and moves caret.
text_edit->delete_selection();
- CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_text() == "thissome text\nfor selection");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ // Undo delete works.
text_edit->undo();
- CHECK(text_edit->has_selection());
CHECK(text_edit->get_text() == "this is some text\nfor selection");
+ CHECK(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 8);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 4);
+ // Redo delete works.
text_edit->redo();
- CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_text() == "thissome text\nfor selection");
+ CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 4);
@@ -1267,19 +2284,227 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 8);
+ // Can still delete if not editable.
text_edit->set_editable(false);
text_edit->delete_selection();
text_edit->set_editable(false);
CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_text() == "thissome text\nfor selection");
+ // Cannot undo since it was not editable.
text_edit->undo();
CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_text() == "thissome text\nfor selection");
+
+ // Delete multiple adjacent selections on the same line.
+ text_edit->select(0, 0, 0, 5);
+ text_edit->add_caret(0, 8);
+ text_edit->select(0, 5, 0, 8, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+ text_edit->delete_selection();
+ CHECK(text_edit->get_text() == " text\nfor selection");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+
+ // Delete mulitline selection. Ignore non selections.
+ text_edit->remove_secondary_carets();
+ text_edit->select(1, 3, 0, 2);
+ text_edit->add_caret(1, 7);
+ text_edit->delete_selection();
+ CHECK(text_edit->get_text() == " t selection");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 2);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 6);
}
- // Add readonly test?
SUBCASE("[TextEdit] text drag") {
+ text_edit->set_size(Size2(200, 200));
+ text_edit->set_text("drag test\ndrop here ''");
+ text_edit->grab_click_focus();
+ MessageQueue::get_singleton()->flush();
+
+ // Drag and drop selected text to mouse position.
+ text_edit->select(0, 0, 0, 4);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 11).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 11).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == " test\ndrop here 'drag'");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 15);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 11);
+
+ // Undo.
+ text_edit->undo();
+ CHECK(text_edit->get_text() == "drag test\ndrop here ''");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+
+ // Redo.
+ text_edit->redo();
+ CHECK(text_edit->get_text() == " test\ndrop here 'drag'");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 15);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 11);
+
+ // Hold control when dropping to not delete selected text.
+ text_edit->select(1, 10, 1, 16);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 12).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "'drag'");
+ CHECK(text_edit->has_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_KEY_EVENT(Key::CMD_OR_CTRL);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ SEND_GUI_KEY_UP_EVENT(Key::CMD_OR_CTRL);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "'drag' test\ndrop here 'drag'");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 6);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+
+ // Multiple caret drags entire selection.
+ text_edit->select(0, 11, 0, 7, 0);
+ text_edit->add_caret(1, 2);
+ text_edit->select(1, 2, 1, 4, 1);
+ text_edit->add_caret(1, 12);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection(true, 1));
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 12).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "test\nop");
+ // Carets aren't removed from dragging, only dropping.
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 7);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 11);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK(text_edit->get_selection_origin_line(1) == 1);
+ CHECK(text_edit->get_selection_origin_column(1) == 2);
+ CHECK_FALSE(text_edit->has_selection(2));
+ CHECK(text_edit->get_caret_line(2) == 1);
+ CHECK(text_edit->get_caret_column(2) == 12);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 2);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 7);
+
+ // Drop onto same selection should do effectively nothing.
+ text_edit->select(1, 3, 1, 7);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "here");
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 7);
+ CHECK(text_edit->get_selection_origin_line() == 1);
+ CHECK(text_edit->get_selection_origin_column() == 3);
+
+ // Cannot drag when drag and drop selection is disabled. It becomes regular drag to select.
+ text_edit->set_drag_and_drop_selection_enabled(false);
+ text_edit->select(0, 1, 0, 5);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK_FALSE(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 7);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 2);
+ text_edit->set_drag_and_drop_selection_enabled(true);
+
+ // Cancel drag and drop from Escape key.
+ text_edit->select(0, 1, 0, 5);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag");
+ SEND_GUI_KEY_EVENT(Key::ESCAPE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 5);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 1);
+
+ // Cancel drag and drop from caret move key input.
+ text_edit->select(0, 1, 0, 5);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag");
+ SEND_GUI_KEY_EVENT(Key::RIGHT);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 5);
+
+ // Cancel drag and drop from text key input.
+ text_edit->select(0, 1, 0, 5);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag");
+ SEND_GUI_KEY_EVENT(Key::A);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "'A' \ndr heretest\nop 'drag'");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 2);
+ }
+
+ SUBCASE("[TextEdit] text drag to another text edit") {
TextEdit *target_text_edit = memnew(TextEdit);
SceneTree::get_singleton()->get_root()->add_child(target_text_edit);
@@ -1292,27 +2517,223 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
text_edit->set_text("drag me");
text_edit->select_all();
text_edit->grab_click_focus();
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 7);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 0);
MessageQueue::get_singleton()->flush();
- Point2i line_0 = text_edit->get_pos_at_line_column(0, 0);
- line_0.y /= 2;
- SEND_GUI_MOUSE_BUTTON_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ // Drag text between text edits.
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK(text_edit->is_mouse_over_selection());
- SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 7).get_center(), MouseButtonMask::LEFT, Key::NONE);
CHECK(text_edit->get_viewport()->gui_is_dragging());
CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag me");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 7);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 0);
- line_0 = target_text_edit->get_pos_at_line_column(0, 0);
- line_0.y /= 2;
- line_0.x += 401; // As empty add one.
- SEND_GUI_MOUSE_MOTION_EVENT(line_0, MouseButtonMask::LEFT, Key::NONE);
+ Point2i target_line0 = target_text_edit->get_position() + Point2i(1, target_text_edit->get_line_height() / 2);
+ SEND_GUI_MOUSE_MOTION_EVENT(target_line0, MouseButtonMask::LEFT, Key::NONE);
CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(target_text_edit->get_caret_line() == 0);
+ CHECK(target_text_edit->get_caret_column() == 0);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_line0, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(target_text_edit->get_text() == "drag me");
+ CHECK(target_text_edit->has_selection());
+ CHECK(target_text_edit->get_caret_line() == 0);
+ CHECK(target_text_edit->get_caret_column() == 7);
+ CHECK(target_text_edit->get_selection_origin_line() == 0);
+ CHECK(target_text_edit->get_selection_origin_column() == 0);
- SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ // Undo is separate per TextEdit.
+ text_edit->undo();
+ CHECK(text_edit->get_text() == "drag me");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 7);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+ CHECK(target_text_edit->get_text() == "drag me");
+ CHECK(target_text_edit->has_selection());
+ CHECK(target_text_edit->get_caret_line() == 0);
+ CHECK(target_text_edit->get_caret_column() == 7);
+ CHECK(target_text_edit->get_selection_origin_line() == 0);
+ CHECK(target_text_edit->get_selection_origin_column() == 0);
+
+ target_text_edit->undo();
+ CHECK(text_edit->get_text() == "drag me");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 7);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+ CHECK(target_text_edit->get_text() == "");
+ CHECK_FALSE(target_text_edit->has_selection());
+ CHECK(target_text_edit->get_caret_line() == 0);
+ CHECK(target_text_edit->get_caret_column() == 0);
+
+ // Redo is also separate.
+ text_edit->redo();
+ CHECK(text_edit->get_text() == "");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(target_text_edit->get_text() == "");
+ CHECK_FALSE(target_text_edit->has_selection());
+ CHECK(target_text_edit->get_caret_line() == 0);
+ CHECK(target_text_edit->get_caret_column() == 0);
- CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ target_text_edit->redo();
CHECK(text_edit->get_text() == "");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
CHECK(target_text_edit->get_text() == "drag me");
+ CHECK(target_text_edit->has_selection());
+ CHECK(target_text_edit->get_caret_line() == 0);
+ CHECK(target_text_edit->get_caret_column() == 7);
+ CHECK(target_text_edit->get_selection_origin_line() == 0);
+ CHECK(target_text_edit->get_selection_origin_column() == 0);
+
+ // Hold control to not remove selected text.
+ text_edit->set_text("drag test\ndrop test");
+ MessageQueue::get_singleton()->flush();
+ target_text_edit->select(0, 0, 0, 3, 0);
+ target_text_edit->add_caret(0, 5);
+ text_edit->select(0, 5, 0, 7, 0);
+ text_edit->add_caret(0, 1);
+ text_edit->select(0, 1, 0, 0, 1);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection(true, 0));
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 6).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "d\nte");
+ CHECK(text_edit->has_selection());
+ SEND_GUI_KEY_EVENT(Key::CMD_OR_CTRL);
+ SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ SEND_GUI_KEY_UP_EVENT(Key::CMD_OR_CTRL);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "drag test\ndrop test");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 7);
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK(target_text_edit->get_text() == "drag md\ntee");
+ CHECK(target_text_edit->get_caret_count() == 1);
+ CHECK(target_text_edit->has_selection());
+ CHECK(target_text_edit->get_caret_line() == 1);
+ CHECK(target_text_edit->get_caret_column() == 2);
+ CHECK(target_text_edit->get_selection_origin_line() == 0);
+ CHECK(target_text_edit->get_selection_origin_column() == 6);
+
+ // Drop onto selected text deletes the selected text first.
+ text_edit->set_deselect_on_focus_loss_enabled(false);
+ target_text_edit->set_deselect_on_focus_loss_enabled(false);
+ text_edit->remove_secondary_carets();
+ text_edit->select(0, 5, 0, 9);
+ target_text_edit->select(0, 6, 0, 8);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection(true, 0));
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center(), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "test");
+ CHECK(text_edit->has_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "drag \ndrop test");
+ CHECK(target_text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 5);
+ CHECK(target_text_edit->get_text() == "drag mdtest\ntee");
+ CHECK(target_text_edit->has_selection());
+ CHECK(target_text_edit->get_caret_line() == 0);
+ CHECK(target_text_edit->get_caret_column() == 11);
+ CHECK(target_text_edit->get_selection_origin_line() == 0);
+ CHECK(target_text_edit->get_selection_origin_column() == 7);
+ text_edit->set_deselect_on_focus_loss_enabled(true);
+ target_text_edit->set_deselect_on_focus_loss_enabled(true);
+
+ // Can drop even when drag and drop selection is disabled.
+ target_text_edit->set_drag_and_drop_selection_enabled(false);
+ text_edit->select(0, 4, 0, 5);
+ target_text_edit->deselect();
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == " ");
+ CHECK(text_edit->has_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "drag\ndrop test");
+ CHECK(target_text_edit->get_text() == "drag md test\ntee");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ CHECK(target_text_edit->has_selection());
+ CHECK(target_text_edit->get_caret_line() == 0);
+ CHECK(target_text_edit->get_caret_column() == 8);
+ CHECK(target_text_edit->get_selection_origin_line() == 0);
+ CHECK(target_text_edit->get_selection_origin_column() == 7);
+ target_text_edit->set_drag_and_drop_selection_enabled(true);
+
+ // Cannot drop when not editable.
+ target_text_edit->set_editable(false);
+ text_edit->select(0, 1, 0, 4);
+ target_text_edit->deselect();
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "rag");
+ CHECK(text_edit->has_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "drag\ndrop test");
+ CHECK(target_text_edit->get_text() == "drag md test\ntee");
+ CHECK(text_edit->has_selection());
+ CHECK_FALSE(target_text_edit->has_selection());
+ target_text_edit->set_editable(true);
+
+ // Can drag when not editable, but text will not be removed.
+ text_edit->set_editable(false);
+ text_edit->select(0, 0, 0, 4);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag");
+ CHECK(text_edit->has_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "drag\ndrop test");
+ CHECK(target_text_edit->get_text() == "dragdrag md test\ntee");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ CHECK(target_text_edit->has_selection());
+ CHECK(target_text_edit->get_caret_line() == 0);
+ CHECK(target_text_edit->get_caret_column() == 8);
+ CHECK(target_text_edit->get_selection_origin_line() == 0);
+ CHECK(target_text_edit->get_selection_origin_column() == 4);
+ text_edit->set_editable(true);
memdelete(target_text_edit);
}
@@ -1324,44 +2745,41 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
}
SUBCASE("[TextEdit] overridable actions") {
+ DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton());
+
SIGNAL_WATCH(text_edit, "text_set");
SIGNAL_WATCH(text_edit, "text_changed");
SIGNAL_WATCH(text_edit, "lines_edited_from");
SIGNAL_WATCH(text_edit, "caret_changed");
- Array args1;
- args1.push_back(0);
- args1.push_back(0);
- Array lines_edited_args;
- lines_edited_args.push_back(args1);
+ Array lines_edited_args = build_array(build_array(0, 0));
SUBCASE("[TextEdit] backspace") {
text_edit->set_text("this is\nsome\n");
text_edit->set_caret_line(0);
text_edit->set_caret_column(0);
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
+ // Cannot backspace at start of text.
text_edit->backspace();
MessageQueue::get_singleton()->flush();
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
+ // Backspace at start of line removes the line.
text_edit->set_caret_line(2);
text_edit->set_caret_column(0);
MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(2, 1));
- ((Array)lines_edited_args[0])[0] = 2;
- ((Array)lines_edited_args[0])[1] = 1;
text_edit->backspace();
MessageQueue::get_singleton()->flush();
-
CHECK(text_edit->get_text() == "this is\nsome");
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 4);
@@ -1369,10 +2787,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- ((Array)lines_edited_args[0])[0] = 1;
+ // Backspace removes a character.
+ lines_edited_args = build_array(build_array(1, 1));
text_edit->backspace();
MessageQueue::get_singleton()->flush();
-
CHECK(text_edit->get_text() == "this is\nsom");
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 3);
@@ -1380,11 +2798,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Backspace when text is selected removes the selection.
text_edit->end_complex_operation();
text_edit->select(1, 0, 1, 3);
text_edit->backspace();
MessageQueue::get_singleton()->flush();
-
CHECK(text_edit->get_text() == "this is\n");
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 0);
@@ -1392,11 +2810,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Cannot backspace if not editable.
text_edit->set_editable(false);
text_edit->backspace();
text_edit->set_editable(true);
MessageQueue::get_singleton()->flush();
-
CHECK(text_edit->get_text() == "this is\n");
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 0);
@@ -1404,6 +2822,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
+ // Undo restores text to the previous end of complex operation.
text_edit->undo();
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_text() == "this is\nsom");
@@ -1412,98 +2831,736 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is\n");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // See ui_text_backspace for more backspace tests.
}
SUBCASE("[TextEdit] cut") {
+ // Cut without a selection removes the entire line.
text_edit->set_text("this is\nsome\n");
text_edit->set_caret_line(0);
text_edit->set_caret_column(6);
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 0));
- ERR_PRINT_OFF;
text_edit->cut();
MessageQueue::get_singleton()->flush();
- ERR_PRINT_ON; // Can't check display server content.
-
- ((Array)lines_edited_args[0])[0] = 1;
+ CHECK(DS->clipboard_get() == "this is\n");
CHECK(text_edit->get_text() == "some\n");
CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 4);
+ CHECK(text_edit->get_caret_column() == 3); // In the default font, this is the same position.
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- ((Array)lines_edited_args[0])[0] = 0;
- ((Array)lines_edited_args[0])[1] = 1;
+ // Undo restores the cut text.
text_edit->undo();
MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "this is\n");
CHECK(text_edit->get_text() == "this is\nsome\n");
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 6);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
- SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
- ((Array)lines_edited_args[0])[0] = 1;
- ((Array)lines_edited_args[0])[1] = 0;
+ // Redo.
text_edit->redo();
MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "this is\n");
CHECK(text_edit->get_text() == "some\n");
CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 4);
+ CHECK(text_edit->get_caret_column() == 3);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Cut with a selection removes just the selection.
text_edit->set_text("this is\nsome\n");
+ text_edit->select(0, 5, 0, 7);
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0));
- ((Array)lines_edited_args[0])[0] = 0;
- text_edit->select(0, 5, 0, 7);
- ERR_PRINT_OFF;
SEND_GUI_ACTION("ui_cut");
CHECK(text_edit->get_viewport()->is_input_handled());
MessageQueue::get_singleton()->flush();
- ERR_PRINT_ON; // Can't check display server content.
+ CHECK(DS->clipboard_get() == "is");
CHECK(text_edit->get_text() == "this \nsome\n");
+ CHECK_FALSE(text_edit->get_caret_line());
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 5);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Cut does not change the text if not editable. Text is still added to clipboard.
+ text_edit->set_text("this is\nsome\n");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(5);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
text_edit->set_editable(false);
text_edit->cut();
MessageQueue::get_singleton()->flush();
text_edit->set_editable(true);
- CHECK(text_edit->get_text() == "this \nsome\n");
+ CHECK(DS->clipboard_get() == "this is\n");
+ CHECK(text_edit->get_text() == "this is\nsome\n");
CHECK(text_edit->get_caret_line() == 0);
CHECK(text_edit->get_caret_column() == 5);
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Cut line with multiple carets.
+ text_edit->set_text("this is\nsome\n");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(3);
+ text_edit->add_caret(0, 2);
+ text_edit->add_caret(0, 4);
+ text_edit->add_caret(2, 0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 0), build_array(1, 0));
+
+ text_edit->cut();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "this is\n\n");
+ CHECK(text_edit->get_text() == "some");
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 2); // In the default font, this is the same position.
+ // The previous caret at index 1 was merged.
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 3); // In the default font, this is the same position.
+ CHECK_FALSE(text_edit->has_selection(2));
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ text_edit->remove_secondary_carets();
+
+ // Cut on the only line removes the contents.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0));
+
+ text_edit->cut();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "some\n");
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_line_count() == 1);
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Cut empty line.
+ text_edit->cut();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "\n");
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ // These signals are emitted even if there is no change.
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Cut multiple lines, in order.
+ text_edit->set_text("this is\nsome\ntext to\nbe\n\ncut");
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(7);
+ text_edit->add_caret(3, 0);
+ text_edit->add_caret(0, 2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 0), build_array(3, 2), build_array(2, 1));
+
+ text_edit->cut();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "this is\ntext to\nbe\n");
+ CHECK(text_edit->get_text() == "some\n\ncut");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ text_edit->remove_secondary_carets();
+
+ // Cut multiple selections, in order. Ignores regular carets.
+ text_edit->set_text("this is\nsome\ntext to\nbe\n\ncut");
+ text_edit->add_caret(3, 0);
+ text_edit->add_caret(0, 2);
+ text_edit->add_caret(2, 0);
+ text_edit->select(1, 0, 1, 2, 0);
+ text_edit->select(3, 0, 4, 0, 1);
+ text_edit->select(0, 5, 0, 3, 2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 1), build_array(4, 3), build_array(0, 0));
+
+ text_edit->cut();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "s \nso\nbe\n");
+ CHECK(text_edit->get_text() == "thiis\nme\ntext to\n\ncut");
+ CHECK(text_edit->get_caret_count() == 4);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 3);
+ CHECK(text_edit->get_caret_line(3) == 2);
+ CHECK(text_edit->get_caret_column(3) == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
}
SUBCASE("[TextEdit] copy") {
- // TODO: Cannot test need display server support.
+ text_edit->set_text("this is\nsome\ntest\n\ntext");
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // Copy selected text.
+ text_edit->select(0, 0, 1, 2, 0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ DS->clipboard_set_primary("");
+
+ text_edit->copy();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "this is\nso");
+ CHECK(DS->clipboard_get_primary() == "");
+ CHECK(text_edit->get_text() == "this is\nsome\ntest\n\ntext");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 0);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 2);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Copy with GUI action.
+ text_edit->select(0, 0, 0, 2, 0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION("ui_copy");
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "th");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Can copy even if not editable.
+ text_edit->select(2, 4, 1, 2, 0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_editable(false);
+ text_edit->copy();
+ text_edit->set_editable(true);
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "me\ntest");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->deselect();
+
+ // Copy full line when there is no selection.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->copy();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "this is\n");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Copy empty line.
+ text_edit->set_caret_line(3);
+ text_edit->set_caret_column(0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->copy();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "\n");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->deselect();
+
+ // Copy full line with multiple carets on that line only copies once.
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(2);
+ text_edit->add_caret(1, 0);
+ text_edit->add_caret(1, 4);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->copy();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "some\n");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->remove_secondary_carets();
+
+ // Copy selected text from all selections with `\n` in between, in order. Ignore regular carets.
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret(4, 0);
+ text_edit->add_caret(0, 4);
+ text_edit->add_caret(1, 0);
+ text_edit->select(1, 3, 2, 4, 0);
+ text_edit->select(4, 4, 4, 0, 1);
+ text_edit->select(0, 5, 0, 4, 2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->copy();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == " \ne\ntest\ntext");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+
+ // Copy multiple lines with multiple carets, in order.
+ text_edit->set_caret_line(3);
+ text_edit->set_caret_column(0);
+ text_edit->add_caret(4, 2);
+ text_edit->add_caret(0, 4);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->copy();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "this is\n\ntext\n");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
}
SUBCASE("[TextEdit] paste") {
- // TODO: Cannot test need display server support.
+ // Paste text from clipboard at caret.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 1));
+ DS->clipboard_set("paste");
+
+ text_edit->paste();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "paste");
+ CHECK(text_edit->get_text() == "this is\nsopasteme\n\ntext");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 7);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Undo.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "paste");
+ CHECK(text_edit->get_text() == "this is\nsome\n\ntext");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 2);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "paste");
+ CHECK(text_edit->get_text() == "this is\nsopasteme\n\ntext");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 7);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Paste on empty line. Use GUI action.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(2, 2));
+ DS->clipboard_set("paste2");
+
+ SEND_GUI_ACTION("ui_paste");
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "paste2");
+ CHECK(text_edit->get_text() == "this is\nsome\npaste2\ntext");
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 6);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Paste removes selection before pasting.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->select(0, 5, 1, 3);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 0), build_array(0, 0));
+ DS->clipboard_set("paste");
+
+ text_edit->paste();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "paste");
+ CHECK(text_edit->get_text() == "this pastee\n\ntext");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 10);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Paste multiple lines.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 3));
+ DS->clipboard_set("multi\n\nline\npaste");
+
+ text_edit->paste();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "multi\n\nline\npaste");
+ CHECK(text_edit->get_text() == "tmulti\n\nline\npastehis is\nsome\n\ntext");
+ CHECK(text_edit->get_caret_line() == 3);
+ CHECK(text_edit->get_caret_column() == 5);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Paste full line after copying it.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 2));
+ DS->clipboard_set("");
+ text_edit->copy();
+ text_edit->set_caret_column(3);
+ CHECK(DS->clipboard_get() == "some\n");
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->paste();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "some\n");
+ CHECK(text_edit->get_text() == "this is\nsome\nsome\n\ntext");
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 3);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Do not paste as line since it wasn't copied.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 1));
+ DS->clipboard_set("paste\n");
+
+ text_edit->paste();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "paste\n");
+ CHECK(text_edit->get_text() == "thispaste\n is\nsome\n\ntext");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Paste text at each caret.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(2);
+ text_edit->add_caret(3, 4);
+ text_edit->add_caret(0, 4);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 1), build_array(2, 3), build_array(5, 6));
+ DS->clipboard_set("paste\ntest");
+
+ text_edit->paste();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "paste\ntest");
+ CHECK(text_edit->get_text() == "thispaste\ntest is\nsopaste\ntestme\n\ntextpaste\ntest");
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_caret_line(0) == 3);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_caret_line(1) == 6);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK(text_edit->get_caret_line(2) == 1);
+ CHECK(text_edit->get_caret_column(2) == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ text_edit->remove_secondary_carets();
+
+ // Paste line per caret when the amount of lines is equal to the number of carets.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(2);
+ text_edit->add_caret(3, 4);
+ text_edit->add_caret(0, 4);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0), build_array(1, 1), build_array(3, 3));
+ DS->clipboard_set("paste\ntest\n1");
+
+ text_edit->paste();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "paste\ntest\n1");
+ CHECK(text_edit->get_text() == "thispaste is\nsotestme\n\ntext1");
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 6);
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 5);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 9);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ text_edit->remove_secondary_carets();
+
+ // Cannot paste when not editable.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ DS->clipboard_set("no paste");
+
+ text_edit->set_editable(false);
+ text_edit->paste();
+ text_edit->set_editable(true);
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "no paste");
+ CHECK(text_edit->get_text() == "this is\nsome\n\ntext");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
}
SUBCASE("[TextEdit] paste primary") {
- // TODO: Cannot test need display server support.
+ // Set size for mouse input.
+ text_edit->set_size(Size2(200, 200));
+
+ text_edit->grab_focus();
+ DS->clipboard_set("");
+ DS->clipboard_set_primary("");
+ CHECK(DS->clipboard_get_primary() == "");
+
+ // Select text with mouse to put into primary clipboard.
+ text_edit->set_text("this is\nsome\n\ntext");
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+ CHECK(DS->clipboard_get() == "");
+ CHECK(DS->clipboard_get_primary() == "is is\nsom");
+ CHECK(text_edit->get_text() == "this is\nsome\n\ntext");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "is is\nsom");
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 2);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 3);
+ SIGNAL_CHECK_FALSE("text_set");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Middle click to paste at mouse.
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(3, 4));
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(3, 2).get_center() + Point2i(2, 0), MouseButton::MIDDLE, MouseButtonMask::MIDDLE, Key::NONE);
+ CHECK(DS->clipboard_get_primary() == "is is\nsom");
+ CHECK(text_edit->get_text() == "this is\nsome\n\nteis is\nsomxt");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 4);
+ CHECK(text_edit->get_caret_column() == 3);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Paste at mouse position if there is only one caret.
+ text_edit->set_text("this is\nsome\n\ntext");
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ DS->clipboard_set_primary("paste");
+ lines_edited_args = build_array(build_array(0, 0));
+
+ text_edit->paste_primary_clipboard();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get_primary() == "paste");
+ CHECK(text_edit->get_text() == "tpastehis is\nsome\n\ntext");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 6);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Paste at all carets if there are multiple carets.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(0);
+ text_edit->add_caret(2, 0);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ DS->clipboard_set_primary("paste");
+ lines_edited_args = build_array(build_array(1, 1), build_array(2, 2));
+
+ text_edit->paste_primary_clipboard();
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get_primary() == "paste");
+ CHECK(text_edit->get_text() == "this is\npastesome\npaste\ntext");
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 5);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Cannot paste if not editable.
+ text_edit->set_text("this is\nsome\n\ntext");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ DS->clipboard_set("no paste");
+
+ text_edit->set_editable(false);
+ text_edit->paste_primary_clipboard();
+ text_edit->set_editable(true);
+ MessageQueue::get_singleton()->flush();
+ CHECK(DS->clipboard_get() == "no paste");
+ CHECK(text_edit->get_text() == "this is\nsome\n\ntext");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
}
SIGNAL_UNWATCH(text_edit, "text_set");
@@ -1512,60 +3569,77 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_UNWATCH(text_edit, "caret_changed");
}
- // Add undo / redo tests?
SUBCASE("[TextEdit] input") {
SIGNAL_WATCH(text_edit, "text_set");
SIGNAL_WATCH(text_edit, "text_changed");
SIGNAL_WATCH(text_edit, "lines_edited_from");
SIGNAL_WATCH(text_edit, "caret_changed");
- Array args1;
- args1.push_back(0);
- args1.push_back(0);
- Array lines_edited_args;
- lines_edited_args.push_back(args1);
+ Array lines_edited_args = build_array(build_array(0, 0));
SUBCASE("[TextEdit] ui_text_newline_above") {
text_edit->set_text("this is some test text.\nthis is some test text.");
- text_edit->select(0, 0, 0, 4);
- text_edit->set_caret_column(4);
-
- text_edit->add_caret(1, 4);
- text_edit->select(1, 0, 1, 4, 1);
- CHECK(text_edit->get_caret_count() == 2);
-
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // For the second caret.
- Array args2;
- args2.push_back(0);
- args2.push_back(1);
- lines_edited_args.push_front(args2);
+ // Insert new line above.
+ text_edit->select(0, 0, 0, 4);
+ text_edit->add_caret(1, 4);
+ CHECK(text_edit->get_caret_count() == 2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 1), build_array(2, 3));
- ((Array)lines_edited_args[1])[1] = 1;
SEND_GUI_ACTION("ui_text_newline_above");
CHECK(text_edit->get_viewport()->is_input_handled());
CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Undo.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text.");
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text.");
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 0);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Does not work if not editable.
text_edit->set_caret_line(1);
text_edit->set_caret_column(4);
-
- text_edit->set_caret_line(3, false, true, 0, 1);
+ text_edit->set_caret_line(3, false, true, -1, 1);
text_edit->set_caret_column(4, false, 1);
MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("caret_changed");
@@ -1574,31 +3648,57 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SEND_GUI_ACTION("ui_text_newline_above");
CHECK(text_edit->get_viewport()->is_input_handled());
CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text.");
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 4);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 3);
CHECK(text_edit->get_caret_column(1) == 4);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
text_edit->set_editable(true);
- ((Array)lines_edited_args[0])[0] = 2;
- ((Array)lines_edited_args[0])[1] = 3;
+ // Works on first line, empty lines, and only happens at caret for selections.
+ text_edit->select(1, 10, 0, 0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 1), build_array(4, 5));
SEND_GUI_ACTION("ui_text_newline_above");
CHECK(text_edit->get_viewport()->is_input_handled());
CHECK(text_edit->get_text() == "\n\nthis is some test text.\n\n\nthis is some test text.");
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 4);
CHECK(text_edit->get_caret_column(1) == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Insert multiple new lines above from one line.
+ text_edit->set_text("test");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(1);
+ text_edit->add_caret(0, 3);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 1), build_array(1, 2));
+
+ SEND_GUI_ACTION("ui_text_newline_above");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n\ntest");
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
@@ -1606,34 +3706,55 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SUBCASE("[TextEdit] ui_text_newline_blank") {
text_edit->set_text("this is some test text.\nthis is some test text.");
- text_edit->select(0, 0, 0, 4);
- text_edit->set_caret_column(4);
-
- text_edit->add_caret(1, 4);
- text_edit->select(1, 0, 1, 4, 1);
- CHECK(text_edit->get_caret_count() == 2);
-
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
- SIGNAL_DISCARD("caret_changed");
- // For the second caret.
- Array args2;
- args2.push_back(1);
- args2.push_back(2);
- lines_edited_args.push_front(args2);
+ // Insert new line below.
+ text_edit->select(0, 0, 0, 4);
+ text_edit->add_caret(1, 4);
+ CHECK(text_edit->get_caret_count() == 2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 1), build_array(2, 3));
- ((Array)lines_edited_args[1])[1] = 1;
SEND_GUI_ACTION("ui_text_newline_blank");
CHECK(text_edit->get_viewport()->is_input_handled());
CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n");
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Undo.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text.");
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
CHECK(text_edit->get_caret_line(1) == 3);
CHECK(text_edit->get_caret_column(1) == 0);
CHECK_FALSE(text_edit->has_selection(1));
@@ -1641,75 +3762,119 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Does not work if not editable.
text_edit->set_editable(false);
SEND_GUI_ACTION("ui_text_newline_blank");
CHECK(text_edit->get_viewport()->is_input_handled());
CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n");
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 3);
CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
text_edit->set_editable(true);
+
+ // Insert multiple new lines below from one line.
+ text_edit->set_text("test");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(1);
+ text_edit->add_caret(0, 3);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 1), build_array(0, 1));
+
+ SEND_GUI_ACTION("ui_text_newline_blank");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "test\n\n");
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 2);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
}
SUBCASE("[TextEdit] ui_text_newline") {
text_edit->set_text("this is some test text.\nthis is some test text.");
- text_edit->select(0, 0, 0, 4);
- text_edit->set_caret_column(4);
-
- text_edit->add_caret(1, 4);
- text_edit->select(1, 0, 1, 4, 1);
- CHECK(text_edit->get_caret_count() == 2);
-
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // For the second caret.
- Array args2;
- args2.push_back(1);
- args2.push_back(1);
- lines_edited_args.push_front(args2);
- lines_edited_args.push_front(args2.duplicate());
- ((Array)lines_edited_args[1])[1] = 2;
-
- lines_edited_args.push_back(lines_edited_args[2].duplicate());
- ((Array)lines_edited_args[3])[1] = 1;
+ // Insert new line at caret.
+ text_edit->select(0, 0, 0, 4);
+ text_edit->add_caret(1, 4);
+ CHECK(text_edit->get_caret_count() == 2);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ // Lines edited: deletion, insert line, insert line.
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 1), build_array(2, 3));
SEND_GUI_ACTION("ui_text_newline");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "\n is some test text.\nthis\n is some test text.");
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 3);
CHECK(text_edit->get_caret_column(1) == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Undo.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text.");
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "\n is some test text.\nthis\n is some test text.");
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Does not work if not editable.
text_edit->set_editable(false);
SEND_GUI_ACTION("ui_text_newline");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "\n is some test text.\nthis\n is some test text.");
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 3);
CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
@@ -1717,255 +3882,399 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
}
SUBCASE("[TextEdit] ui_text_backspace_all_to_left") {
- text_edit->set_text("\nthis is some test text.\n\nthis is some test text.");
- text_edit->select(1, 0, 1, 4);
- text_edit->set_caret_line(1);
- text_edit->set_caret_column(4);
-
- text_edit->add_caret(3, 4);
- text_edit->select(3, 0, 3, 4, 1);
- CHECK(text_edit->get_caret_count() == 2);
-
- MessageQueue::get_singleton()->flush();
-
Ref<InputEvent> tmpevent = InputEventKey::create_reference(Key::BACKSPACE | KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL);
InputMap::get_singleton()->action_add_event("ui_text_backspace_all_to_left", tmpevent);
+ text_edit->set_text("\nthis is some test text.\n\nthis is some test text.");
+ MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // For the second caret.
- Array args2;
- args2.push_back(3);
- args2.push_back(3);
- lines_edited_args.push_front(args2);
-
- // With selection should be a normal backspace.
- ((Array)lines_edited_args[1])[0] = 1;
- ((Array)lines_edited_args[1])[1] = 1;
+ // Remove all text to the left.
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(5);
+ text_edit->add_caret(1, 2);
+ text_edit->add_caret(1, 8);
+ lines_edited_args = build_array(build_array(1, 1));
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
SEND_GUI_ACTION("ui_text_backspace_all_to_left");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
+ CHECK(text_edit->get_text() == "\nsome test text.\n\nthis is some test text.");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_line() == 1);
CHECK(text_edit->get_caret_column() == 0);
- CHECK_FALSE(text_edit->has_selection(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- CHECK(text_edit->get_caret_line(1) == 3);
- CHECK(text_edit->get_caret_column(1) == 0);
+ // Undo.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text.");
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 5);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ CHECK_FALSE(text_edit->has_selection(2));
+ CHECK(text_edit->get_caret_line(2) == 1);
+ CHECK(text_edit->get_caret_column(2) == 8);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- ((Array)lines_edited_args[0])[1] = 2;
- ((Array)lines_edited_args[1])[1] = 0;
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "\nsome test text.\n\nthis is some test text.");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Acts as a normal backspace with selections.
+ text_edit->select(1, 5, 1, 9, 0);
+ text_edit->add_caret(3, 4);
+ text_edit->select(3, 7, 3, 4, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(3, 3), build_array(1, 1));
- // Start of line should also be a normal backspace.
SEND_GUI_ACTION("ui_text_backspace_all_to_left");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "\nsome text.\n\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Acts as a normal backspace when at the start of a line.
+ text_edit->set_caret_column(0);
+ text_edit->set_caret_column(0, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(3, 2), build_array(1, 0));
+ SEND_GUI_ACTION("ui_text_backspace_all_to_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "some text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Does not work if not editable.
text_edit->set_caret_column(text_edit->get_line(0).length());
text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
MessageQueue::get_singleton()->flush();
-
- SIGNAL_DISCARD("text_set");
- SIGNAL_DISCARD("text_changed");
- SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
text_edit->set_editable(false);
SEND_GUI_ACTION("ui_text_backspace_all_to_left");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
+ CHECK(text_edit->get_text() == "some text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length());
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length());
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
text_edit->set_editable(true);
- ((Array)lines_edited_args[0])[0] = 1;
- ((Array)lines_edited_args[0])[1] = 1;
- ((Array)lines_edited_args[1])[0] = 0;
+ // Remove entire line content when at the end of the line.
+ lines_edited_args = build_array(build_array(1, 1), build_array(0, 0));
SEND_GUI_ACTION("ui_text_backspace_all_to_left");
CHECK(text_edit->get_viewport()->is_input_handled());
CHECK(text_edit->get_text() == "\n");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ text_edit->remove_secondary_carets();
+
+ // Removing newline effectively happens after removing text.
+ text_edit->set_text("test\nlines");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(0);
+ text_edit->add_caret(1, 4);
+
+ SEND_GUI_ACTION("ui_text_backspace_all_to_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "tests");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ text_edit->remove_secondary_carets();
+
+ // Removing newline effectively happens after removing text, reverse caret order.
+ text_edit->set_text("test\nlines");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret(1, 0);
+
+ SEND_GUI_ACTION("ui_text_backspace_all_to_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "tests");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ text_edit->remove_secondary_carets();
InputMap::get_singleton()->action_erase_event("ui_text_backspace_all_to_left", tmpevent);
}
SUBCASE("[TextEdit] ui_text_backspace_word") {
text_edit->set_text("\nthis is some test text.\n\nthis is some test text.");
- text_edit->select(1, 0, 1, 4);
- text_edit->set_caret_line(1);
- text_edit->set_caret_column(4);
-
- text_edit->add_caret(3, 4);
- text_edit->select(3, 0, 3, 4, 1);
- CHECK(text_edit->get_caret_count() == 2);
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
- SIGNAL_DISCARD("caret_changed");
- // For the second caret.
- Array args2;
- args2.push_back(3);
- args2.push_back(3);
- lines_edited_args.push_front(args2);
-
- // With selection should be a normal backspace.
- ((Array)lines_edited_args[1])[0] = 1;
- ((Array)lines_edited_args[1])[1] = 1;
+ // Acts as a normal backspace with selections.
+ text_edit->select(1, 8, 1, 15);
+ text_edit->add_caret(3, 6);
+ text_edit->select(3, 10, 3, 6, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(3, 3), build_array(1, 1));
SEND_GUI_ACTION("ui_text_backspace_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "\nthis is st text.\n\nthis ime test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
- CHECK(text_edit->get_caret_line(1) == 3);
- CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 8);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 6);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
text_edit->end_complex_operation();
- ((Array)lines_edited_args[0])[1] = 2;
- ((Array)lines_edited_args[1])[1] = 0;
+ lines_edited_args = build_array(build_array(3, 2), build_array(1, 0));
// Start of line should also be a normal backspace.
+ text_edit->set_caret_column(0);
+ text_edit->set_caret_column(0, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
SEND_GUI_ACTION("ui_text_backspace_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "this is st text.\nthis ime test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Does not work if not editable.
text_edit->set_editable(false);
SEND_GUI_ACTION("ui_text_backspace_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "this is st text.\nthis ime test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
text_edit->set_editable(true);
+ // FIXME: Remove after GH-77101 is fixed.
+ text_edit->start_action(TextEdit::ACTION_NONE);
+
+ // Remove text to the start of the word to the left of the caret.
text_edit->set_caret_column(text_edit->get_line(0).length());
- text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
+ text_edit->set_caret_column(12, false, 1);
MessageQueue::get_singleton()->flush();
-
- SIGNAL_DISCARD("text_set");
- SIGNAL_DISCARD("text_changed");
- SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
-
- ((Array)lines_edited_args[0])[0] = 1;
- ((Array)lines_edited_args[0])[1] = 1;
- ((Array)lines_edited_args[1])[0] = 0;
+ lines_edited_args = build_array(build_array(1, 1), build_array(0, 0));
SEND_GUI_ACTION("ui_text_backspace_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test \n is some test ");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 14);
+ CHECK(text_edit->get_text() == "this is st \nthis ime t text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 11);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 9);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Undo.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is st text.\nthis ime test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 16);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
- CHECK(text_edit->get_caret_column(1) == 14);
+ CHECK(text_edit->get_caret_column(1) == 12);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is st \nthis ime t text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 11);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 9);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- }
- SUBCASE("[TextEdit] ui_text_backspace_word same line") {
- text_edit->set_text("test test test");
- text_edit->set_caret_column(4);
- text_edit->add_caret(0, 9);
- text_edit->add_caret(0, 15);
+ // Removing newline effectively happens after removing text.
+ text_edit->set_text("test\nlines");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(0);
+ text_edit->add_caret(1, 4);
- // For the second caret.
- Array args2;
- args2.push_back(0);
- lines_edited_args.push_front(args2);
+ SEND_GUI_ACTION("ui_text_backspace_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "tests");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ text_edit->remove_secondary_carets();
- // For the third caret.
- Array args3;
- args2.push_back(0);
- lines_edited_args.push_front(args2);
+ // Removing newline effectively happens after removing text, reverse caret order.
+ text_edit->set_text("test\nlines");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret(1, 0);
- CHECK(text_edit->get_caret_count() == 3);
- MessageQueue::get_singleton()->flush();
+ SEND_GUI_ACTION("ui_text_backspace_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "tests");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ text_edit->remove_secondary_carets();
+ }
+ SUBCASE("[TextEdit] ui_text_backspace_word same line") {
+ text_edit->set_text("test longwordtest test");
+ MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
+ // Multiple carets on the same line is handled.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret(0, 11);
+ text_edit->add_caret(0, 15);
+ text_edit->add_caret(0, 9);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 0));
+
SEND_GUI_ACTION("ui_text_backspace_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " ");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == " st test");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 0);
CHECK(text_edit->get_caret_column(1) == 1);
- CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- CHECK(text_edit->get_caret_line(2) == 0);
- CHECK(text_edit->get_caret_column(2) == 2);
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test longwordtest test");
+ CHECK(text_edit->get_caret_count() == 4);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 11);
+ CHECK_FALSE(text_edit->has_selection(2));
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 15);
+ CHECK_FALSE(text_edit->has_selection(3));
+ CHECK(text_edit->get_caret_line(3) == 0);
+ CHECK(text_edit->get_caret_column(3) == 9);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " st test");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 1);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
@@ -1973,130 +4282,267 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SUBCASE("[TextEdit] ui_text_backspace") {
text_edit->set_text("\nthis is some test text.\n\nthis is some test text.");
- text_edit->select(1, 0, 1, 4);
- text_edit->set_caret_line(1);
- text_edit->set_caret_column(4);
-
- text_edit->add_caret(3, 4);
- text_edit->select(3, 0, 3, 4, 1);
- CHECK(text_edit->get_caret_count() == 2);
-
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
- SIGNAL_DISCARD("caret_changed");
-
- // For the second caret.
- Array args2;
- args2.push_back(3);
- args2.push_back(3);
- lines_edited_args.push_front(args2);
- // With selection should be a normal backspace.
- ((Array)lines_edited_args[1])[0] = 1;
- ((Array)lines_edited_args[1])[1] = 1;
+ // Remove selected text when there are selections.
+ text_edit->select(1, 0, 1, 4);
+ text_edit->add_caret(3, 4);
+ text_edit->select(3, 5, 3, 2, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(3, 3), build_array(1, 1));
SEND_GUI_ACTION("ui_text_backspace");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "\n is some test text.\n\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Undo remove selection.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
+ CHECK(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 3);
- CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ CHECK(text_edit->get_selection_origin_line(1) == 3);
+ CHECK(text_edit->get_selection_origin_column(1) == 5);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+
+ // Redo remove selection.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "\n is some test text.\n\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 2);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- ((Array)lines_edited_args[0])[1] = 2;
- ((Array)lines_edited_args[1])[1] = 0;
+ // Remove the newline when at start of line.
+ text_edit->set_caret_column(0, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(3, 2), build_array(1, 0));
- // Start of line should also be a normal backspace.
SEND_GUI_ACTION("ui_text_backspace");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == " is some test text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Undo remove newline.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "\n is some test text.\n\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+
+ // Redo remove newline.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " is some test text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Does not work if not editable.
text_edit->set_caret_column(text_edit->get_line(0).length());
- text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
+ text_edit->set_caret_column(15, false, 1);
MessageQueue::get_singleton()->flush();
-
- SIGNAL_DISCARD("text_set");
- SIGNAL_DISCARD("text_changed");
- SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
text_edit->set_editable(false);
SEND_GUI_ACTION("ui_text_backspace");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
+ CHECK(text_edit->get_text() == " is some test text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
- CHECK(text_edit->get_caret_line(1) == 1);
- CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length());
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length());
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 15);
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
text_edit->set_editable(true);
- ((Array)lines_edited_args[0])[0] = 1;
- ((Array)lines_edited_args[0])[1] = 1;
- ((Array)lines_edited_args[1])[0] = 0;
+ // FIXME: Remove after GH-77101 is fixed.
+ text_edit->start_action(TextEdit::ACTION_NONE);
+
+ // Backspace removes character to the left.
+ lines_edited_args = build_array(build_array(1, 1), build_array(0, 0));
SEND_GUI_ACTION("ui_text_backspace");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text\n is some test text");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 18);
+ CHECK(text_edit->get_text() == " is some test text\nthis some testtext.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 18);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 14);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Backspace another character without changing caret.
+ SEND_GUI_ACTION("ui_text_backspace");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test tex\nthis some testext.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 17);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 13);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Undo both backspaces.
+ lines_edited_args = build_array(build_array(1, 1), build_array(0, 0), build_array(1, 1), build_array(0, 0));
+
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " is some test text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 19);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
- CHECK(text_edit->get_caret_column(1) == 18);
+ CHECK(text_edit->get_caret_column(1) == 15);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+
+ // Redo both backspaces.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " is some test tex\nthis some testext.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 17);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 13);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- // Select the entire text, from right to left
- text_edit->select(0, 18, 0, 0);
+ // Backspace with multiple carets that will overlap.
+ text_edit->remove_secondary_carets();
text_edit->set_caret_line(0);
- text_edit->set_caret_column(0);
-
- text_edit->select(1, 18, 1, 0, 1);
- text_edit->set_caret_line(1, false, true, 0, 1);
- text_edit->set_caret_column(0, false, 1);
+ text_edit->set_caret_column(8);
+ text_edit->add_caret(0, 7);
+ text_edit->add_caret(0, 9);
MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 0), build_array(0, 0));
- SIGNAL_DISCARD("text_set");
- SIGNAL_DISCARD("text_changed");
- SIGNAL_DISCARD("lines_edited_from");
+ SEND_GUI_ACTION("ui_text_backspace");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is sotest tex\nthis some testext.");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 6);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Select each line of text, from right to left. Remove selection to column 0.
+ text_edit->select(0, text_edit->get_line(0).length(), 0, 0);
+ text_edit->add_caret(1, 0);
+ text_edit->select(1, text_edit->get_line(1).length(), 1, 0, 1);
+ MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 1), build_array(0, 0));
SEND_GUI_ACTION("ui_text_backspace");
CHECK(text_edit->get_text() == "\n");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 0);
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Backspace at start of first line does nothing.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION("ui_text_backspace");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
}
SUBCASE("[TextEdit] ui_text_delete_all_to_right") {
@@ -2104,101 +4550,138 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
InputMap::get_singleton()->action_add_event("ui_text_delete_all_to_right", tmpevent);
text_edit->set_text("this is some test text.\nthis is some test text.\n");
- text_edit->select(0, 0, 0, 4);
- text_edit->set_caret_line(0);
- text_edit->set_caret_column(4);
-
- text_edit->add_caret(1, 4);
- text_edit->select(1, 0, 1, 4, 1);
- CHECK(text_edit->get_caret_count() == 2);
-
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
- SIGNAL_DISCARD("caret_changed");
- // For the second caret.
- Array args2;
- args2.push_back(1);
- args2.push_back(1);
- lines_edited_args.push_front(args2);
+ // Remove all text to right of caret.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(18);
+ text_edit->add_caret(0, 16);
+ text_edit->add_caret(0, 20);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0));
- // With selection should be a normal delete.
SEND_GUI_ACTION("ui_text_delete_all_to_right");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "this is some tes\nthis is some test text.\n");
+ CHECK(text_edit->get_caret_count() == 1);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 16);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- CHECK(text_edit->get_caret_line(1) == 1);
- CHECK(text_edit->get_caret_column(1) == 0);
+ // Undo.
+ lines_edited_args = build_array(build_array(0, 0));
+
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text.\n");
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 18);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 16);
+ CHECK_FALSE(text_edit->has_selection(2));
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 20);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- // End of line should not do anything.
- text_edit->set_caret_column(text_edit->get_line(0).length());
- text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
+ // Redo.
+ text_edit->redo();
MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is some tes\nthis is some test text.\n");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 16);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- SIGNAL_DISCARD("text_set");
- SIGNAL_DISCARD("text_changed");
- SIGNAL_DISCARD("lines_edited_from");
+ // Acts as a normal delete with selections.
+ text_edit->select(0, 0, 0, 4);
+ text_edit->add_caret(1, 4);
+ text_edit->select(1, 8, 1, 4, 1);
+ MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0), build_array(1, 1));
SEND_GUI_ACTION("ui_text_delete_all_to_right");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
+ CHECK(text_edit->get_text() == " is some tes\nthissome test text.\n");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Does nothing when caret is at end of line.
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+ text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION("ui_text_delete_all_to_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some tes\nthissome test text.\n");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length());
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length());
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
+ // Does not work if not editable.
text_edit->set_caret_column(0);
text_edit->set_caret_column(0, false, 1);
MessageQueue::get_singleton()->flush();
-
- SIGNAL_DISCARD("text_set");
- SIGNAL_DISCARD("text_changed");
- SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
text_edit->set_editable(false);
SEND_GUI_ACTION("ui_text_delete_all_to_right");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == " is some tes\nthissome test text.\n");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
text_edit->set_editable(true);
+ // Delete entire line.
SEND_GUI_ACTION("ui_text_delete_all_to_right");
CHECK(text_edit->get_viewport()->is_input_handled());
CHECK(text_edit->get_text() == "\n\n");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
@@ -2210,302 +4693,589 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
text_edit->set_caret_mid_grapheme_enabled(true);
CHECK(text_edit->is_caret_mid_grapheme_enabled());
- text_edit->set_text("this ffi some test text.\n\nthis ffi some test text.\n");
- text_edit->select(0, 0, 0, 4);
- text_edit->set_caret_line(0);
- text_edit->set_caret_column(4);
-
- text_edit->add_caret(2, 4);
- text_edit->select(2, 0, 2, 4, 1);
- CHECK(text_edit->get_caret_count() == 2);
-
+ text_edit->set_text("this is some test text.\n\nthis is some test text.\n");
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // For the second caret.
- Array args2;
- args2.push_back(2);
- args2.push_back(2);
- lines_edited_args.push_front(args2);
+ // Acts as a normal delete with selections.
+ text_edit->select(0, 8, 0, 15);
+ text_edit->add_caret(2, 6);
+ text_edit->select(2, 10, 2, 6, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0), build_array(2, 2));
- // With selection should be a normal delete.
SEND_GUI_ACTION("ui_text_delete_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " ffi some test text.\n\n ffi some test text.\n");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "this is st text.\n\nthis ime test text.\n");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
- CHECK(text_edit->get_caret_line(1) == 2);
- CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 8);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 6);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- // With selection should be a normal delete.
- ((Array)lines_edited_args[0])[0] = 3;
- ((Array)lines_edited_args[1])[0] = 1;
+ // Removes newlines when at end of line.
text_edit->set_caret_column(text_edit->get_line(0).length());
text_edit->set_caret_column(text_edit->get_line(2).length(), false, 1);
MessageQueue::get_singleton()->flush();
-
- SIGNAL_DISCARD("text_set");
- SIGNAL_DISCARD("text_changed");
- SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 0), build_array(2, 1));
SEND_GUI_ACTION("ui_text_delete_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
- CHECK_FALSE(text_edit->has_selection());
-
+ CHECK(text_edit->get_text() == "this is st text.\nthis ime test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length());
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length());
- CHECK_FALSE(text_edit->has_selection(0));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- ((Array)lines_edited_args[1])[0] = 0;
- ((Array)lines_edited_args[0])[0] = 1;
- ((Array)lines_edited_args[0])[1] = 1;
+ // Does not work if not editable.
text_edit->set_caret_column(0);
- text_edit->set_caret_column(0, false, 1);
+ text_edit->set_caret_column(10, false, 1);
MessageQueue::get_singleton()->flush();
-
- SIGNAL_DISCARD("text_set");
- SIGNAL_DISCARD("text_changed");
- SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
text_edit->set_editable(false);
SEND_GUI_ACTION("ui_text_delete_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "this is st text.\nthis ime test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
- CHECK(text_edit->get_caret_line(1) == 1);
- CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 10);
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
text_edit->set_editable(true);
+ // FIXME: Remove after GH-77101 is fixed.
+ text_edit->start_action(TextEdit::ACTION_NONE);
+
+ // Delete to the end of the word right of the caret.
+ lines_edited_args = build_array(build_array(0, 0), build_array(1, 1));
+
SEND_GUI_ACTION("ui_text_delete_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " some test text.\n some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == " is st text.\nthis ime t text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 10);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Undo.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is st text.\nthis ime test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
- CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 10);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " is st text.\nthis ime t text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 10);
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- }
- SUBCASE("[TextEdit] ui_text_delete") {
- text_edit->set_caret_mid_grapheme_enabled(true);
- CHECK(text_edit->is_caret_mid_grapheme_enabled());
+ // Delete one word with multiple carets.
+ text_edit->remove_secondary_carets();
+ text_edit->set_text("onelongword test");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(6);
+ text_edit->add_caret(0, 9);
+ text_edit->add_caret(0, 3);
+ lines_edited_args = build_array(build_array(0, 0));
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
- text_edit->set_text("this ffi some test text.\nthis ffi some test text.");
- text_edit->select(0, 0, 0, 4);
+ SEND_GUI_ACTION("ui_text_delete_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "one test");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 3);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Removing newline effectively happens after removing text.
+ text_edit->set_text("test\nlines");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(2);
+ text_edit->add_caret(0, 4);
+
+ SEND_GUI_ACTION("ui_text_delete_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "telines");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 2);
+ text_edit->remove_secondary_carets();
+
+ // Removing newline effectively happens after removing text, reverse caret order.
+ text_edit->set_text("test\nlines");
text_edit->set_caret_line(0);
text_edit->set_caret_column(4);
+ text_edit->add_caret(0, 2);
- text_edit->add_caret(1, 4);
- text_edit->select(1, 0, 1, 4, 1);
- CHECK(text_edit->get_caret_count() == 2);
+ SEND_GUI_ACTION("ui_text_delete_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "telines");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 2);
+ text_edit->remove_secondary_carets();
+ }
+ SUBCASE("[TextEdit] ui_text_delete_word same line") {
+ text_edit->set_text("test longwordtest test");
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // For the second caret.
- Array args2;
- args2.push_back(1);
- args2.push_back(1);
- lines_edited_args.push_front(args2);
+ // Multiple carets on the same line is handled.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+ text_edit->add_caret(0, 11);
+ text_edit->add_caret(0, 15);
+ text_edit->add_caret(0, 9);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 0));
- // With selection should be a normal delete.
- SEND_GUI_ACTION("ui_text_delete");
+ SEND_GUI_ACTION("ui_text_delete_word");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == " long test");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
- CHECK(text_edit->get_caret_line(1) == 1);
- CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 5);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- // With selection should be a normal delete.
- lines_edited_args.remove_at(0);
- ((Array)lines_edited_args[0])[0] = 1;
- text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
- text_edit->set_caret_column(text_edit->get_line(0).length());
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 0));
+
+ text_edit->undo();
MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test longwordtest test");
+ CHECK(text_edit->get_caret_count() == 4);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 11);
+ CHECK_FALSE(text_edit->has_selection(2));
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 15);
+ CHECK_FALSE(text_edit->has_selection(3));
+ CHECK(text_edit->get_caret_line(3) == 0);
+ CHECK(text_edit->get_caret_column(3) == 9);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " long test");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 5);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ }
+
+ SUBCASE("[TextEdit] ui_text_delete") {
+ text_edit->set_caret_mid_grapheme_enabled(true);
+ CHECK(text_edit->is_caret_mid_grapheme_enabled());
+
+ text_edit->set_text("this is some test text.\n\nthis is some test text.\n");
+ MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
+ // Remove selected text when there are selections.
+ text_edit->select(0, 0, 0, 4);
+ text_edit->add_caret(2, 2);
+ text_edit->select(2, 5, 2, 2, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(0, 0), build_array(2, 2));
+
SEND_GUI_ACTION("ui_text_delete");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " ffi some test text. ffi some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 20);
+ CHECK(text_edit->get_text() == " is some test text.\n\nthis some test text.\n");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 2);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- // Caret should be removed due to column preservation.
- CHECK(text_edit->get_caret_count() == 1);
-
- // Lets add it back.
- text_edit->set_caret_column(0);
- text_edit->add_caret(0, 20);
+ // Undo remove selection.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 5);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
- ((Array)lines_edited_args[0])[0] = 0;
- lines_edited_args.push_back(args2);
- ((Array)lines_edited_args[1])[0] = 0;
- ((Array)lines_edited_args[1])[1] = 0;
+ // Redo remove selection.
+ text_edit->redo();
MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " is some test text.\n\nthis some test text.\n");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- SIGNAL_DISCARD("text_set");
- SIGNAL_DISCARD("text_changed");
- SIGNAL_DISCARD("lines_edited_from");
+ // Remove newline when at end of line.
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+ text_edit->set_caret_column(text_edit->get_line(2).length(), false, 1);
+ MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("caret_changed");
+ lines_edited_args = build_array(build_array(1, 0), build_array(2, 1));
- text_edit->set_editable(false);
SEND_GUI_ACTION("ui_text_delete");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == " ffi some test text. ffi some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == " is some test text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 19);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 20);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- CHECK(text_edit->get_caret_line(1) == 0);
+ // Undo remove newline.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " is some test text.\n\nthis some test text.\n");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 19);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 20);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+
+ // Redo remove newline.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " is some test text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 19);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 20);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Does not work if not editable.
+ text_edit->set_caret_column(0);
+ text_edit->set_caret_column(15, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION("ui_text_delete");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 15);
SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
text_edit->set_editable(true);
+ // FIXME: Remove after GH-77101 is fixed.
text_edit->start_action(TextEdit::EditAction::ACTION_NONE);
+ // Delete removes character to the right.
+ lines_edited_args = build_array(build_array(0, 0), build_array(1, 1));
+
SEND_GUI_ACTION("ui_text_delete");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == "ffi some test text.ffi some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_text() == "is some test text.\nthis some test ext.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 15);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- CHECK(text_edit->get_caret_line(1) == 0);
- CHECK(text_edit->get_caret_column(1) == 19);
+ // Delete another character without changing caret.
+ SEND_GUI_ACTION("ui_text_delete");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "s some test text.\nthis some test xt.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
- SIGNAL_CHECK("caret_changed", empty_signal_args);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 15);
+ SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
- text_edit->start_action(TextEdit::EditAction::ACTION_NONE);
+ // Undo both deletes.
+ lines_edited_args = build_array(build_array(0, 0), build_array(1, 1), build_array(0, 0), build_array(1, 1));
- SEND_GUI_ACTION("ui_text_delete");
- CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_text() == "fi some test text.fi some test text.");
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == " is some test text.\nthis some test text.");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 15);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
- CHECK(text_edit->get_caret_line(1) == 0);
- CHECK(text_edit->get_caret_column(1) == 18);
+ // Redo both deletes.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "s some test text.\nthis some test xt.");
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
- SIGNAL_CHECK("caret_changed", empty_signal_args);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 15);
+ SIGNAL_CHECK_FALSE("caret_changed");
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Delete at end of last line does nothing.
+ text_edit->remove_secondary_carets();
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(18);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION("ui_text_delete");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "s some test text.\nthis some test xt.");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 18);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
}
SUBCASE("[TextEdit] ui_text_caret_word_left") {
text_edit->set_text("\nthis is some test text.\nthis is some test text.");
text_edit->set_caret_line(1);
- text_edit->set_caret_column(7);
-
- text_edit->add_caret(2, 7);
- CHECK(text_edit->get_caret_count() == 2);
+ text_edit->set_caret_column(15);
+ text_edit->add_caret(2, 10);
+ text_edit->select(1, 10, 1, 15);
+ text_edit->select(2, 15, 2, 10, 1);
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // Shift should select.
+ // Deselect to start of previous word when selection is right to left.
+ // Select to start of next word when selection is left to right.
#ifdef MACOS_ENABLED
SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::ALT | KeyModifierMask::SHIFT);
#else
SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
#endif
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 5);
- CHECK(text_edit->get_selected_text(0) == "is");
+ CHECK(text_edit->get_caret_count() == 2);
+
CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "me ");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 13);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 10);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
- CHECK(text_edit->get_caret_line(1) == 2);
- CHECK(text_edit->get_caret_column(1) == 5);
- CHECK(text_edit->get_selected_text(1) == "is");
CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "some te");
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 8);
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 15);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // Should still move caret with selection.
- SEND_GUI_ACTION("ui_text_caret_word_left");
+ // Select to start of word with shift.
+ text_edit->deselect();
+ text_edit->set_caret_column(7);
+ text_edit->set_caret_column(16, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::ALT | KeyModifierMask::SHIFT);
+#else
+ SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#endif
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
- CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "is");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 7);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(0));
+
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "tes");
CHECK(text_edit->get_caret_line(1) == 2);
- CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_column(1) == 13);
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 16);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // Normal word left.
+ // Deselect and move caret to start of next word without shift.
SEND_GUI_ACTION("ui_text_caret_word_left");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 8);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ // Moves to end of previous line when at start of line. Does nothing at start of text.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+ text_edit->set_caret_column(0, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION("ui_text_caret_word_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 23);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
@@ -2515,249 +5285,417 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
text_edit->set_text("\nthis is some test text.\nthis is some test text.");
text_edit->set_caret_line(1);
text_edit->set_caret_column(7);
- text_edit->select(1, 2, 1, 7);
-
- text_edit->add_caret(2, 7);
- text_edit->select(2, 2, 2, 7, 1);
- CHECK(text_edit->get_caret_count() == 2);
-
+ text_edit->select(1, 3, 1, 7);
+ text_edit->add_caret(2, 3);
+ text_edit->select(2, 7, 2, 3, 1);
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // Normal left should deselect and place at selection start.
- SEND_GUI_ACTION("ui_text_caret_left");
+ // Remove one character from selection when selection is left to right.
+ // Add one character to selection when selection is right to left.
+ SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT);
CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "s i");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 6);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 3);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 2);
- CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "is is");
CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 2);
- CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 7);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // With shift should select.
- SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT);
+ // Deselect and put caret at selection start without shift.
+ SEND_GUI_ACTION("ui_text_caret_left");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 1);
- CHECK(text_edit->get_selected_text(0) == "h");
- CHECK(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 3);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 2);
- CHECK(text_edit->get_caret_column(1) == 1);
- CHECK(text_edit->get_selected_text(1) == "h");
- CHECK(text_edit->has_selection(1));
-
+ CHECK(text_edit->get_caret_column(1) == 2);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // All ready at select left, should only deselect.
+ // Move caret one character to the left.
SEND_GUI_ACTION("ui_text_caret_left");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 1);
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 2);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 1);
- CHECK_FALSE(text_edit->has_selection(1));
-
- SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // Normal left.
- SEND_GUI_ACTION("ui_text_caret_left");
+ // Select one character to the left with shift and no existing selection.
+ SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT);
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
- CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "h");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 1);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 2);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(0));
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "t");
CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 1);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(1));
+
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // Left at col 0 should go up a line.
+ // Moves to end of previous line when at start of line. Does nothing at start of text.
+ text_edit->deselect();
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+ text_edit->set_caret_column(0, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
SEND_GUI_ACTION("ui_text_caret_left");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 1);
CHECK(text_edit->get_caret_column(1) == 23);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- }
- SUBCASE("[TextEdit] ui_text_caret_word_right") {
- text_edit->set_text("this is some test text\n\nthis is some test text\n");
- text_edit->set_caret_line(0);
- text_edit->set_caret_column(13);
+ // Selects to end of previous line when at start of line.
+ text_edit->remove_secondary_carets();
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(0);
+ text_edit->select(1, 1, 1, 0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
- text_edit->add_caret(2, 13);
- CHECK(text_edit->get_caret_count() == 2);
+ SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "\nt");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 1);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ // Merge selections when they overlap.
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(4);
+ text_edit->select(1, 6, 1, 4);
+ text_edit->add_caret(1, 8);
+ text_edit->select(1, 8, 1, 6, 1);
MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ CHECK(text_edit->get_caret_count() == 2);
+ SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "s is ");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 3);
+ CHECK(text_edit->get_selection_origin_line(0) == 1);
+ CHECK(text_edit->get_selection_origin_column(0) == 8);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_word_right") {
+ text_edit->set_text("this is some test text\n\nthis is some test text");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(15);
+ text_edit->add_caret(2, 10);
+ text_edit->select(0, 10, 0, 15);
+ text_edit->select(2, 15, 2, 10, 1);
+ MessageQueue::get_singleton()->flush();
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // Shift should select.
+ // Select to end of next word when selection is right to left.
+ // Deselect to end of previous word when selection is left to right.
#ifdef MACOS_ENABLED
SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::ALT | KeyModifierMask::SHIFT);
#else
SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
#endif
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 17);
- CHECK(text_edit->get_selected_text(0) == "test");
+ CHECK(text_edit->get_caret_count() == 2);
CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "me test");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 17);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 10);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == " te");
CHECK(text_edit->get_caret_line(1) == 2);
- CHECK(text_edit->get_caret_column(1) == 17);
- CHECK(text_edit->get_selected_text(1) == "test");
+ CHECK(text_edit->get_caret_column(1) == 12);
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 15);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Select to end of word with shift.
+ text_edit->deselect();
+ text_edit->set_caret_column(13);
+ text_edit->set_caret_column(15, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::ALT | KeyModifierMask::SHIFT);
+#else
+ SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#endif
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "test");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 17);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 13);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+
CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "st");
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 17);
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 15);
+ CHECK(text_edit->is_caret_after_selection_origin(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // Should still move caret with selection.
+ // Deselect and move caret to end of next word without shift.
SEND_GUI_ACTION("ui_text_caret_word_right");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 22);
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 22);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 22);
- CHECK_FALSE(text_edit->has_selection(1));
-
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // Normal word right.
+ // Moves to start of next line when at end of line. Does nothing at end of text.
SEND_GUI_ACTION("ui_text_caret_word_right");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
- CHECK(text_edit->get_caret_line(1) == 3);
- CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 22);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
}
SUBCASE("[TextEdit] ui_text_caret_right") {
- text_edit->set_text("this is some test text\n\nthis is some test text\n");
+ text_edit->set_text("this is some test text\n\nthis is some test text");
text_edit->set_caret_line(0);
- text_edit->set_caret_column(16);
- text_edit->select(0, 16, 0, 20);
-
- text_edit->add_caret(2, 16);
- text_edit->select(2, 16, 2, 20, 1);
- CHECK(text_edit->get_caret_count() == 2);
-
+ text_edit->set_caret_column(19);
+ text_edit->select(0, 15, 0, 19);
+ text_edit->add_caret(2, 15);
+ text_edit->select(2, 19, 2, 15, 1);
MessageQueue::get_singleton()->flush();
-
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // Normal right should deselect and place at selection start.
- SEND_GUI_ACTION("ui_text_caret_right");
+ // Remove one character from selection when selection is right to left.
+ // Add one character to selection when selection is left to right.
+ SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT);
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 20);
- CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "st te");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 20);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 15);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "t t");
CHECK(text_edit->get_caret_line(1) == 2);
- CHECK(text_edit->get_caret_column(1) == 20);
- CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_column(1) == 16);
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 19);
+ CHECK_FALSE(text_edit->is_caret_after_selection_origin(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // With shift should select.
- SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT);
+ // Deselect and put caret at selection end without shift.
+ SEND_GUI_ACTION("ui_text_caret_right");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 21);
- CHECK(text_edit->get_selected_text(0) == "x");
- CHECK(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 20);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 2);
- CHECK(text_edit->get_caret_column(1) == 21);
- CHECK(text_edit->get_selected_text(1) == "x");
- CHECK(text_edit->has_selection(1));
-
+ CHECK(text_edit->get_caret_column(1) == 19);
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // All ready at select right, should only deselect.
+ // Move caret one character to the right.
SEND_GUI_ACTION("ui_text_caret_right");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 21);
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 21);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 20);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ // Select one character to the right with shift and no existing selection.
+ SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "t");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 22);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 21);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "x");
CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 21);
- CHECK_FALSE(text_edit->has_selection(1));
- SIGNAL_CHECK_FALSE("caret_changed");
+ CHECK(text_edit->get_selection_origin_line(1) == 2);
+ CHECK(text_edit->get_selection_origin_column(1) == 20);
+ CHECK(text_edit->is_caret_after_selection_origin(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // Normal right.
+ // Moves to start of next line when at end of line. Does nothing at end of text.
+ text_edit->deselect();
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(22);
+ text_edit->set_caret_column(22, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
SEND_GUI_ACTION("ui_text_caret_right");
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 0);
- CHECK(text_edit->get_caret_column() == 22);
+ CHECK(text_edit->get_caret_count() == 2);
CHECK_FALSE(text_edit->has_selection(0));
-
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 22);
- CHECK_FALSE(text_edit->has_selection(1));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
- // Right at end col should go down a line.
- SEND_GUI_ACTION("ui_text_caret_right");
+ // Selects to start of next line when at end of line.
+ text_edit->remove_secondary_carets();
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(22);
+ text_edit->select(0, 21, 0, 22);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT);
CHECK(text_edit->get_viewport()->is_input_handled());
- CHECK(text_edit->get_caret_line() == 1);
- CHECK(text_edit->get_caret_column() == 0);
- CHECK_FALSE(text_edit->has_selection(0));
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "t\n");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 21);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
- CHECK(text_edit->get_caret_line(1) == 3);
- CHECK(text_edit->get_caret_column(1) == 0);
- CHECK_FALSE(text_edit->has_selection(1));
+ // Merge selections when they overlap.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ text_edit->select(0, 4, 0, 6);
+ text_edit->add_caret(0, 8);
+ text_edit->select(0, 6, 0, 8, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+ CHECK(text_edit->get_caret_count() == 2);
+
+ SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == " is s");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 9);
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 4);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
@@ -2775,7 +5713,6 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_caret_count() == 2);
MessageQueue::get_singleton()->flush();
-
CHECK(text_edit->is_line_wrapped(0));
SIGNAL_DISCARD("text_set");
SIGNAL_DISCARD("text_changed");
@@ -3156,11 +6093,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_DISCARD("lines_edited_from");
SIGNAL_DISCARD("caret_changed");
- // For the second caret.
- Array args2;
- args2.push_back(1);
- args2.push_back(1);
- lines_edited_args.push_front(args2);
+ lines_edited_args = build_array(build_array(0, 0), build_array(1, 1));
SEND_GUI_KEY_EVENT(Key::A);
CHECK(text_edit->get_viewport()->is_input_handled());
@@ -3171,6 +6104,27 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("text_changed", empty_signal_args);
SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ // Undo reverts both carets.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "a\na");
+ CHECK(text_edit->get_caret_column() == 1);
+ CHECK(text_edit->get_caret_column(1) == 1);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args));
+
+ // Redo.
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "aA\naA");
+ CHECK(text_edit->get_caret_column() == 2);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Does not work if not editable.
text_edit->set_editable(false);
SEND_GUI_KEY_EVENT(Key::A);
CHECK_FALSE(text_edit->get_viewport()->is_input_handled()); // Should this be handled?
@@ -3182,8 +6136,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK_FALSE("lines_edited_from");
text_edit->set_editable(true);
- lines_edited_args.push_back(lines_edited_args[1].duplicate());
- lines_edited_args.push_front(args2.duplicate());
+ lines_edited_args = build_array(build_array(0, 0), build_array(0, 0), build_array(1, 1), build_array(1, 1));
text_edit->select(0, 0, 0, 1);
text_edit->select(1, 0, 1, 1, 1);
@@ -3220,8 +6173,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
text_edit->set_overtype_mode_enabled(false);
CHECK_FALSE(text_edit->is_overtype_mode_enabled());
- lines_edited_args.remove_at(0);
- lines_edited_args.remove_at(1);
+ lines_edited_args = build_array(build_array(0, 0), build_array(1, 1));
SEND_GUI_KEY_EVENT(Key::TAB);
CHECK(text_edit->get_viewport()->is_input_handled());
@@ -3576,6 +6528,11 @@ TEST_CASE("[SceneTree][TextEdit] caret") {
text_edit->set_caret_column(4);
CHECK(text_edit->get_word_under_caret() == "Lorem");
+ text_edit->set_caret_column(1);
+ text_edit->add_caret(0, 15);
+ CHECK(text_edit->get_word_under_caret() == "Lorem\ndolor");
+ text_edit->remove_secondary_carets();
+
// Should this work?
text_edit->set_caret_column(5);
CHECK(text_edit->get_word_under_caret() == "");
@@ -3616,18 +6573,20 @@ TEST_CASE("[SceneTree][TextEdit] multicaret") {
SIGNAL_DISCARD("caret_changed");
SUBCASE("[TextEdit] add remove caret") {
- // Overlapping
+ // Overlapping.
CHECK(text_edit->add_caret(0, 0) == -1);
MessageQueue::get_singleton()->flush();
SIGNAL_CHECK_FALSE("caret_changed");
- // Selection
- text_edit->select(0, 0, 2, 4);
+ // Select.
+ text_edit->select(2, 4, 0, 0);
+
+ // Cannot add in selection.
CHECK(text_edit->add_caret(0, 0) == -1);
CHECK(text_edit->add_caret(2, 4) == -1);
CHECK(text_edit->add_caret(1, 2) == -1);
- // Out of bounds
+ // Cannot add when out of bounds.
CHECK(text_edit->add_caret(-1, 0) == -1);
CHECK(text_edit->add_caret(5, 0) == -1);
CHECK(text_edit->add_caret(0, 100) == -1);
@@ -3670,23 +6629,276 @@ TEST_CASE("[SceneTree][TextEdit] multicaret") {
ERR_PRINT_ON;
}
- SUBCASE("[TextEdit] caret index edit order") {
- Vector<int> caret_index_get_order;
- caret_index_get_order.push_back(1);
- caret_index_get_order.push_back(0);
+ SUBCASE("[TextEdit] sort carets") {
+ Vector<int> sorted_carets = { 0, 1, 2 };
- CHECK(text_edit->add_caret(1, 0));
- CHECK(text_edit->get_caret_count() == 2);
- CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order);
+ // Ascending order.
+ text_edit->remove_secondary_carets();
+ text_edit->add_caret(0, 1);
+ text_edit->add_caret(1, 0);
+ CHECK(text_edit->get_sorted_carets() == sorted_carets);
+ // Descending order.
+ sorted_carets = { 2, 1, 0 };
text_edit->remove_secondary_carets();
text_edit->set_caret_line(1);
- CHECK(text_edit->add_caret(0, 0));
+ text_edit->add_caret(0, 1);
+ text_edit->add_caret(0, 0);
+ CHECK(text_edit->get_sorted_carets() == sorted_carets);
+
+ // Mixed order.
+ sorted_carets = { 0, 2, 1, 3 };
+ text_edit->remove_secondary_carets();
+ text_edit->set_caret_line(0);
+ text_edit->add_caret(1, 0);
+ text_edit->add_caret(0, 1);
+ text_edit->add_caret(1, 1);
+ CHECK(text_edit->get_sorted_carets() == sorted_carets);
+
+ // Overlapping carets.
+ sorted_carets = { 0, 1, 3, 2 };
+ text_edit->remove_secondary_carets();
+ text_edit->add_caret(0, 1);
+ text_edit->add_caret(1, 2);
+ text_edit->add_caret(0, 2);
+ text_edit->set_caret_column(1, false, 3);
+ CHECK(text_edit->get_sorted_carets() == sorted_carets);
+
+ // Sorted by selection start.
+ sorted_carets = { 1, 0 };
+ text_edit->remove_secondary_carets();
+ text_edit->select(1, 3, 1, 5);
+ text_edit->add_caret(2, 0);
+ text_edit->select(1, 0, 2, 0, 1);
+ CHECK(text_edit->get_sorted_carets() == sorted_carets);
+ }
+
+ SUBCASE("[TextEdit] merge carets") {
+ text_edit->set_text("this is some text\nfor selection");
+ MessageQueue::get_singleton()->flush();
+
+ // Don't merge carets that are not overlapping.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret(0, 6);
+ text_edit->add_caret(1, 6);
+ text_edit->merge_overlapping_carets();
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 6);
+ CHECK(text_edit->get_caret_line(2) == 1);
+ CHECK(text_edit->get_caret_column(2) == 6);
+ text_edit->remove_secondary_carets();
+
+ // Don't merge when in a multicaret edit.
+ text_edit->begin_multicaret_edit();
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret(0, 4);
+ text_edit->merge_overlapping_carets();
+ CHECK(text_edit->is_in_mulitcaret_edit());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 4);
+
+ // Merge overlapping carets. Merge at the end of the multicaret edit.
+ text_edit->end_multicaret_edit();
+ CHECK_FALSE(text_edit->is_in_mulitcaret_edit());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+
+ // Don't merge selections that are not overlapping.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret(0, 2);
+ text_edit->add_caret(1, 4);
+ text_edit->select(0, 4, 1, 2, 0);
+ text_edit->select(0, 2, 0, 3, 1);
+ text_edit->select(1, 4, 1, 8, 2);
+ text_edit->merge_overlapping_carets();
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->has_selection(2));
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+
+ // Don't merge selections that are only touching.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret(1, 2);
+ text_edit->select(0, 4, 1, 2, 0);
+ text_edit->select(1, 2, 1, 5, 1);
+ text_edit->merge_overlapping_carets();
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->has_selection(1));
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+
+ // Merge carets into selection.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(3);
+ text_edit->add_caret(0, 2);
+ text_edit->add_caret(1, 4);
+ text_edit->add_caret(1, 8);
+ text_edit->add_caret(1, 10);
+ text_edit->select(0, 2, 1, 8, 0);
+ text_edit->merge_overlapping_carets();
CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_from_line(0) == 0);
+ CHECK(text_edit->get_selection_from_column(0) == 2);
+ CHECK(text_edit->get_selection_to_line(0) == 1);
+ CHECK(text_edit->get_selection_to_column(0) == 8);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 10);
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
- caret_index_get_order.write[0] = 0;
- caret_index_get_order.write[1] = 1;
- CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order);
+ // Merge partially overlapping selections.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(1);
+ text_edit->add_caret(0, 2);
+ text_edit->add_caret(0, 3);
+ text_edit->select(0, 2, 0, 6, 0);
+ text_edit->select(0, 4, 1, 3, 1);
+ text_edit->select(1, 0, 1, 5, 2);
+ text_edit->merge_overlapping_carets();
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_from_line(0) == 0);
+ CHECK(text_edit->get_selection_from_column(0) == 2);
+ CHECK(text_edit->get_selection_to_line(0) == 1);
+ CHECK(text_edit->get_selection_to_column(0) == 5);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+
+ // Merge smaller overlapping selection into a bigger one.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(1);
+ text_edit->add_caret(0, 2);
+ text_edit->add_caret(0, 3);
+ text_edit->select(0, 2, 0, 6, 0);
+ text_edit->select(0, 8, 1, 3, 1);
+ text_edit->select(0, 2, 1, 5, 2);
+ text_edit->merge_overlapping_carets();
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_from_line(0) == 0);
+ CHECK(text_edit->get_selection_from_column(0) == 2);
+ CHECK(text_edit->get_selection_to_line(0) == 1);
+ CHECK(text_edit->get_selection_to_column(0) == 5);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+
+ // Merge equal overlapping selections.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(1);
+ text_edit->add_caret(0, 2);
+ text_edit->select(0, 2, 1, 6, 0);
+ text_edit->select(0, 2, 1, 6, 1);
+ text_edit->merge_overlapping_carets();
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_from_line(0) == 0);
+ CHECK(text_edit->get_selection_from_column(0) == 2);
+ CHECK(text_edit->get_selection_to_line(0) == 1);
+ CHECK(text_edit->get_selection_to_column(0) == 6);
+ CHECK(text_edit->is_caret_after_selection_origin(0));
+ }
+
+ SUBCASE("[TextEdit] collapse carets") {
+ text_edit->set_text("this is some text\nfor selection");
+
+ // Collapse carets in range, dont affect other carets.
+ text_edit->add_caret(0, 9);
+ text_edit->add_caret(1, 0);
+ text_edit->add_caret(1, 2);
+ text_edit->add_caret(1, 6);
+ text_edit->begin_multicaret_edit();
+
+ text_edit->collapse_carets(0, 8, 1, 2);
+ CHECK(text_edit->get_caret_count() == 5);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 8);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 8);
+ CHECK(text_edit->get_caret_line(3) == 1);
+ CHECK(text_edit->get_caret_column(3) == 2);
+ CHECK(text_edit->get_caret_line(4) == 1);
+ CHECK(text_edit->get_caret_column(4) == 6);
+ CHECK_FALSE(text_edit->multicaret_edit_ignore_caret(0));
+ CHECK(text_edit->multicaret_edit_ignore_caret(1));
+ CHECK(text_edit->multicaret_edit_ignore_caret(2));
+ CHECK_FALSE(text_edit->multicaret_edit_ignore_caret(3));
+ CHECK_FALSE(text_edit->multicaret_edit_ignore_caret(4));
+
+ // Collapsed carets get merged at the end of the edit.
+ text_edit->end_multicaret_edit();
+ CHECK(text_edit->get_caret_count() == 4);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 8);
+ CHECK(text_edit->get_caret_line(2) == 1);
+ CHECK(text_edit->get_caret_column(2) == 2);
+ CHECK(text_edit->get_caret_line(3) == 1);
+ CHECK(text_edit->get_caret_column(3) == 6);
+ text_edit->remove_secondary_carets();
+
+ // Collapse inclusive.
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(3);
+ text_edit->add_caret(1, 2);
+ text_edit->collapse_carets(0, 3, 1, 2, true);
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 3);
+ text_edit->remove_secondary_carets();
+
+ // Deselect if selection was encompassed.
+ text_edit->select(0, 5, 0, 7);
+ text_edit->collapse_carets(0, 3, 1, 2);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 3);
+
+ // Clamp only caret end of selection.
+ text_edit->select(0, 1, 0, 7);
+ text_edit->collapse_carets(0, 3, 1, 2);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 3);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 1);
+ text_edit->deselect();
+
+ // Clamp only selection origin end of selection.
+ text_edit->select(0, 7, 0, 1);
+ text_edit->collapse_carets(0, 3, 1, 2);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 1);
+ CHECK(text_edit->get_selection_origin_line() == 0);
+ CHECK(text_edit->get_selection_origin_column() == 3);
+ text_edit->deselect();
}
SUBCASE("[TextEdit] add caret at carets") {
@@ -3694,36 +6906,320 @@ TEST_CASE("[SceneTree][TextEdit] multicaret") {
text_edit->set_caret_line(1);
text_edit->set_caret_column(9);
+ // Add caret below. Column will clamp.
text_edit->add_caret_at_carets(true);
CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 9);
CHECK(text_edit->get_caret_line(1) == 2);
CHECK(text_edit->get_caret_column(1) == 4);
+ // Cannot add below when at last line.
text_edit->add_caret_at_carets(true);
CHECK(text_edit->get_caret_count() == 2);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 9);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ // Add caret above. Column will clamp.
text_edit->add_caret_at_carets(false);
CHECK(text_edit->get_caret_count() == 3);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 9);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 4);
CHECK(text_edit->get_caret_line(2) == 0);
CHECK(text_edit->get_caret_column(2) == 7);
+ // Cannot add above when at first line.
+ text_edit->add_caret_at_carets(false);
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 9);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 7);
+
+ // Cannot add below when at the last line for selection.
+ text_edit->remove_secondary_carets();
+ text_edit->select(2, 1, 2, 4);
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 2);
+ CHECK(text_edit->get_selection_origin_column(0) == 1);
+ CHECK(text_edit->get_caret_line(0) == 2);
+ CHECK(text_edit->get_caret_column(0) == 4);
+
+ // Cannot add above when at the first line for selection.
+ text_edit->select(0, 1, 0, 4);
+ text_edit->add_caret_at_carets(false);
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+
+ // Add selection below.
+ text_edit->select(0, 0, 0, 4);
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selection_origin_line(1) == 1);
+ CHECK(text_edit->get_selection_origin_column(1) == 0);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 3); // In the default font, this is the same position.
+
+ // Add selection below again.
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 0);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selection_origin_line(1) == 1);
+ CHECK(text_edit->get_selection_origin_column(1) == 0);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 3);
+ CHECK(text_edit->has_selection(2));
+ CHECK(text_edit->get_selection_origin_line(2) == 2);
+ CHECK(text_edit->get_selection_origin_column(2) == 0);
+ CHECK(text_edit->get_caret_line(2) == 2);
+ CHECK(text_edit->get_caret_column(2) == 4);
+
+ text_edit->set_text("\tthis is\nsome\n\ttest text");
+ MessageQueue::get_singleton()->flush();
+
+ // Last fit x is preserved when adding below.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(6);
+ text_edit->add_caret_at_carets(true);
+ text_edit->add_caret_at_carets(true);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 6);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK(text_edit->get_caret_line(2) == 2);
+ CHECK(text_edit->get_caret_column(2) == 6);
+
+ // Last fit x is preserved when adding above.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(9);
+ text_edit->add_caret_at_carets(false);
+ text_edit->add_caret_at_carets(false);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_caret_line(0) == 2);
+ CHECK(text_edit->get_caret_column(0) == 9);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 8);
+
+ // Last fit x is preserved when selection adding below.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->select(0, 8, 0, 5);
+ text_edit->add_caret_at_carets(true);
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 8);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK(text_edit->has_selection(2));
+ CHECK(text_edit->get_selection_origin_line(2) == 2);
+ CHECK(text_edit->get_selection_origin_column(2) == 7);
+ CHECK(text_edit->get_caret_line(2) == 2);
+ CHECK(text_edit->get_caret_column(2) == 5);
+
+ // Last fit x is preserved when selection adding above.
text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->select(2, 9, 2, 5);
+ text_edit->add_caret_at_carets(false);
+ text_edit->add_caret_at_carets(false);
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 2);
+ CHECK(text_edit->get_selection_origin_column(0) == 9);
+ CHECK(text_edit->get_caret_line(0) == 2);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK_FALSE(text_edit->has_selection(1));
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK(text_edit->has_selection(2));
+ CHECK(text_edit->get_selection_origin_line(2) == 0);
+ CHECK(text_edit->get_selection_origin_column(2) == 8);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 5);
+
+ // Selections are merged when they overlap.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->select(0, 1, 0, 5);
+ text_edit->add_caret(1, 0);
+ text_edit->select(1, 1, 1, 3, 1);
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selection_origin_line(1) == 1);
+ CHECK(text_edit->get_selection_origin_column(1) == 1);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK(text_edit->has_selection(2));
+ CHECK(text_edit->get_selection_origin_line(2) == 2);
+ CHECK(text_edit->get_selection_origin_column(2) == 0);
+ CHECK(text_edit->get_caret_line(2) == 2);
+ CHECK(text_edit->get_caret_column(2) == 3);
+
+ // Multiline selection.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(1);
+ text_edit->select(0, 3, 1, 1);
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selection_origin_line(0) == 0);
+ CHECK(text_edit->get_selection_origin_column(0) == 3);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 1);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selection_origin_line(1) == 1);
+ CHECK(text_edit->get_selection_origin_column(1) == 3);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 0);
+
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+ text_edit->set_size(Size2(50, 100));
+ // Line wraps: `\t,this, is\nso,me\n\t,test, ,text`.
+ CHECK(text_edit->is_line_wrapped(0));
+ MessageQueue::get_singleton()->flush();
+
+ // Add caret below on next line wrap.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
text_edit->set_caret_line(0);
text_edit->set_caret_column(4);
- text_edit->select(0, 0, 0, 4);
text_edit->add_caret_at_carets(true);
+ CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_count() == 2);
- CHECK(text_edit->get_selection_from_line(1) == 1);
- CHECK(text_edit->get_selection_to_line(1) == 1);
- CHECK(text_edit->get_selection_from_column(1) == 0);
- CHECK(text_edit->get_selection_to_column(1) == 3);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 8);
+ // Add caret below from end of line wrap.
text_edit->add_caret_at_carets(true);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 8);
+ CHECK(text_edit->get_caret_line(2) == 1);
+ CHECK(text_edit->get_caret_column(2) == 1);
+
+ // Add caret below from last line and not last line wrap.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(5);
+ text_edit->add_caret_at_carets(true);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_line(0) == 2);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 10);
+
+ // Cannot add caret below from last line last line wrap.
+ text_edit->add_caret_at_carets(true);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_line(0) == 2);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 10);
+
+ // Add caret above from not first line wrap.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(4);
+ text_edit->add_caret_at_carets(false);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 1);
+
+ // Add caret above from first line wrap.
+ text_edit->add_caret_at_carets(false);
+ CHECK_FALSE(text_edit->has_selection());
CHECK(text_edit->get_caret_count() == 3);
- CHECK(text_edit->get_selection_from_line(2) == 2);
- CHECK(text_edit->get_selection_to_line(2) == 2);
- CHECK(text_edit->get_selection_from_column(2) == 0);
- CHECK(text_edit->get_selection_to_column(2) == 4);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 1);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 8);
+
+ // Add caret above from first line and not first line wrap.
+ text_edit->add_caret_at_carets(false);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 4);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 1);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 8);
+ CHECK(text_edit->get_caret_line(3) == 0);
+ CHECK(text_edit->get_caret_column(3) == 4);
+
+ // Cannot add caret above from first line first line wrap.
+ text_edit->remove_secondary_carets();
+ text_edit->deselect();
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+ text_edit->add_caret_at_carets(false);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
}
memdelete(text_edit);
@@ -3992,7 +7488,7 @@ TEST_CASE("[SceneTree][TextEdit] viewport") {
CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
- // Wrap
+ // Wrap.
text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
MessageQueue::get_singleton()->flush();
CHECK(text_edit->get_total_visible_line_count() > total_visible_lines);
@@ -4242,7 +7738,7 @@ TEST_CASE("[SceneTree][TextEdit] viewport") {
CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
CHECK(text_edit->get_caret_wrap_index() == 0);
- // Typing and undo / redo should adjust viewport
+ // Typing and undo / redo should adjust viewport.
text_edit->set_caret_line(0);
text_edit->set_caret_column(0);
text_edit->set_line_as_first_visible(5);
diff --git a/tests/test_macros.h b/tests/test_macros.h
index a173b37a2d..927884dced 100644
--- a/tests/test_macros.h
+++ b/tests/test_macros.h
@@ -136,6 +136,7 @@ int register_test_command(String p_command, TestFunc p_function);
// Requires Message Queue and InputMap to be setup.
// SEND_GUI_ACTION - takes an input map key. e.g SEND_GUI_ACTION("ui_text_newline").
// SEND_GUI_KEY_EVENT - takes a keycode set. e.g SEND_GUI_KEY_EVENT(Key::A | KeyModifierMask::META).
+// SEND_GUI_KEY_UP_EVENT - takes a keycode set. e.g SEND_GUI_KEY_UP_EVENT(Key::A | KeyModifierMask::META).
// SEND_GUI_MOUSE_BUTTON_EVENT - takes a position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_EVENT(Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None);
// SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT - takes a position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None);
// SEND_GUI_MOUSE_MOTION_EVENT - takes a position, mouse mask and modifiers e.g SEND_GUI_MOUSE_MOTION_EVENT(Vector2(50, 50), MouseButtonMask::LEFT, KeyModifierMask::META);
@@ -161,6 +162,14 @@ int register_test_command(String p_command, TestFunc p_function);
MessageQueue::get_singleton()->flush(); \
}
+#define SEND_GUI_KEY_UP_EVENT(m_input) \
+ { \
+ Ref<InputEventKey> event = InputEventKey::create_reference(m_input); \
+ event->set_pressed(false); \
+ _SEND_DISPLAYSERVER_EVENT(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
#define _UPDATE_EVENT_MODIFERS(m_event, m_modifers) \
m_event->set_shift_pressed(((m_modifers) & KeyModifierMask::SHIFT) != Key::NONE); \
m_event->set_alt_pressed(((m_modifers) & KeyModifierMask::ALT) != Key::NONE); \
diff --git a/thirdparty/README.md b/thirdparty/README.md
index f2ddd5ebf9..4a7ab7314a 100644
--- a/thirdparty/README.md
+++ b/thirdparty/README.md
@@ -679,6 +679,11 @@ Collection of single-file libraries used in Godot components.
* Version: git (7bdffb428b2b19ad1c43aa44c714dcc104177e84, 2021)
* Modifications: Change from STL to Godot types (see provided patch).
* License: MIT
+- `qoa.h`
+ * Upstream: https://github.com/phoboslab/qoa
+ * Version: git (e4c751d61af2c395ea828c5888e728c1953bf09f, 2024)
+ * Modifications: Inlined functions and patched compiler warnings.
+ * License: MIT
- `r128.{c,h}`
* Upstream: https://github.com/fahickman/r128
* Version: git (6fc177671c47640d5bb69af10cf4ee91050015a1, 2023)
diff --git a/thirdparty/misc/patches/qoa-min-fix.patch b/thirdparty/misc/patches/qoa-min-fix.patch
new file mode 100644
index 0000000000..1043d8bbe7
--- /dev/null
+++ b/thirdparty/misc/patches/qoa-min-fix.patch
@@ -0,0 +1,155 @@
+diff --git a/qoa.h b/qoa.h
+index aa8fb59434..2dde8df098 100644
+--- a/qoa.h
++++ b/qoa.h
+@@ -140,14 +140,14 @@ typedef struct {
+ #endif
+ } qoa_desc;
+
+-unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes);
+-unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes);
+-void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len);
++inline unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes);
++inline unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes);
++inline void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len);
+
+-unsigned int qoa_max_frame_size(qoa_desc *qoa);
+-unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa);
+-unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len);
+-short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *file);
++inline unsigned int qoa_max_frame_size(qoa_desc *qoa);
++inline unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa);
++inline unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len);
++inline short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *file);
+
+ #ifndef QOA_NO_STDIO
+
+@@ -366,7 +366,7 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
+ ), bytes, &p);
+
+
+- for (int c = 0; c < channels; c++) {
++ for (unsigned int c = 0; c < channels; c++) {
+ /* Write the current LMS state */
+ qoa_uint64_t weights = 0;
+ qoa_uint64_t history = 0;
+@@ -380,9 +380,9 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
+
+ /* We encode all samples with the channels interleaved on a slice level.
+ E.g. for stereo: (ch-0, slice 0), (ch 1, slice 0), (ch 0, slice 1), ...*/
+- for (int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) {
++ for (unsigned int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) {
+
+- for (int c = 0; c < channels; c++) {
++ for (unsigned int c = 0; c < channels; c++) {
+ int slice_len = qoa_clamp(QOA_SLICE_LEN, 0, frame_len - sample_index);
+ int slice_start = sample_index * channels + c;
+ int slice_end = (sample_index + slice_len) * channels + c;
+@@ -391,10 +391,9 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
+ 16 scalefactors, encode all samples for the current slice and
+ meassure the total squared error. */
+ qoa_uint64_t best_rank = -1;
+- qoa_uint64_t best_error = -1;
+- qoa_uint64_t best_slice;
+- qoa_lms_t best_lms;
+- int best_scalefactor;
++ qoa_uint64_t best_slice = -1;
++ qoa_lms_t best_lms = {{-1, -1, -1, -1}, {-1, -1, -1, -1}};
++ int best_scalefactor = -1;
+
+ for (int sfi = 0; sfi < 16; sfi++) {
+ /* There is a strong correlation between the scalefactors of
+@@ -408,7 +407,6 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
+ qoa_lms_t lms = qoa->lms[c];
+ qoa_uint64_t slice = scalefactor;
+ qoa_uint64_t current_rank = 0;
+- qoa_uint64_t current_error = 0;
+
+ for (int si = slice_start; si < slice_end; si += channels) {
+ int sample = sample_data[si];
+@@ -438,7 +436,6 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
+ qoa_uint64_t error_sq = error * error;
+
+ current_rank += error_sq + weights_penalty * weights_penalty;
+- current_error += error_sq;
+ if (current_rank > best_rank) {
+ break;
+ }
+@@ -449,7 +446,6 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
+
+ if (current_rank < best_rank) {
+ best_rank = current_rank;
+- best_error = current_error;
+ best_slice = slice;
+ best_lms = lms;
+ best_scalefactor = scalefactor;
+@@ -492,9 +488,9 @@ void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len)
+ num_frames * QOA_LMS_LEN * 4 * qoa->channels + /* 4 * 4 bytes lms state per channel */
+ num_slices * 8 * qoa->channels; /* 8 byte slices */
+
+- unsigned char *bytes = QOA_MALLOC(encoded_size);
++ unsigned char *bytes = (unsigned char *)QOA_MALLOC(encoded_size);
+
+- for (int c = 0; c < qoa->channels; c++) {
++ for (unsigned int c = 0; c < qoa->channels; c++) {
+ /* Set the initial LMS weights to {0, 0, -1, 2}. This helps with the
+ prediction of the first few ms of a file. */
+ qoa->lms[c].weights[0] = 0;
+@@ -517,7 +513,7 @@ void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len)
+ #endif
+
+ int frame_len = QOA_FRAME_LEN;
+- for (int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) {
++ for (unsigned int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) {
+ frame_len = qoa_clamp(QOA_FRAME_LEN, 0, qoa->samples - sample_index);
+ const short *frame_samples = sample_data + sample_index * qoa->channels;
+ unsigned int frame_size = qoa_encode_frame(frame_samples, qoa, frame_len, bytes + p);
+@@ -580,14 +576,14 @@ unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa
+
+ /* Read and verify the frame header */
+ qoa_uint64_t frame_header = qoa_read_u64(bytes, &p);
+- int channels = (frame_header >> 56) & 0x0000ff;
+- int samplerate = (frame_header >> 32) & 0xffffff;
+- int samples = (frame_header >> 16) & 0x00ffff;
+- int frame_size = (frame_header ) & 0x00ffff;
++ unsigned int channels = (frame_header >> 56) & 0x0000ff;
++ unsigned int samplerate = (frame_header >> 32) & 0xffffff;
++ unsigned int samples = (frame_header >> 16) & 0x00ffff;
++ unsigned int frame_size = (frame_header ) & 0x00ffff;
+
+ int data_size = frame_size - 8 - QOA_LMS_LEN * 4 * channels;
+ int num_slices = data_size / 8;
+- int max_total_samples = num_slices * QOA_SLICE_LEN;
++ unsigned int max_total_samples = num_slices * QOA_SLICE_LEN;
+
+ if (
+ channels != qoa->channels ||
+@@ -600,7 +596,7 @@ unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa
+
+
+ /* Read the LMS state: 4 x 2 bytes history, 4 x 2 bytes weights per channel */
+- for (int c = 0; c < channels; c++) {
++ for (unsigned int c = 0; c < channels; c++) {
+ qoa_uint64_t history = qoa_read_u64(bytes, &p);
+ qoa_uint64_t weights = qoa_read_u64(bytes, &p);
+ for (int i = 0; i < QOA_LMS_LEN; i++) {
+@@ -613,8 +609,8 @@ unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa
+
+
+ /* Decode all slices for all channels in this frame */
+- for (int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) {
+- for (int c = 0; c < channels; c++) {
++ for (unsigned int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) {
++ for (unsigned int c = 0; c < channels; c++) {
+ qoa_uint64_t slice = qoa_read_u64(bytes, &p);
+
+ int scalefactor = (slice >> 60) & 0xf;
+@@ -647,7 +643,7 @@ short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *qoa) {
+
+ /* Calculate the required size of the sample buffer and allocate */
+ int total_samples = qoa->samples * qoa->channels;
+- short *sample_data = QOA_MALLOC(total_samples * sizeof(short));
++ short *sample_data = (short *)QOA_MALLOC(total_samples * sizeof(short));
+
+ unsigned int sample_index = 0;
+ unsigned int frame_len;
diff --git a/thirdparty/misc/qoa.h b/thirdparty/misc/qoa.h
new file mode 100644
index 0000000000..2dde8df098
--- /dev/null
+++ b/thirdparty/misc/qoa.h
@@ -0,0 +1,728 @@
+/*
+
+Copyright (c) 2023, Dominic Szablewski - https://phoboslab.org
+SPDX-License-Identifier: MIT
+
+QOA - The "Quite OK Audio" format for fast, lossy audio compression
+
+
+-- Data Format
+
+QOA encodes pulse-code modulated (PCM) audio data with up to 255 channels,
+sample rates from 1 up to 16777215 hertz and a bit depth of 16 bits.
+
+The compression method employed in QOA is lossy; it discards some information
+from the uncompressed PCM data. For many types of audio signals this compression
+is "transparent", i.e. the difference from the original file is often not
+audible.
+
+QOA encodes 20 samples of 16 bit PCM data into slices of 64 bits. A single
+sample therefore requires 3.2 bits of storage space, resulting in a 5x
+compression (16 / 3.2).
+
+A QOA file consists of an 8 byte file header, followed by a number of frames.
+Each frame contains an 8 byte frame header, the current 16 byte en-/decoder
+state per channel and 256 slices per channel. Each slice is 8 bytes wide and
+encodes 20 samples of audio data.
+
+All values, including the slices, are big endian. The file layout is as follows:
+
+struct {
+ struct {
+ char magic[4]; // magic bytes "qoaf"
+ uint32_t samples; // samples per channel in this file
+ } file_header;
+
+ struct {
+ struct {
+ uint8_t num_channels; // no. of channels
+ uint24_t samplerate; // samplerate in hz
+ uint16_t fsamples; // samples per channel in this frame
+ uint16_t fsize; // frame size (includes this header)
+ } frame_header;
+
+ struct {
+ int16_t history[4]; // most recent last
+ int16_t weights[4]; // most recent last
+ } lms_state[num_channels];
+
+ qoa_slice_t slices[256][num_channels];
+
+ } frames[ceil(samples / (256 * 20))];
+} qoa_file_t;
+
+Each `qoa_slice_t` contains a quantized scalefactor `sf_quant` and 20 quantized
+residuals `qrNN`:
+
+.- QOA_SLICE -- 64 bits, 20 samples --------------------------/ /------------.
+| Byte[0] | Byte[1] | Byte[2] \ \ Byte[7] |
+| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | 7 6 5 / / 2 1 0 |
+|------------+--------+--------+--------+---------+---------+-\ \--+---------|
+| sf_quant | qr00 | qr01 | qr02 | qr03 | qr04 | / / | qr19 |
+`-------------------------------------------------------------\ \------------`
+
+Each frame except the last must contain exactly 256 slices per channel. The last
+frame may contain between 1 .. 256 (inclusive) slices per channel. The last
+slice (for each channel) in the last frame may contain less than 20 samples; the
+slice still must be 8 bytes wide, with the unused samples zeroed out.
+
+Channels are interleaved per slice. E.g. for 2 channel stereo:
+slice[0] = L, slice[1] = R, slice[2] = L, slice[3] = R ...
+
+A valid QOA file or stream must have at least one frame. Each frame must contain
+at least one channel and one sample with a samplerate between 1 .. 16777215
+(inclusive).
+
+If the total number of samples is not known by the encoder, the samples in the
+file header may be set to 0x00000000 to indicate that the encoder is
+"streaming". In a streaming context, the samplerate and number of channels may
+differ from frame to frame. For static files (those with samples set to a
+non-zero value), each frame must have the same number of channels and same
+samplerate.
+
+Note that this implementation of QOA only handles files with a known total
+number of samples.
+
+A decoder should support at least 8 channels. The channel layout for channel
+counts 1 .. 8 is:
+
+ 1. Mono
+ 2. L, R
+ 3. L, R, C
+ 4. FL, FR, B/SL, B/SR
+ 5. FL, FR, C, B/SL, B/SR
+ 6. FL, FR, C, LFE, B/SL, B/SR
+ 7. FL, FR, C, LFE, B, SL, SR
+ 8. FL, FR, C, LFE, BL, BR, SL, SR
+
+QOA predicts each audio sample based on the previously decoded ones using a
+"Sign-Sign Least Mean Squares Filter" (LMS). This prediction plus the
+dequantized residual forms the final output sample.
+
+*/
+
+
+
+/* -----------------------------------------------------------------------------
+ Header - Public functions */
+
+#ifndef QOA_H
+#define QOA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define QOA_MIN_FILESIZE 16
+#define QOA_MAX_CHANNELS 8
+
+#define QOA_SLICE_LEN 20
+#define QOA_SLICES_PER_FRAME 256
+#define QOA_FRAME_LEN (QOA_SLICES_PER_FRAME * QOA_SLICE_LEN)
+#define QOA_LMS_LEN 4
+#define QOA_MAGIC 0x716f6166 /* 'qoaf' */
+
+#define QOA_FRAME_SIZE(channels, slices) \
+ (8 + QOA_LMS_LEN * 4 * channels + 8 * slices * channels)
+
+typedef struct {
+ int history[QOA_LMS_LEN];
+ int weights[QOA_LMS_LEN];
+} qoa_lms_t;
+
+typedef struct {
+ unsigned int channels;
+ unsigned int samplerate;
+ unsigned int samples;
+ qoa_lms_t lms[QOA_MAX_CHANNELS];
+ #ifdef QOA_RECORD_TOTAL_ERROR
+ double error;
+ #endif
+} qoa_desc;
+
+inline unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes);
+inline unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes);
+inline void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len);
+
+inline unsigned int qoa_max_frame_size(qoa_desc *qoa);
+inline unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa);
+inline unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len);
+inline short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *file);
+
+#ifndef QOA_NO_STDIO
+
+int qoa_write(const char *filename, const short *sample_data, qoa_desc *qoa);
+void *qoa_read(const char *filename, qoa_desc *qoa);
+
+#endif /* QOA_NO_STDIO */
+
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* QOA_H */
+
+
+/* -----------------------------------------------------------------------------
+ Implementation */
+
+#ifdef QOA_IMPLEMENTATION
+#include <stdlib.h>
+
+#ifndef QOA_MALLOC
+ #define QOA_MALLOC(sz) malloc(sz)
+ #define QOA_FREE(p) free(p)
+#endif
+
+typedef unsigned long long qoa_uint64_t;
+
+
+/* The quant_tab provides an index into the dequant_tab for residuals in the
+range of -8 .. 8. It maps this range to just 3bits and becomes less accurate at
+the higher end. Note that the residual zero is identical to the lowest positive
+value. This is mostly fine, since the qoa_div() function always rounds away
+from zero. */
+
+static const int qoa_quant_tab[17] = {
+ 7, 7, 7, 5, 5, 3, 3, 1, /* -8..-1 */
+ 0, /* 0 */
+ 0, 2, 2, 4, 4, 6, 6, 6 /* 1.. 8 */
+};
+
+
+/* We have 16 different scalefactors. Like the quantized residuals these become
+less accurate at the higher end. In theory, the highest scalefactor that we
+would need to encode the highest 16bit residual is (2**16)/8 = 8192. However we
+rely on the LMS filter to predict samples accurately enough that a maximum
+residual of one quarter of the 16 bit range is sufficient. I.e. with the
+scalefactor 2048 times the quant range of 8 we can encode residuals up to 2**14.
+
+The scalefactor values are computed as:
+scalefactor_tab[s] <- round(pow(s + 1, 2.75)) */
+
+static const int qoa_scalefactor_tab[16] = {
+ 1, 7, 21, 45, 84, 138, 211, 304, 421, 562, 731, 928, 1157, 1419, 1715, 2048
+};
+
+
+/* The reciprocal_tab maps each of the 16 scalefactors to their rounded
+reciprocals 1/scalefactor. This allows us to calculate the scaled residuals in
+the encoder with just one multiplication instead of an expensive division. We
+do this in .16 fixed point with integers, instead of floats.
+
+The reciprocal_tab is computed as:
+reciprocal_tab[s] <- ((1<<16) + scalefactor_tab[s] - 1) / scalefactor_tab[s] */
+
+static const int qoa_reciprocal_tab[16] = {
+ 65536, 9363, 3121, 1457, 781, 475, 311, 216, 156, 117, 90, 71, 57, 47, 39, 32
+};
+
+
+/* The dequant_tab maps each of the scalefactors and quantized residuals to
+their unscaled & dequantized version.
+
+Since qoa_div rounds away from the zero, the smallest entries are mapped to 3/4
+instead of 1. The dequant_tab assumes the following dequantized values for each
+of the quant_tab indices and is computed as:
+float dqt[8] = {0.75, -0.75, 2.5, -2.5, 4.5, -4.5, 7, -7};
+dequant_tab[s][q] <- round_ties_away_from_zero(scalefactor_tab[s] * dqt[q])
+
+The rounding employed here is "to nearest, ties away from zero", i.e. positive
+and negative values are treated symmetrically.
+*/
+
+static const int qoa_dequant_tab[16][8] = {
+ { 1, -1, 3, -3, 5, -5, 7, -7},
+ { 5, -5, 18, -18, 32, -32, 49, -49},
+ { 16, -16, 53, -53, 95, -95, 147, -147},
+ { 34, -34, 113, -113, 203, -203, 315, -315},
+ { 63, -63, 210, -210, 378, -378, 588, -588},
+ { 104, -104, 345, -345, 621, -621, 966, -966},
+ { 158, -158, 528, -528, 950, -950, 1477, -1477},
+ { 228, -228, 760, -760, 1368, -1368, 2128, -2128},
+ { 316, -316, 1053, -1053, 1895, -1895, 2947, -2947},
+ { 422, -422, 1405, -1405, 2529, -2529, 3934, -3934},
+ { 548, -548, 1828, -1828, 3290, -3290, 5117, -5117},
+ { 696, -696, 2320, -2320, 4176, -4176, 6496, -6496},
+ { 868, -868, 2893, -2893, 5207, -5207, 8099, -8099},
+ {1064, -1064, 3548, -3548, 6386, -6386, 9933, -9933},
+ {1286, -1286, 4288, -4288, 7718, -7718, 12005, -12005},
+ {1536, -1536, 5120, -5120, 9216, -9216, 14336, -14336},
+};
+
+
+/* The Least Mean Squares Filter is the heart of QOA. It predicts the next
+sample based on the previous 4 reconstructed samples. It does so by continuously
+adjusting 4 weights based on the residual of the previous prediction.
+
+The next sample is predicted as the sum of (weight[i] * history[i]).
+
+The adjustment of the weights is done with a "Sign-Sign-LMS" that adds or
+subtracts the residual to each weight, based on the corresponding sample from
+the history. This, surprisingly, is sufficient to get worthwhile predictions.
+
+This is all done with fixed point integers. Hence the right-shifts when updating
+the weights and calculating the prediction. */
+
+static int qoa_lms_predict(qoa_lms_t *lms) {
+ int prediction = 0;
+ for (int i = 0; i < QOA_LMS_LEN; i++) {
+ prediction += lms->weights[i] * lms->history[i];
+ }
+ return prediction >> 13;
+}
+
+static void qoa_lms_update(qoa_lms_t *lms, int sample, int residual) {
+ int delta = residual >> 4;
+ for (int i = 0; i < QOA_LMS_LEN; i++) {
+ lms->weights[i] += lms->history[i] < 0 ? -delta : delta;
+ }
+
+ for (int i = 0; i < QOA_LMS_LEN-1; i++) {
+ lms->history[i] = lms->history[i+1];
+ }
+ lms->history[QOA_LMS_LEN-1] = sample;
+}
+
+
+/* qoa_div() implements a rounding division, but avoids rounding to zero for
+small numbers. E.g. 0.1 will be rounded to 1. Note that 0 itself still
+returns as 0, which is handled in the qoa_quant_tab[].
+qoa_div() takes an index into the .16 fixed point qoa_reciprocal_tab as an
+argument, so it can do the division with a cheaper integer multiplication. */
+
+static inline int qoa_div(int v, int scalefactor) {
+ int reciprocal = qoa_reciprocal_tab[scalefactor];
+ int n = (v * reciprocal + (1 << 15)) >> 16;
+ n = n + ((v > 0) - (v < 0)) - ((n > 0) - (n < 0)); /* round away from 0 */
+ return n;
+}
+
+static inline int qoa_clamp(int v, int min, int max) {
+ if (v < min) { return min; }
+ if (v > max) { return max; }
+ return v;
+}
+
+/* This specialized clamp function for the signed 16 bit range improves decode
+performance quite a bit. The extra if() statement works nicely with the CPUs
+branch prediction as this branch is rarely taken. */
+
+static inline int qoa_clamp_s16(int v) {
+ if ((unsigned int)(v + 32768) > 65535) {
+ if (v < -32768) { return -32768; }
+ if (v > 32767) { return 32767; }
+ }
+ return v;
+}
+
+static inline qoa_uint64_t qoa_read_u64(const unsigned char *bytes, unsigned int *p) {
+ bytes += *p;
+ *p += 8;
+ return
+ ((qoa_uint64_t)(bytes[0]) << 56) | ((qoa_uint64_t)(bytes[1]) << 48) |
+ ((qoa_uint64_t)(bytes[2]) << 40) | ((qoa_uint64_t)(bytes[3]) << 32) |
+ ((qoa_uint64_t)(bytes[4]) << 24) | ((qoa_uint64_t)(bytes[5]) << 16) |
+ ((qoa_uint64_t)(bytes[6]) << 8) | ((qoa_uint64_t)(bytes[7]) << 0);
+}
+
+static inline void qoa_write_u64(qoa_uint64_t v, unsigned char *bytes, unsigned int *p) {
+ bytes += *p;
+ *p += 8;
+ bytes[0] = (v >> 56) & 0xff;
+ bytes[1] = (v >> 48) & 0xff;
+ bytes[2] = (v >> 40) & 0xff;
+ bytes[3] = (v >> 32) & 0xff;
+ bytes[4] = (v >> 24) & 0xff;
+ bytes[5] = (v >> 16) & 0xff;
+ bytes[6] = (v >> 8) & 0xff;
+ bytes[7] = (v >> 0) & 0xff;
+}
+
+
+/* -----------------------------------------------------------------------------
+ Encoder */
+
+unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes) {
+ unsigned int p = 0;
+ qoa_write_u64(((qoa_uint64_t)QOA_MAGIC << 32) | qoa->samples, bytes, &p);
+ return p;
+}
+
+unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes) {
+ unsigned int channels = qoa->channels;
+
+ unsigned int p = 0;
+ unsigned int slices = (frame_len + QOA_SLICE_LEN - 1) / QOA_SLICE_LEN;
+ unsigned int frame_size = QOA_FRAME_SIZE(channels, slices);
+ int prev_scalefactor[QOA_MAX_CHANNELS] = {0};
+
+ /* Write the frame header */
+ qoa_write_u64((
+ (qoa_uint64_t)qoa->channels << 56 |
+ (qoa_uint64_t)qoa->samplerate << 32 |
+ (qoa_uint64_t)frame_len << 16 |
+ (qoa_uint64_t)frame_size
+ ), bytes, &p);
+
+
+ for (unsigned int c = 0; c < channels; c++) {
+ /* Write the current LMS state */
+ qoa_uint64_t weights = 0;
+ qoa_uint64_t history = 0;
+ for (int i = 0; i < QOA_LMS_LEN; i++) {
+ history = (history << 16) | (qoa->lms[c].history[i] & 0xffff);
+ weights = (weights << 16) | (qoa->lms[c].weights[i] & 0xffff);
+ }
+ qoa_write_u64(history, bytes, &p);
+ qoa_write_u64(weights, bytes, &p);
+ }
+
+ /* We encode all samples with the channels interleaved on a slice level.
+ E.g. for stereo: (ch-0, slice 0), (ch 1, slice 0), (ch 0, slice 1), ...*/
+ for (unsigned int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) {
+
+ for (unsigned int c = 0; c < channels; c++) {
+ int slice_len = qoa_clamp(QOA_SLICE_LEN, 0, frame_len - sample_index);
+ int slice_start = sample_index * channels + c;
+ int slice_end = (sample_index + slice_len) * channels + c;
+
+ /* Brute for search for the best scalefactor. Just go through all
+ 16 scalefactors, encode all samples for the current slice and
+ meassure the total squared error. */
+ qoa_uint64_t best_rank = -1;
+ qoa_uint64_t best_slice = -1;
+ qoa_lms_t best_lms = {{-1, -1, -1, -1}, {-1, -1, -1, -1}};
+ int best_scalefactor = -1;
+
+ for (int sfi = 0; sfi < 16; sfi++) {
+ /* There is a strong correlation between the scalefactors of
+ neighboring slices. As an optimization, start testing
+ the best scalefactor of the previous slice first. */
+ int scalefactor = (sfi + prev_scalefactor[c]) % 16;
+
+ /* We have to reset the LMS state to the last known good one
+ before trying each scalefactor, as each pass updates the LMS
+ state when encoding. */
+ qoa_lms_t lms = qoa->lms[c];
+ qoa_uint64_t slice = scalefactor;
+ qoa_uint64_t current_rank = 0;
+
+ for (int si = slice_start; si < slice_end; si += channels) {
+ int sample = sample_data[si];
+ int predicted = qoa_lms_predict(&lms);
+
+ int residual = sample - predicted;
+ int scaled = qoa_div(residual, scalefactor);
+ int clamped = qoa_clamp(scaled, -8, 8);
+ int quantized = qoa_quant_tab[clamped + 8];
+ int dequantized = qoa_dequant_tab[scalefactor][quantized];
+ int reconstructed = qoa_clamp_s16(predicted + dequantized);
+
+
+ /* If the weights have grown too large, we introduce a penalty
+ here. This prevents pops/clicks in certain problem cases */
+ int weights_penalty = ((
+ lms.weights[0] * lms.weights[0] +
+ lms.weights[1] * lms.weights[1] +
+ lms.weights[2] * lms.weights[2] +
+ lms.weights[3] * lms.weights[3]
+ ) >> 18) - 0x8ff;
+ if (weights_penalty < 0) {
+ weights_penalty = 0;
+ }
+
+ long long error = (sample - reconstructed);
+ qoa_uint64_t error_sq = error * error;
+
+ current_rank += error_sq + weights_penalty * weights_penalty;
+ if (current_rank > best_rank) {
+ break;
+ }
+
+ qoa_lms_update(&lms, reconstructed, dequantized);
+ slice = (slice << 3) | quantized;
+ }
+
+ if (current_rank < best_rank) {
+ best_rank = current_rank;
+ best_slice = slice;
+ best_lms = lms;
+ best_scalefactor = scalefactor;
+ }
+ }
+
+ prev_scalefactor[c] = best_scalefactor;
+
+ qoa->lms[c] = best_lms;
+ #ifdef QOA_RECORD_TOTAL_ERROR
+ qoa->error += best_error;
+ #endif
+
+ /* If this slice was shorter than QOA_SLICE_LEN, we have to left-
+ shift all encoded data, to ensure the rightmost bits are the empty
+ ones. This should only happen in the last frame of a file as all
+ slices are completely filled otherwise. */
+ best_slice <<= (QOA_SLICE_LEN - slice_len) * 3;
+ qoa_write_u64(best_slice, bytes, &p);
+ }
+ }
+
+ return p;
+}
+
+void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len) {
+ if (
+ qoa->samples == 0 ||
+ qoa->samplerate == 0 || qoa->samplerate > 0xffffff ||
+ qoa->channels == 0 || qoa->channels > QOA_MAX_CHANNELS
+ ) {
+ return NULL;
+ }
+
+ /* Calculate the encoded size and allocate */
+ unsigned int num_frames = (qoa->samples + QOA_FRAME_LEN-1) / QOA_FRAME_LEN;
+ unsigned int num_slices = (qoa->samples + QOA_SLICE_LEN-1) / QOA_SLICE_LEN;
+ unsigned int encoded_size = 8 + /* 8 byte file header */
+ num_frames * 8 + /* 8 byte frame headers */
+ num_frames * QOA_LMS_LEN * 4 * qoa->channels + /* 4 * 4 bytes lms state per channel */
+ num_slices * 8 * qoa->channels; /* 8 byte slices */
+
+ unsigned char *bytes = (unsigned char *)QOA_MALLOC(encoded_size);
+
+ for (unsigned int c = 0; c < qoa->channels; c++) {
+ /* Set the initial LMS weights to {0, 0, -1, 2}. This helps with the
+ prediction of the first few ms of a file. */
+ qoa->lms[c].weights[0] = 0;
+ qoa->lms[c].weights[1] = 0;
+ qoa->lms[c].weights[2] = -(1<<13);
+ qoa->lms[c].weights[3] = (1<<14);
+
+ /* Explicitly set the history samples to 0, as we might have some
+ garbage in there. */
+ for (int i = 0; i < QOA_LMS_LEN; i++) {
+ qoa->lms[c].history[i] = 0;
+ }
+ }
+
+
+ /* Encode the header and go through all frames */
+ unsigned int p = qoa_encode_header(qoa, bytes);
+ #ifdef QOA_RECORD_TOTAL_ERROR
+ qoa->error = 0;
+ #endif
+
+ int frame_len = QOA_FRAME_LEN;
+ for (unsigned int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) {
+ frame_len = qoa_clamp(QOA_FRAME_LEN, 0, qoa->samples - sample_index);
+ const short *frame_samples = sample_data + sample_index * qoa->channels;
+ unsigned int frame_size = qoa_encode_frame(frame_samples, qoa, frame_len, bytes + p);
+ p += frame_size;
+ }
+
+ *out_len = p;
+ return bytes;
+}
+
+
+
+/* -----------------------------------------------------------------------------
+ Decoder */
+
+unsigned int qoa_max_frame_size(qoa_desc *qoa) {
+ return QOA_FRAME_SIZE(qoa->channels, QOA_SLICES_PER_FRAME);
+}
+
+unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa) {
+ unsigned int p = 0;
+ if (size < QOA_MIN_FILESIZE) {
+ return 0;
+ }
+
+
+ /* Read the file header, verify the magic number ('qoaf') and read the
+ total number of samples. */
+ qoa_uint64_t file_header = qoa_read_u64(bytes, &p);
+
+ if ((file_header >> 32) != QOA_MAGIC) {
+ return 0;
+ }
+
+ qoa->samples = file_header & 0xffffffff;
+ if (!qoa->samples) {
+ return 0;
+ }
+
+ /* Peek into the first frame header to get the number of channels and
+ the samplerate. */
+ qoa_uint64_t frame_header = qoa_read_u64(bytes, &p);
+ qoa->channels = (frame_header >> 56) & 0x0000ff;
+ qoa->samplerate = (frame_header >> 32) & 0xffffff;
+
+ if (qoa->channels == 0 || qoa->samples == 0 || qoa->samplerate == 0) {
+ return 0;
+ }
+
+ return 8;
+}
+
+unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len) {
+ unsigned int p = 0;
+ *frame_len = 0;
+
+ if (size < 8 + QOA_LMS_LEN * 4 * qoa->channels) {
+ return 0;
+ }
+
+ /* Read and verify the frame header */
+ qoa_uint64_t frame_header = qoa_read_u64(bytes, &p);
+ unsigned int channels = (frame_header >> 56) & 0x0000ff;
+ unsigned int samplerate = (frame_header >> 32) & 0xffffff;
+ unsigned int samples = (frame_header >> 16) & 0x00ffff;
+ unsigned int frame_size = (frame_header ) & 0x00ffff;
+
+ int data_size = frame_size - 8 - QOA_LMS_LEN * 4 * channels;
+ int num_slices = data_size / 8;
+ unsigned int max_total_samples = num_slices * QOA_SLICE_LEN;
+
+ if (
+ channels != qoa->channels ||
+ samplerate != qoa->samplerate ||
+ frame_size > size ||
+ samples * channels > max_total_samples
+ ) {
+ return 0;
+ }
+
+
+ /* Read the LMS state: 4 x 2 bytes history, 4 x 2 bytes weights per channel */
+ for (unsigned int c = 0; c < channels; c++) {
+ qoa_uint64_t history = qoa_read_u64(bytes, &p);
+ qoa_uint64_t weights = qoa_read_u64(bytes, &p);
+ for (int i = 0; i < QOA_LMS_LEN; i++) {
+ qoa->lms[c].history[i] = ((signed short)(history >> 48));
+ history <<= 16;
+ qoa->lms[c].weights[i] = ((signed short)(weights >> 48));
+ weights <<= 16;
+ }
+ }
+
+
+ /* Decode all slices for all channels in this frame */
+ for (unsigned int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) {
+ for (unsigned int c = 0; c < channels; c++) {
+ qoa_uint64_t slice = qoa_read_u64(bytes, &p);
+
+ int scalefactor = (slice >> 60) & 0xf;
+ int slice_start = sample_index * channels + c;
+ int slice_end = qoa_clamp(sample_index + QOA_SLICE_LEN, 0, samples) * channels + c;
+
+ for (int si = slice_start; si < slice_end; si += channels) {
+ int predicted = qoa_lms_predict(&qoa->lms[c]);
+ int quantized = (slice >> 57) & 0x7;
+ int dequantized = qoa_dequant_tab[scalefactor][quantized];
+ int reconstructed = qoa_clamp_s16(predicted + dequantized);
+
+ sample_data[si] = reconstructed;
+ slice <<= 3;
+
+ qoa_lms_update(&qoa->lms[c], reconstructed, dequantized);
+ }
+ }
+ }
+
+ *frame_len = samples;
+ return p;
+}
+
+short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *qoa) {
+ unsigned int p = qoa_decode_header(bytes, size, qoa);
+ if (!p) {
+ return NULL;
+ }
+
+ /* Calculate the required size of the sample buffer and allocate */
+ int total_samples = qoa->samples * qoa->channels;
+ short *sample_data = (short *)QOA_MALLOC(total_samples * sizeof(short));
+
+ unsigned int sample_index = 0;
+ unsigned int frame_len;
+ unsigned int frame_size;
+
+ /* Decode all frames */
+ do {
+ short *sample_ptr = sample_data + sample_index * qoa->channels;
+ frame_size = qoa_decode_frame(bytes + p, size - p, qoa, sample_ptr, &frame_len);
+
+ p += frame_size;
+ sample_index += frame_len;
+ } while (frame_size && sample_index < qoa->samples);
+
+ qoa->samples = sample_index;
+ return sample_data;
+}
+
+
+
+/* -----------------------------------------------------------------------------
+ File read/write convenience functions */
+
+#ifndef QOA_NO_STDIO
+#include <stdio.h>
+
+int qoa_write(const char *filename, const short *sample_data, qoa_desc *qoa) {
+ FILE *f = fopen(filename, "wb");
+ unsigned int size;
+ void *encoded;
+
+ if (!f) {
+ return 0;
+ }
+
+ encoded = qoa_encode(sample_data, qoa, &size);
+ if (!encoded) {
+ fclose(f);
+ return 0;
+ }
+
+ fwrite(encoded, 1, size, f);
+ fclose(f);
+
+ QOA_FREE(encoded);
+ return size;
+}
+
+void *qoa_read(const char *filename, qoa_desc *qoa) {
+ FILE *f = fopen(filename, "rb");
+ int size, bytes_read;
+ void *data;
+ short *sample_data;
+
+ if (!f) {
+ return NULL;
+ }
+
+ fseek(f, 0, SEEK_END);
+ size = ftell(f);
+ if (size <= 0) {
+ fclose(f);
+ return NULL;
+ }
+ fseek(f, 0, SEEK_SET);
+
+ data = QOA_MALLOC(size);
+ if (!data) {
+ fclose(f);
+ return NULL;
+ }
+
+ bytes_read = fread(data, 1, size, f);
+ fclose(f);
+
+ sample_data = qoa_decode(data, bytes_read, qoa);
+ QOA_FREE(data);
+ return sample_data;
+}
+
+#endif /* QOA_NO_STDIO */
+#endif /* QOA_IMPLEMENTATION */