diff --git a/godot/LICENSE b/godot/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/godot/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableControl.svg b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableControl.svg
new file mode 100644
index 0000000..83df2f9
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableControl.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableControl.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableControl.svg.import
new file mode 100644
index 0000000..ff99cd5
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableControl.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b61f2s0sdeivn"
+path="res://.godot/imported/AnimatableControl.svg-ea6d8910d2b3058bfac46c0758163c04.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/AnimatableControl.svg"
+dest_files=["res://.godot/imported/AnimatableControl.svg-ea6d8910d2b3058bfac46c0758163c04.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableMount.svg b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableMount.svg
new file mode 100644
index 0000000..7da422c
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableMount.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableMount.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableMount.svg.import
new file mode 100644
index 0000000..c640fde
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableMount.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://td5qpky5w1jp"
+path="res://.godot/imported/AnimatableMount.svg-c55e9d75134e187a953286234a72f1d8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/AnimatableMount.svg"
+dest_files=["res://.godot/imported/AnimatableMount.svg-c55e9d75134e187a953286234a72f1d8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableScrollControl.svg b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableScrollControl.svg
new file mode 100644
index 0000000..533bf7e
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableScrollControl.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableScrollControl.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableScrollControl.svg.import
new file mode 100644
index 0000000..216bb56
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableScrollControl.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d0wty6o4l0n16"
+path="res://.godot/imported/AnimatableScrollControl.svg-82ff252057e45e07beda388858928afc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/AnimatableScrollControl.svg"
+dest_files=["res://.godot/imported/AnimatableScrollControl.svg-82ff252057e45e07beda388858928afc.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableTransformationMount.svg b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableTransformationMount.svg
new file mode 100644
index 0000000..5cef084
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableTransformationMount.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableTransformationMount.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableTransformationMount.svg.import
new file mode 100644
index 0000000..6e9a33f
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableTransformationMount.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c05srll0xjnus"
+path="res://.godot/imported/AnimatableTransformationMount.svg-3384cfe8cf7ff650173fe5f295bf32bd.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/AnimatableTransformationMount.svg"
+dest_files=["res://.godot/imported/AnimatableTransformationMount.svg-3384cfe8cf7ff650173fe5f295bf32bd.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableVisibleControl.svg b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableVisibleControl.svg
new file mode 100644
index 0000000..ec72ab6
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableVisibleControl.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableVisibleControl.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableVisibleControl.svg.import
new file mode 100644
index 0000000..01017ce
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableVisibleControl.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://3bug5vfuq1vw"
+path="res://.godot/imported/AnimatableVisibleControl.svg-231049b2351dc86fdfb539faef83c988.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/AnimatableVisibleControl.svg"
+dest_files=["res://.godot/imported/AnimatableVisibleControl.svg-231049b2351dc86fdfb539faef83c988.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableZoneControl.svg b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableZoneControl.svg
new file mode 100644
index 0000000..cb303f8
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableZoneControl.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatableZoneControl.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableZoneControl.svg.import
new file mode 100644
index 0000000..9aba2a1
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatableZoneControl.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cice0m617g5ks"
+path="res://.godot/imported/AnimatableZoneControl.svg-52fb2c5380b72522d734b8b9ba5fc752.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/AnimatableZoneControl.svg"
+dest_files=["res://.godot/imported/AnimatableZoneControl.svg-52fb2c5380b72522d734b8b9ba5fc752.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatedSwitch.svg b/godot/addons/FreeControl/assets/icons/CustomType/AnimatedSwitch.svg
new file mode 100644
index 0000000..c6e0000
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatedSwitch.svg
@@ -0,0 +1,51 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/AnimatedSwitch.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/AnimatedSwitch.svg.import
new file mode 100644
index 0000000..2825389
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/AnimatedSwitch.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b40v8yi30eall"
+path="res://.godot/imported/AnimatedSwitch.svg-76f991124fb01f03d12bfe5e3a3c31ba.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/AnimatedSwitch.svg"
+dest_files=["res://.godot/imported/AnimatedSwitch.svg-76f991124fb01f03d12bfe5e3a3c31ba.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/BoundsCheck.svg b/godot/addons/FreeControl/assets/icons/CustomType/BoundsCheck.svg
new file mode 100644
index 0000000..ca2498b
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/BoundsCheck.svg
@@ -0,0 +1,50 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/BoundsCheck.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/BoundsCheck.svg.import
new file mode 100644
index 0000000..939597c
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/BoundsCheck.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bladvsiu7rha1"
+path="res://.godot/imported/BoundsCheck.svg-02b170ef6b912468a1f094b439993793.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/BoundsCheck.svg"
+dest_files=["res://.godot/imported/BoundsCheck.svg-02b170ef6b912468a1f094b439993793.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/Carousel.svg b/godot/addons/FreeControl/assets/icons/CustomType/Carousel.svg
new file mode 100644
index 0000000..1bc75d3
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/Carousel.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/Carousel.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/Carousel.svg.import
new file mode 100644
index 0000000..eafbc98
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/Carousel.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://vu8og3rtsflq"
+path="res://.godot/imported/Carousel.svg-24a12fa029ec5088ae1cb278064dea40.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/Carousel.svg"
+dest_files=["res://.godot/imported/Carousel.svg-24a12fa029ec5088ae1cb278064dea40.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/CircularContainer.svg b/godot/addons/FreeControl/assets/icons/CustomType/CircularContainer.svg
new file mode 100644
index 0000000..b65620d
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/CircularContainer.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/CircularContainer.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/CircularContainer.svg.import
new file mode 100644
index 0000000..691b1f3
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/CircularContainer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c00ttqalyh2ck"
+path="res://.godot/imported/CircularContainer.svg-1dd941875614a9abaf152e4d2a3a686b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/CircularContainer.svg"
+dest_files=["res://.godot/imported/CircularContainer.svg-1dd941875614a9abaf152e4d2a3a686b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/DistanceCheck.svg b/godot/addons/FreeControl/assets/icons/CustomType/DistanceCheck.svg
new file mode 100644
index 0000000..44d1953
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/DistanceCheck.svg
@@ -0,0 +1,50 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/DistanceCheck.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/DistanceCheck.svg.import
new file mode 100644
index 0000000..4eb267f
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/DistanceCheck.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://den188fn44ahq"
+path="res://.godot/imported/DistanceCheck.svg-cc9439b9bfdf47f875ceab8615f27948.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/DistanceCheck.svg"
+dest_files=["res://.godot/imported/DistanceCheck.svg-cc9439b9bfdf47f875ceab8615f27948.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/Drawer.svg b/godot/addons/FreeControl/assets/icons/CustomType/Drawer.svg
new file mode 100644
index 0000000..8cc649a
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/Drawer.svg
@@ -0,0 +1,62 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/Drawer.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/Drawer.svg.import
new file mode 100644
index 0000000..60cee16
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/Drawer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://lje075l441hl"
+path="res://.godot/imported/Drawer.svg-7020a44fe2fbf3bc7e577f6647e28243.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/Drawer.svg"
+dest_files=["res://.godot/imported/Drawer.svg-7020a44fe2fbf3bc7e577f6647e28243.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/HoldButton.svg b/godot/addons/FreeControl/assets/icons/CustomType/HoldButton.svg
new file mode 100644
index 0000000..f069eb5
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/HoldButton.svg
@@ -0,0 +1,51 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/HoldButton.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/HoldButton.svg.import
new file mode 100644
index 0000000..5fa6ea9
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/HoldButton.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ceqkp35yu0v27"
+path="res://.godot/imported/HoldButton.svg-059d8d0ebedf00f7d7ee555447e9ac85.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/HoldButton.svg"
+dest_files=["res://.godot/imported/HoldButton.svg-059d8d0ebedf00f7d7ee555447e9ac85.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/MaxRatioContainer.svg b/godot/addons/FreeControl/assets/icons/CustomType/MaxRatioContainer.svg
new file mode 100644
index 0000000..e97ad44
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/MaxRatioContainer.svg
@@ -0,0 +1,57 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/MaxRatioContainer.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/MaxRatioContainer.svg.import
new file mode 100644
index 0000000..a6a212a
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/MaxRatioContainer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://crwd74vrtbrmm"
+path="res://.godot/imported/MaxRatioContainer.svg-69252897895331776d794bb3e7d4450a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/MaxRatioContainer.svg"
+dest_files=["res://.godot/imported/MaxRatioContainer.svg-69252897895331776d794bb3e7d4450a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/MaxSizeContainer.svg b/godot/addons/FreeControl/assets/icons/CustomType/MaxSizeContainer.svg
new file mode 100644
index 0000000..964b200
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/MaxSizeContainer.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/MaxSizeContainer.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/MaxSizeContainer.svg.import
new file mode 100644
index 0000000..14cec2f
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/MaxSizeContainer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cj5vpwdi8b2t4"
+path="res://.godot/imported/MaxSizeContainer.svg-c449fe736ac19e4da4954b352d7a4bc0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/MaxSizeContainer.svg"
+dest_files=["res://.godot/imported/MaxSizeContainer.svg-c449fe736ac19e4da4954b352d7a4bc0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionButton.svg b/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionButton.svg
new file mode 100644
index 0000000..578dcd1
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionButton.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionButton.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionButton.svg.import
new file mode 100644
index 0000000..6e62576
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionButton.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bf8ifq48pj2iv"
+path="res://.godot/imported/ModulateTransitionButton.svg-124cc46ae542378e7b346986eb52d0c7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/ModulateTransitionButton.svg"
+dest_files=["res://.godot/imported/ModulateTransitionButton.svg-124cc46ae542378e7b346986eb52d0c7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionContainer.svg b/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionContainer.svg
new file mode 100644
index 0000000..65bb359
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionContainer.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionContainer.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionContainer.svg.import
new file mode 100644
index 0000000..5eaed84
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/ModulateTransitionContainer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://s3hp67f43n0x"
+path="res://.godot/imported/ModulateTransitionContainer.svg-41f98f4655621d9ed04421a4b13baee4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/ModulateTransitionContainer.svg"
+dest_files=["res://.godot/imported/ModulateTransitionContainer.svg-41f98f4655621d9ed04421a4b13baee4.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/MotionCheck.svg b/godot/addons/FreeControl/assets/icons/CustomType/MotionCheck.svg
new file mode 100644
index 0000000..03fe805
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/MotionCheck.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/MotionCheck.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/MotionCheck.svg.import
new file mode 100644
index 0000000..498b4b8
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/MotionCheck.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://jywj8x8u5pop"
+path="res://.godot/imported/MotionCheck.svg-5facd6c8a5eea77e7e6606c52a8d456d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/MotionCheck.svg"
+dest_files=["res://.godot/imported/MotionCheck.svg-5facd6c8a5eea77e7e6606c52a8d456d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/PaddingContainer.svg b/godot/addons/FreeControl/assets/icons/CustomType/PaddingContainer.svg
new file mode 100644
index 0000000..f1edea1
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/PaddingContainer.svg
@@ -0,0 +1,104 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/PaddingContainer.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/PaddingContainer.svg.import
new file mode 100644
index 0000000..3a078c4
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/PaddingContainer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c6o6frklk2afj"
+path="res://.godot/imported/PaddingContainer.svg-9d29b77cc431f8e4258d6655803790fe.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/PaddingContainer.svg"
+dest_files=["res://.godot/imported/PaddingContainer.svg-9d29b77cc431f8e4258d6655803790fe.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/Page.svg b/godot/addons/FreeControl/assets/icons/CustomType/Page.svg
new file mode 100644
index 0000000..fd3cb6c
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/Page.svg
@@ -0,0 +1,51 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/Page.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/Page.svg.import
new file mode 100644
index 0000000..d3301b3
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/Page.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cqtdlpg3h0j5d"
+path="res://.godot/imported/Page.svg-d8188a8e86ac732bd815acd1e14bc1cd.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/Page.svg"
+dest_files=["res://.godot/imported/Page.svg-d8188a8e86ac732bd815acd1e14bc1cd.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/PageInfo.svg b/godot/addons/FreeControl/assets/icons/CustomType/PageInfo.svg
new file mode 100644
index 0000000..61c8cc8
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/PageInfo.svg
@@ -0,0 +1 @@
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/PageInfo.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/PageInfo.svg.import
new file mode 100644
index 0000000..8315bbb
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/PageInfo.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b2wlsev3wn5rq"
+path="res://.godot/imported/PageInfo.svg-cac5d98635bfc001a76d9044e0d773df.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/PageInfo.svg"
+dest_files=["res://.godot/imported/PageInfo.svg-cac5d98635bfc001a76d9044e0d773df.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/ProportionalContainer.svg b/godot/addons/FreeControl/assets/icons/CustomType/ProportionalContainer.svg
new file mode 100644
index 0000000..19afd07
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/ProportionalContainer.svg
@@ -0,0 +1,52 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/ProportionalContainer.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/ProportionalContainer.svg.import
new file mode 100644
index 0000000..5d6f67a
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/ProportionalContainer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d08yq8cd27f3f"
+path="res://.godot/imported/ProportionalContainer.svg-71fab0e839b225c75a1402a08ebf61b4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/ProportionalContainer.svg"
+dest_files=["res://.godot/imported/ProportionalContainer.svg-71fab0e839b225c75a1402a08ebf61b4.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/RouterStack.svg b/godot/addons/FreeControl/assets/icons/CustomType/RouterStack.svg
new file mode 100644
index 0000000..fcebc78
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/RouterStack.svg
@@ -0,0 +1,51 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/RouterStack.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/RouterStack.svg.import
new file mode 100644
index 0000000..a6c4585
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/RouterStack.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bx72t0x80xoc"
+path="res://.godot/imported/RouterStack.svg-943b2eec7eb910602fd6b012fee2113d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/RouterStack.svg"
+dest_files=["res://.godot/imported/RouterStack.svg-943b2eec7eb910602fd6b012fee2113d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionButton.svg b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionButton.svg
new file mode 100644
index 0000000..bd3be2d
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionButton.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionButton.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionButton.svg.import
new file mode 100644
index 0000000..99b1c0b
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionButton.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://beombqir8gvsg"
+path="res://.godot/imported/StyleTransitionButton.svg-31239487ac3b145d1d200758ae441fa7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/StyleTransitionButton.svg"
+dest_files=["res://.godot/imported/StyleTransitionButton.svg-31239487ac3b145d1d200758ae441fa7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionContainer.svg b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionContainer.svg
new file mode 100644
index 0000000..9426722
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionContainer.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionContainer.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionContainer.svg.import
new file mode 100644
index 0000000..4351a9b
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionContainer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c6dj30vpi468i"
+path="res://.godot/imported/StyleTransitionContainer.svg-d423c826296d72d4b9686de202d9c396.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/StyleTransitionContainer.svg"
+dest_files=["res://.godot/imported/StyleTransitionContainer.svg-d423c826296d72d4b9686de202d9c396.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionPanel.svg b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionPanel.svg
new file mode 100644
index 0000000..83d7088
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionPanel.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionPanel.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionPanel.svg.import
new file mode 100644
index 0000000..6814903
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/StyleTransitionPanel.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cbu1p6if3bhsq"
+path="res://.godot/imported/StyleTransitionPanel.svg-6e7803726b3f0f97dacbb20fcb7a611c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/StyleTransitionPanel.svg"
+dest_files=["res://.godot/imported/StyleTransitionPanel.svg-6e7803726b3f0f97dacbb20fcb7a611c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/SwapContainer.svg b/godot/addons/FreeControl/assets/icons/CustomType/SwapContainer.svg
new file mode 100644
index 0000000..adc6a25
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/SwapContainer.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/godot/addons/FreeControl/assets/icons/CustomType/SwapContainer.svg.import b/godot/addons/FreeControl/assets/icons/CustomType/SwapContainer.svg.import
new file mode 100644
index 0000000..9db3b9f
--- /dev/null
+++ b/godot/addons/FreeControl/assets/icons/CustomType/SwapContainer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b6ebfwbec45wl"
+path="res://.godot/imported/SwapContainer.svg-2f0195c9a4f5afd5ec3524f1349104ed.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/FreeControl/assets/icons/CustomType/SwapContainer.svg"
+dest_files=["res://.godot/imported/SwapContainer.svg-2f0195c9a4f5afd5ec3524f1349104ed.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/FreeControl/main.gd b/godot/addons/FreeControl/main.gd
new file mode 100644
index 0000000..ececaf3
--- /dev/null
+++ b/godot/addons/FreeControl/main.gd
@@ -0,0 +1,258 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+extends EditorPlugin
+
+const GLOBAL_FOLDER := "res://addons/FreeControl/src/Other/Global/"
+const CUSTOM_CLASS_FOLDER := "res://addons/FreeControl/src/CustomClasses/"
+const ICON_FOLDER := "res://addons/FreeControl/assets/icons/CustomType/"
+
+func _enter_tree() -> void:
+ # AnimatableControls
+ # Control
+ add_custom_type(
+ "AnimatableControl",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "AnimatableControl/control/AnimatableControl.gd"),
+ load(ICON_FOLDER + "AnimatableControl.svg")
+ )
+ add_custom_type(
+ "AnimatableScrollControl",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "AnimatableControl/control/AnimatableScrollControl.gd"),
+ load(ICON_FOLDER + "AnimatableScrollControl.svg")
+ )
+ add_custom_type(
+ "AnimatableZoneControl",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "AnimatableControl/control/AnimatableZoneControl.gd"),
+ load(ICON_FOLDER + "AnimatableZoneControl.svg")
+ )
+ add_custom_type(
+ "AnimatableVisibleControl",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "AnimatableControl/control/AnimatableVisibleControl.gd"),
+ load(ICON_FOLDER + "AnimatableVisibleControl.svg")
+ )
+ # Mount
+ add_custom_type(
+ "AnimatableMount",
+ "Control",
+ load(CUSTOM_CLASS_FOLDER + "AnimatableControl/mount/AnimatableMount.gd"),
+ load(ICON_FOLDER + "AnimatableMount.svg")
+ )
+ add_custom_type(
+ "AnimatableTransformationMount",
+ "Control",
+ load(CUSTOM_CLASS_FOLDER + "AnimatableControl/mount/AnimatableTransformationMount.gd"),
+ load(ICON_FOLDER + "AnimatableTransformationMount.svg")
+ )
+
+ # Buttons
+ # Base
+ add_custom_type(
+ "AnimatedSwitch",
+ "BaseButton",
+ load(CUSTOM_CLASS_FOLDER + "Buttons/Base/AnimatedSwitch.gd"),
+ load(ICON_FOLDER + "AnimatedSwitch.svg")
+ )
+ add_custom_type(
+ "HoldButton",
+ "BaseButton",
+ load(CUSTOM_CLASS_FOLDER + "Buttons/Base/HoldButton.gd"),
+ load(ICON_FOLDER + "HoldButton.svg")
+ )
+ # MotionCheck
+ add_custom_type(
+ "BoundsCheck",
+ "Control",
+ load(CUSTOM_CLASS_FOLDER + "Buttons/Base/MotionCheck/BoundsCheck.gd"),
+ load(ICON_FOLDER + "BoundsCheck.svg")
+ )
+ add_custom_type(
+ "DistanceCheck",
+ "Control",
+ load(CUSTOM_CLASS_FOLDER + "Buttons/Base/MotionCheck/DistanceCheck.gd"),
+ load(ICON_FOLDER + "DistanceCheck.svg")
+ )
+ add_custom_type(
+ "MotionCheck",
+ "Control",
+ load(CUSTOM_CLASS_FOLDER + "Buttons/Base/MotionCheck/MotionCheck.gd"),
+ load(ICON_FOLDER + "MotionCheck.svg")
+ )
+
+ # Complex
+ add_custom_type(
+ "ModulateTransitionButton",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "Buttons/Complex/ModulateTransitionButton.gd"),
+ load(ICON_FOLDER + "ModulateTransitionButton.svg")
+ )
+ add_custom_type(
+ "StyleTransitionButton",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "Buttons/Complex/StyleTransitionButton.gd"),
+ load(ICON_FOLDER + "StyleTransitionButton.svg")
+ )
+
+ # Carousel
+ add_custom_type(
+ "Carousel",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "Carousel/Carousel.gd"),
+ load(ICON_FOLDER + "Carousel.svg")
+ )
+
+ # CircularContainer
+ add_custom_type(
+ "CircularContainer",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "CircularContainer/CircularContainer.gd"),
+ load(ICON_FOLDER + "CircularContainer.svg")
+ )
+
+ # Drawer
+ add_custom_type(
+ "Drawer",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "Drawer/Drawer.gd"),
+ load(ICON_FOLDER + "Drawer.svg")
+ )
+
+ # PaddingContainer
+ add_custom_type(
+ "PaddingContainer",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "PaddingContainer/PaddingContainer.gd"),
+ load(ICON_FOLDER + "PaddingContainer.svg")
+ )
+
+ # ProportionalContainer
+ add_custom_type(
+ "ProportionalContainer",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "ProportionalContainer/ProportionalContainer.gd"),
+ load(ICON_FOLDER + "ProportionalContainer.svg")
+ )
+
+ # Routers
+ add_custom_type(
+ "RouterStack",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "Routers/RouterStack.gd"),
+ load(ICON_FOLDER + "RouterStack.svg")
+ )
+ # Base
+ add_custom_type(
+ "Page",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "Routers/Base/Page.gd"),
+ load(ICON_FOLDER + "Page.svg")
+ )
+ add_custom_type(
+ "PageInfo",
+ "Resource",
+ load(CUSTOM_CLASS_FOLDER + "Routers/Base/PageInfo.gd"),
+ load(ICON_FOLDER + "PageInfo.svg")
+ )
+
+ # SizeControllers
+ # MaxSizeContainer
+ add_custom_type(
+ "MaxSizeContainer",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "SizeController/MaxSizeContainer.gd"),
+ load(ICON_FOLDER + "MaxSizeContainer.svg")
+ )
+ # MaxRatioContainer
+ add_custom_type(
+ "MaxRatioContainer",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "SizeController/MaxRatioContainer.gd"),
+ load(ICON_FOLDER + "MaxRatioContainer.svg")
+ )
+
+ # SwapContainer
+ add_custom_type(
+ "SwapContainer",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "SwapContainer/SwapContainer.gd"),
+ load(ICON_FOLDER + "SwapContainer.svg")
+ )
+
+ # TransitionContainers
+ add_custom_type(
+ "ModulateTransitionContainer",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "TransitionContainers/ModulateTransitionContainer.gd"),
+ load(ICON_FOLDER + "ModulateTransitionContainer.svg")
+ )
+ add_custom_type(
+ "StyleTransitionContainer",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "TransitionContainers/StyleTransitionContainer.gd"),
+ load(ICON_FOLDER + "StyleTransitionContainer.svg")
+ )
+ add_custom_type(
+ "StyleTransitionPanel",
+ "Container",
+ load(CUSTOM_CLASS_FOLDER + "TransitionContainers/StyleTransitionPanel.gd"),
+ load(ICON_FOLDER + "StyleTransitionPanel.svg")
+ )
+
+func _exit_tree() -> void:
+ # AnimatableControls
+ # Control
+ remove_custom_type("AnimatableControl")
+ remove_custom_type("AnimatableScrollControl")
+ remove_custom_type("AnimatableZoneControl")
+ remove_custom_type("AnimatableVisibleControl")
+ # Mount
+ remove_custom_type("AnimatableMount")
+ remove_custom_type("AnimatableTransformationMount")
+
+ # Buttons
+ # Base
+ remove_custom_type("AnimatedSwitch")
+ remove_custom_type("HoldButton")
+ # MotionCheck
+ remove_custom_type("BoundsCheck")
+ remove_custom_type("DistanceCheck")
+ remove_custom_type("MotionCheck")
+
+ # Complex
+ remove_custom_type("ModulateTransitionButton")
+ remove_custom_type("StyleTransitionButton")
+
+ # Carousel
+ remove_custom_type("Carousel")
+
+ # CircularContainer
+ remove_custom_type("CircularContainer")
+
+ # Drawer
+ remove_custom_type("Drawer")
+
+ # PaddingContainer
+ remove_custom_type("PaddingContainer")
+
+ # ProportionalContainer
+ remove_custom_type("ProportionalContainer")
+
+ # Routers
+ remove_custom_type("RouterStack")
+ # Base
+ remove_custom_type("Page")
+ remove_custom_type("PageInfo")
+
+ # SizeControllers
+ remove_custom_type("MaxSizeContainer")
+ remove_custom_type("MaxRatioContainer")
+
+ # SwapContainer
+ remove_custom_type("SwapContainer")
+
+ # TransitionContainers
+ remove_custom_type("ModulateTransitionContainer")
+ remove_custom_type("StyleTransitionContainer")
+ remove_custom_type("StyleTransitionPanel")
diff --git a/godot/addons/FreeControl/plugin.cfg b/godot/addons/FreeControl/plugin.cfg
new file mode 100644
index 0000000..836b476
--- /dev/null
+++ b/godot/addons/FreeControl/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="FreeControl"
+description="Multiple Control nodes for easier UI manipulation"
+author="Xavier Alvarez"
+version="1.5.1"
+script="main.gd"
diff --git a/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableControl.gd b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableControl.gd
new file mode 100644
index 0000000..b487b5f
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableControl.gd
@@ -0,0 +1,139 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name AnimatableControl extends Container
+## A container to be used for free transformation within a UI.
+
+## This signal emits when one of the following properties change: scale, position,
+## rotation, pivot_offset
+signal transformation_changed
+
+## The size mode this node's size will be bounded by.
+enum SIZE_MODE {
+ NONE = 0b00, ## This node's size is not bounded
+ MIN = 0b01, ## This node's size will be greater than or equal to this node's mount size
+ MAX = 0b10, ## This node's size will be less than or equal to this node's mount size
+ EXACT = 0b11 ## This node's size will be the same as this node's mount size
+}
+
+## Controls how this node's size is bounded, according to the node's mount size
+@export var size_mode : SIZE_MODE = SIZE_MODE.EXACT:
+ set(val):
+ if size_mode != val:
+ size_mode = val
+ if _mount:
+ _mount.update_minimum_size()
+ _bound_size()
+ notify_property_list_changed()
+## Auto sets the pivot to be at some position percentage of the size.
+@export var pivot_ratio : Vector2:
+ set(val):
+ if pivot_ratio != val:
+ pivot_ratio = val
+ pivot_offset = size * val
+
+var _mount : AnimatableMount
+
+func _get_configuration_warnings() -> PackedStringArray:
+ if get_parent() is AnimatableMount:
+ return []
+ return ["This node only serves to be animatable within a UI. Please only attach as a child to a 'AnimatableMount' node."]
+func _validate_property(property: Dictionary) -> void:
+ if property.name in ["layout_mode", "anchors_preset"]:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ elif property.name == "size":
+ if size_mode == SIZE_MODE.EXACT:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+func _set(property: StringName, value: Variant) -> bool:
+ if property in ["scale", "position", "rotation"]:
+ transformation_changed.emit()
+ _bound_size()
+ elif property == "pivot_offset":
+ if is_node_ready() && size > Vector2.ZERO && pivot_offset != value:
+ pivot_ratio = pivot_offset / size
+ transformation_changed.emit()
+ return false
+
+func _init() -> void:
+ resized.connect(_handle_resize)
+ sort_children.connect(_sort_children)
+ tree_exited.connect(_on_tree_exit)
+ tree_entered.connect(_on_tree_enter)
+ item_rect_changed.connect(transformation_changed.emit)
+func _on_tree_enter() -> void:
+ _mount = (get_parent() as AnimatableMount)
+ if _mount:
+ _mount._on_mount(self)
+func _on_tree_exit() -> void:
+ if _mount:
+ _mount._on_unmount(self)
+ _mount = null
+
+
+func _handle_resize() -> void:
+ _bound_size()
+ _update_pivot()
+func _update_pivot() -> void:
+ set_pivot_offset(pivot_ratio * size)
+ transformation_changed.emit()
+func _sort_children() -> void:
+ for child : Control in _get_control_children():
+ _resize_child(child)
+func _resize_child(child : Control) -> void:
+ var child_size := child.get_combined_minimum_size()
+ var set_pos : Vector2
+
+ match child.size_flags_horizontal & ~SIZE_EXPAND:
+ SIZE_FILL:
+ set_pos.x = 0
+ child_size.x = max(child_size.x, size.x)
+ SIZE_SHRINK_BEGIN:
+ set_pos.x = 0
+ SIZE_SHRINK_CENTER:
+ set_pos.x = (size.x - child_size.x) * 0.5
+ SIZE_SHRINK_END:
+ set_pos.x = size.x - child_size.x
+ match child.size_flags_vertical & ~SIZE_EXPAND:
+ SIZE_FILL:
+ set_pos.y = 0
+ child_size.y = max(child_size.y, size.y)
+ SIZE_SHRINK_BEGIN:
+ set_pos.y = 0
+ SIZE_SHRINK_CENTER:
+ set_pos.y = (size.y - child_size.y) * 0.5
+ SIZE_SHRINK_END:
+ set_pos.y = size.y - child_size.y
+
+ fit_child_in_rect(child, Rect2(set_pos, child_size))
+func _get_minimum_size() -> Vector2:
+ if clip_children: return Vector2.ZERO
+
+ var min_size := Vector2.ZERO
+ for child : Control in _get_control_children():
+ min_size = min_size.max(child.get_combined_minimum_size())
+ return min_size
+
+func _bound_size() -> void:
+ if !_mount: return
+
+ var new_size : Vector2 = size
+ if size_mode == SIZE_MODE.MAX:
+ new_size = _mount.get_relative_size(self).min(size)
+ elif size_mode == SIZE_MODE.MIN:
+ new_size = _mount.get_relative_size(self).max(size)
+ elif size_mode == SIZE_MODE.EXACT:
+ new_size = _mount.get_relative_size(self)
+
+ if new_size != size:
+ size = new_size
+
+func _get_control_children() -> Array[Control]:
+ var ret : Array[Control]
+ ret.assign(get_children().filter(func(child : Node): return child is Control && child.visible))
+ return ret
+
+## Gets the mount this node is currently a child to.[br]
+## If this node is not a child to any [AnimatableMount] nodes, this returns [code]null[/code] instead.
+func get_mount() -> AnimatableMount:
+ return _mount
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableScrollControl.gd b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableScrollControl.gd
new file mode 100644
index 0000000..b63d7cb
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableScrollControl.gd
@@ -0,0 +1,72 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name AnimatableScrollControl extends AnimatableControl
+## A container to be used for free transformation, within a UI, depended on a [ScrollContainer]'s scroll progress.
+
+## The [ScrollContainer] this node will consider for operations. Is automatically
+## set to the closet parent [ScrollContainer] in the tree if [member scroll] is
+## [code]null[/code] and [Engine] is in editor mode.
+## [br][br]
+## [b]NOTE[/b]: It is recomended that this node's [AnimatableMount] is a child of
+## [member scroll].
+@export var scroll : ScrollContainer:
+ set(val):
+ if scroll != val:
+ if scroll:
+ scroll.get_h_scroll_bar().value_changed.disconnect(_scrolled_horizontal)
+ scroll.get_v_scroll_bar().value_changed.disconnect(_scrolled_vertical)
+ scroll = val
+ if val:
+ val.get_h_scroll_bar().value_changed.connect(_scrolled_horizontal)
+ val.get_v_scroll_bar().value_changed.connect(_scrolled_vertical)
+
+ if is_node_ready():
+ _scrolled_horizontal(val.get_h_scroll_bar().value)
+ _scrolled_vertical(val.get_v_scroll_bar().value)
+
+func _enter_tree() -> void:
+ if !scroll && Engine.is_editor_hint(): scroll = get_parent_scroll()
+
+## A virtual function that is called when [member scroll] is horizontally scrolled.
+## [br][br]
+## Paramter [param scroll] is the current horizontal progress of the scroll.
+func _scrolled_horizontal(scroll_hor : float) -> void: pass
+## A virtual function that is called when [member scroll] is vertically scrolled.
+## [br][br]
+## Paramter [param scroll] is the current vertical progress of the scroll.
+func _scrolled_vertical(scroll_ver : float) -> void: pass
+
+## Returns the global difference between this node's [AnimatableMount] and
+## [member scroll] positions.
+func get_origin_offset() -> Vector2:
+ if !_mount || !scroll: return Vector2.ZERO
+ return _mount.global_position - scroll.global_position
+## Returns the horizontal and vertical progress of [member scroll].
+func get_scroll_offset() -> Vector2:
+ if !scroll: return Vector2.ZERO
+ return Vector2(scroll.scroll_horizontal, scroll.scroll_vertical)
+## Gets the closet parent [ScrollContainer] in the tree.
+func get_parent_scroll() -> ScrollContainer:
+ var ret : Control = (get_parent() as Control)
+ while ret != null:
+ if ret is ScrollContainer: return ret
+ ret = (ret.get_parent() as Control)
+ return null
+
+## Returns a percentage of how visible this node's [AnimatableMount] is, within
+## the rect of [member scroll].
+func is_visible_percent() -> float:
+ if !_mount || !scroll: return 0
+ return (_mount.get_global_rect().intersection(scroll.get_global_rect()).get_area()) / (_mount.size.x * _mount.size.y)
+## Returns a percentage of how visible this node's [AnimatableMount] is, within the
+## horizontal bounds of [member scroll].
+func get_visible_horizontal_percent() -> float:
+ if !_mount || !scroll: return 0
+ return (min(_mount.global_position.x + _mount.size.x, scroll.global_position.x + scroll.size.x) - max(_mount.global_position.x, scroll.global_position.x)) / _mount.size.x
+## Returns a percentage of how visible this node's [AnimatableMount] is, within the
+## vertical bounds of [member scroll].
+func get_visible_vertical_percent() -> float:
+ if !_mount || !scroll: return 0
+ return (min(_mount.global_position.y + _mount.size.y, scroll.global_position.y + scroll.size.y) - max(_mount.global_position.y, scroll.global_position.y)) / _mount.size.y
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableVisibleControl.gd b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableVisibleControl.gd
new file mode 100644
index 0000000..4cc258a
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableVisibleControl.gd
@@ -0,0 +1,397 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name AnimatableVisibleControl extends AnimatableScrollControl
+## A container to be used for free transformation, within a UI, depending on if
+## the node is visible in a [ScrollContainer] scroll.
+
+## Emitted when requested threshold has been entered.
+signal entered_threshold
+## Emitted when requested threshold has been exited.
+signal exited_threshold
+## Emitted when this node's [AnimatableMount]'s rect entered visible range.
+signal entered_screen
+## Emitted when this node's [AnimatableMount]'s rect exited visible range.
+signal exited_screen
+
+## Modes of threshold type checking.
+enum CHECK_MODE {
+ NONE = 0b000, ## No behavior.
+ HORIZONTAL = 0b001, ## Only checks horizontally using [member threshold_horizontal].
+ VERTICAL = 0b010, ## Only checks vertically using [member threshold_vertical].
+ BOTH = 0b011 ## Checks horizontally and vertically.
+}
+
+## Modes of threshold size.
+enum THRESHOLD_EDITOR_DIMS {
+ None = 0b00, ## Both horizontal and vertical axis are based on ratio.
+ Horizontal = 0b01, ## Horizontal axis is based on exact pixel and vertical on ratio.
+ Vertical = 0b10, ## Horizontal axis is based on ratio and vertical on exact pixel.
+ Both = 0b11, ## Both horizontal and vertical axis are based on exact pixel.
+}
+
+## Color for inner highlighting - Indicates when visiblity is required to met
+## threshold.
+const HIGHLIGHT_COLOR := Color(Color.RED, 0.3)
+## Color for overlap highlighting - Indicates when visiblity is required, starting
+## from the far end, to met threshold.
+const ANTI_HIGHLIGHT_COLOR := Color(Color.DARK_CYAN, 1)
+## Color for helpful lines to make highlighting for clear.
+const INTERSECT_HIGHLIGHT_COLOR := Color(Color.RED, 0.8)
+
+@export_group("Mode")
+## Sets the mode of threshold type checking.
+@export var check_mode: CHECK_MODE = CHECK_MODE.NONE:
+ set(val):
+ if check_mode != val:
+ check_mode = val
+ notify_property_list_changed()
+ queue_redraw()
+
+## A flag variable used to distinguish if the threshold amount is described by
+## a ratio of the size of the [member AnimatableScrollControl.scroll] value, or
+## by a const pixel value.[br]
+## Horizontal and vertical axis are consistered differently.
+## [br][br]
+## See [enum THRESHOLD_EDITOR_DIMS], [member threshold_horizontal], and [member threshold_vertical].
+var threshold_pixel : int:
+ set(val):
+ if (threshold_pixel ^ val) & THRESHOLD_EDITOR_DIMS.Horizontal:
+ _scrolled_horizontal(get_scroll_offset().x)
+ if (threshold_pixel ^ val) & THRESHOLD_EDITOR_DIMS.Vertical:
+ _scrolled_vertical(get_scroll_offset().y)
+ threshold_pixel = val
+ notify_property_list_changed()
+ queue_redraw()
+## The minimum horizontal percentage this node's [AnimatableMount]'s rect must be
+## visible in [member scroll] for this node to be consistered visible.
+var threshold_horizontal : float = 0.5:
+ set(val):
+ if threshold_horizontal != val:
+ threshold_horizontal = val
+ _scrolled_horizontal(0)
+ queue_redraw()
+## The minimum vertical percentage this node's [AnimatableMount]'s rect must be
+## visible in [member scroll] for this node to be consistered visible.
+var threshold_vertical : float = 0.5:
+ set(val):
+ if threshold_vertical != val:
+ threshold_vertical = val
+ _scrolled_vertical(0)
+ queue_redraw()
+## [b]Editor usage only.[/b] Shows or hides the helpful threshold highlighter.
+var hide_indicator : bool = true:
+ set(val):
+ if hide_indicator != val:
+ hide_indicator = val
+ queue_redraw()
+
+var _last_threshold_horizontal : float
+var _last_threshold_vertical : float
+var _last_visible : bool
+
+func _get_property_list() -> Array[Dictionary]:
+ var ret : Array[Dictionary] = []
+ var horizontal : int = 0 if check_mode & CHECK_MODE.HORIZONTAL else PROPERTY_USAGE_READ_ONLY
+ var vertical : int = 0 if check_mode & CHECK_MODE.VERTICAL else PROPERTY_USAGE_READ_ONLY
+ var either : int = horizontal & vertical
+
+ var options : String
+ if !horizontal: options = "Horizontal:1,"
+ if !vertical: options += "Vertical:2"
+
+ ret.append({
+ "name": "Threshold",
+ "type": TYPE_NIL,
+ "usage": PROPERTY_USAGE_GROUP,
+ "hint_string": ""
+ })
+ ret.append({
+ "name": "threshold_pixel",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_FLAGS,
+ "hint_string": options,
+ "usage": PROPERTY_USAGE_DEFAULT | either
+ })
+ ret.append({
+ "name": "threshold_horizontal",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT | horizontal
+ }.merged({} if threshold_pixel & 1 else {
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001"
+ }))
+ ret.append({
+ "name": "threshold_vertical",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT | vertical
+ }.merged({} if threshold_pixel & 2 else {
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001"
+ }))
+
+ ret.append({
+ "name": "Indicator",
+ "type": TYPE_NIL,
+ "usage": PROPERTY_USAGE_GROUP,
+ "hint_str": ""
+ })
+ ret.append({
+ "name": "hide_indicator",
+ "type": TYPE_BOOL,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+ return ret
+func _property_can_revert(property: StringName) -> bool:
+ if property == "threshold_pixel":
+ if self[property] != 0: return true
+ elif property in ["threshold_horizontal", "threshold_vertical"]:
+ if self[property] != 0.5: return true
+ elif property == "hide_indicator":
+ return !hide_indicator
+ return false
+func _property_get_revert(property: StringName) -> Variant:
+ if property == "threshold_pixel":
+ return 0
+ elif property in ["threshold_horizontal", "threshold_vertical"]:
+ return 0.5
+ elif property == "hide_indicator":
+ return true
+ return null
+
+func _init() -> void:
+ item_rect_changed.connect(queue_redraw)
+
+func _get_threshold_size() -> Array[Vector2]:
+ var ratio_thr : Vector2
+ var full_thr : Vector2
+
+ if is_zero_approx(_mount.size.x):
+ ratio_thr.x = 1
+ full_thr.x = _mount.size.x
+ elif threshold_pixel & THRESHOLD_EDITOR_DIMS.Horizontal:
+ var hor := clamp(threshold_horizontal, 0, _mount.size.x)
+ ratio_thr.x = hor / _mount.size.x
+ full_thr.x = hor
+ else:
+ var hor := clamp(threshold_horizontal, 0, 1)
+ ratio_thr.x = hor
+ full_thr.x = hor * _mount.size.x
+
+ if is_zero_approx(_mount.size.y):
+ ratio_thr.y = 1
+ full_thr.y = _mount.size.y
+ elif threshold_pixel & THRESHOLD_EDITOR_DIMS.Vertical:
+ var vec := clamp(threshold_vertical, 0, _mount.size.y)
+ ratio_thr.y = vec / _mount.size.y
+ full_thr.y = vec
+ else:
+ var vec := clamp(threshold_vertical, 0, 1)
+ ratio_thr.y = vec
+ full_thr.y = vec * _mount.size.y
+
+ return [ratio_thr, full_thr]
+func _draw() -> void:
+ if !_mount || !Engine.is_editor_hint() || hide_indicator: return
+
+ var threshold_adjust := _get_threshold_size()
+
+ draw_set_transform(-position)
+ draw_rect(Rect2(Vector2.ZERO, size), Color.CORAL, false)
+
+ match check_mode:
+ CHECK_MODE.HORIZONTAL:
+ var left := threshold_adjust[1].x
+ var right := size.x - left
+
+ if threshold_adjust[0].x > 0.5:
+ left = size.x - left
+ right = size.x - right
+
+ _draw_highlight(
+ left,
+ 0,
+ right,
+ size.y,
+ threshold_adjust[0].x < 0.5
+ )
+ CHECK_MODE.VERTICAL:
+ var top := threshold_adjust[1].y
+ var bottom := size.y - top
+
+ if threshold_adjust[0].y > 0.5:
+ top = size.y - top
+ bottom = size.y - bottom
+
+ _draw_highlight(
+ 0,
+ top,
+ size.x,
+ bottom,
+ threshold_adjust[0].y < 0.5
+ )
+ CHECK_MODE.BOTH:
+ var left := threshold_adjust[1].x
+ var right := size.x - left
+ var top := threshold_adjust[1].y
+ var bottom := size.y - top
+
+ var draw_middle : bool = true
+ if threshold_adjust[0].x >= 0.5:
+ left = size.x - left
+ right = size.x - right
+ draw_middle = false
+ if threshold_adjust[0].y >= 0.5:
+ top = size.y - top
+ bottom = size.y - bottom
+ draw_middle = false
+
+ _draw_highlight(
+ left,
+ top,
+ right,
+ bottom,
+ draw_middle
+ )
+
+ if !draw_middle:
+ if threshold_adjust[0].x >= 0.5:
+ if threshold_adjust[0].y < 0.5:
+ draw_line(
+ Vector2(left, top),
+ Vector2(right, top),
+ INTERSECT_HIGHLIGHT_COLOR,
+ 5
+ )
+ draw_line(
+ Vector2(left, bottom),
+ Vector2(right, bottom),
+ INTERSECT_HIGHLIGHT_COLOR,
+ 5
+ )
+ elif threshold_adjust[0].y >= 0.5:
+ draw_line(
+ Vector2(left, top),
+ Vector2(left, bottom),
+ INTERSECT_HIGHLIGHT_COLOR,
+ 5
+ )
+ draw_line(
+ Vector2(right, top),
+ Vector2(right, bottom),
+ INTERSECT_HIGHLIGHT_COLOR,
+ 5
+ )
+func _draw_highlight(
+ left : float,
+ top : float,
+ right : float,
+ bottom : float,
+ draw_middle : bool
+ ) -> void:
+ # Middle
+ if draw_middle:
+ draw_rect(Rect2(Vector2(left, top), Vector2(right - left, bottom - top)), HIGHLIGHT_COLOR)
+ return
+ # Outer
+ # Left
+ draw_rect(Rect2(Vector2(0, 0), Vector2(left, size.y)), ANTI_HIGHLIGHT_COLOR)
+ # Right
+ draw_rect(Rect2(Vector2(right, 0), Vector2(size.x - right, size.y)), ANTI_HIGHLIGHT_COLOR)
+ # Top
+ draw_rect(Rect2(Vector2(left, 0), Vector2(right - left, top)), ANTI_HIGHLIGHT_COLOR)
+ # Bottom
+ draw_rect(Rect2(Vector2(left, bottom), Vector2(right - left, size.y - bottom)), ANTI_HIGHLIGHT_COLOR)
+
+func _scrolled_horizontal(_scroll_hor : float) -> void:
+ if !(check_mode & CHECK_MODE.HORIZONTAL): return
+
+ var threshold_adjust := _get_threshold_size()
+ var val : float = is_visible_percent()
+
+ # Checks if visible
+ if val > 0:
+ # If visible, but wasn't visible last scroll, then it entered visible area
+ if !_last_visible:
+ entered_screen.emit()
+ _last_visible = true
+ # Calls the while function
+ _while_visible(val)
+ # Else, if visible last frame, then it exited visible area
+ elif _last_visible:
+ _while_visible(0)
+ exited_screen.emit()
+ _last_visible = false
+
+ val = get_visible_horizontal_percent()
+ # Checks if in threshold
+ if val >= threshold_adjust[0].x:
+ # If in threshold, but not last frame, then it entered threshold area
+ if _last_threshold_horizontal < threshold_adjust[0].x:
+ entered_threshold.emit()
+ # Calls the while function
+ _while_threshold(val)
+ # If in threshold, but not last frame, then it entered threshold area
+ elif _last_threshold_horizontal > threshold_adjust[0].x:
+ _while_threshold(0)
+ exited_threshold.emit()
+ _last_threshold_horizontal = val
+func _scrolled_vertical(_scroll_ver : float) -> void:
+ if !(check_mode & CHECK_MODE.VERTICAL): return
+
+ var threshold_adjust := _get_threshold_size()
+ var val : float = is_visible_percent()
+
+ # Checks if visible
+ if val > 0:
+ # If visible, but wasn't visible last scroll, then it entered visible area
+ if !_last_visible:
+ entered_screen.emit()
+ _last_visible = true
+ # Calls the while function
+ _while_visible(val)
+ # Else, if visible last frame, then it exited visible area
+ elif _last_visible:
+ _while_visible(0)
+ exited_screen.emit()
+ _last_visible = false
+
+ val = get_visible_vertical_percent()
+ # Checks if in threshold
+ if val >= threshold_adjust[0].y:
+ # If in threshold, but not last frame, then it entered threshold area
+ if _last_threshold_vertical < threshold_adjust[0].y:
+ entered_threshold.emit()
+ # Calls the while function
+ _while_threshold(val)
+ # If in threshold, but not last frame, then it entered threshold area
+ elif _last_threshold_vertical > threshold_adjust[0].y:
+ _while_threshold(0)
+ exited_threshold.emit()
+ _last_threshold_vertical = val
+
+
+
+# Public Functions
+
+## Returns the rect [threshold_horizontal] and [threshold_vertical] create.
+func get_threshold_rect(consider_mode : bool = false) -> Rect2:
+ var threshold_adjust := _get_threshold_size()
+ return Rect2(threshold_adjust[1], size - threshold_adjust[1])
+
+
+
+# Virtual Functions
+
+## A virtual function that is called while this node is in the visible area of it's
+## scroll. Is called after each scroll of [member scroll].
+## [br][br]
+## Paramter [param intersect] is the current visible percent.
+func _while_visible(intersect : float) -> void: pass
+## A virtual function that is called while this node's visible threshold is met. Is
+## called after each scroll of [member scroll].
+## [br][br]
+## Paramter [param intersect] is the current threshold value met.
+func _while_threshold(intersect : float) -> void: pass
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableZoneControl.gd b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableZoneControl.gd
new file mode 100644
index 0000000..daa4116
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/control/AnimatableZoneControl.gd
@@ -0,0 +1,386 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name AnimatableZoneControl extends AnimatableScrollControl
+## A container to be used for free transformation, within a UI, depended on a
+## [ScrollContainer]'s scroll progress.
+
+## Modes of zone type checking.
+enum CHECK_MODE {
+ NONE = 0b000, ## No behavior.
+ HORIZONTAL = 0b001, ## Only checks if this node's mount is in the zone horizontally.
+ VERTICAL = 0b010, ## Only checks if this node's mount is in the zone vertically.
+ BOTH = 0b011 ## Checks horizontally and vertically.
+}
+
+## Modes of zone size and center position.
+enum ZONE_EDITOR_DIMS {
+ None = 0b00, ## Both horizontal and vertical axis are based on ratio.
+ Horizontal = 0b01, ## Horizontal axis is based on exact pixel and vertical on ratio.
+ Vertical = 0b10, ## Horizontal axis is based on ratio and vertical on exact pixel.
+ Both = 0b11, ## Both horizontal and vertical axis are based on exact pixel.
+}
+
+## Emitted when this node's [AnimatableMount]'s entered the zone area.
+signal entered_zone
+## Emitted when this node's [AnimatableMount]'s exited the zone area.
+signal exited_zone
+
+## Color for inner highlighting - Indicates when visiblity is required to met threshold.
+const HIGHLIGHT_COLOR := Color(Color.RED, 0.3)
+
+@export_group("Mode")
+## Sets the mode of zone checking.
+@export var check_mode: CHECK_MODE = CHECK_MODE.NONE:
+ set(val):
+ if check_mode != val:
+ check_mode = val
+ notify_property_list_changed()
+ queue_redraw()
+
+## A flag variable used to distinguish if the center of the zone is described by
+## a ratio of the size of the [member AnimatableScrollControl.scroll] value, or
+## by a const pixel value.[br]
+## Horizontal and vertical axis are consistered differently.
+## [br][br]
+## See [enum ZONE_EDITOR_DIMS], [member zone_horizontal], and [member zone_vertical].
+var zone_point_pixel : int = 0:
+ set(val):
+ if (zone_point_pixel ^ val) & ZONE_EDITOR_DIMS.Horizontal:
+ _scrolled_horizontal(get_scroll_offset().x)
+ if (zone_point_pixel ^ val) & ZONE_EDITOR_DIMS.Vertical:
+ _scrolled_vertical(get_scroll_offset().y)
+ zone_point_pixel = val
+ notify_property_list_changed()
+ queue_redraw()
+var _zone_horizontal : float = 0.5
+## The horizontal position of the zone's center, described either as the ratio of
+## the size of the [member AnimatableScrollControl.scroll] value, or by a const
+## pixel value.[br]
+## [br][br]
+## See [member zone_point_pixel]
+var zone_horizontal : float:
+ get: return _zone_horizontal
+ set(val):
+ if _zone_horizontal != val:
+ _zone_horizontal = val
+ _scrolled_horizontal(get_scroll_offset().x)
+ queue_redraw()
+var _zone_vertical : float = 0.5
+## The vertical position of the zone's center, described either as the ratio of
+## the size of the [member AnimatableScrollControl.scroll] value, or by a const
+## pixel value.[br]
+## [br][br]
+## See [member zone_point_pixel]
+var zone_vertical : float = 0.5:
+ get: return _zone_vertical
+ set(val):
+ if _zone_vertical != val:
+ _zone_vertical = val
+ _scrolled_vertical(get_scroll_offset().y)
+ queue_redraw()
+
+## A flag variable used to distinguish if the size of the zone is described by
+## a ratio of the size of the [member AnimatableScrollControl.scroll] value, or
+## by a const pixel value.[br]
+## Horizontal and vertical axis are consistered differently.
+## [br][br]
+## See [enum ZONE_EDITOR_DIMS], [member zone_horizontal], and [member zone_vertical].
+var zone_range_by_pixel : int = 0:
+ set(val):
+ if (zone_point_pixel ^ val) & ZONE_EDITOR_DIMS.Horizontal:
+ _scrolled_horizontal(get_scroll_offset().x)
+ if (zone_point_pixel ^ val) & ZONE_EDITOR_DIMS.Vertical:
+ _scrolled_horizontal(get_scroll_offset().y)
+ zone_range_by_pixel = val
+ notify_property_list_changed()
+ queue_redraw()
+var _zone_range_horizontal : float = 0.05
+## The horizontal size of the zone, described either as the ratio of the size
+## of the [member AnimatableScrollControl.scroll] value, or by a const pixel
+## value.[br]
+## [br][br]
+## See [member zone_vertical]
+var zone_range_horizontal : float:
+ get: return _zone_range_horizontal
+ set(val):
+ if _zone_range_horizontal != val:
+ _zone_range_horizontal = val
+ _scrolled_horizontal(get_scroll_offset().x)
+ queue_redraw()
+var _zone_range_vertical : float = 0.05
+## The vertical size of the zone, described either as the ratio of the size
+## of the [member AnimatableScrollControl.scroll] value, or by a const pixel
+## value.[br]
+## [br][br]
+## See [member zone_vertical]
+var zone_range_vertical : float:
+ get: return _zone_range_vertical
+ set(val):
+ if _zone_range_vertical != val:
+ _zone_range_vertical = val
+ _scrolled_vertical(get_scroll_offset().y)
+ queue_redraw()
+
+## [b]Editor usage only.[/b] Shows or hides the helpful threshold highlighter.
+var hide_indicator : bool = true:
+ set(val):
+ if hide_indicator != val:
+ hide_indicator = val
+ queue_redraw()
+
+var _last_overlapped : int = 2
+
+func _draw() -> void:
+ if !_mount || !Engine.is_editor_hint() || hide_indicator || !scroll || check_mode == CHECK_MODE.NONE: return
+
+ var draw_rect := get_zone_rect()
+ var scroll_transform := scroll.get_global_transform()
+ var transform := _mount.get_global_transform()
+
+ draw_set_transform(scroll_transform.get_origin() - transform.get_origin(),
+ scroll_transform.get_rotation() - transform.get_rotation(),
+ scroll_transform.get_scale() / transform.get_scale())
+ draw_rect(draw_rect, HIGHLIGHT_COLOR)
+func _get_property_list() -> Array[Dictionary]:
+ var ret : Array[Dictionary] = []
+ var horizontal : int = 0 if check_mode & CHECK_MODE.HORIZONTAL else PROPERTY_USAGE_READ_ONLY
+ var vertical : int = 0 if check_mode & CHECK_MODE.VERTICAL else PROPERTY_USAGE_READ_ONLY
+ var either : int = horizontal & vertical
+
+ var options : String
+ if !horizontal: options = "Horizontal:1,"
+ if !vertical: options += "Vertical:2"
+
+ ret.append({
+ "name": "Zone Point",
+ "type": TYPE_NIL,
+ "usage": PROPERTY_USAGE_GROUP,
+ "hint_string": ""
+ })
+ ret.append({
+ "name": "zone_point_pixel",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_FLAGS,
+ "hint_string": options,
+ "usage": PROPERTY_USAGE_DEFAULT | either
+ })
+ ret.append({
+ "name": "zone_horizontal",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT | horizontal
+ }.merged({} if zone_point_pixel & 1 else {
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001,or_less,or_greater"
+ }))
+ ret.append({
+ "name": "zone_vertical",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT | vertical
+ }.merged({} if zone_point_pixel & 2 else {
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001,or_less,or_greater"
+ }))
+
+ ret.append({
+ "name": "Zone Range",
+ "type": TYPE_NIL,
+ "usage": PROPERTY_USAGE_GROUP,
+ "hint_string": ""
+ })
+ ret.append({
+ "name": "zone_range_by_pixel",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_FLAGS,
+ "hint_string": options,
+ "usage": PROPERTY_USAGE_DEFAULT | either
+ })
+ ret.append({
+ "name": "zone_range_horizontal",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT | horizontal
+ }.merged({} if zone_range_by_pixel & 1 else {
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001,or_less,or_greater"
+ }))
+ ret.append({
+ "name": "zone_range_vertical",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT | vertical
+ }.merged({} if zone_range_by_pixel & 2 else {
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001,or_less,or_greater"
+ }))
+
+ ret.append({
+ "name": "Indicator",
+ "type": TYPE_NIL,
+ "usage": PROPERTY_USAGE_GROUP,
+ "hint_str": ""
+ })
+ ret.append({
+ "name": "hide_indicator",
+ "type": TYPE_BOOL,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+ return ret
+func _property_can_revert(property: StringName) -> bool:
+ if property in ["zone_point_pixel", "zone_range_by_pixel"]:
+ if self[property] != 0: return true
+ elif property in ["zone_horizontal", "zone_vertical"]:
+ if self[property] != 0.5: return true
+ elif property in ["zone_range_horizontal", "zone_range_vertical"]:
+ if self[property] != 0.05: return true
+ elif property == "hide_indicator":
+ return !hide_indicator
+ return false
+func _property_get_revert(property: StringName) -> Variant:
+ if property in ["zone_point_pixel", "zone_range_by_pixel"]:
+ return 0
+ elif property in ["zone_horizontal", "zone_vertical"]:
+ return 0.5
+ elif property in ["zone_range_horizontal", "zone_range_vertical"]:
+ return 0.05
+ elif property == "hide_indicator":
+ return true
+ return null
+
+func _scrolled_horizontal(scroll_hor : float) -> void:
+ if !(check_mode & CHECK_MODE.HORIZONTAL) || !scroll: return
+
+ var overlapped := is_overlaped_with_activate_zone()
+ if overlapped:
+ if _last_overlapped != 1:
+ entered_zone.emit()
+ _last_overlapped = 1
+ _while_in_zone(zone_local_scroll().x)
+ elif _last_overlapped:
+ _last_overlapped = 0
+ _while_in_zone(1 if zone_local_scroll().x > 0.5 else 0)
+ exited_zone.emit()
+func _scrolled_vertical(scroll_ver : float) -> void:
+ if !(check_mode & CHECK_MODE.VERTICAL) || !scroll: return
+
+ var overlapped := is_overlaped_with_activate_zone()
+ if overlapped:
+ if _last_overlapped != 1:
+ entered_zone.emit()
+ _last_overlapped = 1
+ _while_in_zone(zone_local_scroll().y)
+ elif _last_overlapped:
+ _last_overlapped = 0
+ _while_in_zone(1 if zone_local_scroll().y > 0.5 else 0)
+ exited_zone.emit()
+
+func _get_zone_pos() -> Vector2:
+ var ret := Vector2(zone_horizontal, zone_vertical)
+ if zone_point_pixel == ZONE_EDITOR_DIMS.None:
+ ret *= scroll.size
+ elif zone_point_pixel == ZONE_EDITOR_DIMS.Vertical:
+ ret.x *= scroll.size.x
+ elif zone_point_pixel == ZONE_EDITOR_DIMS.Horizontal:
+ ret.y *= scroll.size.y
+ return ret
+func _get_zone_range() -> Vector2:
+ var ret := Vector2(zone_range_horizontal, zone_range_vertical) * 0.5
+ if zone_range_by_pixel == ZONE_EDITOR_DIMS.None:
+ ret *= scroll.size
+ elif zone_range_by_pixel == ZONE_EDITOR_DIMS.Vertical:
+ ret.x *= scroll.size.x
+ elif zone_range_by_pixel == ZONE_EDITOR_DIMS.Horizontal:
+ ret.y *= scroll.size.y
+ return ret
+
+
+
+# Public Functions
+
+## Returns [code]true[/code] if this node's mount is overlaping the zone area.[br]
+## This function's value is dependant on the value of [member check_mode].
+func is_overlaped_with_activate_zone() -> bool:
+ var item_pos_start := get_origin_offset()
+ var item_pos_end := item_pos_start + size
+
+ var zone_pos := _get_zone_pos()
+ var zone_range := _get_zone_range()
+ var zone_pos_start := zone_pos - zone_range
+ var zone_pos_end := zone_pos + zone_range
+
+ if (check_mode == CHECK_MODE.VERTICAL):
+ return (zone_pos_start.y <= item_pos_end.y && zone_pos_end.y >= item_pos_start.y)
+ elif (check_mode == CHECK_MODE.HORIZONTAL):
+ return (zone_pos_start.x <= item_pos_end.x && zone_pos_end.x >= item_pos_start.x)
+ elif (check_mode == CHECK_MODE.BOTH):
+ return (zone_pos_start.y <= item_pos_end.y && zone_pos_end.y >= item_pos_start.y) && (zone_pos_start.x <= item_pos_end.x && zone_pos_end.x >= item_pos_start.x)
+ return false
+
+## Gets the Rect2 associated to the zone.
+## [br][br]
+## Also see [method get_zone_global_rect], [member zone_horizontal],
+## [member zone_vertical], [member zone_range_horizontal], [member zone_range_vertical].
+func get_zone_rect() -> Rect2:
+ if check_mode == CHECK_MODE.NONE || !scroll: return Rect2()
+
+ var ret : Rect2 = scroll.get_rect()
+ var zone_pos := _get_zone_pos()
+ var zone_range := _get_zone_range()
+
+ if (check_mode == CHECK_MODE.VERTICAL):
+ var pos := zone_pos.y - zone_range.y
+ var max_pos := max(pos, 0)
+
+ ret.position.y = max_pos
+ ret.size.y = min(zone_range.y + zone_range.y + pos, scroll.size.y) - max_pos
+ elif (check_mode == CHECK_MODE.HORIZONTAL):
+ var pos := zone_pos.x - zone_range.x
+ var max_pos := max(pos, 0)
+
+ ret.position.x = max_pos
+ ret.size.x = min(zone_range.x + zone_range.x + pos, scroll.size.x) - max_pos
+ elif (check_mode == CHECK_MODE.BOTH):
+ var pos := zone_pos - zone_range
+ var max_pos := pos.max(Vector2.ZERO)
+
+ ret.position = max_pos
+ ret.size = scroll.size.min(zone_range + zone_range + pos) - max_pos
+ return ret
+## Gets the global Rect2 associated to the zone.
+## [br][br]
+## Also see [method get_zone_rect], [member zone_horizontal], [member zone_vertical],
+## [member zone_range_horizontal], [member zone_range_vertical].
+func get_zone_global_rect() -> Rect2:
+ if !scroll: return Rect2()
+
+ var zone_rect := get_zone_rect()
+ zone_rect.position += scroll.global_position
+ return zone_rect
+## Gets the percentage of this node's mount intersection with the zone.
+## [br][br]
+## Also see [method get_zone_rect], [method get_zone_global_rect].
+func in_zone_percent() -> float:
+ if !_mount: return 0
+ return (_mount.get_global_rect().intersection(get_zone_global_rect()).get_area()) / (_mount.size.x * _mount.size.y)
+## The local scroll within the zone zone. Returns [code]0[/code] if this node's
+## mount is not inside the zone area.
+## [br][br]
+## Also see [method get_zone_rect], [method get_zone_global_rect].
+func zone_local_scroll() -> Vector2:
+ if !_mount: return Vector2.ZERO
+
+ var zone := get_zone_global_rect()
+ var mount := _mount.get_global_rect()
+
+ if zone.size + mount.size == Vector2.ZERO: return Vector2.ZERO
+ return Vector2.ONE + ((zone.position - _mount.global_position - mount.size) / (zone.size + mount.size)).clampf(-1, 0)
+
+
+
+# Virtual Functions
+
+## A virtual function that is called while this node is in the zone area. Is called
+## after each scroll of [member scroll].
+## [br][br]
+## Paramter [param _scroll] is the local scroll within the zone.
+func _while_in_zone(scroll : float) -> void: pass
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/mount/AnimatableMount.gd b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/mount/AnimatableMount.gd
new file mode 100644
index 0000000..a7a839a
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/mount/AnimatableMount.gd
@@ -0,0 +1,50 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name AnimatableMount extends Control
+## Used as a mount for size consistency between children [AnimatableControl] nodes.
+
+## Emits before children are sorted
+signal pre_sort_children
+## Emits after children have been sorted
+signal sort_children
+
+var _min_size : Vector2
+
+func _get_configuration_warnings() -> PackedStringArray:
+ for child : Node in get_children():
+ if child is AnimatableControl: return []
+ return ["This node has no 'AnimatableControl' nodes as children"]
+func _get_minimum_size() -> Vector2:
+ if clip_children: return Vector2.ZERO
+ _update_children_minimum_size()
+ return _min_size
+func _update_children_minimum_size() -> void:
+ _min_size = Vector2.ZERO
+
+ # Ensures size is the same as the largest size (of both axis) of children
+ for child : Node in get_children():
+ if child is AnimatableControl:
+ if child.size_mode & child.SIZE_MODE.MIN:
+ _min_size = _min_size.max(child.get_combined_minimum_size())
+
+func _init() -> void:
+ resized.connect(_sort_children, CONNECT_DEFERRED)
+ size_flags_changed.connect(_sort_children, CONNECT_DEFERRED)
+func _sort_children() -> void:
+ pre_sort_children.emit()
+ for child : Node in get_children():
+ if child is AnimatableControl:
+ child._bound_size()
+ sort_children.emit()
+
+## A virtual helper function that should be used when creating your own mounts.[br]
+## Is called upon an [AnimatableControl] being added as a child.
+func _on_mount(control : AnimatableControl) -> void: pass
+## A virtual helper function that should be used when creating your own mounts.[br]
+## Is called upon an [AnimatableControl] being removed as a child.
+func _on_unmount(control : AnimatableControl) -> void: pass
+## A helper function that should be used when creating your own mounts.[br]
+## Returns size of this mount.
+func get_relative_size(control : AnimatableControl) -> Vector2: return size
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/mount/AnimatableTransformationMount.gd b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/mount/AnimatableTransformationMount.gd
new file mode 100644
index 0000000..b305fe0
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/AnimatableControl/mount/AnimatableTransformationMount.gd
@@ -0,0 +1,138 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name AnimatableTransformationMount extends AnimatableMount
+## An [AnimatableMount] that adjusts for it's children 2D transformations: Rotation, Position, and Scale.
+
+## If [code]true[/code] this node will adjust it's size to fit its children's scales.
+@export var adjust_scale : bool:
+ set(val):
+ if val != adjust_scale:
+ adjust_scale = val
+ update_minimum_size()
+## If [code]true[/code] this node will adjust it's size to fit its children's rotations.[br]
+## [b]NOTE[/b]: A large [member pivot_offset] can cause floating point precision issues.
+@export var adjust_rotate : bool:
+ set(val):
+ if val != adjust_rotate:
+ adjust_rotate = val
+ update_minimum_size()
+## If [code]true[/code] this node adjust its children's positions inside it's size.
+@export var adjust_position : bool:
+ set(val):
+ if val != adjust_position:
+ adjust_position = val
+ update_minimum_size()
+
+var _child_min_size : Vector2
+
+func _update_children_minimum_size() -> void:
+ var _old_min_size := _min_size
+ _min_size = Vector2.ZERO
+ _child_min_size = Vector2.ZERO
+
+ var children_info: Array[Array] = []
+
+ for child : AnimatableControl in get_children():
+ if child:
+ var child_size : Vector2
+ var child_offset : Vector2
+
+ # Scale child size, if needed
+ if adjust_scale:
+ child_size = child.get_combined_minimum_size() * child.scale
+ else:
+ child_size = child.get_combined_minimum_size()
+ _child_min_size = _child_min_size.max(child_size)
+
+ # Rotates child size, if needed.
+ if adjust_rotate:
+ child_offset = child.pivot_offset
+ if adjust_scale: child_offset *= child.scale
+
+ # Gets the bounding box of the rect, when rotated around a pivot
+ var bb_rect := _get_rotated_rect_bounding_box(
+ Rect2(Vector2.ZERO, child_size),
+ child_offset,
+ child.rotation
+ )
+
+ child_size = bb_rect.size
+ child_offset = bb_rect.position
+
+ children_info.append([child, child_size, child_offset])
+ _min_size = _min_size.max(child_size)
+
+ # Leaves position adjusts after so size, after calculating min-size of children, can be used
+ if adjust_position:
+ if _old_min_size != _min_size:
+ # If in Container, and min_size changed, update after the container as resorted children
+ if get_parent_control() is Container:
+ get_parent_control().sort_children.connect(_adjust_children_positions.bind(children_info), CONNECT_ONE_SHOT)
+ else:
+ # Otherwise, if min_size changed still, update after minimum_size_changed changed
+ minimum_size_changed.connect(_adjust_children_positions.bind(children_info), CONNECT_ONE_SHOT | CONNECT_DEFERRED)
+ update_minimum_size()
+ else:
+ # If min_size did not change, deffer children position changes
+ call_deferred("_adjust_children_positions", children_info)
+ elif _old_min_size != _min_size:
+ update_minimum_size()
+func _adjust_children_positions(children_info: Array[Array]) -> void:
+ for child_info : Array in children_info:
+ var child : AnimatableControl = child_info[0]
+ var child_size : Vector2 = child_info[1]
+ var child_offset : Vector2 = child_info[2]
+
+ var piv_offset : Vector2
+ # Rotates pivot, if needed
+ if adjust_rotate:
+ piv_offset = -child.pivot_offset.rotated(rotation)
+ else:
+ piv_offset = -child.pivot_offset
+ # If adjusts the pivot by scale, if needed
+ if adjust_scale:
+ piv_offset *= (child.scale - Vector2.ONE)
+
+ # Not clamp, because min should have priorty
+ # max_size_adjusted_for_child_size = size - child_size - child_offset - piv_offset
+ # min_size_adjusted_for_child_size = -piv_offset - child_offset
+ var new_pos := child.position.min(size - child_size - child_offset - piv_offset).max(-piv_offset - child_offset)
+
+ # Changes position, if needed
+ if child.position != new_pos: child.position = new_pos
+
+func _get_rotated_rect_bounding_box(rect : Rect2, pivot : Vector2, angle : float) -> Rect2:
+ # Base Values
+ var pos := rect.position
+ var sze := rect.size
+ var trig := Vector2(cos(angle), sin(angle))
+
+ # Simplified equation for centerPoint - bb_size*0.5
+ var bb_pos := Vector2(
+ (sze.x * (trig.x - abs(trig.x)) - sze.y * (trig.y + abs(trig.y))) * 0.5 + pivot.x * (1 - trig.x) + trig.y * pivot.y + pos.x,
+ (sze.x * (trig.y - abs(trig.y)) + sze.y * (trig.x - abs(trig.x))) * 0.5 + pivot.y * (1 - trig.x) - trig.y * pivot.x + pos.y
+ )
+ trig = trig.abs()
+ ## Finds the fix of the bounding box of the rotated rectangle
+ var bb_size := Vector2(
+ sze.x * trig.x + sze.y * trig.y,
+ sze.x * trig.y + sze.y * trig.x
+ )
+
+ return Rect2(bb_pos, bb_size)
+
+func _init() -> void:
+ size_flags_changed.connect(update_minimum_size, CONNECT_DEFERRED)
+
+func _on_mount(control : AnimatableControl) -> void:
+ control.transformation_changed.connect(update_minimum_size, CONNECT_DEFERRED)
+func _on_unmount(control : AnimatableControl) -> void:
+ control.transformation_changed.disconnect(update_minimum_size)
+
+## Returns the adjusted size of this mount.
+func get_relative_size(control : AnimatableControl) -> Vector2:
+ if adjust_scale:
+ return _child_min_size / control.scale
+ return _child_min_size
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/AnimatedSwitch.gd b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/AnimatedSwitch.gd
new file mode 100644
index 0000000..c07d40a
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/AnimatedSwitch.gd
@@ -0,0 +1,307 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name AnimatedSwitch extends BaseButton
+## Animated verison of a switch button.
+
+
+
+@export_group("Redirect")
+## If [code]true[/code], the switch will be displayed vertical.
+@export var vertical : bool = false:
+ set(val):
+ if vertical != val:
+ vertical = val
+ force_state(button_pressed)
+## If [code]true[/code], the switch will be flipped.
+@export var flip : bool = false:
+ set(val):
+ if flip != val:
+ flip = val
+ force_state(button_pressed)
+
+@export_group("Size")
+## The size of the switch's base.
+@export var switch_size : Vector2 = Vector2(100, 50):
+ set(val):
+ if switch_size != val:
+ switch_size = val
+ _handle_resize()
+ update_minimum_size()
+## The size of the switch's knob.
+@export var knob_size : Vector2 = Vector2(40, 40):
+ set(val):
+ if knob_size != val:
+ knob_size = val
+ _handle_resize()
+ update_minimum_size()
+## The base offset of the knob from it's set position.
+@export var knob_offset : Vector2 = Vector2.ZERO:
+ set(val):
+ if knob_offset != val:
+ knob_offset = val
+ _handle_resize()
+ update_minimum_size()
+## An amount of pixels the knob will extend past the switch's base.
+## [br][br]
+## Also see [member switch_size].
+@export var knob_overextend : float = 10:
+ set(val):
+ if knob_overextend != val:
+ knob_overextend = val
+ _handle_resize()
+ update_minimum_size()
+
+@export_group("Display")
+## The style of the switch.
+@export var switch_bg : StyleBox:
+ set(val):
+ if switch_bg != val:
+ switch_bg = val
+
+ if knob_bg:
+ _switch.add_theme_stylebox_override("panel", switch_bg)
+ else:
+ _switch.remove_theme_stylebox_override("panel")
+## The style of the knob.
+@export var knob_bg : StyleBox:
+ set(val):
+ if knob_bg != val:
+ knob_bg = val
+
+ if knob_bg:
+ _knob.add_theme_stylebox_override("panel", knob_bg)
+ else:
+ _knob.remove_theme_stylebox_override("panel")
+
+@export_group("Colors")
+@export_subgroup("Switch")
+## The color of the switch's base when unfocused.
+@export var switch_bg_normal : Color:
+ set(val):
+ if switch_bg_normal != val:
+ switch_bg_normal = val
+
+ _kill_color_animation()
+ _animate_color(false)
+## The color of the switch's base when focused.
+@export var switch_bg_focus : Color:
+ set(val):
+ if switch_bg_focus != val:
+ switch_bg_focus = val
+
+ _kill_color_animation()
+ _animate_color(false)
+## The color of the switch's base when disabled.
+@export var switch_bg_disabled : Color:
+ set(val):
+ if switch_bg_disabled != val:
+ switch_bg_disabled = val
+
+ _kill_color_animation()
+ _animate_color(false)
+
+@export_subgroup("Knob")
+## The color of the switch's knob when unfocused.
+@export var knob_bg_normal : Color:
+ set(val):
+ if knob_bg_normal != val:
+ knob_bg_normal = val
+
+ _kill_color_animation()
+ _animate_color(false)
+## The color of the switch's knob when focused.
+@export var knob_bg_focus : Color:
+ set(val):
+ if knob_bg_focus != val:
+ knob_bg_focus = val
+
+ _kill_color_animation()
+ _animate_color(false)
+## The color of the switch's knob when disabled.
+@export var knob_bg_disabled : Color:
+ set(val):
+ if knob_bg_disabled != val:
+ knob_bg_disabled = val
+
+ _kill_color_animation()
+ _animate_color(false)
+
+
+@export_group("Animation Properties")
+@export_subgroup("Main")
+## The ease of the knob's movement across the base.
+@export var main_ease : Tween.EaseType
+## The transition of the knob's movement across the base.
+@export var main_transition : Tween.TransitionType
+## The duration of the knob's movement across the base.
+@export_range(0.001, 0.5, 0.001, "or_greater") var main_duration : float = 0.15
+
+@export_subgroup("Knob Color")
+## If [code]true[/code], then the knob will change color according to this node's state.
+@export var animate_knob_color : bool = true
+## The ease of the knob's color change.
+@export var knob_color_ease : Tween.EaseType
+## The transition of the knob's color change.
+@export var knob_color_transition : Tween.TransitionType
+## The duration of the knob's color change.
+@export_range(0.001, 0.5, 0.001, "or_greater") var knob_color_duration : float = 0.1
+
+@export_subgroup("Switch Color")
+## If [code]true[/code], then the base will change color according to this node's state.
+@export var animate_switch_color : bool = true
+## The ease of the base's color change.
+@export var switch_color_ease : Tween.EaseType
+## The transition of the base's color change.
+@export var switch_color_transition : Tween.TransitionType
+## The duration of the base's color change.
+@export_range(0.001, 0.5, 0.001, "or_greater") var switch_color_duration : float = 0.1
+
+
+
+var _knob : Panel
+var _switch : Panel
+
+var _main_animate_tween : Tween
+var _knob_color_animate_tween : Tween
+var _switch_color_animate_tween : Tween
+
+
+
+## Instantly changes the switch's state without animation.
+func force_state(knob_state : bool) -> void:
+ _handle_animations(false, knob_state)
+## Changes the switch's state with animation.
+func toggle_state(knob_state : bool) -> void:
+ _handle_animations(true, knob_state)
+
+
+## Gets the current knob color.
+func get_knob_color() -> Color:
+ if disabled:
+ return knob_bg_disabled
+ return knob_bg_focus if button_pressed else knob_bg_normal
+## Gets the current switch base color.
+func get_switch_color() -> Color:
+ if disabled:
+ return switch_bg_disabled
+ return switch_bg_focus if button_pressed else switch_bg_normal
+
+
+
+func _kill_main_animation() -> void:
+ if _main_animate_tween && _main_animate_tween.is_running():
+ _main_animate_tween.kill()
+func _kill_color_animation() -> void:
+ if _knob_color_animate_tween && _knob_color_animate_tween.is_running():
+ _knob_color_animate_tween.kill()
+ if _switch_color_animate_tween && _switch_color_animate_tween.is_running():
+ _main_animate_tween.kill()
+
+
+func _handle_animations(animate : bool, knob_state : bool) -> void:
+ _kill_main_animation()
+ _kill_color_animation()
+ button_pressed = knob_state
+
+ if animate:
+ _animate_knob(knob_state)
+ _animate_color(true)
+ return
+
+ _position_knob(float(knob_state))
+ _animate_color(false)
+
+func _animate_knob(knob_state : bool) -> void:
+ _main_animate_tween = create_tween()
+ _main_animate_tween.set_ease(main_ease)
+ _main_animate_tween.set_trans(main_transition)
+ _main_animate_tween.tween_method(
+ _position_knob,
+ 1.0 - float(knob_state),
+ 0.0 + float(knob_state),
+ main_duration
+ )
+func _position_knob(delta : float) -> void:
+ if flip:
+ delta = 1.0 - delta
+
+ var offset : Vector2
+ var delta_v : Vector2
+ if vertical:
+ offset = Vector2(0.0, knob_overextend)
+ delta_v = Vector2(0.5, delta)
+ else:
+ offset = Vector2(knob_overextend, 0.0)
+ delta_v = Vector2(delta, 0.5)
+
+ _knob.position = (
+ (switch_size - knob_size + offset + offset) * delta_v # Size
+ + (_switch.position - offset) # Position
+ + knob_offset # Offset
+ )
+func _animate_color(animate : bool = false) -> void:
+ if animate && animate_knob_color:
+ _knob_color_animate_tween = create_tween()
+ _knob_color_animate_tween.set_ease(knob_color_ease)
+ _knob_color_animate_tween.set_trans(knob_color_transition)
+ _knob_color_animate_tween.tween_property(
+ _knob,
+ "self_modulate",
+ get_knob_color(),
+ knob_color_duration
+ )
+ else:
+ _knob.self_modulate = get_knob_color()
+
+ if animate && animate_switch_color:
+ _switch_color_animate_tween = create_tween()
+ _switch_color_animate_tween.set_ease(switch_color_ease)
+ _switch_color_animate_tween.set_trans(switch_color_transition)
+ _switch_color_animate_tween.tween_property(
+ _switch,
+ "self_modulate",
+ get_switch_color(),
+ switch_color_duration
+ )
+ else:
+ _switch.self_modulate = get_switch_color()
+
+
+
+func _init() -> void:
+ toggle_mode = true
+ _switch = Panel.new()
+ _knob = Panel.new()
+
+ _switch.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ _knob.mouse_filter = Control.MOUSE_FILTER_IGNORE
+
+ add_child(_switch)
+ add_child(_knob)
+
+ resized.connect(_handle_resize)
+ toggled.connect(toggle_state)
+ _handle_resize()
+
+ _animate_color(false)
+func _handle_resize() -> void:
+ _switch.position = (size - switch_size) * 0.5
+ _switch.size = switch_size
+
+ _knob.size = knob_size
+ force_state(button_pressed)
+
+func _get_minimum_size() -> Vector2:
+ return (knob_size + (knob_offset.abs() * 0.5)).max(switch_size + Vector2(max(0, knob_overextend) * 2, 0))
+func _validate_property(property: Dictionary) -> void:
+ if property.name == "toggle_mode":
+ property.usage &= ~PROPERTY_USAGE_EDITOR
+
+func _set(property: StringName, value: Variant) -> bool:
+ if property == "disabled":
+ disabled = value
+ _animate_color()
+ return true
+ return false
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/HoldButton.gd b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/HoldButton.gd
new file mode 100644
index 0000000..214ed0d
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/HoldButton.gd
@@ -0,0 +1,178 @@
+@tool
+class_name HoldButton extends Control
+## A [Control] node used for hold buttons.
+
+
+
+## Emits the state of the button as it is pressed.
+## [br][br]
+## Also see [member toggle_mode] and [signal button_state].
+signal pressed_state(val : bool)
+## Emits the state of the button as it is released.
+## [br][br]
+## Also see [member toggle_mode] and [signal button_state].
+signal release_state(val : bool)
+## Emits the state of the button as it is pressed or released.
+## [br][br]
+## Also see [signal pressed_state] and [signal release_state].
+signal button_state(val : bool)
+
+
+## Emits when button is released with all vaild conditions.
+## [br][br]
+## Also see [member release_when_outside] and [member cancel_when_outside].
+signal press_vaild
+## Emits when button is released without all vaild conditions.
+## [br][br]
+## Also see [member release_when_outside] and [member cancel_when_outside].
+signal press_invaild
+
+
+## Emits when press starts.
+signal press_start
+## Emits when press ends.
+signal press_end
+
+
+
+## If [code]true[/code], the button's state is pressed. Means the button is pressed down
+## or toggled (if [member toggle_mode] is active). Only works if [member toggle_mode] is
+## [code]false[/code].
+@export var button_pressed : bool
+## If [code]true[/code], the button is in [member toggle_mode]. Makes the button
+## flip state between pressed and unpressed each time its area is clicked.
+@export var toggle_mode : bool:
+ set(val):
+ if toggle_mode != val:
+ toggle_mode = val
+ if !val:
+ button_pressed = false
+
+ notify_property_list_changed()
+## If [code]true[/code], then this node does not accept input.
+@export var disabled : bool:
+ set(val):
+ if disabled != val:
+ disabled = val
+ _bounds_check.disabled = val
+ _distance_check.disabled = val
+
+
+@export_group("Release At")
+## If [code]true[/code], the button's held state is released if input moves outside of
+## bounds.
+@export var release_when_outside : bool = true:
+ set(val):
+ release_when_outside = val
+ _bounds_check.release_when_outside = val
+## If [code]true[/code], the button's held state is released and all checking is stopped
+## if input moves outside of bounds.
+@export var cancel_when_outside : bool = true:
+ set(val):
+ cancel_when_outside = val
+ _bounds_check.cancel_when_outside = val
+
+@export_group("Release On Drag")
+## The current check mode.
+##
+## Also see [enum CHECK_MODE].
+@export var mode : DistanceCheck.CHECK_MODE = DistanceCheck.CHECK_MODE.BOTH:
+ set(val):
+ mode = val
+ _distance_check.mode = val
+## The max pixels difference, between the start and current position, that can be tolerated.
+@export var distance : float = 30:
+ set(val):
+ distance = val
+ _distance_check.distance = val
+
+
+var _bounds_check : BoundsCheck
+var _distance_check : DistanceCheck
+
+
+
+## Forcibly stops this node's check.
+func force_release() -> void:
+ _bounds_check.force_release()
+ _distance_check.force_release()
+
+ _on_end_invaild()
+## Returns if mouse or touch is being held (mouse or touch outside of limit without being released).
+## [br][br]
+## Also see [method force_release].
+func is_held() -> bool:
+ return _bounds_check.is_checking()
+
+
+
+func _init() -> void:
+ _distance_check = DistanceCheck.new()
+ _distance_check.name = "distance_check"
+ _distance_check.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ add_child(_distance_check)
+
+ _distance_check.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ _distance_check.cancel_when_outside = true
+ _distance_check.disabled = disabled
+ _distance_check.mode = mode
+ _distance_check.distance = distance
+
+ _distance_check.pos_exceeded.connect(force_release)
+
+
+ _bounds_check = BoundsCheck.new()
+ _bounds_check.name = "bounds_check"
+ _bounds_check.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ add_child(_bounds_check)
+
+ _bounds_check.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ _bounds_check.disabled = disabled
+ _bounds_check.release_when_outside = release_when_outside
+ _bounds_check.cancel_when_outside = cancel_when_outside
+
+ _bounds_check.end_vaild.connect(_on_end_vaild)
+ _bounds_check.end_invaild.connect(_on_end_invaild)
+ _bounds_check.start_check.connect(_on_start_check)
+ _bounds_check.end_check.connect(_on_end_check)
+func _validate_property(property: Dictionary) -> void:
+ if property.name == "button_pressed":
+ if !toggle_mode:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+
+
+func _gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion || event is InputEventScreenDrag || event is InputEventMouseButton || event is InputEventScreenTouch:
+ event.position += global_position
+ _bounds_check._gui_input(event)
+ _distance_check._gui_input(event)
+
+
+func _on_start_check() -> void:
+ press_start.emit()
+
+ if toggle_mode:
+ button_pressed = !button_pressed
+ else:
+ button_pressed = true
+ button_state.emit(button_pressed)
+ pressed_state.emit(button_pressed)
+func _on_end_check() -> void:
+ _distance_check.force_release()
+ press_end.emit()
+func _on_end_vaild() -> void:
+ press_vaild.emit()
+
+ if !toggle_mode:
+ button_pressed = false
+ button_state.emit(button_pressed)
+ release_state.emit(button_pressed)
+func _on_end_invaild() -> void:
+ press_invaild.emit()
+
+ if toggle_mode:
+ button_pressed = !button_pressed
+ else:
+ button_pressed = false
+ button_state.emit(button_pressed)
+ release_state.emit(button_pressed)
diff --git a/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/MotionCheck/BoundsCheck.gd b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/MotionCheck/BoundsCheck.gd
new file mode 100644
index 0000000..181a887
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/MotionCheck/BoundsCheck.gd
@@ -0,0 +1,9 @@
+@tool
+class_name BoundsCheck extends MotionCheck
+## A [Control] node used to check if a mouse or touch moved outside this node's bounds after
+## a vaild press inside.
+
+
+
+func _pos_check(pos : Vector2) -> bool:
+ return get_global_rect().has_point(pos)
diff --git a/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/MotionCheck/DistanceCheck.gd b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/MotionCheck/DistanceCheck.gd
new file mode 100644
index 0000000..317b420
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/MotionCheck/DistanceCheck.gd
@@ -0,0 +1,46 @@
+@tool
+class_name DistanceCheck extends MotionCheck
+## A [Control] node used to check if a mouse or touch moved a distance away from where
+## the user originally pressed.
+
+
+
+## How this node will determine the distance between the starting and current location
+## of mouse and touch movement.
+enum CHECK_MODE {
+ NONE = 0b000, ## No action.
+ HORIZONTAL = 0b001, ## Only checks the horizontal difference.
+ VERTICAL = 0b010, ## Only checks the vertical difference.
+ BOTH = 0b011, ## Only checks orthogonal difference.
+ DISTANCE = 0b100 ## Uses the Pythagorean Theorem.
+}
+
+
+## The current check mode.
+##
+## Also see [enum CHECK_MODE].
+@export var mode : CHECK_MODE
+## The max pixels difference, between the start and current position, that can be tolerated.
+@export var distance : float = 30
+
+
+
+var _prev_pos : Vector2
+
+
+func _pos_check(pos : Vector2) -> bool:
+ if mode & CHECK_MODE.DISTANCE:
+ if _prev_pos.distance_squared_to(pos) > distance * distance:
+ return false
+ return true
+
+ var diff := (_prev_pos - pos).abs()
+ if mode & CHECK_MODE.HORIZONTAL:
+ if diff.x > distance:
+ return false
+ if mode & CHECK_MODE.VERTICAL:
+ if diff.y > distance:
+ return false
+ return true
+func _on_check_start(event: InputEvent) -> void:
+ _prev_pos = event.position
diff --git a/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/MotionCheck/MotionCheck.gd b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/MotionCheck/MotionCheck.gd
new file mode 100644
index 0000000..54e2dcb
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Buttons/Base/MotionCheck/MotionCheck.gd
@@ -0,0 +1,165 @@
+@tool
+class_name MotionCheck extends Control
+## Checks for motion of the mouse or touch input after a press.
+
+## Emited when check has started (mouse or touch is pressed).
+signal start_check
+## Emited when check has ended (mouse or touch is released or distance limit has exceeded).
+signal end_check
+
+## Emited when mouse or touch is released within the distence limit.
+signal end_vaild
+## Emited when check has ended without mouse or touch being released within the distence limit.
+## [br][br]
+## Also see [member cancel_when_outside] and [method _pos_check].
+signal end_invaild
+
+## Emited when mouse or touch is moved outside the distence limit.
+## [br][br]
+## Also see [member cancel_when_outside] and [method _pos_check].
+signal pos_exceeded
+## Emited when the current held state changes.
+signal held_state(state : bool)
+
+
+## If [code]true[/code], the button's held state is released if input moves outside of
+## bounds.
+## [br][br]
+## Also see [member cancel_when_outside] and [method _pos_check].
+@export var release_when_outside : bool = false:
+ set(val):
+ if release_when_outside != val:
+ release_when_outside = val
+## If [code]true[/code], then the check will end when mouse or touch is moved outside the distence limit.
+## [br][br]
+## Also see [member release_when_outside] and [method _pos_check].
+@export var cancel_when_outside : bool = false:
+ set(val):
+ if cancel_when_outside != val:
+ cancel_when_outside = val
+## If [code]true[/code], then this node does not accept input.
+@export var disabled : bool:
+ set(val):
+ if disabled != val:
+ disabled = val
+ if val:
+ force_release()
+
+
+var _checking : bool = false
+var _holding : bool = false
+
+
+# Public Methods
+
+## Forcibly stops this node's check.
+func force_release() -> void:
+ if _holding:
+ held_state.emit(false)
+ _holding = false
+ if _checking:
+ _invaild_end()
+ _checking = false
+## Returns if this node is currently checking a mouse or touch press.
+func is_checking() -> bool: return _checking
+## Returns if mouse or touch is being held (mouse or touch outside of limit without being released).
+## [br][br]
+## Also see [method force_release].
+func is_held() -> bool: return _holding
+
+
+# Virtual Methods
+
+## A virtual method that should be overloaded. Returns [code]true[/code] if [param pos] is
+## within the distance limit. [code]false[/code] otherwise.
+func _pos_check(pos : Vector2) -> bool:
+ return false
+## A virtual method that should be overloaded. This is called when an input starts a
+## check.
+func _on_check_start(event: InputEvent) -> void:
+ pass
+## A virtual method that should be overloaded. This is called when an input releases a
+## check.
+func _on_check_release(event: InputEvent) -> void:
+ pass
+## A virtual method that should be overloaded. This is called when an input exceeds a
+## check.
+## [br][br]
+## Also see [method _pos_check].
+func _on_check_exceeded(event: InputEvent) -> void:
+ pass
+
+
+# Private Methods
+
+func _init() -> void:
+ mouse_filter = MOUSE_FILTER_PASS
+ tree_exiting.connect(force_release)
+func _property_can_revert(property: StringName) -> bool:
+ if property == "mouse_filter":
+ return mouse_filter == MOUSE_FILTER_PASS
+ return false
+func _property_get_revert(property: StringName) -> Variant:
+ if property == "mouse_filter":
+ return MOUSE_FILTER_PASS
+ return null
+
+
+func _gui_input(event: InputEvent) -> void:
+ if disabled: return
+
+ if event is InputEventMouseButton || event is InputEventScreenTouch:
+ if event.pressed:
+ if !_checking && get_global_rect().has_point(event.position):
+ _on_check_start(event)
+
+ _checking = true
+
+ held_state.emit(true)
+ start_check.emit()
+ elif _checking:
+ _on_check_release(event)
+
+ _checking = false
+ if !_holding:
+ held_state.emit(false)
+ else:
+ _holding = false
+
+ if _pos_check(event.position):
+ _vaild_end()
+ else:
+ _invaild_end()
+ if mouse_filter == MOUSE_FILTER_STOP: accept_event()
+
+ if _checking:
+ if event is InputEventMouseMotion || event is InputEventScreenDrag:
+ if _holding:
+ if _pos_check(event.position):
+ held_state.emit(true)
+ _checking = true
+ _holding = false
+ return
+
+ if !_pos_check(event.position):
+ pos_exceeded.emit()
+ if release_when_outside:
+ held_state.emit(false)
+ _holding = true
+
+ if cancel_when_outside:
+ _on_check_exceeded(event)
+ _holding = false
+ _checking = false
+
+ _invaild_end()
+ return
+
+ if mouse_filter == MOUSE_FILTER_STOP: accept_event()
+
+func _invaild_end() -> void:
+ end_invaild.emit()
+ end_check.emit()
+func _vaild_end() -> void:
+ end_vaild.emit()
+ end_check.emit()
diff --git a/godot/addons/FreeControl/src/CustomClasses/Buttons/Complex/ModulateTransitionButton.gd b/godot/addons/FreeControl/src/CustomClasses/Buttons/Complex/ModulateTransitionButton.gd
new file mode 100644
index 0000000..749119d
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Buttons/Complex/ModulateTransitionButton.gd
@@ -0,0 +1,151 @@
+@tool
+class_name ModulateTransitionButton extends ModulateTransitionContainer
+## A button that inherts from [ModulateTransitionContainer] and uses [HoldButton] as
+## input.
+
+
+
+## Emits the state of the button as it is released.
+signal release_state(toggle : bool)
+
+## Emits when button is released with all vaild conditions.
+signal press_vaild
+
+## Emits when press starts.
+signal press_start
+## Emits when press ends.
+signal press_end
+
+
+
+@export_group("Toggleable")
+## If [code]true[/code], the button's state is pressed. Means the button is pressed down
+## or toggled (if [member toggle_mode] is active). Only works if [member toggle_mode] is
+## [code]false[/code].
+@export var button_pressed : bool:
+ set(val):
+ if button_pressed != val:
+ button_pressed = val
+ _set_button_color(val)
+## If [code]true[/code], the button is in [member toggle_mode]. Makes the button
+## flip state between pressed and unpressed each time its area is clicked.
+@export var toggle_mode : bool:
+ set(val):
+ if toggle_mode != val:
+ toggle_mode = val
+ button_pressed = false
+ notify_property_list_changed()
+
+ if _button: _button.toggle_mode = val
+## If [code]true[/code], then this node does not accept input.
+@export var disabled : bool:
+ set = _set_disabled
+
+@export_group("Alpha")
+## The color to modulate to when this node is unfocused.
+@export var normal_color : Color = Color(1.0, 1.0, 1.0, 1.0):
+ set(val):
+ if normal_color != val:
+ normal_color = val
+ if is_node_ready(): colors[0] = val
+ force_color(focused_color)
+## The color to modulate to when this node is focused.
+@export var focus_color : Color = Color(1.0, 1.0, 1.0, 0.75):
+ set(val):
+ if focus_color != val:
+ focus_color = val
+ if is_node_ready(): colors[1] = val
+ force_color(focused_color)
+## The color to modulate to when this node is disabled.
+@export var disabled_color : Color = Color(1.0, 1.0, 1.0, 0.5):
+ set(val):
+ if disabled_color != val:
+ disabled_color = val
+ if is_node_ready(): colors[2] = val
+ force_color(focused_color)
+
+
+var _button : HoldButton
+
+
+
+## Forcibly stops this node's check.
+func force_release() -> void:
+ if _button: _button.force_release()
+## Returns if mouse or touch is being held (mouse or touch outside of limit without being released).
+
+func is_held() -> bool:
+ return _button && _button.is_held()
+
+
+
+func _init() -> void:
+ super()
+
+ _button = HoldButton.new()
+ add_child(_button)
+ _button.move_to_front()
+
+ if !Engine.is_editor_hint():
+ child_order_changed.connect(_button.move_to_front, CONNECT_DEFERRED)
+
+ _button.button_state.connect(_set_button_color)
+ _button.release_state.connect(_emit_vaild_release)
+
+ _button.press_start.connect(press_start.emit)
+ _button.press_end.connect(press_end.emit)
+ _button.press_vaild.connect(press_vaild.emit)
+
+ _button.mouse_filter = mouse_filter
+ _button.mouse_force_pass_scroll_events = mouse_force_pass_scroll_events
+ _button.mouse_default_cursor_shape = mouse_default_cursor_shape
+
+ _button.toggle_mode = toggle_mode
+ _button.button_pressed = button_pressed
+ _button.disabled = disabled
+func _ready() -> void:
+ colors = [normal_color, focus_color, disabled_color]
+ force_color(2 if disabled else (1 if button_pressed else 0))
+
+func _validate_property(property: Dictionary) -> void:
+ match property.name:
+ "pressed":
+ if !toggle_mode:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ "focused_alpha", "alphas":
+ property.usage &= ~PROPERTY_USAGE_EDITOR
+
+func _set(property: StringName, value: Variant) -> bool:
+ if _button:
+ match property:
+ "mouse_filter":
+ _button.mouse_filter = value
+ "mouse_force_pass_scroll_events":
+ _button.mouse_force_pass_scroll_events = value
+ "mouse_default_cursor_shape":
+ _button.mouse_default_cursor_shape = value
+ return false
+func _get(property: StringName) -> Variant:
+ if _button:
+ match property:
+ "mouse_filter":
+ return _button.mouse_filter
+ "mouse_force_pass_scroll_events":
+ return _button.mouse_force_pass_scroll_events
+ "mouse_default_cursor_shape":
+ return _button.mouse_default_cursor_shape
+ return null
+
+func _set_disabled(val : bool) -> void:
+ disabled = val
+
+ _set_button_color(button_pressed)
+ if _button:
+ _button.disabled = disabled
+func _set_button_color(val : bool) -> void:
+ if disabled: set_color(2)
+ else: set_color(int(val))
+
+func _emit_vaild_release(release : bool) -> void:
+ _set_button_color(release)
+ release_state.emit(release)
diff --git a/godot/addons/FreeControl/src/CustomClasses/Buttons/Complex/StyleTransitionButton.gd b/godot/addons/FreeControl/src/CustomClasses/Buttons/Complex/StyleTransitionButton.gd
new file mode 100644
index 0000000..ce31ae1
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Buttons/Complex/StyleTransitionButton.gd
@@ -0,0 +1,158 @@
+@tool
+class_name StyleTransitionButton extends StyleTransitionContainer
+## A button that inherts from [StyleTransitionContainer] and uses [HoldButton] as
+## input.
+
+
+
+## Emits the state of the button as it is released.
+signal release_state(toggle : bool)
+
+## Emits when button is released with all vaild conditions.
+signal press_vaild
+
+## Emits when press starts.
+signal press_start
+## Emits when press ends.
+signal press_end
+
+
+
+@export_group("Toggleable")
+## If [code]true[/code], the button's state is pressed. Means the button is pressed down
+## or toggled (if [member toggle_mode] is active). Only works if [member toggle_mode] is
+## [code]false[/code].
+@export var button_pressed : bool:
+ set(val):
+ if button_pressed != val:
+ button_pressed = val
+ _set_button_color(val)
+## If [code]true[/code], the button is in [member toggle_mode]. Makes the button
+## flip state between pressed and unpressed each time its area is clicked.
+@export var toggle_mode : bool:
+ set(val):
+ if toggle_mode != val:
+ toggle_mode = val
+ button_pressed = false
+ notify_property_list_changed()
+
+ if _button: _button.toggle_mode = val
+var _disabled : bool:
+ set = _set_disabled
+## If [code]true[/code], then this node does not accept input.
+@export var disabled : bool:
+ set(val):
+ if disabled != val:
+ disabled = val
+ _set_disabled(val)
+
+@export_group("Colors")
+## The color to modulate to when this node is unfocused.
+@export var normal_color : Color = Color(0.525, 0.329, 0.808):
+ set(val):
+ if normal_color != val:
+ normal_color = val
+ if is_node_ready(): colors[0] = val
+ force_color(focused_color)
+## The color to modulate to when this node is focused.
+@export var focus_color : Color = Color(0.611, 0.441, 0.886):
+ set(val):
+ if focus_color != val:
+ focus_color = val
+ if is_node_ready(): colors[1] = val
+ force_color(focused_color)
+## The color to modulate to when this node is disabled.
+@export var disabled_color : Color = Color(0.318, 0.247, 0.565):
+ set(val):
+ if disabled_color != val:
+ disabled_color = val
+ if is_node_ready(): colors[2] = val
+ force_color(focused_color)
+
+
+var _button : HoldButton
+
+
+
+## Forcibly stops this node's check.
+func force_release() -> void:
+ if _button: _button.force_release()
+## Returns if mouse or touch is being held (mouse or touch outside of limit without being released).
+func is_held() -> bool:
+ return _button && _button.is_held()
+
+
+
+func _init() -> void:
+ _button = HoldButton.new()
+ add_child(_button)
+
+ _button.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ _button.move_to_front()
+
+ if !Engine.is_editor_hint():
+ child_order_changed.connect(_button.move_to_front, CONNECT_DEFERRED)
+
+ _button.button_state.connect(_set_button_color)
+ _button.release_state.connect(_emit_vaild_release)
+
+ _button.press_start.connect(press_start.emit)
+ _button.press_end.connect(press_end.emit)
+ _button.press_vaild.connect(press_vaild.emit)
+
+ _button.mouse_filter = mouse_filter
+ _button.mouse_force_pass_scroll_events = mouse_force_pass_scroll_events
+ _button.mouse_default_cursor_shape = mouse_default_cursor_shape
+
+ _button.toggle_mode = toggle_mode
+ _button.button_pressed = button_pressed
+ _button.disabled = _disabled
+func _ready() -> void:
+ super()
+ colors = [normal_color, focus_color, disabled_color]
+ force_color(2 if _disabled else (1 if button_pressed else 0))
+
+
+func _validate_property(property: Dictionary) -> void:
+ match property.name:
+ "pressed":
+ if !toggle_mode:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ "focused_color", "colors":
+ property.usage &= ~PROPERTY_USAGE_EDITOR
+
+func _set(property: StringName, value: Variant) -> bool:
+ if _button:
+ match property:
+ "mouse_filter":
+ _button.mouse_filter = value
+ "mouse_force_pass_scroll_events":
+ _button.mouse_force_pass_scroll_events = value
+ "mouse_default_cursor_shape":
+ _button.mouse_default_cursor_shape = value
+ return false
+func _get(property: StringName) -> Variant:
+ if _button:
+ match property:
+ "mouse_filter":
+ return _button.mouse_filter
+ "mouse_force_pass_scroll_events":
+ return _button.mouse_force_pass_scroll_events
+ "mouse_default_cursor_shape":
+ return _button.mouse_default_cursor_shape
+ return null
+
+func _set_disabled(val : bool) -> void:
+ _disabled = val || disabled
+
+ _set_button_color(button_pressed)
+ if _button:
+ _button.disabled = _disabled
+func _set_button_color(val : bool) -> void:
+ if _disabled: set_color(2)
+ else: set_color(int(val))
+
+
+func _emit_vaild_release(release : bool) -> void:
+ _set_button_color(release)
+ release_state.emit(release)
diff --git a/godot/addons/FreeControl/src/CustomClasses/Carousel/Carousel.gd b/godot/addons/FreeControl/src/CustomClasses/Carousel/Carousel.gd
new file mode 100644
index 0000000..512f1ca
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Carousel/Carousel.gd
@@ -0,0 +1,616 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name Carousel extends Container
+## A container for Carousel Display of [Control] nodes.
+
+
+
+## Changes the behavior of how draging scrolls the carousel items. Also see [member snap_carousel_transtion_type], [member snap_carousel_ease_type], and [member paging_requirement].
+enum SNAP_BEHAVIOR {
+ NONE = 0b00, ## No behavior.
+ SNAP = 0b01, ## Once drag is released, the carousel will snap to the nearest item.x
+ PAGING = 0b10 ## Carousel items will not scroll when dragged, unless [member paging_requirement] threshold is met. [member hard_stop] will be assumed as [code]true[/code] for this.
+}
+## Internel enum used to differentiate what animation is currently playing
+enum ANIMATION_TYPE {
+ NONE = 0b00, ## No behavior.
+ MANUAL = 0b01, ## Currently animating via request by [method go_to_index].
+ SNAP = 0b10 ## Currently animating via an auto-item snapping request.
+}
+
+
+## This signal is emited when a snap reaches it's destination.
+signal snap_end
+## This signal is emited when a snap begins.
+signal snap_begin
+## This signal is emited when a drag finishes. This does not include the slowdown caused when [member hard_stop] is [code]false[/code].
+signal drag_end
+## This signal is emited when a drag begins.
+signal drag_begin
+## This signal is emited when the slowdown, caused when [member hard_stop] is [code]false[/code], finished naturally.
+signal slowdown_end
+## This signal is emited when the slowdown, caused when [member hard_stop] is [code]false[/code], is interrupted by another drag or other feature.
+signal slowdown_interupted
+
+
+
+@export_group("Carousel Options")
+## The index of the item this carousel will start at.
+@export var starting_index : int = 0:
+ set(val):
+ if starting_index != val:
+ starting_index = val
+ go_to_index(-val, false)
+## The size of each item in the carousel.
+@export var item_size : Vector2 = Vector2(200, 200):
+ set(val):
+ if item_size != val:
+ var current_index := get_carousel_index()
+ item_size = val
+ _settup_children()
+ go_to_index(current_index, false)
+## The space between each item in the carousel.
+@export var item_seperation : int = 0:
+ set(val):
+ if item_seperation != val:
+ item_seperation = val
+ _kill_animation()
+ _adjust_children()
+## The orientation the carousel items will be displayed in.
+@export_range(0, 360, 0.001, "or_less", "or_greater") var carousel_angle : float = 0.0:
+ set(val):
+ if carousel_angle != val:
+ var current_index := get_carousel_index()
+
+ carousel_angle = val
+ _angle_vec = Vector2.RIGHT.rotated(deg_to_rad(carousel_angle))
+
+ _kill_animation()
+ _adjust_children()
+ go_to_index(current_index, false)
+
+@export_group("Loop Options")
+## Allows looping from the last item to the first and vice versa.
+@export var allow_loop : bool = true
+## If [code]true[/code], the carousel will display it's items as if looping. Otherwise, the items will not loop.
+## [br][br]
+## also see [member enforce_border] and [member border_limit].
+@export var display_loop : bool = true:
+ set(val):
+ if val != display_loop:
+ display_loop = val
+ _adjust_children()
+ notify_property_list_changed()
+## The number of items, surrounding the current item of the current index, that will be visible.
+## If [code]-1[/code], all items will be visible.
+@export var display_range : int = -1:
+ set(val):
+ val = max(-1, val)
+ if val != display_range:
+ display_range = val
+ _adjust_children()
+
+@export_group("Snap")
+## Assigns the behavior of how draging scrolls the carousel items. Also see [member snap_carousel_transtion_type], [member snap_carousel_ease_type], and [member paging_requirement].
+@export var snap_behavior : SNAP_BEHAVIOR = SNAP_BEHAVIOR.SNAP:
+ set(val):
+ if val != snap_behavior:
+ snap_behavior = val
+ _end_drag_slowdown()
+ _create_animation(get_carousel_index(), ANIMATION_TYPE.SNAP)
+ notify_property_list_changed()
+## If [member snap_behavior] is [SNAP_BEHAVIOR.PAGING], this is the draging threshold needed to page to the next carousel item.
+@export var paging_requirement : int = 200:
+ set(val):
+ val = max(1, val)
+ if val != paging_requirement:
+ paging_requirement = val
+ _adjust_children()
+
+@export_group("Animation Options")
+@export_subgroup("Manual")
+## The duration of the animation any call to [method go_to_index] will cause, if the animation option is requested.
+@export_range(0.001, 2.0, 0.001, "or_greater") var manual_carousel_duration : float = 0.4
+## The [enum Tween.TransitionType] of the animation any call to [method go_to_index] will cause, if the animation option is requested.
+@export var manual_carousel_transtion_type : Tween.TransitionType
+## The [enum Tween.EaseType] of the animation any call to [method go_to_index] will cause, if the animation option is requested.
+@export var manual_carousel_ease_type : Tween.EaseType
+
+@export_subgroup("Snap")
+## The duration of the animation when snapping to an item.
+@export_range(0.001, 2.0, 0.001, "or_greater") var snap_carousel_duration : float = 0.2
+## The [enum Tween.TransitionType] of the animation when snapping to an item.
+@export var snap_carousel_transtion_type : Tween.TransitionType
+## The [enum Tween.EaseType] of the animation when snapping to an item.
+@export var snap_carousel_ease_type : Tween.EaseType
+
+@export_group("Drag")
+## If [code]true[/code], the user is allowed to drag via their mouse or touch.
+@export var can_drag : bool = true:
+ set(val):
+ if val != can_drag:
+ can_drag = val
+ notify_property_list_changed()
+ if !val:
+ _drag_scroll_value = 0
+ if _is_dragging:
+ _adjust_children()
+## If [code]true[/code], the user is allowed to drag outisde the drawer's bounding box.
+## [br][br]
+## Also see [member can_drag].
+@export var drag_outside : bool = false:
+ set(val):
+ if val != drag_outside:
+ drag_outside = val
+@export_subgroup("Limits")
+## The max amount a user can drag in either direction. If [code]0[/code], then the user can drag any amount they wish.
+@export var drag_limit : int = 0:
+ set(val):
+ val = max(0, val)
+ if val != drag_limit: drag_limit = val
+## When dragging, the user will not be able to move past the last or first item, besides for [member border_limit] number of extra pixels.
+## [br][br]
+## This value is assumed [code]false[/code] is [member display_loop] is [code]true[/code].
+@export var enforce_border : bool = false:
+ set(val):
+ if val != enforce_border:
+ enforce_border = val
+ _adjust_children()
+ notify_property_list_changed()
+## The amount of extra pixels a user can drag past the last and before the first item in the carousel.
+## [br][br]
+## This property does nothing if enforce_border is [code]false[/code].
+@export var border_limit : int = 0:
+ set(val):
+ if val != border_limit:
+ border_limit = val
+ _adjust_children()
+
+@export_subgroup("Slowdown")
+## If [code]true[/code] the carousel will immediately stop when not being dragged. Otherwise, drag speed will be gradually decreased.
+## [br][br]
+## This property is assumed [code]true[/code] if [member snap_behavior] is set to [SNAP_BEHAVIOR.PAGING]. Also see [member slowdown_drag], [member slowdown_friction], and [member slowdown_cutoff].
+@export var hard_stop : bool = true:
+ set(val):
+ if val != hard_stop:
+ hard_stop = val
+ _end_drag_slowdown()
+ notify_property_list_changed()
+## The percentage multiplier the drag velocity will experience each frame.
+## [br][br]
+## This property does nothing if [member hard_stop] is [code]true[/code].
+@export_range(0.0, 1.0, 0.001) var slowdown_drag : float = 0.9
+## The constant decrease the drag velocity will experience each frame.
+## [br][br]
+## This property does nothing if [member hard_stop] is [code]true[/code].
+@export_range(0.0, 5.0, 0.001, "or_greater", "hide_slider") var slowdown_friction : float = 0.1
+## The cutoff amount. If drag velocity magnitude drops below this amount, the slowdown has finished.
+## [br][br]
+## This property does nothing if [member hard_stop] is [code]true[/code].
+@export_range(0.01, 10.0, 0.001, "or_greater", "hide_slider") var slowdown_cutoff : float = 0.01
+
+
+var _scroll_value : int
+var _drag_scroll_value : int
+var _drag_velocity : float
+
+var _item_count : int = 0
+var _item_infos : Array
+
+var _scroll_tween : Tween
+var _is_dragging : bool = false
+
+var _last_animation : ANIMATION_TYPE = ANIMATION_TYPE.NONE
+
+var _angle_vec : Vector2
+var _mouse_checking : bool
+
+
+
+
+# Public Functions
+
+## Gets the index of the current carousel item.[br]
+## If [param with_drag] is [code]true[/code] the current drag will also be considered.[br]
+## If [param with_clamp] is [code]true[/code] the index will be looped if [member allow_loop] is true or clamped to a vaild index within the carousel.
+func get_carousel_index(with_drag : bool = false, with_clamp : bool = true) -> int:
+ if _item_count == 0: return -1
+
+ var scroll : int = _scroll_value
+ if with_drag: scroll += _drag_scroll_value
+
+ var calculated := floori((float(scroll) / float(_get_relevant_axis())) + 0.5)
+ if with_clamp:
+ if allow_loop:
+ calculated = posmod(calculated, _item_count)
+ else:
+ calculated = clampi(calculated, 0, _item_count - 1)
+
+ return calculated
+## Moves to an item of the given index within the carousel. If an invalid index is given, it will be posmod into a vaild index.
+func go_to_index(idx : int, animation : bool = true) -> void:
+ if _item_count == 0: return
+
+ if allow_loop:
+ idx = (((idx % _item_count) - _item_count) % _item_count)
+ else:
+ idx = clamp(idx, 0, _item_count - 1)
+
+ if animation:
+ _create_animation(idx, ANIMATION_TYPE.MANUAL)
+ else:
+ _kill_animation()
+ _scroll_value = -_get_relevant_axis() * idx
+ _adjust_children()
+## Moves to the previous item in the carousel, if there is one.
+func prev(animation : bool = true) -> void:
+ go_to_index(get_carousel_index() - 1, animation)
+## Moves to the next item in the carousel, if there is one.
+func next(animation : bool = true) -> void:
+ go_to_index(get_carousel_index() + 1, animation)
+## Enacts a manual drag on the carousel. This can be used even if [member can_drag] is [code]false[/code].
+## Note that [param from] and [param dir] are considered in local coordinates.
+## [br][br]
+## Is not affected by [member hard_stop], [member drag_outside], and [member drag_limit].
+func flick(from : Vector2, dir : Vector2) -> void:
+ drag_begin.emit()
+ _kill_animation()
+ _end_drag_slowdown()
+
+ _handle_drag_angle(dir - from)
+
+ _on_drag_release()
+## Returns if the carousel is currening scrolling via na animation
+func is_animating() -> bool:
+ return _scroll_tween.is_running()
+## Returns if the carousel is currening being dragged by player input.
+func being_dragged() -> bool:
+ return _is_dragging
+## Returns the current scroll value.
+func get_scroll(with_drag : bool = false) -> int:
+ if with_drag:
+ return _scroll_value + _drag_scroll_value
+ return _scroll_value
+## Returns the current number of items in the carousel
+func get_item_count() -> int:
+ return _item_count
+
+
+# Virtual Functions
+
+## A virtual function that is is called whenever the scroll changes.
+func _on_progress(scroll : int) -> void: pass
+## A virtual function that is is called whenever the scroll changes, for each visible item in the carousel
+func _on_item_progress(item : Control, local_scroll : int, scroll : int, local_index : int, index : int) -> void: pass
+
+
+
+func _handle_drag_angle(local_pos : Vector2) -> void:
+ var projected_scalar : float = -local_pos.dot(_angle_vec) / _angle_vec.length_squared()
+ _drag_velocity = projected_scalar
+ _drag_scroll_value += projected_scalar
+
+ if drag_limit != 0:
+ _drag_scroll_value = clampi(_drag_scroll_value, -drag_limit, drag_limit)
+
+ if snap_behavior == SNAP_BEHAVIOR.PAGING:
+ if paging_requirement < _drag_scroll_value:
+ _drag_scroll_value = 0
+ var desired := get_carousel_index() + 1
+ if allow_loop || desired < _item_count:
+ _create_animation(desired, ANIMATION_TYPE.SNAP)
+ elif -paging_requirement > _drag_scroll_value:
+ _drag_scroll_value = 0
+ var desired := get_carousel_index() - 1
+ if allow_loop || desired >= 0:
+ _create_animation(desired, ANIMATION_TYPE.SNAP)
+ else:
+ _adjust_children()
+
+
+# Private Functions
+
+func _get_child_rect(child : Control) -> Rect2:
+ var child_pos : Vector2 = (size - item_size) * 0.5
+ var child_size : Vector2
+
+ child_size = child.get_combined_minimum_size()
+ match child.size_flags_horizontal:
+ SIZE_FILL: child_size.x = item_size.x
+ SIZE_SHRINK_BEGIN: pass
+ SIZE_SHRINK_CENTER: child_pos.x += (item_size.x - child_size.x) * 0.5
+ SIZE_SHRINK_END: child_pos.x += (item_size.x - child_size.x)
+ match child.size_flags_vertical:
+ SIZE_FILL: child_size.y = item_size.y
+ SIZE_SHRINK_BEGIN: pass
+ SIZE_SHRINK_CENTER: child_pos.y += (item_size.y - child_size.y) * 0.5
+ SIZE_SHRINK_END: child_pos.y += (item_size.y - child_size.y)
+
+ child.size = child_size
+ child.position = child_pos
+
+ return Rect2(child_pos, child_size)
+func _get_control_children() -> Array[Control]:
+ var ret : Array[Control]
+ ret.assign(get_children().filter(func(child : Node): return child is Control && child.visible))
+ return ret
+func _get_relevant_axis() -> int:
+ var abs_angle_vec = _angle_vec.abs()
+
+ if abs_angle_vec.y >= abs_angle_vec.x:
+ return (item_size.x / abs_angle_vec.y) + item_seperation
+ return (item_size.y / abs_angle_vec.x) + item_seperation
+func _get_adjusted_scroll() -> int:
+ var scroll := _scroll_value
+ if snap_behavior != SNAP_BEHAVIOR.PAGING:
+ scroll += _drag_scroll_value
+ if enforce_border:
+ scroll = clampi(scroll, -border_limit, _get_relevant_axis() * (_item_count - 1) + border_limit)
+ elif display_loop:
+ scroll = posmod(scroll, _get_relevant_axis() * _item_count)
+ return scroll
+
+
+func _create_animation(idx : int, animation_type : ANIMATION_TYPE) -> void:
+ _kill_animation()
+ if _item_count == 0: return
+ _scroll_tween = create_tween()
+
+ var axis := _get_relevant_axis()
+ var max_scroll := axis * _item_count
+ if max_scroll == 0: max_scroll = 1
+
+ var desired_scroll := posmod(axis * idx, max_scroll)
+
+ if allow_loop && display_loop:
+ _scroll_value = posmod(_scroll_value, max_scroll)
+ if abs(_scroll_value - desired_scroll) > (max_scroll >> 1):
+ var left_distance := posmod(_scroll_value - desired_scroll, max_scroll)
+ var right_distance := posmod(desired_scroll - _scroll_value, max_scroll)
+
+ if left_distance < right_distance:
+ desired_scroll -= max_scroll
+ else:
+ desired_scroll += max_scroll
+
+ _last_animation = animation_type
+ match animation_type:
+ ANIMATION_TYPE.MANUAL:
+ _scroll_tween.set_ease(manual_carousel_ease_type)
+ _scroll_tween.set_trans(manual_carousel_transtion_type)
+ _scroll_tween.tween_method(
+ _animation_method,
+ _scroll_value,
+ desired_scroll,
+ manual_carousel_duration)
+ ANIMATION_TYPE.SNAP:
+ snap_begin.emit()
+ _scroll_tween.set_ease(snap_carousel_ease_type)
+ _scroll_tween.set_trans(snap_carousel_transtion_type)
+ _scroll_tween.tween_method(
+ _animation_method,
+ _scroll_value,
+ desired_scroll,
+ snap_carousel_duration)
+ _scroll_tween.tween_callback(_kill_animation)
+ _scroll_tween.play()
+func _animation_method(scroll : int) -> void:
+ _scroll_value = scroll
+ _adjust_children()
+func _kill_animation() -> void:
+ if _last_animation == ANIMATION_TYPE.SNAP:
+ snap_end.emit()
+ _last_animation = ANIMATION_TYPE.NONE
+
+ if _scroll_tween && _scroll_tween.is_running():
+ _scroll_tween.kill()
+
+
+func _sort_children() -> void:
+ _settup_children()
+ _adjust_children()
+func _settup_children() -> void:
+ var children : Array[Control] = _get_control_children()
+ _item_count = children.size()
+
+ _item_infos.resize(_item_count)
+ for i : int in range(0, _item_count):
+ var item_info := ItemInfo.new()
+ item_info.node = children[i]
+ item_info.rect = _get_child_rect(children[i])
+ _item_infos[i] = item_info
+func _adjust_children() -> void:
+ if _item_count == 0: return
+
+ var range : Array
+ var max_local_offset : int
+ var children : Array[Control] = _get_control_children()
+ var axis := _get_relevant_axis()
+ var scroll := _get_adjusted_scroll()
+
+ var index : int
+ var local_scroll : int
+ var adjustment : int
+
+ if axis == 0:
+ index = 0
+ local_scroll = 0
+ adjustment = 0
+ else:
+ index = int(scroll / axis)
+ local_scroll = fposmod(scroll, axis)
+ adjustment = int(scroll < 0)
+
+ index += adjustment & int(local_scroll == 0)
+
+ _on_progress(scroll)
+
+ if display_loop:
+ if display_range == -1:
+ max_local_offset = (_item_count >> 1)
+ range = range(0, _item_count)
+ for i : int in range:
+ _item_infos[i].loaded = false
+ else:
+ max_local_offset = (display_range >> 1)
+ range = range(0, (display_range << 1) + 1)
+ for i : int in range(0, _item_count):
+ var item_info : ItemInfo = _item_infos[i]
+ item_info.loaded = false
+ item_info.node.visible = false
+
+ for item : int in range:
+ var local_offset := (item >> 1) * (((item & 1) << 1) - 1) + (item & 1)
+ var local_index := posmod(index + local_offset, _item_count)
+ var item_info : ItemInfo = _item_infos[local_index]
+ if item_info.loaded: break
+
+ local_offset += adjustment
+ var rect : Rect2 = item_info.rect
+
+ rect.position += _angle_vec * (local_offset * axis - local_scroll)
+
+ fit_child_in_rect(item_info.node, rect)
+ item_info.loaded = true
+ item_info.node.visible = true
+ item_info.node.z_index = max_local_offset - abs(local_offset)
+ _on_item_progress(item_info.node, local_scroll, scroll, item, local_index)
+ else:
+ if display_range == -1:
+ max_local_offset = (_item_count >> 1) + (_item_count & 1) + 1
+ range = range(0, _item_count)
+ else:
+ max_local_offset = display_range
+ range = range(max(0, index - display_range), min(_item_count, index + display_range + 1))
+ for info : ItemInfo in _item_infos:
+ info.node.visible = false
+
+ for item : int in range:
+ var local_index := item - index
+ var item_info : ItemInfo = _item_infos[item]
+
+ local_index += adjustment
+ var rect : Rect2 = item_info.rect
+
+ rect.position += _angle_vec * (local_index * axis - local_scroll)
+
+ fit_child_in_rect(item_info.node, rect)
+ item_info.node.visible = true
+ item_info.node.z_index = max_local_offset - abs(local_index)
+ _on_item_progress(item_info.node, local_scroll, scroll, item, local_index)
+
+func _start_drag_slowdown() -> void:
+ if is_inside_tree() && !get_tree().process_frame.is_connected(_handle_drag_slowdown):
+ get_tree().process_frame.connect(_handle_drag_slowdown)
+func _end_drag_slowdown() -> void:
+ if abs(_drag_velocity) < slowdown_cutoff:
+ slowdown_interupted.emit()
+ _drag_velocity = 0
+ if snap_behavior == SNAP_BEHAVIOR.SNAP:
+ _create_animation(get_carousel_index(), ANIMATION_TYPE.SNAP)
+ if is_inside_tree() && get_tree().process_frame.is_connected(_handle_drag_slowdown):
+ get_tree().process_frame.disconnect(_handle_drag_slowdown)
+func _handle_drag_slowdown() -> void:
+ if abs(_drag_velocity) < slowdown_cutoff:
+ slowdown_end.emit()
+ _end_drag_slowdown()
+ return
+
+ if _drag_velocity > 0:
+ _drag_velocity = max(0, _drag_velocity - slowdown_friction)
+ else:
+ _drag_velocity = min(0, _drag_velocity + slowdown_friction)
+ _drag_velocity *= slowdown_drag
+ _scroll_value += _drag_velocity
+ _adjust_children()
+
+
+
+func _init() -> void:
+ sort_children.connect(_sort_children)
+ tree_exiting.connect(_end_drag_slowdown)
+ mouse_exited.connect(_mouse_check)
+
+ _angle_vec = Vector2.RIGHT.rotated(deg_to_rad(carousel_angle))
+func _ready() -> void:
+ _settup_children()
+ if _item_count > 0:
+ starting_index = posmod(starting_index, _item_count)
+ go_to_index(-starting_index, false)
+
+func _validate_property(property: Dictionary) -> void:
+ if property.name == "enforce_border":
+ if display_loop:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ elif property.name == "border_limit":
+ if display_loop || !enforce_border:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ elif property.name == "paging_requirement":
+ if snap_behavior != SNAP_BEHAVIOR.PAGING:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ elif property.name == "hard_stop":
+ if snap_behavior == SNAP_BEHAVIOR.PAGING:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ elif property.name in ["slowdown_drag", "slowdown_friction", "slowdown_cutoff"]:
+ if hard_stop || snap_behavior == SNAP_BEHAVIOR.PAGING:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ elif property.name in ["drag_outside"]:
+ if !can_drag:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+
+func _gui_input(event: InputEvent) -> void:
+ if event is not InputEventMouse:
+ return
+
+ var has_point := get_viewport_rect().has_point(event.position)
+
+ if (
+ (event is InputEventScreenDrag || event is InputEventMouseMotion) &&
+ (drag_outside || has_point)
+ ):
+ if event.pressure == 0:
+ if _is_dragging: _on_drag_release()
+ return
+
+ if !_is_dragging && has_point:
+ drag_begin.emit()
+ _mouse_checking = true
+ _end_drag_slowdown()
+ _kill_animation()
+ _is_dragging = true
+
+ _handle_drag_angle(event.relative)
+ elif (event is InputEventScreenTouch || event is InputEventMouseButton):
+ if !event.pressed: _on_drag_release()
+func _on_drag_release() -> void:
+ _mouse_checking = false
+ _is_dragging = false
+ drag_end.emit()
+
+ if snap_behavior != SNAP_BEHAVIOR.PAGING:
+ _scroll_value = _get_adjusted_scroll()
+ _drag_scroll_value = 0
+ if snap_behavior == SNAP_BEHAVIOR.NONE:
+ if !hard_stop: _start_drag_slowdown()
+ elif snap_behavior == SNAP_BEHAVIOR.SNAP:
+ if hard_stop: _create_animation(get_carousel_index(), ANIMATION_TYPE.SNAP)
+ else: _start_drag_slowdown()
+
+func _mouse_check() -> void:
+ if _mouse_checking:
+ _on_drag_release()
+
+
+func _get_allowed_size_flags_horizontal() -> PackedInt32Array:
+ return [SIZE_FILL, SIZE_SHRINK_BEGIN, SIZE_SHRINK_CENTER, SIZE_SHRINK_END]
+func _get_allowed_size_flags_vertical() -> PackedInt32Array:
+ return [SIZE_FILL, SIZE_SHRINK_BEGIN, SIZE_SHRINK_CENTER, SIZE_SHRINK_END]
+
+
+# Used to hold data about a carousel item
+class ItemInfo:
+ var node : Control
+ var rect : Rect2
+ var loaded : bool
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/CircularContainer/CircularContainer.gd b/godot/addons/FreeControl/src/CustomClasses/CircularContainer/CircularContainer.gd
new file mode 100644
index 0000000..07df9fc
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/CircularContainer/CircularContainer.gd
@@ -0,0 +1,277 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name CircularContainer extends Container
+## A container that positions children in a ellipse within the bounds of this node.
+
+
+
+## Behavior the auto angle setter will exhibit.
+enum BOUND_BEHAVIOR {
+ NONE, ## No end bound for angles
+ STOP, ## Angles, if exceeding the max, will be hard stopped at the max.
+ LOOP, ## Angles, if exceeding the max, will loop back to the begining.
+ MIRRIOR ## Angles, if exceeding the max, will bounce back and forth between the min and max angles.
+}
+
+
+## The horizontal offset of the ellipse's center.
+## [br][br]
+## [code]0[/code] is fully left and [code]1[/code] is fully right.
+@export_range(0, 1) var origin_x : float = 0.5:
+ set(val):
+ origin_x = val
+ queue_sort()
+## The vertical offset of the ellipse's center.
+## [br][br]
+## [code]0[/code] is fully top and [code]1[/code] is fully bottom.
+@export_range(0, 1) var origin_y : float = 0.5:
+ set(val):
+ origin_y = val
+ queue_sort()
+
+## The horizontal radius of the ellipse's center.
+## [br][br]
+## [code]0[/code] is [code]0[/code], [code]0.5[/code] is half of [member Control.size].x, and [code]1[/code] is [member Control.size].x.
+@export_range(0, 1) var xRadius : float = 0.5:
+ set(val):
+ xRadius = val
+ queue_sort()
+## The vertical radius of the ellipse's center.
+## [br][br]
+## [code]0[/code] is [code]0[/code], [code]0.5[/code] is half of [member Control.size].y, and [code]1[/code] is [member Control.size].y.
+@export_range(0, 1) var yRadius : float = 0.5:
+ set(val):
+ yRadius = val
+ queue_sort()
+
+@export_group("Angles")
+## If [code]false[/code], the node will automatically write the angles of children.[br]
+## If [code]true[/code], you will be required to manually input angles for each child.
+@export var manual : bool = false:
+ set(val):
+ if manual != val:
+ manual = val
+ notify_property_list_changed()
+ _calculate_angles()
+
+## The behavior this node will have if the angle exceeds the max set angle in auto-mode.
+## [br][br]
+## See [member manual] and [member angle_end],
+var bound_behavior : BOUND_BEHAVIOR:
+ set(val):
+ if bound_behavior != val:
+ bound_behavior = val
+ notify_property_list_changed()
+ _calculate_angles()
+## If true, the nodes will be equal-distantantly placed from each on, between the start and end angles. Overides all other [member bound_behavior].
+## [br][br]
+## See [member manual], [member bound_behavior], [member angle_start], and [member angle_end].
+var equal_distant : bool:
+ set(val):
+ if equal_distant != val:
+ equal_distant = val
+ notify_property_list_changed()
+ _calculate_angles()
+
+## The angle auto-mode will increment from.
+## [br][br]
+## See [member manual], [member bound_behavior], [member angle_start], and [member angle_end].
+var angle_start : float = 0:
+ set(val):
+ if angle_start != val:
+ angle_start = val
+ _calculate_angles()
+## The angle auto-mode will increment with.
+## [br][br]
+## See [member manual].
+var angle_step : float = 10:
+ set(val):
+ if angle_step != val:
+ angle_step = val
+ _calculate_angles()
+## The angle auto-mode will increment to.
+## [br][br]
+## See [member manual] and [member bound_behavior].
+var angle_end : float = 360:
+ set(val):
+ if angle_end != val:
+ angle_end = val
+ _calculate_angles()
+
+@export_storage var _container_angles : PackedFloat32Array
+## The list of angles this ndoe uses to position each child, within the order they are positions in the tree.
+var angles : PackedFloat32Array:
+ set(val):
+ _container_angles.resize(max(_get_control_children().size(), val.size()))
+
+ for i in range(0, val.size()):
+ _container_angles[i] = deg_to_rad(val[i])
+ for i in range(val.size(), _container_angles.size()):
+ _container_angles[i] = 0
+
+ _fix_childrend()
+ get:
+ var ret : PackedFloat32Array
+ ret.resize(_container_angles.size())
+
+ for i in range(0, _container_angles.size()):
+ ret[i] = rad_to_deg(_container_angles[i])
+ return ret
+
+
+
+func _init() -> void:
+ sort_children.connect(_fix_childrend)
+ child_order_changed.connect(_childrend_changed)
+func _ready() -> void:
+ _fix_childrend()
+
+func _childrend_changed() -> void:
+ _calculate_angles()
+ _fix_childrend()
+func _get_control_children() -> Array[Control]:
+ var ret : Array[Control]
+ ret.assign(get_children().filter(func(child : Node): return child is Control && child.visible))
+ return ret
+func _fix_childrend() -> void:
+ var children := _get_control_children()
+
+ for index : int in range(0, children.size()):
+ var child : Control = children[index]
+ if child: _fix_child(child, index)
+func _fix_child(child : Control, index : int) -> void:
+ if _container_angles.is_empty(): return
+ child.reset_size()
+
+ # Calculates child position
+ var child_size := child.get_combined_minimum_size()
+ var child_pos := -(child_size * 0.5) + (Vector2(origin_x, origin_y) + (Vector2(xRadius, yRadius) * Vector2(cos(_container_angles[index]), sin(_container_angles[index])))) * size
+
+ # Keeps children in the rect's top-left boundards
+ if child_pos.x < 0:
+ child_pos.x = 0
+ if child_pos.y < 0:
+ child_pos.y = 0
+
+ # Keeps children in the rect's bottom-right boundards
+ if child_pos.x + child_size.x > size.x:
+ child_pos.x += size.x - (child_pos.x + child_size.x)
+ if child_pos.y + child_size.y > size.y:
+ child_pos.y += size.y - (child_pos.y + child_size.y)
+ if child_pos.y + child_size.y > size.y:
+ child_size.y = size.y - child_pos.y
+
+ fit_child_in_rect(child, Rect2(child_pos, child_size))
+
+func _get_property_list() -> Array[Dictionary]:
+ var properties : Array[Dictionary]
+
+ if manual:
+ properties.append({
+ "name": "angles",
+ "type": TYPE_PACKED_FLOAT32_ARRAY,
+ "usage" : PROPERTY_USAGE_EDITOR
+ })
+ else:
+ var unbounded = PROPERTY_USAGE_READ_ONLY if bound_behavior == BOUND_BEHAVIOR.NONE else 0
+ var allow_step = PROPERTY_USAGE_READ_ONLY if equal_distant && bound_behavior == BOUND_BEHAVIOR.STOP else 0
+
+ properties.append({
+ "name": "bound_behavior",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": ",".join(BOUND_BEHAVIOR.keys()),
+ "usage" : PROPERTY_USAGE_DEFAULT
+ })
+ properties.append({
+ "name": "equal_distant",
+ "type": TYPE_BOOL,
+ "usage" : PROPERTY_USAGE_DEFAULT
+ })
+ properties.append({
+ "name": "Values",
+ "type": TYPE_NIL,
+ "usage" : PROPERTY_USAGE_SUBGROUP,
+ "hint_string": ""
+ })
+ properties.append({
+ "name": "angle_start",
+ "type": TYPE_FLOAT,
+ "usage" : PROPERTY_USAGE_DEFAULT
+ })
+ properties.append({
+ "name": "angle_step",
+ "type": TYPE_FLOAT,
+ "usage" : PROPERTY_USAGE_DEFAULT | allow_step
+ })
+ properties.append({
+ "name": "angle_end",
+ "type": TYPE_FLOAT,
+ "usage" : PROPERTY_USAGE_DEFAULT | unbounded
+ })
+
+ return properties
+func _property_can_revert(property: StringName) -> bool:
+ match property:
+ "bound_behavior":
+ return bound_behavior != BOUND_BEHAVIOR.NONE
+ "equal_distant":
+ return equal_distant
+ "angle_start":
+ return angle_start != 0
+ "angle_step":
+ return angle_step != 10
+ "angle_end":
+ return angle_end != 360
+
+ return false
+func _property_get_revert(property: StringName) -> Variant:
+ match property:
+ "bound_behavior":
+ return BOUND_BEHAVIOR.NONE
+ "equal_distant":
+ return false
+ "angle_start":
+ return 0
+ "angle_step":
+ return 10
+ "angle_end":
+ return 360
+
+ return null
+
+func _calculate_angles() -> void:
+ if manual: return
+
+ var count := _get_control_children().size()
+ _container_angles.resize(count)
+
+ var start := deg_to_rad(angle_start)
+ var end := deg_to_rad(angle_end)
+
+ var step : float
+ if equal_distant:
+ if count != 0: step = deg_to_rad((angle_end - angle_start) / (count - 1))
+ else:
+ step = deg_to_rad(angle_step)
+
+ var inc_func : Callable
+ if equal_distant || bound_behavior == BOUND_BEHAVIOR.NONE:
+ inc_func = func(i : int): return (i * step) + start
+ elif bound_behavior == BOUND_BEHAVIOR.STOP:
+ inc_func = func(i : int): return min(i * step, end) + start
+ elif bound_behavior == BOUND_BEHAVIOR.LOOP:
+ inc_func = func(i : int): return fmod(i * step, end) + start
+ elif bound_behavior == BOUND_BEHAVIOR.MIRRIOR:
+ inc_func = func(i : int): return abs(fmod((i * step) - end, 2 * end) - end) + start
+
+ for i : int in range(0, _container_angles.size()):
+ _container_angles[i] = fposmod(inc_func.call(i), TAU)
+ _fix_childrend()
+
+func _get_allowed_size_flags_horizontal() -> PackedInt32Array:
+ return []
+func _get_allowed_size_flags_vertical() -> PackedInt32Array:
+ return []
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/Drawer/Drawer.gd b/godot/addons/FreeControl/src/CustomClasses/Drawer/Drawer.gd
new file mode 100644
index 0000000..15b0cb2
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Drawer/Drawer.gd
@@ -0,0 +1,798 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name Drawer extends Container
+## A [Container] node used for easy UI Drawers.
+
+## A flag enum used to classify which input type is allowed.
+enum ActionMode {
+ ACTION_MODE_BUTTON_NONE = 0, ## Allows no input
+ ACTION_MODE_BUTTON_PRESS = 1, ## Toggles the drawer on tap/click press
+ ACTION_MODE_BUTTON_RELEASE = 2, ## Toggles the drawer on tap/click release
+ ACTION_MODE_BUTTON_DRAG = 4, ## Allows user to drag the drawer
+}
+
+## An enum used to classify where input is accepted.
+enum InputAreaMode {
+ Nowhere = 0, ## No input is alloed anywhere on the screen.
+ Anywhere = 1, ## Input accepted anywere on the screen.
+ WithinBounds = 2, ## Input is accepted only within this node's rect.
+ ExcludeDrawer = 3, ## Input is accepted anywhere except on the drawer's rect.
+ WithinEmptyBounds = 4, ## Input is accepted only within this node's rect, outside of the drawer's rect.
+}
+
+## An enum used to classify when dragging is allowed.
+enum DragMode {
+ NEVER = 0, ## No dragging allowed.
+ ON_OPEN = 1, ## Dragging is allowed to open the drawer.
+ ON_CLOSE = 2, ## Dragging is allowed to close the drawer.
+ ON_OPEN_OR_CLOSE = 0b11 ## Dragging is allowed to open or close the drawer.
+}
+
+
+## Emited when drawer is begining an opening/closing animation.
+## [br][br]
+## Also see: [member state], [method toggle_drawer].
+signal slide_begin
+## Emited when drawer is ending an opening/closing animation.
+## [br][br]
+## Also see: [member state], [method toggle_drawer].
+signal slide_end
+## Emited when state has changed, but animation has not began.
+## [br][br]
+## Also see: [member state], [method toggle_drawer].
+signal state_toggle_begin(toggle : bool)
+## Emited when state has changed and animation has finished.
+## [br][br]
+## Also see: [member state], [method toggle_drawer].
+signal state_toggle_end(toggle : bool)
+## Emited when drag has began.
+## [br][br]
+## Also see: [member allow_drag].
+signal drag_start
+## Emited when drag has ended.
+## [br][br]
+## Also see: [member allow_drag].
+signal drag_end
+
+
+
+@export_storage var _state : bool
+## The state of the drawer. If [code]true[/code], the drawer is open. Otherwise closed.
+## [br][br]
+## Also see: [method toggle_drawer].
+var state : bool:
+ get: return _state
+ set(val):
+ if _state != val:
+ _toggle_drawer(val)
+
+#@export_group("Drawer Angle")
+## The angle in which the drawer will open/close from.
+## [br][br]
+## Also see: [member drawer_angle_axis_snap].
+var drawer_angle : float = 0.0:
+ set(val):
+ if drawer_angle != val:
+ drawer_angle = val
+ _angle_vec = Vector2.RIGHT.rotated(deg_to_rad(drawer_angle))
+
+ _kill_animation()
+ _calculate_childrend()
+## If [code]true[/code], the drawer will be snapped to move as strictly cardinally as possible.
+## [br][br]
+## Also see: [member drawer_angle].
+var drawer_angle_axis_snap : bool:
+ set(val):
+ if drawer_angle_axis_snap != val:
+ drawer_angle_axis_snap = val
+
+ _kill_animation()
+ _calculate_childrend()
+
+#@export_group("Drawer Span")
+## If [code]false[/code], [member drawer_width] is equal to a ratio of this node's [Control.size]'s x component.
+## [br]. Else, [member drawer_width] is directly editable.
+var drawer_width_by_pixel : bool:
+ set(val):
+ if val != drawer_width_by_pixel:
+ drawer_width_by_pixel = val
+ if val:
+ drawer_width *= size.x
+ else:
+ if size.x == 0:
+ drawer_width = 0
+ else:
+ drawer_width /= size.x
+
+ notify_property_list_changed()
+## The width of the drawer.
+## [br][br]
+## Also see: [member drawer_width_by_pixel].
+var drawer_width : float = 1:
+ set(val):
+ if val != drawer_width:
+ drawer_width = val
+ _calculate_childrend()
+## If [code]false[/code], [member drawer_height] is equal to a ratio of this node's [Control.size]'s y component.
+## [br]. Else, [member drawer_height] is directly editable.
+var drawer_height_by_pixel : bool:
+ set(val):
+ if val != drawer_height_by_pixel:
+ drawer_height_by_pixel = val
+ if val:
+ drawer_height *= size.y
+ else:
+ if size.y == 0:
+ drawer_height = 0
+ else:
+ drawer_height /= size.y
+
+ notify_property_list_changed()
+## The height of the drawer.
+## [br][br]
+## Also see: [member drawer_height_by_pixel].
+var drawer_height : float = 1:
+ set(val):
+ if val != drawer_height:
+ drawer_height = val
+ _calculate_childrend()
+
+#@export_group("Input Options")
+## A flag enum used to classify which input type is allowed.
+var action_mode : ActionMode = ActionMode.ACTION_MODE_BUTTON_PRESS:
+ set(val):
+ if val != action_mode:
+ action_mode = val
+ _is_dragging = false
+
+#@export_subgroup("Margins")
+## Extra pixels to where the open drawer lies when open.
+var open_margin : int = 0:
+ set(val):
+ if val != open_margin:
+ open_margin = val
+ _calculate_childrend()
+## Extra pixels to where the open drawer lies when closed.
+var close_margin : int = 0:
+ set(val):
+ if val != close_margin:
+ close_margin = val
+ _calculate_childrend()
+
+#@export_subgroup("Drag Options")
+## Permissions on how the user may drag to open/close the drawer.
+## [br][br]
+## Also see: [member allow_drag], [member smooth_drag].
+var allow_drag : DragMode = DragMode.ON_OPEN_OR_CLOSE
+## If [code]true[/code], the drawer will react while the user drags.
+var smooth_drag : bool = true
+## The amount of extra the user is allowed to drag (in the open direction) before being stopped.
+var drag_give : int = 0
+
+#@export_subgroup("Open Input")
+## A node to determine where vaild input, when closed, may start at.
+## [br][br]
+## Also see: [member allow_drag].
+var open_bounds : InputAreaMode = InputAreaMode.WithinEmptyBounds
+## The minimum amount you need to drag before your drag is considered to have closed the drawer.
+## [br][br]
+## Also see: [member allow_drag].
+var open_drag_threshold : int = 50:
+ set(val):
+ val = max(0, val)
+ if val != open_drag_threshold:
+ open_drag_threshold = val
+
+#@export_subgroup("Close Input")
+## A node to determine where vaild input, when open, may start at.
+## [br][br]
+## Also see: [member allow_drag].
+var close_bounds : InputAreaMode = InputAreaMode.WithinEmptyBounds
+## The minimum amount you need to drag before your drag is considered to have opened the drawer.
+## [br][br]
+## Also see: [member allow_drag].
+var close_drag_threshold : int = 50:
+ set(val):
+ val = max(0, val)
+ if val != close_drag_threshold:
+ close_drag_threshold = val
+
+#@export_group("Animation")
+#@export_subgroup("Manual Animation")
+## The [enum Tween.TransitionType] used when manually opening and closing drawer.
+## [br][br]
+## Also see: [member state], [method toggle_drawer].
+var manual_drawer_translate : Tween.TransitionType
+## The [enum Tween.EaseType] used when manually opening and closing drawer.
+## [br][br]
+## Also see: [member state], [method toggle_drawer].
+var manual_drawer_ease : Tween.EaseType
+## The animation duration used when manually opening and closing drawer.
+## [br][br]
+## Also see: [member state], [method toggle_drawer].
+var manual_drawer_duration : float = 0.2
+
+#@export_subgroup("Drag Animation")
+## The [enum Tween.TransitionType] used when snapping after a drag.
+var drag_drawer_translate : Tween.TransitionType
+## The [enum Tween.EaseType] used when snapping after a drag.
+var drag_drawer_ease : Tween.EaseType
+## The animation duration used when snapping after a drag.
+var drag_drawer_duration : float = 0.2
+
+
+
+var _min_size : Vector2
+var _angle_vec : Vector2
+
+var _animation_tween : Tween
+var _current_progress : float
+var _drag_value : float
+var _is_dragging : bool
+var _has_dragged : bool
+
+var _inner_offset : Vector2
+var _outer_offset : Vector2
+var _max_offset : float
+
+
+## Returns if the drawer is currently open.
+func is_open() -> bool:
+ return get_progress_adjusted() > 0.5
+## Returns if the drawer is expected to be open.
+func is_open_expected() -> bool:
+ return _state
+## Returns if the drawer is currently animating.
+func is_animating() -> bool:
+ return _animation_tween && _animation_tween.is_running()
+## Returns the size of the drawer.
+func get_drawer_size() -> Vector2:
+ var ret := Vector2(drawer_width, drawer_height)
+ if !drawer_width_by_pixel:
+ ret.x *= size.x
+ if !drawer_height_by_pixel:
+ ret.y *= size.y
+ return ret.max(_min_size)
+## Returns the offsert the drawer has, compared to this node's local position.
+func get_drawer_offset(with_drag : bool = false) -> Vector2:
+ return _get_drawer_offset(_inner_offset, _outer_offset, with_drag)
+## Returns the rect the drawer has, compared to this node's local position.
+func get_drawer_rect(with_drag : bool = false) -> Rect2:
+ return Rect2(get_drawer_offset(with_drag), get_drawer_size())
+
+## Gets the current progress the drawer is in animations.
+## Returns the value in pixel distance.
+func get_progress(include_drag : bool = false, with_clamp : bool = true) -> float:
+ var ret : float = _current_progress
+ if include_drag:
+ ret += _drag_value
+ if with_clamp:
+ ret = clampf(ret, -drag_give, _max_offset)
+ return ret
+## Gets the percentage of the drawer's current position between being closed and opened.
+## [code]0.0[/code] when closed and [code]1.0[/code] when opened.
+func get_progress_adjusted(include_drag : bool = false, with_clamp : bool = true) -> float:
+ if _max_offset == 0.0: return 0.0
+ return get_progress(include_drag, with_clamp) / _max_offset
+
+
+
+func _get_relevant_axis() -> float:
+ var drawer_size := get_drawer_size()
+ var abs_angle_vec = _angle_vec.abs()
+
+ if abs_angle_vec.y >= abs_angle_vec.x:
+ return (drawer_size.x / abs_angle_vec.y)
+ return (drawer_size.y / abs_angle_vec.x)
+func _get_control_children() -> Array[Control]:
+ var ret : Array[Control]
+ ret.assign(get_children().filter(func(child : Node): return child is Control && child.visible))
+ return ret
+func _calculate_childrend() -> void:
+ _find_offsets()
+ _current_progress = _max_offset * float(_state)
+ _adjust_children()
+func _adjust_children() -> void:
+ var rect := get_drawer_rect(true)
+ for child : Control in _get_control_children():
+ fit_child_in_rect(child, rect)
+
+
+
+func _find_minimum_size() -> Vector2:
+ var min_size : Vector2 = Vector2.ZERO
+ for child : Control in _get_control_children():
+ min_size = min_size.max(child.get_combined_minimum_size())
+ return min_size
+func _find_offsets() -> void:
+ var drawer_size := get_drawer_size()
+
+ var distances_to_intersection_point := (size / _angle_vec).abs()
+ var inner_distance := minf(distances_to_intersection_point.x, distances_to_intersection_point.y)
+ var inner_point : Vector2 = (inner_distance * _angle_vec + (size - drawer_size)) * 0.5
+ _inner_offset = inner_point.maxf(0).min(size - drawer_size)
+
+ if drawer_angle_axis_snap:
+ var half_drawer_size := drawer_size * 0.5
+ var inner_point_half := inner_point + half_drawer_size
+ _outer_offset = inner_point
+
+ if abs(inner_point_half.x - size.x) < 0.01:
+ _outer_offset.x += half_drawer_size.x
+ elif abs(inner_point_half.x) < 0.01:
+ _outer_offset.x -= half_drawer_size.x
+
+ if abs(inner_point_half.y - size.y) < 0.01:
+ _outer_offset.y += half_drawer_size.y
+ elif abs(inner_point_half.y) < 0.01:
+ _outer_offset.y -= half_drawer_size.y
+ else:
+ var distances_to_outer_center := ((size + drawer_size) / _angle_vec).abs()
+ var outer_distance := minf(distances_to_outer_center.x, distances_to_outer_center.y)
+ _outer_offset = (outer_distance * _angle_vec + (size - drawer_size)) * 0.5
+
+ _max_offset = (_outer_offset - _inner_offset).length()
+ _inner_offset = (_inner_offset + _angle_vec * open_margin).floor()
+ _outer_offset = (_outer_offset - _angle_vec * close_margin).floor()
+
+
+## Allows opening and closing the drawer.
+## [br][br]
+## Also see: [member state] and [method force_drawer].
+func toggle_drawer(open : bool) -> void:
+ _toggle_drawer(open)
+func _toggle_drawer(open : bool, drag_animate : bool = false) -> void:
+ slide_begin.emit()
+ _animate_to_progress(float(open), drag_animate)
+ _animation_tween.tween_callback(slide_end.emit)
+
+ if _state != open:
+ state_toggle_begin.emit(open)
+ _animation_tween.tween_callback(state_toggle_end.emit.bind(open))
+ _state = open
+func _animate_to_progress(
+ to_progress : float,
+ drag_animate : bool = false
+ ) -> void:
+ _kill_animation()
+ _animation_tween = create_tween()
+
+ if drag_animate:
+ _animation_tween.set_trans(drag_drawer_translate)
+ _animation_tween.set_ease(drag_drawer_ease)
+ _animation_tween.tween_method(
+ _animation_method,
+ get_progress(true),
+ to_progress * _max_offset,
+ drag_drawer_duration
+ )
+ else:
+ _animation_tween.set_trans(manual_drawer_translate)
+ _animation_tween.set_ease(manual_drawer_ease)
+ _animation_tween.tween_method(
+ _animation_method,
+ get_progress(true),
+ to_progress * _max_offset,
+ manual_drawer_duration
+ )
+func _kill_animation() -> void:
+ if _animation_tween && _animation_tween.is_running():
+ _animation_tween.kill()
+func _animation_method(progress : float) -> void:
+ _current_progress = progress
+ _progress_changed(get_progress_adjusted())
+ _adjust_children()
+
+
+
+## Allows opening and closing the drawer without animation.
+## [br][br]
+## Also see: [member state] and [method toggle_drawer].
+func force_drawer(open : bool) -> void:
+ _kill_animation()
+ _state = open
+ _animation_method(float(open) * _max_offset)
+
+
+
+func _init() -> void:
+ resized.connect(_calculate_childrend)
+ sort_children.connect(_calculate_childrend)
+
+ _angle_vec = Vector2.RIGHT.rotated(deg_to_rad(drawer_angle))
+func _get_minimum_size() -> Vector2:
+ _min_size = _find_minimum_size()
+ return _min_size
+func _get_property_list() -> Array[Dictionary]:
+ var ret : Array[Dictionary]
+
+ ret.append({
+ "name": "state",
+ "type": TYPE_BOOL,
+ "usage": PROPERTY_USAGE_EDITOR
+ })
+
+
+ ret.append({
+ "name": "Drawer Angle",
+ "type": TYPE_NIL,
+ "hint_string": "drawer_",
+ "usage": PROPERTY_USAGE_GROUP
+ })
+ ret.append({
+ "name": "drawer_angle",
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0, 360, 0.001, or_less, or_greater",
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "drawer_angle_axis_snap",
+ "type": TYPE_BOOL,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+
+ ret.append({
+ "name": "Drawer Span",
+ "type": TYPE_NIL,
+ "hint_string": "drawer_",
+ "usage": PROPERTY_USAGE_GROUP
+ })
+ ret.append({
+ "name": "drawer_width_by_pixel",
+ "type": TYPE_BOOL,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "drawer_width",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT
+ }.merged({} if drawer_width_by_pixel else {
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0, 1, 0.001, or_less, or_greater",
+ }))
+ ret.append({
+ "name": "drawer_height_by_pixel",
+ "type": TYPE_BOOL,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "drawer_height",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT
+ }.merged({} if drawer_height_by_pixel else {
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0, 1, 0.001, or_less, or_greater",
+ }))
+
+ ret.append({
+ "name": "Input Options",
+ "type": TYPE_NIL,
+ "usage": PROPERTY_USAGE_GROUP
+ })
+
+ ret.append({
+ "name": "action_mode",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_FLAGS,
+ "hint_string": "Press Action:1,Release Action:2,Drag Action:4",
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+ ret.append({
+ "name": "Margins",
+ "type": TYPE_NIL,
+ "usage": PROPERTY_USAGE_SUBGROUP
+ })
+ ret.append({
+ "name": "open_margin",
+ "type": TYPE_INT,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "close_margin",
+ "type": TYPE_INT,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+ ret.append({
+ "name": "Drag Options",
+ "type": TYPE_NIL,
+ "usage": PROPERTY_USAGE_SUBGROUP
+ })
+ ret.append({
+ "name": "allow_drag",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": _convert_to_enum(DragMode.keys()),
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "smooth_drag",
+ "type": TYPE_BOOL,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "drag_give",
+ "type": TYPE_INT,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+ ret.append({
+ "name": "Open Input",
+ "type": TYPE_NIL,
+ "hint_string": "",
+ "usage": PROPERTY_USAGE_SUBGROUP
+ })
+ ret.append({
+ "name": "open_bounds",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": _convert_to_enum(InputAreaMode.keys()),
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "open_drag_threshold",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0, 1, 1, or_greater",
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+ ret.append({
+ "name": "Close Input",
+ "type": TYPE_NIL,
+ "hint_string": "",
+ "usage": PROPERTY_USAGE_SUBGROUP
+ })
+ ret.append({
+ "name": "close_bounds",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": _convert_to_enum(InputAreaMode.keys()),
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "close_drag_threshold",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0, 1, 1, or_greater",
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+
+ ret.append({
+ "name": "Animation",
+ "type": TYPE_NIL,
+ "usage": PROPERTY_USAGE_GROUP
+ })
+
+ ret.append({
+ "name": "Manual Animation",
+ "type": TYPE_NIL,
+ "hint_string": "manual_drawer_",
+ "usage": PROPERTY_USAGE_SUBGROUP
+ })
+ ret.append({
+ "name": "manual_drawer_translate",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": _get_enum_string("Tween", "TransitionType"),
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "manual_drawer_ease",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": _get_enum_string("Tween", "EaseType"),
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "manual_drawer_duration",
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0, 1, 0.001, or_greater",
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+ ret.append({
+ "name": "Drag Animation",
+ "type": TYPE_NIL,
+ "hint_string": "drag_drawer_",
+ "usage": PROPERTY_USAGE_SUBGROUP
+ })
+ ret.append({
+ "name": "drag_drawer_translate",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": _get_enum_string("Tween", "TransitionType"),
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "drag_drawer_ease",
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": _get_enum_string("Tween", "EaseType"),
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ ret.append({
+ "name": "drag_drawer_duration",
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0, 1, 0.001, or_greater",
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+ return ret
+func _property_can_revert(property: StringName) -> bool:
+ return property in [
+ "state",
+ "drawer_angle",
+ "drawer_angle_axis_snap",
+ "drawer_width_by_pixel",
+ "drawer_width",
+ "drawer_height_by_pixel",
+ "drawer_height",
+ "action_mode",
+ "drag_give",
+ "open_margin",
+ "close_margin",
+ "allow_drag",
+ "smooth_drag",
+ "open_bounds",
+ "open_drag_threshold",
+ "close_bounds",
+ "close_drag_threshold",
+ "manual_drawer_translate",
+ "manual_drawer_ease",
+ "manual_drawer_duration",
+ "drag_drawer_translate",
+ "drag_drawer_ease",
+ "drag_drawer_duration"
+ ]
+func _property_get_revert(property: StringName) -> Variant:
+ match property:
+ "smooth_drag":
+ return true
+ "state", "drawer_width_by_pixel", "drawer_height_by_pixel", "drawer_angle_axis_snap":
+ return false
+
+ "drag_give", "open_margin", "close_margin":
+ return 0
+ "drawer_angle":
+ return 0.0
+ "manual_drawer_duration", "drag_drawer_duration":
+ return 0.2
+ "open_drag_threshold":
+ return 50
+ "close_drag_threshold":
+ return 50
+
+ "drawer_width":
+ return size.x if drawer_width_by_pixel else 1.0
+ "drawer_height":
+ return size.y if drawer_height_by_pixel else 1.0
+
+ "action_mode":
+ return ActionMode.ACTION_MODE_BUTTON_PRESS
+ "allow_drag":
+ return DragMode.ON_OPEN_OR_CLOSE
+ "open_bounds":
+ return InputAreaMode.WithinEmptyBounds
+ "close_bounds":
+ return InputAreaMode.WithinEmptyBounds
+
+ "manual_drawer_translate", "drag_drawer_translate":
+ return Tween.TransitionType.TRANS_LINEAR
+ "manual_drawer_ease", "drag_drawer_ease":
+ return Tween.EaseType.EASE_IN
+ return null
+func _get_enum_string(className : StringName, enumName : StringName) -> String:
+ var ret : String
+ for constant_name in ClassDB.class_get_enum_constants(className, enumName):
+ var constant_value: int = ClassDB.class_get_integer_constant(className, constant_name)
+ ret += "%s:%d, " % [constant_name, constant_value]
+ return ret.left(-2).replace("_", " ").capitalize().replace(", ", ",")
+func _convert_to_enum(strs : PackedStringArray) -> String:
+ return ", ".join(strs).replace("_", " ").capitalize().replace(", ", ",")
+
+
+
+func _confirm_input_accept(event : InputEvent, drag : bool = false) -> bool:
+ if mouse_filter == MouseFilter.MOUSE_FILTER_IGNORE: return false
+
+ var boundType : InputAreaMode
+ if _state:
+ boundType = close_bounds
+ if drag && !(allow_drag & DragMode.ON_CLOSE):
+ return false
+ else:
+ boundType = open_bounds
+ if drag && !(allow_drag & DragMode.ON_OPEN):
+ return false
+
+ match boundType:
+ InputAreaMode.Nowhere:
+ return false
+ InputAreaMode.Anywhere:
+ pass
+ InputAreaMode.WithinBounds:
+ if !get_rect().has_point(event.position):
+ return false
+ InputAreaMode.ExcludeDrawer:
+ if get_drawer_rect().has_point(event.position):
+ if mouse_filter == MouseFilter.MOUSE_FILTER_STOP:
+ get_viewport().set_input_as_handled()
+ return false
+ InputAreaMode.WithinEmptyBounds:
+ if get_rect().intersection(get_drawer_rect()).has_point(event.position):
+ if mouse_filter == MouseFilter.MOUSE_FILTER_STOP:
+ get_viewport().set_input_as_handled()
+ return false
+
+ if mouse_filter == MouseFilter.MOUSE_FILTER_STOP:
+ get_viewport().set_input_as_handled()
+ return true
+func _gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton || event is InputEventScreenTouch:
+ if event.pressed:
+ if !_is_dragging:
+ if action_mode & ActionMode.ACTION_MODE_BUTTON_PRESS:
+ if !_confirm_input_accept(event, false): return
+ _toggle_drawer(!is_open())
+ return
+
+ if action_mode & ActionMode.ACTION_MODE_BUTTON_DRAG:
+ if !_confirm_input_accept(event, true): return
+ drag_start.emit()
+ _is_dragging = true
+ else:
+ if action_mode & ActionMode.ACTION_MODE_BUTTON_RELEASE:
+ if _confirm_input_accept(event, false) && !_has_dragged:
+ _toggle_drawer(!is_open())
+
+ _has_dragged = false
+ _is_dragging = false
+ _drag_value = 0.0
+ return
+
+ if action_mode & ActionMode.ACTION_MODE_BUTTON_DRAG:
+ if _is_dragging:
+ drag_end.emit()
+ if is_open():
+ _toggle_drawer(_drag_value > -open_drag_threshold, true)
+ else:
+ _toggle_drawer(_drag_value > close_drag_threshold, true)
+
+ set_deferred("_has_dragged", false)
+ set_deferred("_is_dragging", false)
+ set_deferred("_drag_value", 0.0)
+ return
+
+ if _is_dragging:
+ if event is InputEventMouseMotion || event is InputEventScreenDrag:
+ var projected_scalar : float = event.relative.dot(_angle_vec) / _angle_vec.length_squared()
+ _drag_value += projected_scalar
+
+ if is_zero_approx(_drag_value):
+ _has_dragged = true
+
+ _progress_changed(get_progress_adjusted(true))
+ if smooth_drag: _adjust_children()
+
+
+
+# Overload Functions
+## Used by [method get_drawer_offset] to calculate the offset of the drawer, given the current progress.
+## Overload this method to create custom opening/closing behavior.
+func _get_drawer_offset(inner_offset : Vector2, outer_offset : Vector2, with_drag : bool = false) -> Vector2:
+ #return (outer_offset - inner_offset) * get_progress_adjusted(with_drag) + inner_offset
+ return inner_offset.lerp(outer_offset, get_progress_adjusted(with_drag))
+
+# Virtual Functions
+
+## A virtual function that is is called whenever the drawer progress changes.
+func _progress_changed(progress : float) -> void: pass
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/PaddingContainer/PaddingContainer.gd b/godot/addons/FreeControl/src/CustomClasses/PaddingContainer/PaddingContainer.gd
new file mode 100644
index 0000000..6f91fb7
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/PaddingContainer/PaddingContainer.gd
@@ -0,0 +1,222 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name PaddingContainer extends Container
+## A [Container] that provides percentage and numerical padding to it's children.
+
+
+## If [code]true[/code], this [Container]'s minimum size will update according to it's
+## children and numerical pixel padding.
+@export var minimum_size : bool = true:
+ set(val):
+ if minimum_size != val:
+ minimum_size = val
+
+ update_minimum_size()
+
+## The percentage left padding.
+var child_anchor_left : float = 0:
+ set(val):
+ if child_anchor_left != val:
+ child_anchor_left = val
+
+ child_anchor_right = max(val, child_anchor_right)
+## The percentage top padding.
+var child_anchor_top : float = 0:
+ set(val):
+ if child_anchor_top != val:
+ child_anchor_top = val
+
+ child_anchor_bottom = max(val, child_anchor_bottom)
+## The percentage right padding.
+var child_anchor_right : float = 1:
+ set(val):
+ if child_anchor_right != val:
+ child_anchor_right = val
+
+ child_anchor_left = min(val, child_anchor_left)
+## The percentage bottom padding.
+var child_anchor_bottom : float = 1:
+ set(val):
+ if child_anchor_bottom != val:
+ child_anchor_bottom = val
+
+ child_anchor_top = min(val, child_anchor_top)
+
+## The numerical pixel left padding.
+var child_offset_left : int = 0:
+ set(val):
+ if child_offset_left != val:
+ child_offset_left = val
+
+ update_minimum_size()
+## The numerical pixel top padding.
+var child_offset_top : int = 0:
+ set(val):
+ if child_offset_top != val:
+ child_offset_top = val
+
+ update_minimum_size()
+## The numerical pixel right padding.
+var child_offset_right : int = 0:
+ set(val):
+ if child_offset_right != val:
+ child_offset_right = val
+
+ update_minimum_size()
+## The numerical pixel bottom padding.
+var child_offset_bottom : int = 0:
+ set(val):
+ if child_offset_bottom != val:
+ child_offset_bottom = val
+
+ update_minimum_size()
+
+
+func _init() -> void:
+ sort_children.connect(_handel_resize)
+func _ready() -> void:
+ _handel_resize()
+func _get_property_list() -> Array[Dictionary]:
+ var properties : Array[Dictionary] = []
+
+ properties.append({
+ "name" = "Anchors",
+ "type" = TYPE_NIL,
+ "usage" = PROPERTY_USAGE_GROUP,
+ "hint_string" = "child_anchor_"
+ })
+ properties.append({
+ "name": "child_anchor_left",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001,or_greater,or_less"
+ })
+ properties.append({
+ "name": "child_anchor_top",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001,or_greater,or_less"
+ })
+ properties.append({
+ "name": "child_anchor_right",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001,or_greater,or_less"
+ })
+ properties.append({
+ "name": "child_anchor_bottom",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1,0.001,or_greater,or_less"
+ })
+
+ properties.append({
+ "name" = "Offsets",
+ "type" = TYPE_NIL,
+ "usage" = PROPERTY_USAGE_GROUP,
+ "hint_string" = "child_offset_"
+ })
+ properties.append({
+ "name": "child_offset_left",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ properties.append({
+ "name": "child_offset_top",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ properties.append({
+ "name": "child_offset_right",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+ properties.append({
+ "name": "child_offset_bottom",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_DEFAULT
+ })
+
+ return properties
+func _property_can_revert(property: StringName) -> bool:
+ if property in [
+ "child_anchor_left",
+ "child_anchor_top",
+ ]:
+ return self[property] != 0.0
+ elif property in [
+ "child_anchor_right",
+ "child_anchor_bottom",
+ ]:
+ return self[property] != 1.0
+ elif property in [
+ "child_offset_left",
+ "child_offset_top",
+ "child_offset_right",
+ "child_offset_bottom"
+ ]:
+ return self[property] != 0
+ return false
+func _property_get_revert(property: StringName) -> Variant:
+ if property in [
+ "child_anchor_left",
+ "child_anchor_top",
+ ]:
+ return 0.0
+ elif property in [
+ "child_anchor_right",
+ "child_anchor_bottom",
+ ]:
+ return 1.0
+ elif property in [
+ "child_offset_left",
+ "child_offset_top",
+ "child_offset_right",
+ "child_offset_bottom"
+ ]:
+ return 0
+ return null
+
+func _get_minimum_size() -> Vector2:
+ if !minimum_size: return Vector2.ZERO
+
+ var min : Vector2
+ for child : Node in get_children():
+ if child is Control:
+ min = min.max(child.get_combined_minimum_size())
+
+ min += Vector2(
+ child_offset_left + child_offset_right,
+ child_offset_top + child_offset_bottom
+ )
+
+ return min
+
+func _handel_resize() -> void:
+ for child in get_children():
+ if child is Control:
+ child.set_anchor_and_offset(
+ SIDE_LEFT,
+ child_anchor_left,
+ child_offset_left
+ )
+ child.set_anchor_and_offset(
+ SIDE_TOP,
+ child_anchor_top,
+ child_offset_top
+ )
+ child.set_anchor_and_offset(
+ SIDE_RIGHT,
+ child_anchor_right,
+ -child_offset_right
+ )
+ child.set_anchor_and_offset(
+ SIDE_BOTTOM,
+ child_anchor_bottom,
+ -child_offset_bottom
+ )
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/ProportionalContainer/ProportionalContainer.gd b/godot/addons/FreeControl/src/CustomClasses/ProportionalContainer/ProportionalContainer.gd
new file mode 100644
index 0000000..cb8d0e0
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/ProportionalContainer/ProportionalContainer.gd
@@ -0,0 +1,188 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name ProportionalContainer extends Container
+## A container that preserves the proportions of its [member ancher] size.
+## [br][br]
+## [b]WARNING[b]: Is this can cause crashes if misused. Try to use [PaddingContainer] instead, unless required.
+
+
+## The method this node will change in proportion of its [member ancher] size.
+enum PROPORTION_MODE {
+ NONE = 0b000, ## No action. Minimum size will be set at [constant Vector2.ZERO].
+ WIDTH = 0b101, ## Same as [WIDTH_PROPORTION], but also sets children height to be equal to the [member ancher]'s size's height.
+ WIDTH_PROPORTION = 0b001, ## Sets the minimum width to be equal to the [member ancher] width multipled by [member horizontal_ratio].
+ HEIGHT = 0b110, ## Same as [HEIGHT_PROPORTION], but also sets children height to be equal to the [member ancher]'s size's width.
+ HEIGHT_PROPORTION = 0b010, ## Sets the minimum height to be equal to the [member ancher] height multipled by [member vertical_ratio].
+ BOTH = 0b011 ## Sets the minimum size to be equal to the [member ancher] size multipled by [member horizontal_ratio] and [member vertical_ratio] respectively.
+}
+
+
+
+@export_group("Ancher")
+## The ancher node this container proportions itself to. Is used if [member ancher_to_parent] is [code]false[/code].
+## [br][br]
+## If [code]null[/code], then this container proportions itself to it's parent control size.
+@export var ancher : Control:
+ set(val):
+ if ancher != val:
+ if ancher && ancher.resized.is_connected(_sort_children):
+ ancher.resized.disconnect(_sort_children)
+ if val && !val.resized.is_connected(_sort_children):
+ val.resized.connect(_sort_children)
+ ancher = val
+ queue_sort()
+
+@export_group("Proportion")
+## The proportion mode used to scale itself to the [member ancher].
+@export var mode : PROPORTION_MODE = PROPORTION_MODE.NONE:
+ set(val):
+ if mode != val:
+ mode = val
+ notify_property_list_changed()
+ queue_sort()
+## The multiplicative of this node's width to the [member ancher] width.
+@export_range(0., 1., 0.001, "or_greater") var horizontal_ratio : float = 1.:
+ set(val):
+ if horizontal_ratio != val:
+ horizontal_ratio = val
+ queue_sort()
+## The multiplicative of this node's height to the [member ancher] height.
+@export_range(0., 1., 0.001, "or_greater") var vertical_ratio : float = 1.:
+ set(val):
+ if vertical_ratio != val:
+ vertical_ratio = val
+ queue_sort()
+
+
+
+var _min_size : Vector2
+var _ignore_resize : bool
+
+
+
+func _init() -> void:
+ layout_mode = 0
+ sort_children.connect(_sort_children)
+func _ready() -> void:
+ _sort_children()
+
+func _validate_property(property: Dictionary) -> void:
+ if property.name in [
+ "layout_mode",
+ "size",
+ ]:
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ elif property.name == "horizontal_ratio":
+ if !(mode & PROPORTION_MODE.WIDTH_PROPORTION):
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+ elif property.name == "vertical_ratio":
+ if !(mode & PROPORTION_MODE.HEIGHT_PROPORTION):
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+func _get_minimum_size() -> Vector2:
+ return _min_size
+
+
+
+func _sort_children() -> void:
+ if _ignore_resize: return
+ _ignore_resize = true
+
+ if mode != PROPORTION_MODE.NONE:
+ # Sets the min size according to it's dimentions and proportion mode
+
+ var ancher_size : Vector2 = get_parent_area_size()
+ if ancher: ancher_size = ancher.size
+ var child_min_size := _get_children_min_size()
+ var _old_min_size := _min_size
+
+ if mode & PROPORTION_MODE.WIDTH != 0:
+ _min_size.x = ancher_size.x * horizontal_ratio
+ else:
+ _min_size.x = child_min_size.x
+ if mode & PROPORTION_MODE.HEIGHT != 0:
+ _min_size.y = ancher_size.y * vertical_ratio
+ else:
+ _min_size.y = child_min_size.y
+
+ if _min_size != _old_min_size:
+ update_minimum_size()
+ elif _min_size != Vector2.ZERO:
+ # Sets min size to default
+
+ _min_size = Vector2.ZERO
+ update_minimum_size()
+
+ _ignore_resize = false
+ _fit_children()
+
+
+
+func _fit_children() -> void:
+ for child : Control in _get_control_children():
+ _fit_child(child)
+func _fit_child(child : Control) -> void:
+ var child_size := child.get_minimum_size()
+ var ancher_size : Vector2 = ancher.size if ancher else get_parent_area_size()
+ var set_pos : Vector2
+
+ # Gets the ancher_size according to this node's dimentions and proportion mode
+ if mode & PROPORTION_MODE.WIDTH_PROPORTION > 0:
+ ancher_size.x = ancher_size.x * horizontal_ratio
+
+ # Expands or repositions child, according to ancher and size flages
+ match child.size_flags_horizontal & ~SIZE_EXPAND:
+ SIZE_FILL:
+ child_size.x = ancher_size.x
+ set_pos.x = 0
+ SIZE_SHRINK_BEGIN:
+ child_size.x = max(child_size.x, ancher_size.x)
+ set_pos.x = 0
+ SIZE_SHRINK_CENTER:
+ child_size.x = max(child_size.x, ancher_size.x)
+ set_pos.x = max((ancher_size.x - child_size.x) * 0.5, 0)
+ SIZE_SHRINK_END:
+ child_size.x = max(child_size.x, ancher_size.x)
+ set_pos.x = max(ancher_size.x - child_size.x, 0)
+ if mode == PROPORTION_MODE.WIDTH:
+ child_size.y = size.y
+
+ # Gets the ancher_size according to this node's dimentions and proportion mode
+ if mode & PROPORTION_MODE.HEIGHT_PROPORTION > 0:
+ ancher_size.y = ancher_size.y * vertical_ratio
+
+ # Expands or repositions child, according to ancher and size flages
+ match child.size_flags_vertical & ~SIZE_EXPAND:
+ SIZE_FILL:
+ child_size.y = ancher_size.y
+ set_pos.y = 0
+ SIZE_SHRINK_BEGIN:
+ child_size.y = max(child_size.y, ancher_size.y)
+ set_pos.y = 0
+ SIZE_SHRINK_CENTER:
+ child_size.y = max(child_size.y, ancher_size.y)
+ set_pos.y = max((size.y - child_size.y) * 0.5, 0)
+ SIZE_SHRINK_END:
+ child_size.y = max(child_size.y, ancher_size.y)
+ set_pos.y = max(size.y - child_size.y, 0)
+ if mode == PROPORTION_MODE.HEIGHT:
+ child_size.x = size.x
+
+ fit_child_in_rect(child, Rect2(set_pos, child_size))
+func _get_children_min_size() -> Vector2:
+ var ret := Vector2.ZERO
+ for child : Control in _get_control_children():
+ ret = ret.max(child.get_combined_minimum_size())
+ return ret
+func _get_control_children() -> Array[Control]:
+ var ret : Array[Control]
+ ret.assign(get_children().filter(func(child : Node): return child is Control && child.visible))
+ return ret
+
+
+
+func _get_allowed_size_flags_horizontal() -> PackedInt32Array:
+ return [SIZE_FILL, SIZE_SHRINK_BEGIN, SIZE_SHRINK_CENTER, SIZE_SHRINK_END]
+func _get_allowed_size_flags_vertical() -> PackedInt32Array:
+ return [SIZE_FILL, SIZE_SHRINK_BEGIN, SIZE_SHRINK_CENTER, SIZE_SHRINK_END]
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/Routers/Base/Page.gd b/godot/addons/FreeControl/src/CustomClasses/Routers/Base/Page.gd
new file mode 100644
index 0000000..474ec89
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Routers/Base/Page.gd
@@ -0,0 +1,77 @@
+@tool
+class_name Page extends Container
+## A standardized [Container] node for Routers to use, such as [RouterStack].
+
+
+## Emits when an event is requested to the attached Router parent.
+## [br][br]
+## If this Router is a decedent of another [Page], connect that [Page]'s
+## [method emit_event] with this [Signal].
+signal event_action(event_name : String, args : Variant)
+
+@warning_ignore("unused_signal")
+## Emits when this page is added as a child and finished animation by a Router.
+signal entered
+@warning_ignore("unused_signal")
+## Emits when this page is added as a child.
+signal entering
+@warning_ignore("unused_signal")
+## Emits when this page is about to be removed as a child and finished animation by a Router.
+signal exited
+@warning_ignore("unused_signal")
+## Emits when this page is marked to be removed as a child.
+signal exiting
+
+
+
+## Requests an event to the attached Router parent.
+func emit_event(event_name : String, args : Variant) -> void:
+ event_action.emit(event_name, args)
+
+
+
+func _enter_tree() -> void:
+ if !Engine.is_editor_hint():
+ clip_contents = true
+func _init() -> void:
+ sort_children.connect(_sort_children)
+
+
+
+func _sort_children() -> void:
+ for child : Node in get_children():
+ if child is Control: _update_child(child)
+func _update_child(child : Control):
+ var child_min_size := child.get_minimum_size()
+ var result_size := child_min_size
+
+ var set_pos : Vector2
+ match child.size_flags_horizontal & ~SIZE_EXPAND:
+ SIZE_FILL:
+ result_size.x = max(result_size.x, size.x)
+ set_pos.x = (size.x - result_size.x) * 0.5
+ SIZE_SHRINK_BEGIN:
+ set_pos.x = 0
+ SIZE_SHRINK_CENTER:
+ set_pos.x = (size.x - result_size.x) * 0.5
+ SIZE_SHRINK_END:
+ set_pos.x = size.x - result_size.x
+ match child.size_flags_vertical & ~SIZE_EXPAND:
+ SIZE_FILL:
+ result_size.y = max(result_size.y, size.y)
+ set_pos.y = (size.y - result_size.y) * 0.5
+ SIZE_SHRINK_BEGIN:
+ set_pos.y = 0
+ SIZE_SHRINK_CENTER:
+ set_pos.y = (size.y - result_size.y) * 0.5
+ SIZE_SHRINK_END:
+ set_pos.y = size.y - result_size.y
+
+ fit_child_in_rect(child, Rect2(set_pos, result_size))
+
+
+
+func _get_allowed_size_flags_horizontal() -> PackedInt32Array:
+ return [SIZE_FILL, SIZE_SHRINK_BEGIN, SIZE_SHRINK_CENTER, SIZE_SHRINK_END]
+func _get_allowed_size_flags_vertical() -> PackedInt32Array:
+ return [SIZE_FILL, SIZE_SHRINK_BEGIN, SIZE_SHRINK_CENTER, SIZE_SHRINK_END]
diff --git a/godot/addons/FreeControl/src/CustomClasses/Routers/Base/PageInfo.gd b/godot/addons/FreeControl/src/CustomClasses/Routers/Base/PageInfo.gd
new file mode 100644
index 0000000..c6db831
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Routers/Base/PageInfo.gd
@@ -0,0 +1,43 @@
+@tool
+class_name PageInfo extends Resource
+## A [Resource] for keeping stack of [Page] information for a Router, such as [RouterStack].
+
+
+var _page : Control
+var _auto_clean : bool
+var _enter_animate : SwapContainer.ANIMATION_TYPE
+var _exit_animate : SwapContainer.ANIMATION_TYPE
+
+
+## Static create function for this [Resource].
+static func create(
+ page: Page,
+ enter_animate : SwapContainer.ANIMATION_TYPE,
+ exit_animate : SwapContainer.ANIMATION_TYPE,
+ auto_clean : bool
+) -> PageInfo:
+ var info = PageInfo.new()
+ info._page = page
+ info._enter_animate = enter_animate
+ info._exit_animate = exit_animate
+ info._auto_clean = auto_clean
+
+ return info
+
+## Gets the current [Page] held by this [Resource].
+func get_page() -> Page: return _page
+## Gets the saved enter animation.
+func get_enter_animation() -> SwapContainer.ANIMATION_TYPE: return _enter_animate
+## Gets the saved exit animation.
+func get_exit_animation() -> SwapContainer.ANIMATION_TYPE: return _exit_animate
+
+
+func _notification(what):
+ if (
+ what == NOTIFICATION_PREDELETE &&
+ _auto_clean &&
+ _page &&
+ is_instance_valid(_page)
+ ):
+ _page.queue_free()
+ _page = null
diff --git a/godot/addons/FreeControl/src/CustomClasses/Routers/RouterStack.gd b/godot/addons/FreeControl/src/CustomClasses/Routers/RouterStack.gd
new file mode 100644
index 0000000..4509ce6
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/Routers/RouterStack.gd
@@ -0,0 +1,427 @@
+@tool
+class_name RouterStack extends PanelContainer
+## Handles a [Control] stack, between [Page] nodes, using [SwapContainer].
+
+## The Animation type to transition with.
+const ANIMATION_TYPE = SwapContainer.ANIMATION_TYPE
+
+## Emits when the current [Page] requests an event.
+signal event_action(event : String, args : Variant)
+
+## Emits at the start of a transition.
+signal start_animation
+## Emits at the end of a transition.
+signal end_animation
+
+## The filepath to the [Page] node this to load on ready. If this path is invaild,
+## or not to a [PackedScene] with a [Page] node root, nothing will be loaded on ready.
+@export_file("*.tscn") var starting_page : String:
+ set(val):
+ if val != starting_page:
+ starting_page = val
+ if Engine.is_editor_hint() && is_node_ready():
+ _clear_all_pages()
+ if ResourceLoader.exists(starting_page) && starting_page.get_extension() == "tscn":
+ route(starting_page, ANIMATION_TYPE.NONE, ANIMATION_TYPE.NONE)
+## The max size of the stack. If the stack is too big, it will clear the oldest on
+## the stack first.
+@export_range(0, 1000, 1, "or_greater") var max_stack : int = 50:
+ set(val):
+ val = max(val, 1)
+ if max_stack != val:
+ max_stack = val
+
+
+@export_group("Animation")
+## Starts animation with the [Control] node outside of the visisble screen.
+@export var from_outside_screen : bool:
+ set(val):
+ if val != from_outside_screen:
+ from_outside_screen = val
+ _stack.from_outside_screen = val
+## Starts animation an offset of this amount of pixels (away from the center), start
+## at the position the [Control] originally would be placed at.
+@export var offset : float:
+ set(val):
+ if val != offset:
+ offset = val
+ _stack.offset = val
+
+@export_group("Easing")
+## The [enum Tween.EaseType] that will be used as the new [Control] transitions in.
+@export var ease_enter : Tween.EaseType = Tween.EaseType.EASE_IN_OUT:
+ set(val):
+ if val != ease_enter:
+ ease_enter = val
+ _stack.ease_enter = val
+## The [enum Tween.EaseType] that will be used as the current [Control] transitions out.
+@export var ease_exit : Tween.EaseType = Tween.EaseType.EASE_IN_OUT:
+ set(val):
+ if val != ease_exit:
+ ease_exit = val
+ _stack.ease_exit = val
+
+@export_group("Transition")
+## The [enum Tween.TransitionType] that will be used as the new [Control] transitions in.
+@export var transition_enter : Tween.TransitionType = Tween.TransitionType.TRANS_CUBIC:
+ set(val):
+ if val != transition_enter:
+ transition_enter = val
+ _stack.transition_enter = val
+## The [enum Tween.TransitionType] that will be used as the current [Control] transitions out.
+@export var transition_exit : Tween.TransitionType = Tween.TransitionType.TRANS_CUBIC:
+ set(val):
+ if val != transition_exit:
+ transition_exit = val
+ _stack.transition_exit = val
+
+@export_group("Duration")
+## The duration of the animation used as the new [Control] transitions in.
+@export var duration_enter : float = 0.35:
+ set(val):
+ if val != duration_enter:
+ duration_enter = val
+ _stack.duration_enter = val
+## The duration of the animation used as the current [Control] transitions out.
+@export var duration_exit : float = 0.35:
+ set(val):
+ if val != duration_exit:
+ duration_exit = val
+ _stack.duration_exit = val
+
+
+var _page_stack : Array[PageInfo] = []
+var _params : Dictionary = {}
+var _stack : SwapContainer
+
+
+
+## Emits the [Signal Page.entered] signal on the current [Page] displayed.
+## [br][br]
+## If this Router is a decedent of another [Page], connect that [Page]'s
+## [Signal Page.entered] with this method.
+func emit_entered() -> void:
+ var curr_page : Page = null if _page_stack.is_empty() else _page_stack[0].get_page()
+ if curr_page:
+ curr_page.entered.emit()
+## Emits the [Signal Page.entering] signal on the current [Page] displayed.
+## [br][br]
+## If this Router is a decedent of another [Page], connect that [Page]'s
+## [Signal Page.entering] with this method.
+func emit_entering() -> void:
+ var curr_page : Page = null if _page_stack.is_empty() else _page_stack[0].get_page()
+ if curr_page:
+ curr_page.entering.emit()
+## Emits the [Signal Page.exited] signal on the current [Page] displayed.
+## [br][br]
+## If this Router is a decedent of another [Page], connect that [Page]'s
+## [Signal Page.exited] with this method.
+func emit_exited() -> void:
+ var curr_page : Page = null if _page_stack.is_empty() else _page_stack[0].get_page()
+ if curr_page:
+ curr_page.exited.emit()
+## Emits the [Signal Page.exiting] signal on the current [Page] displayed.
+## [br][br]
+## If this Router is a decedent of another [Page], connect that [Page]'s
+## [Signal Page.exiting] with this method.
+func emit_exiting() -> void:
+ var curr_page : Page = null if _page_stack.is_empty() else _page_stack[0].get_page()
+ if curr_page:
+ curr_page.exiting.emit()
+
+
+## Routes to a [Page] node given by a file path to a [PackedScene].
+func route(
+ page_path : String,
+ enter_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ exit_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ params : Dictionary = {},
+ args : Dictionary = {}
+) -> Page:
+ var packed : PackedScene = await _ResourceLoader.new(get_tree().process_frame, page_path).finished
+ if packed == null:
+ push_error("An error occured while attempting to load file at filepath '", page_path, "'")
+ return null
+
+ return await route_packed(
+ packed,
+ enter_animation,
+ exit_animation,
+ params,
+ args
+ )
+## Routes to a [Page] node given by a [PackedScene].
+func route_packed(
+ page_scene : PackedScene,
+ enter_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ exit_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ params : Dictionary = {},
+ args : Dictionary = {}
+) -> Page:
+ if page_scene == null:
+ push_error("page_scene cannot be 'null'")
+ return null
+
+ return await route_node(
+ page_scene.instantiate(),
+ enter_animation,
+ exit_animation,
+ params,
+ args
+ )
+## Routes to a given [Page] node.
+func route_node(
+ page : Page,
+ enter_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ exit_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ params : Dictionary = {},
+ args : Dictionary = {}
+) -> Page:
+ if !page:
+ push_error("page cannot be 'null'")
+ return null
+ _params = params
+
+ var enter_page : PageInfo = PageInfo.create(
+ page,
+ enter_animation,
+ exit_animation,
+ args.get("auto_clean", true)
+ )
+
+ _stack.set_modifers(args)
+ _append_to_page_queue(enter_page)
+
+ await _handle_swap(
+ enter_page.get_page(),
+ enter_animation,
+ exit_animation
+ )
+
+ return page
+
+
+## Routes to a [Page] node given by a file path to a [PackedScene]. Clears stack.
+func navigate(
+ page_path : String,
+ enter_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ exit_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ params : Dictionary = {},
+ args : Dictionary = {}
+) -> Page:
+ var packed : PackedScene = await _ResourceLoader.new(get_tree().process_frame, page_path).finished
+ if packed == null:
+ push_error("An error occured while attempting to load file at filepath '", page_path, "'")
+ return null
+
+ return await navigate_packed(
+ packed,
+ enter_animation,
+ exit_animation,
+ params,
+ args
+ )
+## Routes to a [Page] node given by a [PackedScene]. Clears stack.
+func navigate_packed(
+ page_scene : PackedScene,
+ enter_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ exit_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ params : Dictionary = {},
+ args : Dictionary = {}
+) -> Page:
+ if page_scene == null:
+ push_error("page_scene cannot be 'null'")
+ return null
+
+ return await navigate_node(
+ page_scene.instantiate(),
+ enter_animation,
+ exit_animation,
+ params,
+ args
+ )
+## Routes to a given [Page] node. Clears stack.
+func navigate_node(
+ page : Page,
+ enter_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ exit_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ params : Dictionary = {},
+ args : Dictionary = {}
+) -> Page:
+ if !page:
+ push_error("page cannot be 'null'")
+ return null
+ _params = params
+
+ var enter_info : PageInfo = PageInfo.create(
+ page,
+ enter_animation,
+ exit_animation,
+ args.get("auto_clean", true)
+ )
+
+ _stack.set_modifers(args)
+ _append_to_page_queue(enter_info)
+
+ await _handle_swap(
+ enter_info.get_page(),
+ enter_animation,
+ exit_animation
+ )
+ _clear_stack()
+
+ return page
+
+
+## Routes to the previous [Control] on the stack. If the stack is empty, nothing will happen.
+func back(
+ enter_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ exit_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ params : Dictionary = {},
+ args : Dictionary = {}
+) -> void:
+ if is_empty(): return
+ _params = params
+ _stack.set_modifers(args)
+
+ var exit_page : PageInfo = _page_stack.pop_back()
+ var enter_page : PageInfo = _page_stack.back()
+
+ enter_page.get_page().event_action.connect(event_action.emit)
+
+ if enter_animation == ANIMATION_TYPE.DEFAULT:
+ enter_animation = _reverse_animate(exit_page.get_exit_animation())
+ if exit_animation == ANIMATION_TYPE.DEFAULT:
+ exit_animation = _reverse_animate(exit_page.get_enter_animation())
+
+ await _handle_swap(
+ enter_page.get_page(),
+ enter_animation,
+ exit_animation,
+ false
+ )
+
+
+## Gets the parameters of the last route.
+func get_params() -> Dictionary:
+ return _params
+## Gets the current stack size.
+func stack_size() -> int:
+ return _page_stack.size()
+## Returns if the stack is empty.
+func is_empty() -> bool:
+ return _page_stack.size() <= 1
+## Returns the current [Page] on display.
+func get_current_page() -> PageInfo:
+ return null if _page_stack.is_empty() else _page_stack.back()
+
+
+func _handle_swap(
+ enter_page : Page,
+ enter_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ exit_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ front : bool = true
+) -> void:
+ var exit_page : Page = _stack.get_current()
+
+ if enter_page:
+ enter_page.entering.emit()
+ if exit_page:
+ exit_page.exiting.emit()
+
+ await _stack.swap_control(
+ enter_page,
+ enter_animation,
+ exit_animation,
+ front
+ )
+
+ if enter_page:
+ enter_page.entered.emit()
+ if exit_page:
+ exit_page.exited.emit()
+
+
+
+func _append_to_page_queue(page_node: PageInfo) -> void:
+ if !_page_stack.is_empty():
+ _page_stack.back().get_page().event_action.disconnect(event_action.emit)
+ if _page_stack.size() > max_stack:
+ _page_stack.pop_front()
+ _page_stack.append(page_node)
+
+ var page := page_node.get_page()
+ if !page.event_action.is_connected(event_action.emit):
+ page.event_action.connect(event_action.emit)
+
+func _reverse_animate(animation : ANIMATION_TYPE) -> ANIMATION_TYPE:
+ match animation:
+ ANIMATION_TYPE.NONE:
+ return ANIMATION_TYPE.NONE
+ ANIMATION_TYPE.LEFT:
+ return ANIMATION_TYPE.RIGHT
+ ANIMATION_TYPE.RIGHT:
+ return ANIMATION_TYPE.LEFT
+ ANIMATION_TYPE.TOP:
+ return ANIMATION_TYPE.BOTTOM
+ ANIMATION_TYPE.BOTTOM:
+ return ANIMATION_TYPE.TOP
+ return ANIMATION_TYPE.NONE
+
+
+func _clear_stack() -> void:
+ _page_stack = [_page_stack.back()]
+func _clear_all_pages() -> void:
+ _page_stack = []
+
+
+
+func _init() -> void:
+ _stack = SwapContainer.new()
+ add_child(_stack)
+ _stack.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+
+ _stack.start_animation.connect(start_animation.emit)
+ _stack.end_animation.connect(end_animation.emit)
+
+ _stack.from_outside_screen = from_outside_screen
+ _stack.offset = offset
+
+ _stack.ease_enter = ease_enter
+ _stack.ease_exit = ease_exit
+
+ _stack.transition_enter = transition_enter
+ _stack.transition_exit = transition_exit
+
+ _stack.duration_enter = duration_enter
+ _stack.duration_exit = duration_exit
+
+
+
+class _ResourceLoader:
+ signal finished(scene : PackedScene)
+
+ var _resource_name : String
+
+ func _init(check_signal : Signal, path : StringName) -> void:
+ _resource_name = path
+
+ if !ResourceLoader.exists(_resource_name):
+ push_error("Error - Invaild Resource Loaded")
+ check_signal.connect(_delay_failsave, CONNECT_ONE_SHOT)
+ return
+
+ check_signal.connect(_on_signal)
+ ResourceLoader.load_threaded_request(
+ _resource_name,
+ "PackedScene"
+ )
+
+ func _on_signal() -> void:
+ match ResourceLoader.load_threaded_get_status(_resource_name):
+ ResourceLoader.THREAD_LOAD_INVALID_RESOURCE, ResourceLoader.THREAD_LOAD_FAILED:
+ finished.emit(null)
+ ResourceLoader.THREAD_LOAD_LOADED:
+ finished.emit(ResourceLoader.load_threaded_get(_resource_name))
+ func _delay_failsave() -> void:
+ finished.emit(null)
diff --git a/godot/addons/FreeControl/src/CustomClasses/SizeController/MaxRatioContainer.gd b/godot/addons/FreeControl/src/CustomClasses/SizeController/MaxRatioContainer.gd
new file mode 100644
index 0000000..a682f83
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/SizeController/MaxRatioContainer.gd
@@ -0,0 +1,77 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name MaxRatioContainer extends MaxSizeContainer
+## A container that limits an axis of it's size, to a maximum value, relative
+## to the value of it's other axis.
+
+## The behavior this node will exhibit based on an axis.
+enum MAX_RATIO_MODE {
+ NONE, ## No maximum value for either axis on this container.
+ WIDTH, ## Sets and expands children height to be proportionate of width.
+ WIDTH_PROPORTION, ## Sets the maximum height value of this container to be proportionate of width.
+ HEIGHT, ## Sets and expands children width to be proportionate of height.
+ HEIGHT_PROPORTION ## Sets the maximum width value of this container to be proportionate of height.
+}
+
+## The ratio mode used to expand and limit children.
+@export var mode : MAX_RATIO_MODE = MAX_RATIO_MODE.NONE:
+ set(val):
+ if val != mode:
+ mode = val
+ queue_sort()
+## The ratio value used to expand and limit children.
+@export_range(0.001, 10, 0.001, "or_greater") var ratio : float = 1.0:
+ set(val):
+ if val != ratio:
+ ratio = val
+ queue_sort()
+
+func _validate_property(property: Dictionary) -> void:
+ if property.name == "max_size":
+ property.usage |= PROPERTY_USAGE_READ_ONLY
+func _get_minimum_size() -> Vector2:
+ var parent := get_parent_area_size()
+ var min_size := super()
+
+ var current_size := min_size.max(size)
+ match mode:
+ MAX_RATIO_MODE.NONE:
+ current_size = Vector2(0, 0)
+ MAX_RATIO_MODE.WIDTH, MAX_RATIO_MODE.WIDTH_PROPORTION:
+ current_size = Vector2(0, minf(current_size.x * ratio, parent.y))
+ MAX_RATIO_MODE.HEIGHT, MAX_RATIO_MODE.HEIGHT_PROPORTION:
+ current_size = Vector2(minf(current_size.y * ratio, parent.x), 0)
+
+ min_size = min_size.max(current_size)
+ return min_size
+
+
+
+## Updates the _max_size according to the ratio mode and current dimentions
+func _update_children() -> void:
+ var parent := get_parent_area_size()
+ var min_size := get_combined_minimum_size()
+
+ # Adjusts max_size itself accouring to the ratio mode and current dimentions
+ match mode:
+ MAX_RATIO_MODE.NONE:
+ _max_size = Vector2(-1, -1)
+ MAX_RATIO_MODE.WIDTH:
+ _max_size = Vector2(-1, minf(size.x * ratio, parent.y))
+ MAX_RATIO_MODE.WIDTH_PROPORTION:
+ _max_size = Vector2(-1, min(size.x * ratio, parent.y, min_size.y))
+ MAX_RATIO_MODE.HEIGHT:
+ _max_size = Vector2(minf(size.y * ratio, parent.x), -1)
+ MAX_RATIO_MODE.HEIGHT_PROPORTION:
+ _max_size = Vector2(min(size.y * ratio, parent.x, min_size.x), -1)
+
+ var new_size := size
+ if _max_size.x >= 0:
+ new_size.x = _max_size.x
+ if _max_size.y >= 0:
+ new_size.y = _max_size.y
+ set_deferred("size", new_size)
+
+ super()
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/SizeController/MaxSizeContainer.gd b/godot/addons/FreeControl/src/CustomClasses/SizeController/MaxSizeContainer.gd
new file mode 100644
index 0000000..eaeba1d
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/SizeController/MaxSizeContainer.gd
@@ -0,0 +1,78 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name MaxSizeContainer extends Container
+## A container that limits it's size to a maximum value.
+
+var _max_size := -Vector2.ONE
+## The maximum size this container can possess.
+## [br][br]
+## If one of the axis is [code]-1[/code], then it is boundless.
+@export var max_size : Vector2 = -Vector2.ONE:
+ get: return _max_size
+ set(val):
+ _max_size = val
+ queue_sort()
+
+func _init() -> void:
+ sort_children.connect(_handle_sort, CONNECT_DEFERRED)
+func _set(property: StringName, value: Variant) -> bool:
+ if property == "size":
+ return true
+ return false
+func _get_minimum_size() -> Vector2:
+ var min_size : Vector2 = Vector2.ZERO
+ for child : Control in _get_control_children():
+ min_size = min_size.max(child.get_combined_minimum_size())
+ return min_size
+func _get_control_children() -> Array[Control]:
+ var ret : Array[Control]
+ ret.assign(get_children().filter(func(child : Node): return child is Control && child.visible))
+ return ret
+
+
+
+## A helper function that should be called whenever this node's size needs to be changed, or when it's children are changed.
+func _handle_sort() -> void:
+ update_minimum_size()
+ _update_children()
+
+func _update_children() -> void:
+ for child : Control in _get_control_children():
+ _update_child(child)
+func _update_child(child : Control):
+ var child_min_size := child.get_minimum_size()
+ var set_pos : Vector2
+ var result_size := Vector2(
+ size.x if _max_size.x < 0 else minf(size.x, _max_size.x),
+ size.y if _max_size.y < 0 else minf(size.y, _max_size.y)
+ )
+
+ if child.size_flags_horizontal & SIZE_EXPAND_FILL:
+ result_size.x = maxf(result_size.x, child_min_size.x)
+ else:
+ result_size.x = minf(result_size.x, child_min_size.x)
+ if child.size_flags_vertical & SIZE_EXPAND_FILL:
+ result_size.y = maxf(result_size.y, child_min_size.y)
+ else:
+ result_size.y = minf(result_size.y, child_min_size.y)
+
+
+ match child.size_flags_horizontal & ~SIZE_EXPAND:
+ SIZE_SHRINK_CENTER, SIZE_FILL:
+ set_pos.x = (size.x - result_size.x) * 0.5
+ SIZE_SHRINK_BEGIN:
+ set_pos.x = 0
+ SIZE_SHRINK_END:
+ set_pos.x = size.x - result_size.x
+ match child.size_flags_vertical & ~SIZE_EXPAND:
+ SIZE_SHRINK_CENTER, SIZE_FILL:
+ set_pos.y = (size.y - result_size.y) * 0.5
+ SIZE_SHRINK_BEGIN:
+ set_pos.y = 0
+ SIZE_SHRINK_END:
+ set_pos.y = size.y - result_size.y
+
+ child.position = set_pos
+ child.size = result_size
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/SwapContainer/SwapContainer.gd b/godot/addons/FreeControl/src/CustomClasses/SwapContainer/SwapContainer.gd
new file mode 100644
index 0000000..ab94e29
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/SwapContainer/SwapContainer.gd
@@ -0,0 +1,297 @@
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
+@tool
+class_name SwapContainer extends Container
+## A [Container] node that provides transitions between different [Control] nodes.
+
+## The Animation type to transition with.
+enum ANIMATION_TYPE {
+ DEFAULT, ## The same as [constant LEFT].
+ NONE, ## No Transition
+ LEFT, ## Either moves towards or away the left
+ RIGHT, ## Either moves towards or away the right
+ TOP, ## Either moves towards or away the top
+ BOTTOM ## Either moves towards or away the bottom
+}
+
+## Emits at the start of a transition.
+signal start_animation
+## Emits at the end of a transition.
+signal end_animation
+
+
+var _enter_tween : Tween = null
+var _exit_tween : Tween = null
+var _current_node : Control
+
+
+@export_group("Animation")
+## Starts animation with the [Control] node outside of the visisble screen.
+@export var from_outside_screen : bool
+## Starts animation an offset of this amount of pixels (away from the center), start
+## at the position the [Control] originally would be placed at.
+@export var offset : float
+
+@export_group("Easing")
+## The [enum Tween.EaseType] that will be used as the new [Control] transitions in.
+@export var ease_enter : Tween.EaseType = Tween.EaseType.EASE_IN_OUT
+## The [enum Tween.EaseType] that will be used as the current [Control] transitions out.
+@export var ease_exit : Tween.EaseType = Tween.EaseType.EASE_IN_OUT
+
+@export_group("Transition")
+## The [enum Tween.TransitionType] that will be used as the new [Control] transitions in.
+@export var transition_enter : Tween.TransitionType = Tween.TransitionType.TRANS_CUBIC
+## The [enum Tween.TransitionType] that will be used as the current [Control] transitions out.
+@export var transition_exit : Tween.TransitionType = Tween.TransitionType.TRANS_CUBIC
+
+@export_group("Duration")
+## The duration of the animation used as the new [Control] transitions in.
+@export var duration_enter : float = 0.35
+## The duration of the animation used as the current [Control] transitions out.
+@export var duration_exit : float = 0.35
+
+
+## Causes the current [Control] node to transition out and [param node]
+## to transition in.
+func swap_control(
+ node : Control,
+ enter_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ exit_animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+ front : bool = true
+) -> Control:
+ var _old_node := _current_node
+ _parent_control(node, front)
+
+ start_animation.emit()
+ _current_node = node
+ await _perform_animations(
+ node,
+ _old_node,
+ enter_animation,
+ exit_animation
+ )
+
+ if _old_node: _unparent_control(_old_node)
+ end_animation.emit()
+
+ return _old_node
+## Sets all export members with a simple [Dictionary].
+func set_modifers(args : Dictionary) -> void:
+ if args.has("ease_enter"):
+ ease_enter = args.get("ease_enter")
+ if args.has("ease_exit"):
+ ease_exit = args.get("ease_exit")
+
+ if args.has("transition_enter"):
+ transition_enter = args.get("transition_enter")
+ if args.has("transition_exit"):
+ transition_exit = args.get("transition_exit")
+
+ if args.has("duration_enter"):
+ duration_enter = args.get("duration_enter")
+ if args.has("duration_exit"):
+ duration_exit = args.get("duration_exit")
+
+
+## Gets the current [Control] displayed. [code]null[/code] if there is currently no such
+## [Control].
+func get_current() -> Control:
+ return _current_node
+
+
+
+func _parent_control(node: Control, front : bool) -> void:
+ if !node: return
+ if !node.get_parent():
+ add_child(node)
+
+ if is_inside_tree():
+ node.hide()
+ get_tree().process_frame.connect(node.show, CONNECT_ONE_SHOT)
+
+ if front:
+ node.move_to_front()
+ else:
+ move_child(node, 0)
+
+ node.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+func _unparent_control(node: Control) -> void:
+ remove_child(node)
+
+
+
+func _perform_animations(
+ enter_node: Control,
+ exit_node: Control,
+ enter_animation: ANIMATION_TYPE,
+ exit_animation: ANIMATION_TYPE
+) -> void:
+ if enter_animation == ANIMATION_TYPE.NONE && exit_animation == ANIMATION_TYPE.NONE:
+ if enter_node:
+ enter_node.position = Vector2.ZERO
+ return
+ _tween_settup()
+
+ if enter_node:
+ _handle_enter_animation(
+ enter_node,
+ _enter_tween,
+ enter_animation
+ )
+ else:
+ _enter_tween.finished.emit()
+ _enter_tween.kill()
+ _enter_tween = null
+ if exit_node:
+ _handle_exit_animation(
+ exit_node,
+ _exit_tween,
+ exit_animation,
+ )
+ else:
+ _exit_tween.finished.emit()
+ _exit_tween.kill()
+ _exit_tween = null
+
+ await _await_animations()
+func _handle_enter_animation(
+ node: Control,
+ animation_tween : Tween,
+ animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+) -> void:
+ var border := _get_border()
+
+ match animation:
+ ANIMATION_TYPE.DEFAULT, ANIMATION_TYPE.LEFT:
+ animation_tween.tween_method(
+ func (val): node.position.x = val,
+ border.position.x,
+ 0,
+ duration_enter
+ )
+ ANIMATION_TYPE.RIGHT:
+ animation_tween.tween_method(
+ func (val): node.position.x = val,
+ border.size.x - border.position.x,
+ 0,
+ duration_enter
+ )
+ ANIMATION_TYPE.TOP:
+ animation_tween.tween_method(
+ func (val): node.position.y = val,
+ border.position.y,
+ 0,
+ duration_enter
+ )
+ ANIMATION_TYPE.BOTTOM:
+ animation_tween.tween_method(
+ func (val): node.position.y = val,
+ border.size.y - border.position.y,
+ 0,
+ duration_enter
+ )
+ _:
+ node.position = Vector2.ZERO
+ return
+
+ animation_tween.play()
+func _handle_exit_animation(
+ node: Control,
+ animation_tween : Tween,
+ animation: ANIMATION_TYPE = ANIMATION_TYPE.DEFAULT,
+) -> void:
+ var border : Rect2 = _get_border()
+
+ match animation:
+ ANIMATION_TYPE.DEFAULT, ANIMATION_TYPE.LEFT:
+ animation_tween.tween_method(
+ func (val): node.position.x = val,
+ 0,
+ border.size.x - border.position.x,
+ duration_enter
+ )
+ ANIMATION_TYPE.RIGHT:
+ animation_tween.tween_method(
+ func (val): node.position.x = val,
+ 0,
+ border.position.x,
+ duration_enter
+ )
+ ANIMATION_TYPE.TOP:
+ animation_tween.tween_method(
+ func (val): node.position.y = val,
+ 0,
+ border.size.y - border.position.y,
+ duration_enter
+ )
+ ANIMATION_TYPE.BOTTOM:
+ animation_tween.tween_method(
+ func (val): node.position.y = val,
+ 0,
+ border.position.y,
+ duration_enter
+ )
+ _:
+ node.position = Vector2.ZERO
+ return
+
+ animation_tween.play()
+
+
+func _tween_settup() -> void:
+ if _enter_tween && _enter_tween.is_running():
+ _enter_tween.finished.emit()
+ _enter_tween.kill()
+ _enter_tween = create_tween()
+
+ _enter_tween.set_ease(ease_enter)
+ _enter_tween.set_trans(transition_enter)
+ _enter_tween.stop()
+
+ if _exit_tween && _exit_tween.is_running():
+ _exit_tween.finished.emit()
+ _exit_tween.kill()
+ _exit_tween = create_tween()
+
+ _exit_tween.set_ease(ease_exit)
+ _exit_tween.set_trans(transition_exit)
+ _exit_tween.stop()
+
+func _get_border() -> Rect2:
+ var boarder : Rect2
+
+ if from_outside_screen:
+ boarder.position = -global_position - size
+ boarder.size = get_viewport_rect().size - size
+ else:
+ boarder.position = -size
+ boarder.size = Vector2.ZERO
+
+ boarder.position -= Vector2(offset, offset)
+ return boarder
+
+
+func _await_animations() -> void:
+ @warning_ignore("incompatible_ternary")
+ await _SignalMerge.new(
+ _enter_tween.finished if _enter_tween && _enter_tween.is_running() else null,
+ _exit_tween.finished if _exit_tween && _exit_tween.is_running() else null
+ ) .finished
+
+
+class _SignalMerge:
+ signal finished
+ var _activate : bool = false
+
+ func _init(enter, exit) -> void:
+ _register(enter)
+ _register(exit)
+ func _register(arg) -> void:
+ if arg is Signal:
+ arg.connect(_unleash)
+ return
+ _unleash()
+ func _unleash() -> void:
+ if _activate: finished.emit()
+ _activate = true
+
+# Made by Xavier Alvarez. A part of the "FreeControl" Godot addon.
diff --git a/godot/addons/FreeControl/src/CustomClasses/TransitionContainers/ModulateTransitionContainer.gd b/godot/addons/FreeControl/src/CustomClasses/TransitionContainers/ModulateTransitionContainer.gd
new file mode 100644
index 0000000..f84536a
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/TransitionContainers/ModulateTransitionContainer.gd
@@ -0,0 +1,109 @@
+@tool
+class_name ModulateTransitionContainer extends Container
+## A [Control] node with changable that allows easy [member CanvasItem.modulate] animation between colors.
+
+
+
+@export_group("Alpha Override")
+## The colors to animate between.
+@export var colors : PackedColorArray = [Color.WHITE, Color(1.0, 1.0, 1.0, 0.5)]:
+ set(val):
+ if colors != val:
+ colors = val
+ focused_color = focused_color
+ force_color(_focused_color)
+var _focused_color : int = 0
+## The index of currently used color from [member colors].
+## This member is [code]-1[/code] if [member colors] is empty.
+@export var focused_color : int:
+ get: return _focused_color
+ set(val):
+ if colors.size() == 0:
+ _focused_color = -1
+ return
+
+ val = clampi(val, 0, colors.size() - 1)
+ if _focused_color != val:
+ _focused_color = val
+ _on_set_color()
+## If [code]true[/code] this node will only animate over [member CanvasItem.self_modulate]. Otherwise,
+## it will animate over [member CanvasItem.modulate].
+@export var modulate_self : bool = false
+
+@export_group("Tween Override")
+## The duration of color animations.
+@export var transitionTime : float = 0.2
+## The [Tween.EaseType] of color animations.
+@export var easeType : Tween.EaseType = Tween.EaseType.EASE_OUT_IN
+## The [Tween.TransitionType] of color animations.
+@export var transition : Tween.TransitionType = Tween.TransitionType.TRANS_CIRC
+## If [code]true[/code] animations can be interupted midway. Otherwise, any change in the [param focused_color]
+## will be queued to be reflected after any currently running animation.
+@export var can_cancle : bool = true
+
+
+var _color_tween : Tween = null
+var _current_focused_color : int
+
+
+## Sets the current color index.
+## [br][br]
+## Also see: [member focused_color].
+func set_color(color: int) -> void:
+ focused_color = color
+## Sets the current color index. Performing this will ignore any animation and instantly set the color.
+## [br][br]
+## Also see: [member focused_color].
+func force_color(color: int) -> void:
+ if _color_tween && _color_tween.is_running():
+ if !can_cancle: return
+ _color_tween.kill()
+ _current_focused_color = color
+ modulate = colors[color]
+
+## Gets the current color attributed to the current color index.
+func get_current_color() -> Color:
+ if _focused_color == -1: return 1
+ return colors[_focused_color]
+
+
+
+func _on_set_color():
+ if _focused_color == _current_focused_color:
+ return
+ if can_cancle:
+ if _color_tween: _color_tween.kill()
+ elif _color_tween && _color_tween.is_running():
+ return
+ _current_focused_color = _focused_color
+
+ _color_tween = create_tween()
+ _color_tween.tween_property(
+ self,
+ "self_modulate" if modulate_self else "modulate",
+ get_current_color(),
+ transitionTime
+ )
+ _color_tween.finished.connect(_on_set_color, CONNECT_ONE_SHOT)
+
+
+
+func _init() -> void:
+ _current_focused_color = _focused_color
+ sort_children.connect(_handle_children)
+
+func _property_can_revert(property: StringName) -> bool:
+ if property == "colors":
+ return colors.size() == 2 && colors[0] == Color.WHITE && colors[1] == Color(1.0, 1.0, 1.0, 0.5)
+ return false
+
+
+func _handle_children() -> void:
+ for child in get_children():
+ fit_child_in_rect(child, Rect2(Vector2.ZERO, size))
+func _get_minimum_size() -> Vector2:
+ var min_size : Vector2
+ for child : Node in get_children():
+ if child is Control:
+ min_size = min_size.max(child.get_combined_minimum_size())
+ return min_size
diff --git a/godot/addons/FreeControl/src/CustomClasses/TransitionContainers/StyleTransitionContainer.gd b/godot/addons/FreeControl/src/CustomClasses/TransitionContainers/StyleTransitionContainer.gd
new file mode 100644
index 0000000..aae5ee4
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/TransitionContainers/StyleTransitionContainer.gd
@@ -0,0 +1,126 @@
+@tool
+class_name StyleTransitionContainer extends Container
+## A [Container] node that add a [StyleTransitionPanel] node as the background.
+
+
+
+@export_group("Appearence Override")
+## The stylebox used by [StyleTransitionPanel].
+@export var background : StyleBox:
+ set(val):
+ if _panel:
+ _panel.add_theme_stylebox_override("panel", val)
+ background = val
+ elif background != val:
+ background = val
+
+@export_group("Colors Override")
+## The colors to animate between.
+@export var colors : PackedColorArray = [
+ Color.WEB_GRAY,
+ Color.DIM_GRAY
+]:
+ set(val):
+ if _panel:
+ _panel.colors = val
+ colors = val
+ elif colors != val:
+ colors = val
+
+## The index of currently used color from [member colors].
+## This member is [code]-1[/code] if [member colors] is empty.
+@export var focused_color : int:
+ set(val):
+ if _panel:
+ _panel.focused_color = val
+ focused_color = val
+ elif focused_color != val:
+ focused_color = val
+
+@export_group("Tween Override")
+## The duration of color animations.
+@export var transitionTime : float = 0.2:
+ set(val):
+ if _panel:
+ _panel.transitionTime = val
+ transitionTime = val
+ elif transitionTime != val:
+ transitionTime = val
+## The [Tween.EaseType] of color animations.
+@export var easeType : Tween.EaseType = Tween.EaseType.EASE_OUT_IN:
+ set(val):
+ if _panel:
+ _panel.easeType = val
+ easeType = val
+ elif easeType != val:
+ easeType = val
+## The [Tween.TransitionType] of color animations.
+@export var transition : Tween.TransitionType = Tween.TransitionType.TRANS_CIRC:
+ set(val):
+ if _panel:
+ _panel.transition = val
+ transition = val
+ elif transition != val:
+ transition = val
+## If [code]true[/code] animations can be interupted midway. Otherwise, any change in the [param focused_color]
+## will be queued to be reflected after any currently running animation.
+@export var can_cancle : bool = true:
+ set(val):
+ if _panel:
+ _panel.can_cancle = val
+ can_cancle = val
+ elif can_cancle != val:
+ can_cancle = val
+
+
+var _panel : StyleTransitionPanel
+
+
+## Sets the current color index.
+## [br][br]
+## Also see: [member focused_color].
+func set_color(color: int) -> void:
+ if !_panel: return
+ _panel.set_color(color)
+## Sets the current color index. Performing this will ignore any animation and instantly set the color.
+## [br][br]
+## Also see: [member focused_color].
+func force_color(color: int) -> void:
+ if !_panel: return
+ _panel.force_color(color)
+
+## Gets the current color attributed to the current color index.
+func get_current_color() -> Color:
+ if !_panel: return Color.BLACK
+ return _panel.get_current_color()
+
+
+
+func _init() -> void:
+ _panel = StyleTransitionPanel.new()
+ _panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ _panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ add_child(_panel)
+ move_child(_panel, 0)
+
+ sort_children.connect(_handle_children)
+func _ready() -> void:
+ if background:
+ _panel.add_theme_stylebox_override("panel", background)
+ return
+ background = _panel.get_theme_stylebox("panel")
+
+func _handle_children() -> void:
+ for child in get_children():
+ fit_child_in_rect(child, Rect2(Vector2.ZERO, size))
+func _get_minimum_size() -> Vector2:
+ var min_size : Vector2
+ for child : Node in get_children():
+ if child is Control:
+ min_size = min_size.max(child.get_combined_minimum_size())
+ return min_size
+
+func _property_can_revert(property: StringName) -> bool:
+ if property == "colors":
+ return colors.size() == 2 && colors[0] == Color.WEB_GRAY && colors[1] == Color.DIM_GRAY
+ return false
diff --git a/godot/addons/FreeControl/src/CustomClasses/TransitionContainers/StyleTransitionPanel.gd b/godot/addons/FreeControl/src/CustomClasses/TransitionContainers/StyleTransitionPanel.gd
new file mode 100644
index 0000000..c6f154b
--- /dev/null
+++ b/godot/addons/FreeControl/src/CustomClasses/TransitionContainers/StyleTransitionPanel.gd
@@ -0,0 +1,106 @@
+@tool
+class_name StyleTransitionPanel extends Panel
+## A [Panel] node with changable that allows easy [member CanvasItem.self_modulate] animation between colors.
+
+
+
+@export_group("Colors Override")
+## The colors to animate between.
+@export var colors : PackedColorArray = [
+ Color.WEB_GRAY,
+ Color.DIM_GRAY
+]:
+ set(val):
+ if colors != val:
+ colors = val
+ focused_color = focused_color
+ force_color(_focused_color)
+var _focused_color : int = 0
+## The index of currently used color from [member colors].
+## This member is [code]-1[/code] if [member colors] is empty.
+@export var focused_color : int:
+ get: return _focused_color
+ set(val):
+ if colors.size() == 0:
+ _focused_color = -1
+ return
+
+ val = clampi(val, 0, colors.size() - 1)
+ if _focused_color != val:
+ _focused_color = val
+ _on_set_color()
+
+@export_group("Tween Override")
+## The duration of color animations.
+@export var transitionTime : float = 0.2
+## The [Tween.EaseType] of color animations.
+@export var easeType : Tween.EaseType = Tween.EaseType.EASE_OUT_IN
+## The [Tween.TransitionType] of color animations.
+@export var transition : Tween.TransitionType = Tween.TransitionType.TRANS_CIRC
+## If [code]true[/code] animations can be interupted midway. Otherwise, any change in the [param focused_color]
+## will be queued to be reflected after any currently running animation.
+@export var can_cancle : bool = true
+
+
+var _color_tween : Tween = null
+var _current_focused_color : int
+
+
+## Sets the current color index.
+## [br][br]
+## Also see: [member focused_color].
+func set_color(color: int) -> void:
+ focused_color = color
+## Sets the current color index. Performing this will ignore any animation and instantly set the color.
+## [br][br]
+## Also see: [member focused_color].
+func force_color(color: int) -> void:
+ if _color_tween && _color_tween.is_running():
+ if !can_cancle: return
+ _color_tween.kill()
+ _focused_color = color
+ _safe_base_set_background()
+ self_modulate = get_current_color()
+
+## Gets the current color attributed to the current color index.
+func get_current_color() -> Color:
+ if _focused_color == -1: return Color.BLACK
+ return colors[_focused_color]
+
+
+
+func _safe_base_set_background() -> void:
+ if has_theme_stylebox_override("panel"): return
+
+ var background = StyleBoxFlat.new()
+ background.resource_local_to_scene = true
+ background.bg_color = Color.WHITE
+ add_theme_stylebox_override("panel", background)
+
+func _on_set_color():
+ if _focused_color == _current_focused_color:
+ return
+ if can_cancle:
+ if _color_tween: _color_tween.kill()
+ elif _color_tween && _color_tween.is_running():
+ return
+ _current_focused_color = _focused_color
+
+ _safe_base_set_background()
+ _color_tween = create_tween()
+ _color_tween.tween_property(
+ self,
+ "self_modulate",
+ get_current_color(),
+ transitionTime
+ )
+ _color_tween.finished.connect(_on_set_color, CONNECT_ONE_SHOT)
+
+
+func _init() -> void:
+ _current_focused_color = _focused_color
+ _safe_base_set_background()
+func _property_can_revert(property: StringName) -> bool:
+ if property == "colors":
+ return colors.size() == 2 && colors[0] == Color.WEB_GRAY && colors[1] == Color.DIM_GRAY
+ return false
diff --git a/godot/addons/LICENSE b/godot/addons/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/godot/addons/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/godot/addons/utils/utils.gd b/godot/addons/utils/utils.gd
index 2a846bc..325be72 100644
--- a/godot/addons/utils/utils.gd
+++ b/godot/addons/utils/utils.gd
@@ -1,5 +1,9 @@
class_name Utils
+static func remove_children(node: Node):
+ for i in range(node.get_child_count()):
+ node.remove_child(node.get_child(i))
+
static func get_class_name(value: Object) -> String:
match value.get_script():
var script:
@@ -26,3 +30,20 @@ static func to_str(value: Variant) -> String:
var props: Dictionary = inst_to_dict(value)
return "%s %s" % [name, to_str(props)]
_: return str(value)
+
+static func propagate(node: Node, fn: StringName, args: Array, call_on_self: bool = true):
+ if call_on_self and node.has_method(fn):
+ node.callv(fn, args)
+
+ for child in node.get_children():
+ propagate(child, fn, args)
+
+static func propagate_input_event(node: Node, fn: StringName, event: InputEvent, call_on_self: bool = true):
+ if node.get_viewport().is_input_handled() or event.is_canceled():
+ return
+
+ if call_on_self and node.has_method(fn):
+ node.callv(fn, [event])
+
+ for child in node.get_children():
+ propagate_input_event(child, fn, event)
diff --git a/godot/project.godot b/godot/project.godot
index 8aa8e78..edebd9c 100644
--- a/godot/project.godot
+++ b/godot/project.godot
@@ -21,7 +21,7 @@ PhantomCameraManager="*res://addons/phantom_camera/scripts/managers/phantom_came
[editor_plugins]
-enabled=PackedStringArray("res://addons/godot_object_serializer/plugin.cfg", "res://addons/phantom_camera/plugin.cfg")
+enabled=PackedStringArray("res://addons/FreeControl/plugin.cfg", "res://addons/godot_object_serializer/plugin.cfg", "res://addons/phantom_camera/plugin.cfg")
[global_group]
@@ -103,12 +103,18 @@ reload={
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null)
]
}
-inventory={
+open_inventory={
"deadzone": 0.2,
"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":2,"pressure":0.0,"pressed":true,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
+ui_close_inventory={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
+]
+}
[layer_names]
diff --git a/godot/resources/player_inventory.tres b/godot/resources/player_inventory.tres
index fd529ef..efeade5 100644
--- a/godot/resources/player_inventory.tres
+++ b/godot/resources/player_inventory.tres
@@ -1,10 +1,8 @@
-[gd_resource type="Resource" script_class="Inventory" load_steps=4 format=3 uid="uid://bllq6ri54q3ne"]
+[gd_resource type="Resource" script_class="Inventory" load_steps=2 format=3 uid="uid://bllq6ri54q3ne"]
-[ext_resource type="Script" uid="uid://bqprls343ue6e" path="res://src/item.gd" id="1_uptie"]
[ext_resource type="Script" uid="uid://dh4ytedxidq0x" path="res://src/inventory.gd" id="2_1njko"]
-[ext_resource type="Resource" uid="uid://cqfnwpmo4fyv4" path="res://resources/items/key.tres" id="2_85a8j"]
[resource]
script = ExtResource("2_1njko")
-items = Array[ExtResource("1_uptie")]([ExtResource("2_85a8j"), null, null, null, null, null])
+max_capacity = null
metadata/_custom_type_script = "uid://dh4ytedxidq0x"
diff --git a/godot/scenes/inventory.tscn b/godot/scenes/inventory.tscn
index 5e66e32..ea5eac3 100644
--- a/godot/scenes/inventory.tscn
+++ b/godot/scenes/inventory.tscn
@@ -15,8 +15,10 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
+focus_mode = 2
script = ExtResource("1_qw0r6")
-action_name = &"inventory"
+open_action = &"inventory"
+close_action = &"ui_close_inventory"
[node name="ColorRect" type="ColorRect" parent="."]
layout_mode = 1
@@ -27,7 +29,7 @@ grow_horizontal = 2
grow_vertical = 2
color = Color(0.10748, 0.10748, 0.10748, 1)
-[node name="VBoxContainer" type="VBoxContainer" parent="." node_paths=PackedStringArray("item_list")]
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
clip_contents = true
layout_mode = 1
anchors_preset = 15
@@ -37,25 +39,16 @@ grow_horizontal = 2
grow_vertical = 2
script = ExtResource("2_hj2ta")
inventory = ExtResource("3_ty45s")
-item_list = NodePath("ItemNavigation/HItemList")
[node name="ItemNavigation" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
-[node name="MoveLeft" type="Button" parent="VBoxContainer/ItemNavigation"]
-layout_mode = 2
-text = "<"
-
[node name="HItemList" type="HBoxContainer" parent="VBoxContainer/ItemNavigation"]
layout_mode = 2
size_flags_horizontal = 3
script = ExtResource("4_yyk2a")
item_scene = ExtResource("5_uae8j")
-[node name="MoveRight" type="Button" parent="VBoxContainer/ItemNavigation"]
-layout_mode = 2
-text = ">"
-
[node name="Details" type="HBoxContainer" parent="VBoxContainer" node_paths=PackedStringArray("icon_image", "description_label")]
layout_mode = 2
size_flags_vertical = 3
@@ -91,7 +84,7 @@ grow_horizontal = 2
grow_vertical = 2
autowrap_mode = 2
-[connection signal="pressed" from="." to="." method="set_visible"]
-[connection signal="pressed" from="VBoxContainer/ItemNavigation/MoveLeft" to="VBoxContainer/ItemNavigation/HItemList" method="move_left"]
+[connection signal="closed" from="." to="." method="hide"]
+[connection signal="opened" from="." to="." method="show"]
+[connection signal="opened" from="." to="." method="grab_focus"]
[connection signal="selected" from="VBoxContainer/ItemNavigation/HItemList" to="VBoxContainer/Details" method="_on_updated"]
-[connection signal="pressed" from="VBoxContainer/ItemNavigation/MoveRight" to="VBoxContainer/ItemNavigation/HItemList" method="move_right"]
diff --git a/godot/scenes/inventory2.tscn b/godot/scenes/inventory2.tscn
new file mode 100644
index 0000000..36fed91
--- /dev/null
+++ b/godot/scenes/inventory2.tscn
@@ -0,0 +1,116 @@
+[gd_scene load_steps=10 format=3 uid="uid://cn7tgd4y67wnd"]
+
+[ext_resource type="Script" uid="uid://qvoqvonnxwfc" path="res://src/inventory_ui.gd" id="1_a0rpf"]
+[ext_resource type="Script" uid="uid://bx4wxlm5mv268" path="res://src/root_control.gd" id="1_as33y"]
+[ext_resource type="Resource" uid="uid://bllq6ri54q3ne" path="res://resources/player_inventory.tres" id="2_as33y"]
+[ext_resource type="PackedScene" uid="uid://gn8k2ir47n1m" path="res://scenes/inventory_item.tscn" id="3_tg4gd"]
+[ext_resource type="Script" uid="uid://13lxe4c4fmrp" path="res://addons/FreeControl/src/CustomClasses/Carousel/Carousel.gd" id="4_usnyx"]
+
+[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_as33y"]
+size = Vector2(128, 128)
+
+[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_tg4gd"]
+size = Vector2(128, 128)
+
+[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_usnyx"]
+size = Vector2(128, 64)
+
+[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_sebc8"]
+size = Vector2(256, 256)
+
+[node name="Inventory" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+focus_mode = 2
+script = ExtResource("1_as33y")
+open_action = &"open_inventory"
+close_action = &"ui_close_inventory"
+
+[node name="ColorRect" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+color = Color(0.147672, 0.147672, 0.147672, 1)
+
+[node name="Contents" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="InventoryUI" type="Control" parent="Contents" node_paths=PackedStringArray("carousel")]
+layout_mode = 2
+size_flags_vertical = 3
+script = ExtResource("1_a0rpf")
+inventory = ExtResource("2_as33y")
+item_scene = ExtResource("3_tg4gd")
+carousel = NodePath("Carousel")
+metadata/_custom_type_script = "uid://qvoqvonnxwfc"
+
+[node name="Carousel" type="Container" parent="Contents/InventoryUI"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -104.0
+offset_top = 5.0
+offset_right = -104.0
+offset_bottom = 5.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("4_usnyx")
+display_range = 4
+snap_behavior = 2
+paging_requirement = 100
+metadata/_custom_type_script = "uid://13lxe4c4fmrp"
+
+[node name="ItemDetails" type="HBoxContainer" parent="Contents"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="PlayerInfo" type="VBoxContainer" parent="Contents/ItemDetails"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="TextureRect" type="TextureRect" parent="Contents/ItemDetails/PlayerInfo"]
+layout_mode = 2
+size_flags_horizontal = 4
+texture = SubResource("PlaceholderTexture2D_as33y")
+
+[node name="TextureRect2" type="TextureRect" parent="Contents/ItemDetails/PlayerInfo"]
+layout_mode = 2
+size_flags_horizontal = 4
+texture = SubResource("PlaceholderTexture2D_tg4gd")
+
+[node name="TextureRect3" type="TextureRect" parent="Contents/ItemDetails/PlayerInfo"]
+layout_mode = 2
+size_flags_horizontal = 4
+texture = SubResource("PlaceholderTexture2D_usnyx")
+
+[node name="Display" type="VBoxContainer" parent="Contents/ItemDetails"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="TextureRect" type="TextureRect" parent="Contents/ItemDetails/Display"]
+layout_mode = 2
+size_flags_horizontal = 4
+texture = SubResource("PlaceholderTexture2D_sebc8")
+
+[node name="Description" type="VBoxContainer" parent="Contents/ItemDetails"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Label" type="Label" parent="Contents/ItemDetails/Description"]
+layout_mode = 2
+text = "a description"
diff --git a/godot/scenes/level.tscn b/godot/scenes/level.tscn
index 905ae20..8de5c1b 100644
--- a/godot/scenes/level.tscn
+++ b/godot/scenes/level.tscn
@@ -84,6 +84,7 @@ shape = SubResource("BoxShape3D_mx8sn")
script = ExtResource("3_w8frs")
[node name="CanvasLayer" type="CanvasLayer" parent="."]
+follow_viewport_enabled = true
[node name="Control" type="Control" parent="CanvasLayer"]
layout_mode = 3
@@ -110,6 +111,7 @@ text = "Load
"
[node name="Control2" parent="CanvasLayer" instance=ExtResource("8_b121j")]
+visible = true
[connection signal="interacted" from="Door/Interactable" to="Door" method="_on_interact"]
[connection signal="pressed" from="CanvasLayer/Control/HBoxContainer/SaveButton" to="CanvasLayer/Control/Persistence" method="save" binds= ["save1.sav"]]
diff --git a/godot/src/input_listener.gd b/godot/src/input_listener.gd
index 2b49c25..74553e7 100644
--- a/godot/src/input_listener.gd
+++ b/godot/src/input_listener.gd
@@ -1,15 +1,29 @@
-class_name InputListener extends Node
+class_name InputListener extends Control
-signal pressed(toggle_state: bool)
-signal released(toggle_state: bool)
+enum ActionEvent {
+ Pressed,
+ Released
+}
-@export var toggle_state: bool
-@export var action_name: StringName
+signal opened
+signal closed
-func _input(event: InputEvent) -> void:
- if event.is_action_pressed(action_name):
- self.toggle_state = !toggle_state
- pressed.emit(toggle_state)
+@export var input_event: ActionEvent
+@export var open_action: StringName
+@export var close_action: StringName
- if event.is_action_released(action_name):
- released.emit(toggle_state)
+func _is_active(event: InputEvent, action: StringName) -> bool:
+ match input_event:
+ ActionEvent.Pressed: return event.is_action_pressed(action)
+ ActionEvent.Released: return event.is_action_released(action)
+ _: return false
+
+func _unhandled_input(event: InputEvent) -> void:
+ if _is_active(event, open_action):
+ accept_event()
+ opened.emit()
+
+func _gui_input(event: InputEvent) -> void:
+ if _is_active(event, close_action):
+ accept_event()
+ closed.emit()
diff --git a/godot/src/inventory.gd b/godot/src/inventory.gd
index 48500eb..8b78816 100644
--- a/godot/src/inventory.gd
+++ b/godot/src/inventory.gd
@@ -1,17 +1,63 @@
class_name Inventory extends Resource
-@export var items: Array[Item]
+var initial_items: Array[ItemInstance]
+var items: Dictionary[RID, ItemInstance]
+@export var max_capacity: int = 6
+
+var size: int:
+ get: return items.size()
+
+signal item_added(item: Item, quantity: int)
+signal item_removed(item: Item, remaining: int)
+signal updated
+
+func _init():
+ call_deferred("_ready")
+
+func _ready():
+ print(initial_items.size())
+ for item in initial_items:
+ items[item.get_rid()] = item
+ print('setting', item)
+
+func _item_eq(a: Item, b: ItemInstance) -> bool:
+ return a.item == b
+
+func has_item(item: Item) -> bool:
+ return items.has(item.get_rid())
+
+func find(item: Item) -> Option:
+ match items.get(item.get_rid()):
+ null: return Option.none
+ var found: return Option.some(found)
+
+func add_item(item: Item, quantity: int = 1):
+ var rid = item.get_rid()
+ var inst = items.get_or_add(rid)
+ inst.quantity += quantity
+ item_added.emit(item, inst.quantity)
+ updated.emit()
+
+func remove_item(item: Item, quantity: int = 1):
+ if find(item):
+ item.quantity -= quantity
+ if item.quantity <= 0:
+ items.erase(item.get_rid())
+ item_removed.emit(item, item.quantity)
+ updated.emit()
func _iter_continue(iter: Array) -> bool:
- return iter[0] < len(items)
+ return iter[0].size()
func _iter_init(iter: Array) -> bool:
- iter[0] = 0
+ iter[0] = items.keys()
return _iter_continue(iter)
func _iter_next(iter: Array) -> bool:
iter[0] += 1
return _iter_continue(iter)
-func _iter_get(iter: Variant) -> Item:
- return items[iter]
+func _iter_get(iter: Variant) -> ItemInstance:
+ var rid = iter[0]
+ iter.remove_at(0)
+ return items.get(rid)
diff --git a/godot/src/inventory_ui.gd b/godot/src/inventory_ui.gd
index 2feeafa..115bd15 100644
--- a/godot/src/inventory_ui.gd
+++ b/godot/src/inventory_ui.gd
@@ -1,13 +1,41 @@
-class_name InventoryUI extends VBoxContainer
+class_name InventoryUI extends Control
@export var inventory: Inventory
-@export var item_list: HItemList
+@export var item_scene: PackedScene
+@export var carousel: Carousel
func _ready() -> void:
- item_list.add_items(inventory.items)
+ _build_carousel()
+ inventory.updated.connect(_build_carousel)
-func _input(event: InputEvent) -> void:
+func _build_carousel():
+ Utils.remove_children(carousel)
+
+ for instance in inventory:
+ print('pussy', instance)
+ var scene = create_item()
+ bind_item(scene, Option.from(instance.item))
+ carousel.add_child(scene)
+
+func create_item() -> Node:
+ return item_scene.instantiate()
+
+func bind_item(node: Node, item: Option):
+ if node.has_method('bind'):
+ node.bind(item)
+
+func _gui_input(event: InputEvent) -> void:
if event.is_action_pressed(PlayerInput.UIAction.Right):
- item_list.move_right()
+ move_right()
elif event.is_action_pressed(PlayerInput.UIAction.Left):
- item_list.move_left()
+ move_left()
+
+func move_by(delta: int):
+ var next_index = carousel.get_carousel_index() + delta
+ carousel.go_to_index(next_index)
+
+func move_right():
+ move_by(1)
+
+func move_left():
+ move_by(-1)
diff --git a/godot/src/item_instance.gd b/godot/src/item_instance.gd
new file mode 100644
index 0000000..0c215d3
--- /dev/null
+++ b/godot/src/item_instance.gd
@@ -0,0 +1,24 @@
+class_name ItemInstance extends Resource
+
+@export var item: Item
+@export var quantity: int
+
+@warning_ignore("shadowed_variable")
+func _init(item: Item = null, quantity: int = 1) -> void:
+ self.item = item
+ self.quantity = quantity
+
+@warning_ignore("shadowed_variable")
+func add_quantity(quantity: int = 1) -> int:
+ self.quantity += quantity
+ return self.quantity
+
+@warning_ignore("shadowed_variable")
+func remove_quantity(quantity: int = 1) -> int:
+ self.quantity -= quantity
+ return self.quantity
+
+@warning_ignore("shadowed_variable")
+func set_quantity(quantity: int) -> int:
+ self.quantity = quantity
+ return self.quantity
diff --git a/godot/src/item_ui.gd b/godot/src/item_ui.gd
index 6a2f68a..5f803d3 100644
--- a/godot/src/item_ui.gd
+++ b/godot/src/item_ui.gd
@@ -6,7 +6,7 @@ class_name ItemUI extends Control
var _default_text: String
var _default_icon: Texture2D
-func _ready() -> void:
+func _init() -> void:
_default_text = name_label.text
_default_icon = icon_texture.texture
@@ -19,5 +19,6 @@ func bind(_item: Option):
icon_texture.texture = item.icon
func unbind():
+ print(_default_text)
name_label.text = _default_text
icon_texture.texture = _default_icon
diff --git a/godot/src/player_input.gd b/godot/src/player_input.gd
index 5d19272..a6171e6 100644
--- a/godot/src/player_input.gd
+++ b/godot/src/player_input.gd
@@ -107,7 +107,7 @@ func _get_device(event: InputEvent) -> Device:
else:
return Device.Unknown
-func _input(event: InputEvent) -> void:
+func _unhandled_input(event: InputEvent) -> void:
last_known_device = _get_device(event)
if event.is_action_pressed(GameAction.Interact):
diff --git a/godot/src/root_control.gd b/godot/src/root_control.gd
new file mode 100644
index 0000000..bf7c0d7
--- /dev/null
+++ b/godot/src/root_control.gd
@@ -0,0 +1,23 @@
+class_name RootControl extends InputListener
+
+func _ready() -> void:
+ if self.focus_mode == Control.FOCUS_NONE:
+ self.focus_mode = Control.FOCUS_ALL
+
+ opened.connect(open)
+ closed.connect(close)
+
+ if visible:
+ grab_focus()
+
+func open():
+ show()
+ grab_focus()
+
+func close():
+ hide()
+ release_focus()
+
+func _gui_input(event: InputEvent) -> void:
+ super(event)
+ Utils.propagate_input_event(self, "_gui_input", event, false)