├── framework ├── ViewManager.gd.uid ├── core │ ├── Modal.gd.uid │ ├── Sheet.gd.uid │ ├── UIRoot.gd.uid │ ├── View.gd.uid │ ├── FullScreenCover.gd.uid │ ├── custom types │ │ ├── Flow.gd.uid │ │ └── Flow.gd │ ├── FullScreenCover.gd │ ├── Sheet.gd │ ├── UIRoot.gd │ ├── Modal.gd │ └── View.gd ├── binding │ ├── Binding.gd.uid │ ├── ObserveArray.gd.uid │ ├── Binding.gd │ └── ObserveArray.gd ├── builders │ ├── BaseBuilder.gd.uid │ ├── elements │ │ ├── ButtonBuilder.gd.uid │ │ ├── ColorBuilder.gd.uid │ │ ├── LabelBuilder.gd.uid │ │ ├── SpacerBuilder.gd.uid │ │ ├── TextEditBuilder.gd.uid │ │ ├── TextureRectBuilder.gd.uid │ │ ├── SpacerBuilder.gd │ │ ├── ColorBuilder.gd │ │ ├── TextureRectBuilder.gd │ │ ├── ButtonBuilder.gd │ │ ├── LabelBuilder.gd │ │ └── TextEditBuilder.gd │ ├── containers │ │ ├── ContainerBuilder.gd.uid │ │ ├── ZStackBuilder.gd.uid │ │ ├── ZStackBuilder.gd │ │ └── ContainerBuilder.gd │ └── BaseBuilder.gd ├── shaders │ ├── SimpleBlur.gdshader.uid │ ├── SimpleBlurMat.tres │ └── SimpleBlur.gdshader ├── view_compiler │ ├── ViewParser.gd.uid │ ├── ViewRegistry.gd.uid │ ├── ViewScanner.gd.uid │ ├── ViewCodeGenerator.gd.uid │ ├── ViewParser.gd │ ├── ViewScanner.gd │ ├── ViewRegistry.gd │ └── ViewCodeGenerator.gd └── themes │ ├── SF Pro Fonts │ ├── medium_variation.tres │ ├── regular_variation.tres │ ├── semi_bold_variation.tres │ └── bold_variantion.tres │ └── UITheme.tres ├── examples ├── mobile app │ ├── HomeView.gd.uid │ ├── SheetTest.gd.uid │ ├── SheetTest.gd │ └── HomeView.gd └── ViewExample.tscn ├── script_templates └── View │ ├── ViewExtension.gd.uid │ └── ViewExtension.gd ├── images ├── clock.png └── clock.png.import ├── .gitattributes ├── .gitignore ├── ui_layout.dui ├── icon.svg ├── icon.svg.import ├── LICENSE ├── project.godot └── README.md /framework/ViewManager.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cqefhxvdys0pn 2 | -------------------------------------------------------------------------------- /framework/core/Modal.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ynfckptkyeop 2 | -------------------------------------------------------------------------------- /framework/core/Sheet.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cbiot3hv6afoq 2 | -------------------------------------------------------------------------------- /framework/core/UIRoot.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b4v0e84lkl5mn 2 | -------------------------------------------------------------------------------- /framework/core/View.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ctg4fbmptp1u3 2 | -------------------------------------------------------------------------------- /framework/binding/Binding.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dns57j8fvk4l8 2 | -------------------------------------------------------------------------------- /examples/mobile app/HomeView.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dsm2uvtcgns7h 2 | -------------------------------------------------------------------------------- /examples/mobile app/SheetTest.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cbphxh55bdtnj 2 | -------------------------------------------------------------------------------- /framework/binding/ObserveArray.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ddvb7vls53onf 2 | -------------------------------------------------------------------------------- /framework/builders/BaseBuilder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c2wy5es1t0t0m 2 | -------------------------------------------------------------------------------- /framework/core/FullScreenCover.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cqcvuroyudpip 2 | -------------------------------------------------------------------------------- /framework/core/custom types/Flow.gd.uid: -------------------------------------------------------------------------------- 1 | uid://btrgt74qrf5lk 2 | -------------------------------------------------------------------------------- /framework/shaders/SimpleBlur.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://fykt5yxxo8if 2 | -------------------------------------------------------------------------------- /framework/view_compiler/ViewParser.gd.uid: -------------------------------------------------------------------------------- 1 | uid://mfv1m3kue7vp 2 | -------------------------------------------------------------------------------- /framework/view_compiler/ViewRegistry.gd.uid: -------------------------------------------------------------------------------- 1 | uid://yq1xgmv6a3j8 2 | -------------------------------------------------------------------------------- /framework/view_compiler/ViewScanner.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cecw50khfnx6t 2 | -------------------------------------------------------------------------------- /script_templates/View/ViewExtension.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d36rcr6pw0pne 2 | -------------------------------------------------------------------------------- /framework/builders/elements/ButtonBuilder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dcdlf53h2y6n6 2 | -------------------------------------------------------------------------------- /framework/builders/elements/ColorBuilder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b0bf0o7abvtjq 2 | -------------------------------------------------------------------------------- /framework/builders/elements/LabelBuilder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dmmbrnre71ksq 2 | -------------------------------------------------------------------------------- /framework/builders/elements/SpacerBuilder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bnlbh6n3lnhte 2 | -------------------------------------------------------------------------------- /framework/view_compiler/ViewCodeGenerator.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cxdir0c4f30yb 2 | -------------------------------------------------------------------------------- /framework/builders/containers/ContainerBuilder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://mqci88d1faij 2 | -------------------------------------------------------------------------------- /framework/builders/containers/ZStackBuilder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dy36vw4ilg1j0 2 | -------------------------------------------------------------------------------- /framework/builders/elements/TextEditBuilder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b52ofrswiorqy 2 | -------------------------------------------------------------------------------- /framework/builders/elements/TextureRectBuilder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d0yrdov2kniph 2 | -------------------------------------------------------------------------------- /images/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElvisVilla/GDScriptUI/HEAD/images/clock.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /framework/builders/elements/SpacerBuilder.gd: -------------------------------------------------------------------------------- 1 | extends BaseBuilder 2 | class_name SpacerBuilder 3 | 4 | func _init(): 5 | _content_node = Control.new() 6 | _content_node.size_flags_horizontal = Control.SIZE_EXPAND_FILL 7 | _content_node.size_flags_vertical = Control.SIZE_EXPAND_FILL 8 | -------------------------------------------------------------------------------- /framework/shaders/SimpleBlurMat.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://c2hpvatscrjwk"] 2 | 3 | [ext_resource type="Shader" uid="uid://fykt5yxxo8if" path="res://framework/shaders/SimpleBlur.gdshader" id="1_kp7vt"] 4 | 5 | [resource] 6 | shader = ExtResource("1_kp7vt") 7 | shader_parameter/lod = 0.2 8 | shader_parameter/opacity = 0.23 9 | -------------------------------------------------------------------------------- /framework/shaders/SimpleBlur.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest_mipmap; 3 | uniform float lod: hint_range(0.0, 5) = 1.0; 4 | uniform float opacity: hint_range(0.0, 1.0) = 1.0; 5 | 6 | void fragment(){ 7 | vec4 color = textureLod(screen_texture, SCREEN_UV, lod); 8 | COLOR = vec4(color.rgb, color.a * opacity); 9 | } -------------------------------------------------------------------------------- /framework/themes/SF Pro Fonts/medium_variation.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="FontVariation" load_steps=2 format=3 uid="uid://ckqx1r0csvwnk"] 2 | 3 | [sub_resource type="SystemFont" id="SystemFont_hqikt"] 4 | font_names = PackedStringArray("SF Pro") 5 | 6 | [resource] 7 | base_font = SubResource("SystemFont_hqikt") 8 | variation_opentype = { 9 | 1869640570: 28, 10 | 2003072104: 100, 11 | 2003265652: 500 12 | } 13 | -------------------------------------------------------------------------------- /framework/themes/SF Pro Fonts/regular_variation.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="FontVariation" load_steps=2 format=3 uid="uid://cox56evkfub46"] 2 | 3 | [sub_resource type="SystemFont" id="SystemFont_yvrwr"] 4 | font_names = PackedStringArray("SF Pro") 5 | 6 | [resource] 7 | base_font = SubResource("SystemFont_yvrwr") 8 | variation_opentype = { 9 | 1869640570: 28, 10 | 2003072104: 100, 11 | 2003265652: 400 12 | } 13 | -------------------------------------------------------------------------------- /framework/themes/SF Pro Fonts/semi_bold_variation.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="FontVariation" load_steps=2 format=3 uid="uid://breu0oiqiym3o"] 2 | 3 | [sub_resource type="SystemFont" id="SystemFont_2f02b"] 4 | font_names = PackedStringArray("SF Pro") 5 | 6 | [resource] 7 | base_font = SubResource("SystemFont_2f02b") 8 | variation_opentype = { 9 | 1869640570: 28, 10 | 2003072104: 100, 11 | 2003265652: 600 12 | } 13 | -------------------------------------------------------------------------------- /script_templates/View/ViewExtension.gd: -------------------------------------------------------------------------------- 1 | # meta-name: View Extension 2 | # meta-description: Extension for View 3 | # meta-default: true 4 | # meta-space-indent: 4 5 | 6 | extends _BASE_ 7 | class_name _CLASS_ 8 | 9 | func configure(): 10 | #Body is where all the UI elements are defined 11 | body = ZStack([ 12 | 13 | VBox([ 14 | Label("Hello World"), 15 | Button("Click me!") 16 | ]) 17 | 18 | ]) 19 | -------------------------------------------------------------------------------- /framework/themes/SF Pro Fonts/bold_variantion.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="FontVariation" load_steps=2 format=3 uid="uid://ccn8rnsnrrsf4"] 2 | 3 | [sub_resource type="SystemFont" id="SystemFont_5phad"] 4 | font_names = PackedStringArray("SF Pro") 5 | 6 | [resource] 7 | base_font = SubResource("SystemFont_5phad") 8 | variation_opentype = { 9 | 1869640570: 28, 10 | 2003072104: 100, 11 | 2003265652: 700 12 | } 13 | opentype_features = { 14 | 1801810542: 0 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | # Dummy HTML5 export presets file for continuous integration 9 | !.github/dist/export_presets.cfg 10 | 11 | # Imported translations (automatically generated from CSV files) 12 | *.translation 13 | 14 | # Mono-specific ignores 15 | .mono/ 16 | data_*/ 17 | mono_crash.*.json 18 | 19 | # System/tool-specific ignores 20 | .directory 21 | .DS_Store 22 | *~ 23 | *.blend1 24 | 25 | # Jetbrains IDE files 26 | .idea/ -------------------------------------------------------------------------------- /framework/core/FullScreenCover.gd: -------------------------------------------------------------------------------- 1 | extends Modal 2 | class_name FullScreenCover 3 | 4 | 5 | func present(): 6 | show() 7 | # Fade in 8 | var tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC) 9 | tween.tween_property(self, "modulate", Color(1, 1, 1, 1), 0.3) 10 | 11 | func dismiss(): 12 | # Fade out 13 | var tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC) 14 | tween.tween_property(self, "modulate", Color(1, 1, 1, 0), 0.3) 15 | await tween.finished 16 | hide() 17 | -------------------------------------------------------------------------------- /examples/mobile app/SheetTest.gd: -------------------------------------------------------------------------------- 1 | extends View 2 | class_name SheetTest 3 | 4 | var is_presented: Binding 5 | 6 | func configure(isPresented: Binding): 7 | is_presented = isPresented 8 | # other properties here 9 | 10 | 11 | #Body is where all the UI elements are defined 12 | body = ZStack([ 13 | 14 | VBox([ 15 | 16 | HBox([ 17 | Label(is_presented) 18 | .padding() 19 | .background(Color.RED) 20 | .fontSize(40), 21 | 22 | Button(" X ", func(): 23 | is_presented.value = false) 24 | .fontSize(50), 25 | ]), 26 | 27 | ]) 28 | .frame(Infinity, Infinity) 29 | .padding(16), 30 | ]) 31 | -------------------------------------------------------------------------------- /framework/core/Sheet.gd: -------------------------------------------------------------------------------- 1 | extends Modal 2 | class_name Sheet 3 | 4 | func present(): 5 | # Animate in from bottom 6 | var view_instance = instance_from_id(view_content.get_instance_id()) 7 | print_debug(view_instance) 8 | view_content._in_node(self) 9 | position = initial_position_off_screen_Vertical 10 | show() 11 | var tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC) 12 | tween.tween_property(self, "position", position_in_screen, 0.5) 13 | 14 | func dismiss(): 15 | # Animate out to bottom 16 | var tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC) 17 | tween.tween_property(self, "position", initial_position_off_screen_Vertical, 0.5) 18 | await tween.finished 19 | # var child = get_child(0) 20 | # remove_child(child) 21 | # hide() 22 | self.queue_free() 23 | -------------------------------------------------------------------------------- /framework/view_compiler/ViewParser.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name ViewParser 3 | 4 | 5 | func parse_view(constructor) -> String: 6 | #is an array with dictionaries, first element is name and represent the parameter name, the second is args and is the arguments of the method 7 | var func_call: String = "configure(" 8 | var func_call_args: String = "" 9 | 10 | #args is an array of dictionaries, name is the name of the parameter 11 | var count = 0 12 | while count < constructor.args.size(): 13 | #only add ',' if its the last parameter 14 | func_call_args += constructor.args[count].get("name") 15 | 16 | 17 | if count < constructor.args.size() - 1: 18 | func_call_args += ", " 19 | 20 | count += 1 21 | 22 | func_call += func_call_args + ")" 23 | 24 | 25 | print(func_call) 26 | return func_call 27 | -------------------------------------------------------------------------------- /framework/view_compiler/ViewScanner.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name ViewScanner 3 | 4 | # var obtained_views = [] 5 | var views = [] 6 | var views_info: Array[Dictionary] = [] 7 | 8 | func scan_for_views() -> Array[Dictionary]: 9 | views = ProjectSettings.get_global_class_list().filter(func(element): return element.base == &"View") 10 | 11 | for view in views: 12 | views_info.append({ 13 | "base_type": view.base, 14 | "type": view.class , 15 | "constructor": get_constructor(view.path), 16 | "path": view.path 17 | }) 18 | 19 | return views_info 20 | 21 | func get_constructor(path: String): 22 | var view = load(path).new() 23 | var methods = view.get_method_list() 24 | for method in methods: 25 | if method.name == "configure" and method.flags == METHOD_FLAG_NORMAL: 26 | return method 27 | 28 | return null 29 | -------------------------------------------------------------------------------- /ui_layout.dui: -------------------------------------------------------------------------------- 1 | var a = .234 2 | #This is a comment 3 | 4 | @export var showContent: bool = 4 != 2 5 | var elements = ["grass", "Shovel", "Sword", "Plant"] 6 | 7 | ## This is a comment 8 | VBox { 9 | 10 | Label("Amor!") 11 | Hbox { 12 | 13 | Button("Show") 14 | .onPressed(func(): showContent = true) 15 | .padding(2) 16 | 17 | VBox { 18 | 19 | Label("Hello Mi Amor!") 20 | .fontSize(20) 21 | 22 | Label("Te amo!") 23 | .fontSize(15) 24 | 25 | Label("A ver si es verdad!") 26 | .fontSize(40) 27 | 28 | Label("Te amo mucho!").fontSize(15), 29 | 30 | VBox { 31 | VBox { 32 | Button("Este boton hace algo") 33 | .fontSize(15), 34 | 35 | button("Este boton hace algo") 36 | .fontSize(15) 37 | .onPressed(func(): print("Boton 2 presionado")), 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /images/clock.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://clx58u6fgcj4l" 6 | path="res://.godot/imported/clock.png-ae373d86351a4cca0ccf8680b7ef32ba.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://images/clock.png" 14 | dest_files=["res://.godot/imported/clock.png-ae373d86351a4cca0ccf8680b7ef32ba.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://rrkm4f65sghy" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /examples/ViewExample.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://38xdxhygy60f"] 2 | 3 | [ext_resource type="Theme" uid="uid://dyrw727ts3xo1" path="res://framework/themes/UITheme.tres" id="1_kca8q"] 4 | [ext_resource type="Script" uid="uid://b4v0e84lkl5mn" path="res://framework/core/UIRoot.gd" id="2_byw35"] 5 | 6 | [node name="UIRoot" type="Control"] 7 | unique_name_in_owner = true 8 | layout_mode = 3 9 | anchors_preset = 15 10 | anchor_right = 1.0 11 | anchor_bottom = 1.0 12 | grow_horizontal = 2 13 | grow_vertical = 2 14 | size_flags_horizontal = 3 15 | size_flags_vertical = 3 16 | theme = ExtResource("1_kca8q") 17 | script = ExtResource("2_byw35") 18 | 19 | [node name="safeArea" type="MarginContainer" parent="."] 20 | unique_name_in_owner = true 21 | layout_mode = 1 22 | anchors_preset = -1 23 | anchor_right = 1.0 24 | anchor_bottom = 1.0 25 | grow_horizontal = 2 26 | grow_vertical = 2 27 | theme_override_constants/margin_left = 0 28 | theme_override_constants/margin_top = 0 29 | theme_override_constants/margin_right = 0 30 | theme_override_constants/margin_bottom = 0 31 | -------------------------------------------------------------------------------- /framework/builders/elements/ColorBuilder.gd: -------------------------------------------------------------------------------- 1 | extends BaseBuilder 2 | class_name ColorBuilder 3 | 4 | func _init(color) -> void: 5 | _content_node = Panel.new() 6 | _content_node.name = "Color" 7 | 8 | 9 | if color is GradientTexture2D: 10 | var style = StyleBoxTexture.new() 11 | style.texture = color 12 | _content_node.add_theme_stylebox_override("panel", style) 13 | else: 14 | # Solid color option 15 | var style = StyleBoxFlat.new() 16 | style.bg_color = color 17 | _content_node.add_theme_stylebox_override("panel", style) 18 | 19 | 20 | _content_node.size_flags_horizontal = View.SizeFlags.FILL 21 | _content_node.size_flags_vertical = View.SizeFlags.FILL 22 | 23 | #TODO: Clip with shape to have allow Corner Radius on Gradients 24 | func cornerRadius(radius: int) -> ColorBuilder: 25 | var style = _content_node.get_theme_stylebox("panel") 26 | if style is StyleBoxFlat: # Only works for Color Flat 27 | style.corner_radius_top_left = radius 28 | style.corner_radius_top_right = radius 29 | style.corner_radius_bottom_left = radius 30 | style.corner_radius_bottom_right = radius 31 | return self 32 | -------------------------------------------------------------------------------- /framework/view_compiler/ViewRegistry.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name ViewRegistry 3 | 4 | var view_scanner: ViewScanner 5 | var code_generator: ViewCodeGenerator 6 | const view_script_path: String = "res://framework/core/View.gd" 7 | var views_info = [] 8 | 9 | func _ready() -> void: 10 | view_scanner = ViewScanner.new() 11 | code_generator = ViewCodeGenerator.new() 12 | 13 | refresh_views() 14 | 15 | 16 | func refresh_views() -> void: 17 | var obtained_views = view_scanner.scan_for_views() 18 | var view_parser = ViewParser.new() 19 | 20 | views_info.clear() 21 | 22 | 23 | for view in obtained_views: 24 | var type = view.type 25 | var path = view.path 26 | var parsed_constructor = "" 27 | if view.constructor != null: 28 | parsed_constructor = view_parser.parse_view(view.constructor) 29 | else: 30 | parsed_constructor = "" 31 | 32 | views_info.append({ 33 | "type": type, 34 | "path": path, 35 | "parsed_constructor": parsed_constructor 36 | }) 37 | 38 | code_generator.write_code(views_info, view_script_path) 39 | print("View registry refreshed - found %d views" % views_info.size()) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Elvis Villavicencio Vigil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /framework/builders/elements/TextureRectBuilder.gd: -------------------------------------------------------------------------------- 1 | extends BaseBuilder 2 | class_name TextureRectBuilder 3 | 4 | func _init(image_name: String = ""): 5 | _content_node = TextureRect.new() 6 | _content_node.texture = load(image_name) 7 | 8 | #TODO: Make API image sizing more close to SwiftUI sizing 9 | 10 | ## Sets how the texture's minimum size is determined based on its dimensions 11 | ## See ExpandMode enum for detailed descriptions of each mode 12 | func expandMode(mode: View.ExpandMode) -> TextureRectBuilder: 13 | _content_node.expand_mode = mode 14 | return self 15 | 16 | ## Controls how the texture is displayed within its bounding rectangle 17 | ## See StretchMode enum for detailed descriptions of each mode 18 | func stretchMode(mode: View.StretchMode) -> TextureRectBuilder: 19 | _content_node.stretch_mode = mode 20 | return self 21 | 22 | ## Ignore original Texture size 23 | func resize(): 24 | _content_node.expand_mode = View.ExpandMode.IGNORE_SIZE 25 | return self 26 | 27 | # Flips the texture horizontally 28 | func flipHorizontal(enable: bool = true) -> TextureRectBuilder: 29 | _content_node.flipHorizontal = enable 30 | return self 31 | 32 | # Flips the texture vertically 33 | func flipVertical(enable: bool = true) -> TextureRectBuilder: 34 | _content_node.flipVertical = enable 35 | return self 36 | 37 | func tint(color: Color) -> TextureRectBuilder: 38 | _content_node.modulate = color 39 | return self 40 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="GDScriptUI" 14 | run/main_scene="uid://38xdxhygy60f" 15 | config/features=PackedStringArray("4.4", "Forward Plus") 16 | config/icon="res://icon.svg" 17 | 18 | [autoload] 19 | 20 | ViewRegistryUI="*res://framework/view_compiler/ViewRegistry.gd" 21 | 22 | [display] 23 | 24 | window/size/viewport_width=720 25 | window/size/viewport_height=720 26 | window/size/always_on_top=true 27 | window/size/window_width_override=720 28 | window/size/window_height_override=1280 29 | window/stretch/mode="canvas_items" 30 | window/stretch/aspect="expand" 31 | window/vsync/vsync_mode=0 32 | 33 | [gui] 34 | 35 | theme/default_font_multichannel_signed_distance_field=true 36 | 37 | [input] 38 | 39 | save={ 40 | "deadzone": 0.2, 41 | "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":true,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 42 | ] 43 | } 44 | 45 | [input_devices] 46 | 47 | pointing/emulate_touch_from_mouse=true 48 | 49 | [rendering] 50 | 51 | renderer/rendering_method="gl_compatibility" 52 | renderer/rendering_method.mobile="gl_compatibility" 53 | anti_aliasing/quality/use_debanding=true 54 | -------------------------------------------------------------------------------- /framework/binding/Binding.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name Binding 3 | 4 | signal OnAnimationStart 5 | signal OnAnimationFinish 6 | 7 | var flow: Flow 8 | var _old_value 9 | var skipped_at_start = false 10 | 11 | var value: 12 | set(new_value): 13 | if not skipped_at_start: 14 | skipped_at_start = true 15 | value = new_value 16 | return 17 | 18 | _old_value = value 19 | value = new_value 20 | 21 | if flow: 22 | flow.interpolate(_old_value, new_value, func(interpolate_value): 23 | _notify_binds(interpolate_value)) 24 | else: 25 | _notify_binds(value) 26 | get: 27 | return value 28 | 29 | var _binds = {} # {node_unique_id: {property_name: Callable}} 30 | 31 | 32 | func _init(initial_value) -> void: 33 | value = initial_value 34 | 35 | func bind(node: Node, property_name: String, update_callback: Callable): 36 | var unique_id = node.get_instance_id() 37 | # if node not exist 38 | if not _binds.has(unique_id): 39 | _binds.set(unique_id, {}) 40 | 41 | 42 | _binds[unique_id].set(property_name, update_callback) 43 | 44 | #Initial Update 45 | update_callback.call(value) 46 | 47 | func unbind(node: Control, property_name: String): 48 | var unique_id = node.get_instance_id() 49 | if _binds.has(unique_id): 50 | _binds[unique_id].erase(property_name) 51 | if _binds[unique_id].is_empty(): 52 | _binds.erase(unique_id) 53 | 54 | # This function is called when value change 55 | func _notify_binds(with_value): 56 | for node_binds in _binds.values(): 57 | for update_callback in node_binds.values(): 58 | update_callback.call(with_value) 59 | 60 | 61 | func animation(flow: Flow) -> Binding: 62 | self.flow = flow 63 | return self 64 | -------------------------------------------------------------------------------- /framework/builders/elements/ButtonBuilder.gd: -------------------------------------------------------------------------------- 1 | extends BaseBuilder 2 | class_name ButtonBuilder 3 | 4 | var button_style_box = StyleBoxFlat.new() 5 | 6 | func _init(_text: String, action: Callable): 7 | _content_node = Button.new() 8 | _content_node.name = _text + " Button" 9 | _content_node.text = _text 10 | if not action.is_null(): 11 | _content_node.pressed.connect(action) 12 | _get_parent_node().size_flags_horizontal = View.SizeFlags.SHRINK_CENTER 13 | _get_parent_node().size_flags_vertical = View.SizeFlags.SHRINK_CENTER 14 | 15 | func text(value: String) -> ButtonBuilder: 16 | _content_node.text = value 17 | return self 18 | 19 | func fontSize(font_size: int) -> ButtonBuilder: 20 | _content_node.add_theme_font_size_override("font_size", font_size) 21 | _add_explicit_modifier("fontSize", font_size) 22 | return self 23 | 24 | func onPressed(callback: Callable) -> ButtonBuilder: 25 | _content_node.pressed.connect(callback) 26 | return self 27 | 28 | func onHover(callback: Callable) -> ButtonBuilder: 29 | _content_node.mouse_entered.connect(callback) 30 | return self 31 | 32 | # func background(color: Color) -> ButtonBuilder: 33 | # button_style_box.bg_color = color 34 | # _content_node.add_theme_stylebox_override("normal", button_style_box) 35 | # return self 36 | 37 | func cornerRadius(radius: int) -> ButtonBuilder: 38 | button_style_box.set("corner_radius_top_left", radius) 39 | button_style_box.set("corner_radius_top_right", radius) 40 | button_style_box.set("corner_radius_bottom_left", radius) 41 | button_style_box.set("corner_radius_bottom_right", radius) 42 | _content_node.add_theme_stylebox_override("normal", button_style_box) 43 | return self 44 | 45 | func disabled(value: bool = true) -> ButtonBuilder: 46 | _content_node.disabled = value 47 | return self 48 | -------------------------------------------------------------------------------- /framework/core/UIRoot.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | class_name UIRoot 3 | 4 | var spell_targets = 0 5 | @onready var content: View = load("res://examples/mobile app/HomeView.gd").new() 6 | @onready var safeArea = %"safeArea" 7 | 8 | var modals = [] 9 | 10 | # TODO: Test %UIRoot access 11 | func _enter_tree() -> void: 12 | name = "UIRoot" 13 | self.unique_name_in_owner = true 14 | 15 | func _ready(): 16 | if content: 17 | content.configure() 18 | content.to_parent(safeArea) 19 | connect_all_views(content) 20 | 21 | func connect_all_views(node): 22 | if node is View: 23 | print("Connecting signal to: ", node.name) 24 | if !node.property_changed.is_connected(_on_property_changed): 25 | node.property_changed.connect(_on_property_changed) 26 | 27 | for child in node.get_children(): 28 | connect_all_views(child) 29 | 30 | func _on_property_changed(property_name, new_value): 31 | print("Property changed: ", property_name, " to ", new_value) 32 | rebuild_ui() 33 | 34 | 35 | #Rebuild the whole UI including nested views 36 | #Should rebuild based on the UI that changed 37 | #TODO: Structure has changed, it should in fact erase everything from SafeArea (I think, need to test) 38 | func rebuild_ui(): 39 | # Remove all existing children FROM CONTENT 40 | for child in safeArea.get_children(): 41 | if child is Control: 42 | remove_child(child) 43 | child.queue_free() 44 | 45 | # CRUCIAL STEP: Regenerate the body content with current properties 46 | content.configure() # This will regenerate the body array with updated properties 47 | 48 | # Build UI again 49 | content.to_parent(safeArea) 50 | 51 | 52 | func dismiss(): 53 | if not modals.is_empty(): 54 | var modal = modals.pop_back() as Modal 55 | modal.dismiss() 56 | 57 | 58 | func print_from_UIRoot(): 59 | print("print_from_UIRoot") 60 | -------------------------------------------------------------------------------- /framework/core/Modal.gd: -------------------------------------------------------------------------------- 1 | extends PanelContainer 2 | class_name Modal 3 | 4 | var is_presented: Binding 5 | var view_content: BaseBuilder 6 | var animation_tween: Tween 7 | 8 | var margin_top = 50 9 | var margin_right = 16 10 | var margin_left = 16 11 | var margin_down = 0 12 | 13 | @onready var horizontal_margin = margin_right + margin_left 14 | @onready var vertical_margin = margin_top + margin_down 15 | 16 | var initial_position_offscreen_horizontal: Vector2 17 | var initial_position_off_screen_Vertical: Vector2 18 | var position_in_screen := Vector2(16, 80) 19 | 20 | # Called when the node enters the scene tree for the first time. 21 | func _ready() -> void: 22 | z_index = 1 23 | var viewport_size = get_viewport().size 24 | size = Vector2(viewport_size.x - horizontal_margin, viewport_size.y - vertical_margin) 25 | initial_position_offscreen_horizontal = Vector2(size.x, 0) 26 | initial_position_off_screen_Vertical = Vector2(16, size.y + 80) 27 | set_slide_vertical() 28 | 29 | 30 | func set_slide_vertical(): 31 | var viewport_size = get_viewport().size 32 | size = Vector2(viewport_size.x - horizontal_margin, viewport_size.y - vertical_margin) 33 | position = initial_position_off_screen_Vertical 34 | 35 | func set_slide_horizontal(): 36 | size = Vector2(720, 1280) 37 | initial_position_offscreen_horizontal = Vector2(size.x, 0) 38 | position = Vector2(size.x, 0) 39 | 40 | 41 | # func _init(isPresented: Binding, view_content: BaseBuilder) -> void: 42 | # is_presented = isPresented 43 | # view_content = view_content 44 | 45 | # is_presented.bind(self, "isPresented", func(new_value): 46 | # if new_value: 47 | # present() 48 | # else: 49 | # dismiss() 50 | # ) 51 | 52 | func present(): 53 | # Implement custom behaviour in subclasses 54 | pass 55 | 56 | func dismiss(): 57 | # is_presented.value = false 58 | # Implement custom behaviour in subclasses 59 | pass -------------------------------------------------------------------------------- /examples/mobile app/HomeView.gd: -------------------------------------------------------------------------------- 1 | extends View 2 | class_name HomeView 3 | 4 | var paddingAmount = Binding.new(16.0) 5 | var blurAmount = Binding.new(0.2) 6 | var shouldShowSheet = Binding.new(false) 7 | var shouldPresentHome = Binding.new(false) 8 | 9 | var vis = Binding.new(true) 10 | 11 | func configure(): 12 | body = ZStack([ 13 | 14 | #ProjectStudio Code 15 | 16 | GradientView([Color.BLACK]).frame(Infinity, Infinity) 17 | .ignoreSafeArea(), 18 | 19 | VBox([ 20 | 21 | # Title View 22 | VBox([ 23 | 24 | Label("Welcome Back!").bold() 25 | .fontSize(46), 26 | 27 | Label("Elvis Villavicencio").bold() 28 | .fontSize(30) 29 | .fontColor(Color.DARK_GRAY), 30 | 31 | Label("Projects").semiBold() 32 | .fontSize(36), 33 | 34 | ]) 35 | .spacing(4) 36 | .background(Color.DARK_GRAY, 10).blur(1.7) 37 | .paddingSpecific(8, 8, 8, 24).visible(false), 38 | 39 | VBox([ 40 | 41 | HBox([ 42 | 43 | Button("Animate Padding", func(): 44 | blurAmount.value = 1.7 45 | vis.value = !vis.value) 46 | .padding(paddingAmount) 47 | .fontSize(28), 48 | 49 | Button("Present Sheet") 50 | .onPressed(func(): shouldShowSheet.value = !shouldShowSheet.value) 51 | .sheet(shouldShowSheet, 52 | SheetTest(shouldShowSheet)), 53 | 54 | ]), 55 | 56 | ProjectCard().blur(blurAmount), 57 | ProjectCard().blur(blurAmount), 58 | ProjectCard().blur(blurAmount), 59 | ProjectCard().blur(blurAmount), 60 | ]) 61 | .frame(Infinity, Infinity) 62 | .spacing(24), 63 | ]) 64 | .padding(paddingAmount), 65 | 66 | ], "body").frame(Infinity) 67 | 68 | 69 | func ProjectCard(): return \ 70 | VBox([ 71 | #Title 72 | Label("Project Studio").bold() 73 | .fontSize(36) 74 | .fontColor(Color('#EEBE04')), 75 | 76 | HBox([ 77 | HBox([ 78 | Image("res://images/clock.png") 79 | .resize().frame(20, 20), 80 | Label("9:24").fontSize(26), 81 | ]) 82 | .padding(0), 83 | 84 | Spacer(), 85 | 86 | Label("0").fontSize(26) 87 | ]), 88 | 89 | Label("This project made in Godot Engine 4, using GDscriptUI, mimics Project Studio developed from SwiftUI, mobile application to track your work, notes and task done") 90 | .fontSize(19) 91 | .fontColor(Color.GRAY.darkened(0.2)) 92 | .regular(), 93 | 94 | ]) \ 95 | .padding(paddingAmount) \ 96 | .background(Color('#262626'), 10) 97 | 98 | 99 | # ColorView(Color.BLACK).frame(Infinity, Infinity), 100 | # GradientView( 101 | # [Color('332D56'), Color('4E6688').lightened(0.3)], 102 | # Vector2(randf(), randf()), 103 | # Vector2(randf(), randf()) 104 | # ).cornerRadius(10).frame(Infinity, Infinity), 105 | -------------------------------------------------------------------------------- /framework/binding/ObserveArray.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name ObserveArray 3 | 4 | signal collection_changed 5 | 6 | var _array: Array 7 | var _binds: Dictionary = {} # {node_unique_id: {property_name: Callable}} 8 | 9 | func _init(initial_array: Array = []): 10 | _array = [] 11 | 12 | for item in initial_array: 13 | _validate_item(item) 14 | _array.append(item) 15 | 16 | func _validate_item(item) -> void: 17 | if item is Binding: 18 | push_error("ObserveArray: Cannot store Binding type, instead create bindings at View Level or at Model Objects") 19 | assert(false, "ObserveArray: Binding objects are not allowed in ObserveArray") 20 | 21 | # Binding management 22 | func bind(node: Node, property_name: String, update_callback: Callable) -> void: 23 | var unique_id = node.get_instance_id() 24 | if not _binds.has(unique_id): 25 | _binds[unique_id] = {} 26 | _binds[unique_id][property_name] = update_callback 27 | # Initial update 28 | update_callback.call(_array) 29 | 30 | func unbind(node: Node, property_name: String) -> void: 31 | var unique_id = node.get_instance_id() 32 | if _binds.has(unique_id): 33 | _binds[unique_id].erase(property_name) 34 | if _binds[unique_id].is_empty(): 35 | _binds.erase(unique_id) 36 | 37 | # Array operations 38 | func append(item) -> void: 39 | _validate_item(item) 40 | _array.append(item) 41 | _notify_binds() 42 | 43 | func insert(index: int, item) -> void: 44 | _validate_item(item) 45 | _array.insert(index, item) 46 | _notify_binds() 47 | 48 | func erase(item) -> void: 49 | _array.erase(item) 50 | _notify_binds() 51 | 52 | func remove_at(index: int) -> void: 53 | _array.remove_at(index) 54 | _notify_binds() 55 | 56 | func remove_last() -> void: 57 | _array.remove_at(_array.size() - 1) 58 | _notify_binds() 59 | 60 | func clear() -> void: 61 | _array.clear() 62 | _notify_binds() 63 | 64 | func find(item) -> int: 65 | return _array.find(item) 66 | 67 | func size() -> int: 68 | return _array.size() 69 | 70 | func length() -> int: 71 | return _array.size() 72 | 73 | func at(index: int): 74 | return _array[index] 75 | 76 | func set_at(index: int, item) -> void: 77 | _validate_item(item) 78 | _array[index] = item 79 | _notify_binds() 80 | 81 | func map(func_ref: Callable) -> Array: 82 | return _array.map(func_ref) 83 | 84 | func filter(func_ref: Callable) -> Array: 85 | return _array.filter(func_ref) 86 | 87 | func sort_custom(func_ref: Callable) -> void: 88 | _array.sort_custom(func_ref) 89 | _notify_binds() 90 | 91 | func duplicate() -> Array: 92 | return _array.duplicate() 93 | 94 | func to_array() -> Array: 95 | return _array 96 | 97 | # Notify all bound nodes 98 | func _notify_binds() -> void: 99 | for node_binds in _binds.values(): 100 | for update_callback in node_binds.values(): 101 | update_callback.call(_array) 102 | -------------------------------------------------------------------------------- /framework/builders/containers/ZStackBuilder.gd: -------------------------------------------------------------------------------- 1 | extends BaseBuilder 2 | class_name ZStackBuilder 3 | 4 | var _children: Array = [] 5 | 6 | func _init(children: Array = []): 7 | _children = children 8 | _content_node = PanelContainer.new() 9 | _content_node.name = "ZStack" 10 | 11 | # Set up the content node to fill its parent 12 | _content_node.size_flags_horizontal = View.SizeFlags.SHRINK_CENTER 13 | _content_node.size_flags_vertical = View.SizeFlags.SHRINK_CENTER 14 | 15 | # Add children to the stack 16 | _add_children_to_stack() 17 | _check_explicit_modifier() 18 | _check_ignore_safe_area_modifier() 19 | 20 | func _add_children_to_stack(): 21 | for child in _children: 22 | if child._get_parent_node().get_parent(): 23 | child._get_parent_node().get_parent().remove_child(child._get_parent_node()) 24 | _content_node.add_child(child._get_parent_node()) 25 | # child.direct_container_builder = self 26 | 27 | # Override children property to handle updates 28 | var children: Array: 29 | set(new_children): 30 | _children = new_children 31 | for child in _content_node.get_children(): 32 | child.queue_free() 33 | _add_children_to_stack() 34 | _check_explicit_modifier() 35 | _check_ignore_safe_area_modifier() 36 | get: 37 | return _children 38 | 39 | # Check if any child needs expansion 40 | func _check_explicit_modifier(): 41 | for child in _children: 42 | var expand_horizontal_requested = child._explicit_modifiers.get("expand_horizontal", false) 43 | var expand_vertical_requested = child._explicit_modifiers.get("expand_vertical", false) 44 | 45 | var should_expand = false 46 | 47 | # Default sizing on GDscriptUI 48 | var horizontal_value = View.FitContent 49 | var vertical_value = View.FitContent 50 | 51 | # _get_parent_node() is a helper function that retuns the outermost node of the Builder 52 | var is_parent_already_expanded_horizontally = _get_parent_node().size_flags_horizontal == View.SizeFlags.EXPAND_FILL 53 | var is_parent_already_expanded_vertically = _get_parent_node().size_flags_vertical == View.SizeFlags.EXPAND_FILL 54 | 55 | # if Outermost node in this container is not expanded this means we need to expand this container on width 56 | if expand_horizontal_requested and not is_parent_already_expanded_horizontally: 57 | should_expand = true 58 | horizontal_value = View.Infinity 59 | 60 | #if Outermost node in this container is not expanded this means we need to expand this container on height 61 | if expand_vertical_requested and not is_parent_already_expanded_vertically: 62 | should_expand = true 63 | vertical_value = View.Infinity 64 | 65 | # If custom sizing was defined on this container we drop propagation by setting should expand to false 66 | if _get_parent_node().custom_minimum_size != Vector2.ZERO: 67 | should_expand = false 68 | 69 | # Perform chain of propagation 70 | if should_expand: 71 | print_debug("should expand: ", _content_node.name) 72 | frame(horizontal_value, vertical_value) 73 | 74 | func _check_ignore_safe_area_modifier(): 75 | for child in _children: 76 | if child._explicit_modifiers.get("ignore_safe_area"): 77 | _explicit_modifiers.set("ignore_safe_area", true) 78 | 79 | # Override frame to handle ZStack specific sizing 80 | # func frame(width: int = View.FitContent, height: int = View.FitContent) -> ZStackBuilder: 81 | # super.frame(width, height) 82 | # return self 83 | 84 | # Override background to handle ZStack specific styling 85 | # func background(color: Color, corner_radius: int = 0) -> ZStackBuilder: 86 | # super.background(color, corner_radius) 87 | # return self 88 | 89 | # Override padding to handle ZStack specific spacing 90 | # func padding(amount = 8) -> ZStackBuilder: 91 | # super.padding(amount) 92 | # return self 93 | -------------------------------------------------------------------------------- /framework/themes/UITheme.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Theme" load_steps=11 format=3 uid="uid://dyrw727ts3xo1"] 2 | 3 | [ext_resource type="FontVariation" uid="uid://ckqx1r0csvwnk" path="res://framework/themes/SF Pro Fonts/medium_variation.tres" id="1_bxmjo"] 4 | [ext_resource type="FontVariation" uid="uid://cox56evkfub46" path="res://framework/themes/SF Pro Fonts/regular_variation.tres" id="2_4thnd"] 5 | 6 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bxmjo"] 7 | bg_color = Color(0.137255, 0.137255, 0.137255, 1) 8 | corner_radius_top_left = 10 9 | corner_radius_top_right = 10 10 | corner_radius_bottom_right = 10 11 | corner_radius_bottom_left = 10 12 | 13 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4thnd"] 14 | bg_color = Color(0.137255, 0.137255, 0.137255, 1) 15 | corner_radius_top_left = 10 16 | corner_radius_top_right = 10 17 | corner_radius_bottom_right = 10 18 | corner_radius_bottom_left = 10 19 | 20 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_b45lt"] 21 | content_margin_left = 8.0 22 | content_margin_top = 8.0 23 | content_margin_right = 8.0 24 | content_margin_bottom = 8.0 25 | bg_color = Color(0.137255, 0.137255, 0.137255, 1) 26 | corner_radius_top_left = 10 27 | corner_radius_top_right = 10 28 | corner_radius_bottom_right = 10 29 | corner_radius_bottom_left = 10 30 | 31 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dl7rq"] 32 | bg_color = Color(0.137255, 0.137255, 0.137255, 1) 33 | corner_radius_top_left = 10 34 | corner_radius_top_right = 10 35 | corner_radius_bottom_right = 10 36 | corner_radius_bottom_left = 10 37 | 38 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2yar2"] 39 | bg_color = Color(0, 0, 0, 1) 40 | corner_radius_top_left = 10 41 | corner_radius_top_right = 10 42 | corner_radius_bottom_right = 10 43 | corner_radius_bottom_left = 10 44 | 45 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_iv5sr"] 46 | bg_color = Color(0.137255, 0.137255, 0.137255, 1) 47 | corner_radius_top_left = 8 48 | corner_radius_top_right = 8 49 | corner_radius_bottom_right = 8 50 | corner_radius_bottom_left = 8 51 | 52 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_j0b0a"] 53 | content_margin_left = 16.0 54 | content_margin_top = 16.0 55 | content_margin_right = 16.0 56 | content_margin_bottom = 16.0 57 | bg_color = Color(0.137255, 0.137255, 0.137255, 1) 58 | corner_radius_top_left = 8 59 | corner_radius_top_right = 8 60 | corner_radius_bottom_right = 8 61 | corner_radius_bottom_left = 8 62 | 63 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hrafa"] 64 | bg_color = Color(0.137255, 0.137255, 0.137255, 1) 65 | corner_radius_top_left = 8 66 | corner_radius_top_right = 8 67 | corner_radius_bottom_right = 8 68 | corner_radius_bottom_left = 8 69 | 70 | [resource] 71 | default_font = ExtResource("2_4thnd") 72 | Button/styles/focus = SubResource("StyleBoxFlat_bxmjo") 73 | Button/styles/hover = SubResource("StyleBoxFlat_4thnd") 74 | Button/styles/normal = SubResource("StyleBoxFlat_b45lt") 75 | Button/styles/pressed = SubResource("StyleBoxFlat_dl7rq") 76 | Colors/colors/Golden = Color(0.933333, 0.745098, 0.0156863, 1) 77 | Colors/colors/Gray = Color(0.14902, 0.14902, 0.14902, 1) 78 | HBoxContainer/constants/separation = 8 79 | Label/colors/font_color = Color(1, 1, 1, 1) 80 | Label/constants/line_spacing = 3 81 | Label/constants/paragraph_spacing = 2 82 | Label/fonts/font = ExtResource("1_bxmjo") 83 | MarginContainer/constants/margin_bottom = 8 84 | MarginContainer/constants/margin_left = 8 85 | MarginContainer/constants/margin_right = 8 86 | MarginContainer/constants/margin_top = 8 87 | PanelContainer/styles/panel = SubResource("StyleBoxFlat_2yar2") 88 | TextEdit/styles/focus = SubResource("StyleBoxFlat_iv5sr") 89 | TextEdit/styles/normal = SubResource("StyleBoxFlat_j0b0a") 90 | TextEdit/styles/read_only = SubResource("StyleBoxFlat_hrafa") 91 | VBoxContainer/constants/separation = 8 92 | -------------------------------------------------------------------------------- /framework/builders/elements/LabelBuilder.gd: -------------------------------------------------------------------------------- 1 | extends BaseBuilder 2 | class_name LabelBuilder 3 | 4 | var ratio: float = 1.0 5 | # var text: Variant # Could be String or Binding 6 | 7 | @warning_ignore("shadowed_variable") 8 | func _init(text): 9 | _content_node = Label.new() 10 | _content_node.set_meta("LabelBuilder", self) 11 | 12 | # text = value 13 | 14 | if text is Binding: 15 | _bind_text(text) 16 | _content_node.tree_exiting.connect(func(): 17 | text.unbind(_content_node, "text")) 18 | else: 19 | _content_node.text = text 20 | 21 | align() 22 | autowrap(TextServer.AUTOWRAP_WORD_SMART) 23 | 24 | func fontSize(font_size: int) -> LabelBuilder: 25 | _content_node.add_theme_font_size_override("font_size", font_size) 26 | _add_explicit_modifier("fontSize", font_size) 27 | return self 28 | 29 | func align(horizontal: View.TextAlignment = View.TextAlignment.LEADING, vertical: View.TextAlignment = View.TextAlignment.LEADING) -> LabelBuilder: 30 | _content_node.horizontal_alignment = horizontal 31 | _content_node.vertical_alignment = vertical 32 | return self 33 | 34 | func autowrap(mode: TextServer.AutowrapMode) -> LabelBuilder: 35 | _content_node.autowrap_mode = mode 36 | 37 | _frame(View.Infinity) 38 | _calculate_stretch_ratio() 39 | 40 | 41 | return self 42 | 43 | func frame(width: int = View.FitContent, height: int = View.FitContent): 44 | super.frame(width, height) 45 | 46 | _explicit_modifiers["label_expand_ratio"] = ratio 47 | return self 48 | 49 | func _bind_text(binding: Binding): 50 | var label = _content_node as Label 51 | binding.bind(label, "text", func(new_value): 52 | label.text = str(new_value) 53 | _calculate_stretch_ratio() 54 | if direct_container_builder: 55 | direct_container_builder._check_custom_stretch_ratio() 56 | # var direct_container : BoxContainer = _get_parent_node().get_parent() 57 | ) # calculate stretch ratio when text is being set 58 | 59 | func _calculate_stretch_ratio(): 60 | var font = _content_node.get_theme_default_font() 61 | if not font: 62 | font = _content_node.get_theme_font("font") 63 | 64 | if font: 65 | var font_size = _content_node.get_theme_font_size("font_size") 66 | var text_size = font.get_string_size(_content_node.text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, TextServer.DIRECTION_LTR) 67 | 68 | # Calculate ratio based on text width 69 | # This ensures longer text gets proportionally more space 70 | # We use a minimum of 1.0 to prevent very short text from being squished 71 | var text_length = text_size.x 72 | ratio = text_length / 110.0 73 | # ratio = max(text_length / 110.0, 1.0) 74 | 75 | 76 | _explicit_modifiers["ratio"] = ratio 77 | 78 | _content_node.size_flags_stretch_ratio = ratio 79 | if _margin_node != null: 80 | _margin_node.size_flags_stretch_ratio = ratio 81 | 82 | 83 | if _panel_node != null: 84 | _panel_node.size_flags_stretch_ratio = ratio 85 | 86 | if _panel_margin_node != null: 87 | _panel_margin_node.size_flags_stretch_ratio = ratio 88 | 89 | return self 90 | 91 | # func _notification(what: int) -> void: 92 | # if what == NOTIFICATION_PREDELETE: 93 | # if text is Binding and _content_node: 94 | # text.unbind(_content_node, "text") 95 | 96 | func fontColor(color: Color) -> LabelBuilder: 97 | _content_node.add_theme_color_override("font_color", color) 98 | return self 99 | 100 | func regular() -> LabelBuilder: 101 | _content_node.add_theme_font_override("font", load("res://framework/themes/SF Pro Fonts/regular_variation.tres")) 102 | return self 103 | 104 | func medium() -> LabelBuilder: 105 | _content_node.add_theme_font_override("font", load("res://framework/themes/SF Pro Fonts/medium_variation.tres")) 106 | return self 107 | 108 | func semiBold() -> LabelBuilder: 109 | _content_node.add_theme_font_override("font", load("res://framework/themes/SF Pro Fonts/semi_bold_variation.tres")) 110 | return self 111 | 112 | func bold() -> LabelBuilder: 113 | _content_node.add_theme_font_override("font", load("res://framework/themes/SF Pro Fonts/bold_variantion.tres")) 114 | return self 115 | -------------------------------------------------------------------------------- /framework/view_compiler/ViewCodeGenerator.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name ViewCodeGenerator 3 | 4 | # Markers to identify generated code section 5 | const BEGIN_MARKER = "# BEGIN GENERATED VIEW FUNCTIONS" 6 | const END_MARKER = "# END GENERATED VIEW FUNCTIONS" 7 | 8 | var function_template: String = """ 9 | func %s: 10 | \tvar element = load("%s").new() 11 | \telement.%s #constructor call 12 | \treturn build_nested_view("%s", element, self) 13 | """ # % parse_func_call, file_path, configure_method 14 | 15 | 16 | var function_template_without_constructor: String = """ 17 | func %s: 18 | \tvar element = load("%s").new() 19 | \treturn build_nested_view("%s", element, self) 20 | """ 21 | 22 | var generated_functions = {} 23 | 24 | 25 | func write_code(user_defined_views, view_script_path: String) -> void: 26 | if user_defined_views.is_empty(): 27 | print("No views to generate, no user defined views found") 28 | return 29 | 30 | 31 | var file = FileAccess.open(view_script_path, FileAccess.READ) 32 | if not file: 33 | push_error("View Script on path: %s not found" % view_script_path) 34 | return 35 | 36 | var source_code = file.get_as_text() 37 | file.close() 38 | 39 | var begin_marker_index = source_code.find(BEGIN_MARKER) 40 | var end_marker_index = source_code.find(END_MARKER) 41 | 42 | if begin_marker_index == -1 or end_marker_index == -1: 43 | source_code += "\n\n" + BEGIN_MARKER + "\n" + END_MARKER + "\n" 44 | # push_error("View Script on path: %s does not contain the markers %s and %s" % [view_script_path, BEGIN_MARKER, END_MARKER]) 45 | # return 46 | elif begin_marker_index == -1 or end_marker_index == -1 or end_marker_index <= begin_marker_index: 47 | push_error("View Script has malformed markers - fixing it") 48 | source_code = source_code.replace(BEGIN_MARKER, "").replace(END_MARKER, "") 49 | source_code += "\n\n" + BEGIN_MARKER + "\n" + END_MARKER + "\n" 50 | 51 | #refresh markers positions 52 | begin_marker_index = source_code.find(BEGIN_MARKER) 53 | end_marker_index = source_code.find(END_MARKER) 54 | 55 | var before_marker_code = source_code.substr(0, begin_marker_index + BEGIN_MARKER.length()) 56 | var after_marker_code = source_code.substr(end_marker_index) 57 | 58 | var new_functions = {} 59 | var appended_code = "\n" 60 | 61 | for view in user_defined_views: 62 | #Validate required fields 63 | if not view.has("type") or not view.has("path") or not view.has("parsed_constructor"): 64 | push_error("Invalid view definition: %s" % view) 65 | continue 66 | 67 | var type = view.type 68 | var path = view.path 69 | var parsed_constructor = view.parsed_constructor 70 | var has_constructor = parsed_constructor != "" 71 | 72 | var function_code = "" 73 | 74 | if has_constructor: 75 | var function_name = type 76 | var view_function_name = parsed_constructor.replace("configure", function_name.replace(" ", "")) 77 | function_code = function_template % [view_function_name, path, parsed_constructor, function_name] 78 | else: 79 | var function_name = type 80 | var view_function_name = function_name.replace(" ", "") + "()" 81 | function_code = function_template_without_constructor % [view_function_name, path, function_name] 82 | 83 | new_functions[parsed_constructor] = function_code 84 | 85 | #Add each function to the appended code 86 | appended_code += function_code + "\n" 87 | 88 | var new_source_code = before_marker_code + appended_code + after_marker_code 89 | 90 | if new_source_code == source_code: 91 | print("No changes to View Script on path: %s" % view_script_path) 92 | return 93 | 94 | file = FileAccess.open(view_script_path, FileAccess.WRITE) 95 | if not file: 96 | push_error("Failed to open View Script on path: %s for writing" % view_script_path) 97 | return 98 | 99 | file.store_string(new_source_code) 100 | file.close() 101 | 102 | generated_functions = new_functions 103 | print("Updated View Script on path: %s updated with %d new functions" % [view_script_path, new_functions.size()]) 104 | 105 | ResourceLoader.remove_resource_format_loader(null) 106 | -------------------------------------------------------------------------------- /framework/builders/elements/TextEditBuilder.gd: -------------------------------------------------------------------------------- 1 | extends BaseBuilder 2 | class_name TextEditBuilder 3 | 4 | var text 5 | 6 | func _init(text_value, place_holder: String = ""): 7 | _content_node = TextEdit.new() 8 | # _content_node.name = "Text Edit" 9 | _content_node.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY 10 | 11 | text = text_value 12 | if text is Binding: 13 | _bind_text_edit(text) 14 | _content_node.tree_exiting.connect(func(): text.unbind(_content_node, "text")) 15 | else: 16 | _content_node.text = text 17 | _content_node.placeholder_text = place_holder 18 | 19 | # # Sets the text content 20 | # func text(value: String) -> TextEditBuilder: 21 | # _content_node.text = value 22 | # return self 23 | 24 | # Sets whether the text can be edited 25 | func editable(value: bool = true) -> TextEditBuilder: 26 | _content_node.editable = value 27 | return self 28 | 29 | # Sets the text color 30 | func textColor(color: Color) -> TextEditBuilder: 31 | _content_node.add_theme_color_override("font_color", color) 32 | return self 33 | 34 | # Sets the background color 35 | func backgroundColor(color: Color) -> TextEditBuilder: 36 | _content_node.add_theme_color_override("background_color", color) 37 | return self 38 | 39 | # Controls word wrapping 40 | func wordWrap(enabled: bool = true) -> TextEditBuilder: 41 | _content_node.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if enabled else TextEdit.LINE_WRAPPING_NONE 42 | return self 43 | 44 | # Controls whether to highlight the current line 45 | func highlightCurrentLine(enabled: bool = true) -> TextEditBuilder: 46 | _content_node.highlight_current_line = enabled 47 | return self 48 | 49 | # Controls whether to highlight all occurrences of selected text 50 | func highlightAllOccurrences(enabled: bool = true) -> TextEditBuilder: 51 | _content_node.highlight_all_occurrences = enabled 52 | return self 53 | 54 | # Controls whether line numbers are shown 55 | func showLineNumbers(enabled: bool = true) -> TextEditBuilder: 56 | _content_node.minimap_draw = enabled 57 | _content_node.line_folding = enabled 58 | _content_node.gutters_draw_line_numbers = enabled 59 | return self 60 | 61 | # Controls whether the text is in read-only mode 62 | func readonly(value: bool = true) -> TextEditBuilder: 63 | _content_node.editable = !value 64 | return self 65 | 66 | # Controls syntax highlighting (set to a specific language) 67 | func syntaxHighlighting(_language: String = "") -> TextEditBuilder: 68 | _content_node.syntax_highlighter = CodeHighlighter.new() 69 | # This is a simplified approach - in a real implementation 70 | # you would want to configure the highlighter for the specific language 71 | return self 72 | 73 | # Sets multiple options at once for code editing 74 | func codeEditor(show_line_numbers: bool = true, word_wrap: bool = false, 75 | syntax_language: String = "") -> TextEditBuilder: 76 | showLineNumbers(show_line_numbers) 77 | wordWrap(word_wrap) 78 | if syntax_language: 79 | syntaxHighlighting(syntax_language) 80 | return self 81 | 82 | # Connect to text changed event 83 | func onTextChanged(callback: Callable) -> TextEditBuilder: 84 | _content_node.text_changed.connect(callback) 85 | return self 86 | 87 | # Context menu enabled/disabled 88 | func contextMenu(enabled: bool = true) -> TextEditBuilder: 89 | _content_node.context_menu_enabled = enabled 90 | return self 91 | 92 | # Set tab size 93 | func tabSize(spaces: int = 4) -> TextEditBuilder: 94 | _content_node.indent_size = spaces 95 | return self 96 | 97 | func fontSize(font_size: int) -> TextEditBuilder: 98 | _content_node.add_theme_font_size_override("font_size", font_size) 99 | return self 100 | 101 | # Auto-indent new lines 102 | func autoIndent(enabled: bool = true) -> TextEditBuilder: 103 | _content_node.auto_indent = enabled 104 | return self 105 | 106 | func _bind_text_edit(binding): 107 | var text_edit = _content_node as TextEdit 108 | 109 | binding.bind(text_edit, "text", func(new_text): \ 110 | if !text_edit.has_focus(): text_edit.text = str(new_text)) 111 | 112 | text_edit.text_changed.connect(func(): binding.value = text_edit.text) 113 | 114 | # func _notification(what: int) -> void: 115 | # if what == NOTIFICATION_PREDELETE: 116 | # if text is Binding: 117 | # text.unbind(_content_node, "text") -------------------------------------------------------------------------------- /framework/core/custom types/Flow.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name Flow 3 | #This class helps us to define characteristics of the animation 4 | 5 | signal OnAnimationStart 6 | signal OnAnimationFinish 7 | 8 | # Animation timing functions (easing) 9 | enum Easing { 10 | LINEAR = Tween.EASE_IN_OUT, 11 | EASE_IN = Tween.EASE_IN, 12 | EASE_OUT = Tween.EASE_OUT, 13 | EASE_IN_OUT = Tween.EASE_IN_OUT 14 | } 15 | 16 | # Animation curve types 17 | enum CurveType { 18 | LINEAR = Tween.TRANS_LINEAR, 19 | SINE = Tween.TRANS_SINE, 20 | QUAD = Tween.TRANS_QUAD, 21 | CUBIC = Tween.TRANS_CUBIC, 22 | QUART = Tween.TRANS_QUART, 23 | QUINT = Tween.TRANS_QUINT, 24 | EXPO = Tween.TRANS_EXPO, 25 | ELASTIC = Tween.TRANS_ELASTIC, 26 | BOUNCE = Tween.TRANS_BOUNCE, 27 | BACK = Tween.TRANS_BACK, 28 | SPRING = Tween.TRANS_SPRING, 29 | } 30 | 31 | # Default animation duration in seconds 32 | var _duration: float = 0.3 33 | # Default animation delay in seconds 34 | var _delay: float = 0.0 35 | # Default easing function 36 | var _easing: Easing = Easing.EASE_OUT 37 | # Default curve type 38 | var _curve: CurveType = CurveType.CUBIC 39 | # Whether the animation should repeat 40 | var _repeat: bool = false 41 | # Number of times to repeat (-1 for infinite) 42 | var _repeat_count: int = 0 43 | # Whether the animation should play in reverse when repeating 44 | var _yoyo: bool = false 45 | 46 | # Add a new property to store the current tween 47 | var _current_tween: Tween = null 48 | 49 | # Create a new animation with default settings 50 | static func default() -> Flow: 51 | return Flow.new() 52 | 53 | ## Use decimals for float, integer values will not interpolate 54 | static func duration(seconds: float) -> Flow: 55 | var flow = Flow.new() 56 | flow._duration = float(seconds) 57 | return flow 58 | 59 | # Set the animation delay 60 | func delay(seconds: float) -> Flow: 61 | _delay = seconds 62 | return self 63 | 64 | # Set the easing function 65 | func ease(type: Easing) -> Flow: 66 | _easing = type 67 | return self 68 | 69 | # Set the curve type 70 | func curve(type: CurveType) -> Flow: 71 | _curve = type 72 | return self 73 | 74 | # Enable/disable animation repetition 75 | func repeat(should_repeat: bool = true) -> Flow: 76 | _repeat = should_repeat 77 | return self 78 | 79 | # Set the number of times to repeat 80 | func repeat_count(count: int) -> Flow: 81 | _repeat_count = count 82 | return self 83 | 84 | # Enable/disable yoyo effect (reverse animation when repeating) 85 | func yoyo(should_yoyo: bool = true) -> Flow: 86 | _yoyo = should_yoyo 87 | return self 88 | 89 | # Apply the animation to a property 90 | # this should go inside of BaseBuilder, but what we really want is 91 | # we have the dictionary of the Binding that is conected to all those modifiers 92 | # we know exactly what are those modifiers changing so we can animate those in parallele 93 | # this code serve as a good example of what we want to do in terms of logic 94 | 95 | # func animate(object: Object, property: String, from_value: Variant, to_value: Variant) -> Tween: 96 | # var tween = object.create_tween() 97 | # tween.set_ease(_easing) 98 | # tween.set_trans(_curve) 99 | 100 | # if repeat: 101 | # tween.set_loops(repeat_count) 102 | 103 | # if yoyo: 104 | # tween.set_parallel(false) 105 | # tween.tween_property(object, property, to_value, duration) 106 | # tween.tween_property(object, property, from_value, duration) 107 | # else: 108 | # tween.tween_property(object, property, to_value, duration) 109 | 110 | # if _delay > 0: 111 | # tween.set_delay(delay) 112 | 113 | # return tween 114 | 115 | # Apply the animation to multiple properties in parallel 116 | # func animate_parallel(object: Object, properties: Dictionary) -> Tween: 117 | # var tween = object.create_tween() 118 | # tween.set_ease(_easing) 119 | # tween.set_trans(curve) 120 | # tween.set_parallel(true) 121 | 122 | # if repeat: 123 | # tween.set_loops(repeat_count) 124 | 125 | # for property in properties: 126 | # var from_value = properties[property]["from"] 127 | # var to_value = properties[property]["to"] 128 | 129 | # if yoyo: 130 | # tween.tween_property(object, property, to_value, duration) 131 | # tween.tween_property(object, property, from_value, duration) 132 | # else: 133 | # tween.tween_property(object, property, to_value, duration) 134 | 135 | # if _delay > 0: 136 | # tween.set_delay(delay) 137 | 138 | # return tween 139 | 140 | # Add a method to interpolate between two values 141 | func interpolate(from_value: Variant, to_value: Variant, callback: Callable) -> void: 142 | OnAnimationStart.emit() 143 | # Kill any existing tween 144 | if _current_tween != null: 145 | _current_tween.kill() 146 | 147 | # Create new tween 148 | _current_tween = Engine.get_main_loop().root.create_tween() 149 | _current_tween.set_ease(int(_easing)) 150 | # _current_tween.set_ease(Tween.EASE_IN_OUT) 151 | _current_tween.set_trans(int(_curve)) 152 | # _current_tween.set_trans(Tween.TRANS_CUBIC) 153 | 154 | if _repeat: 155 | _current_tween.set_loops(_repeat_count) 156 | 157 | # Set up the interpolation 158 | _current_tween.tween_method(func(current_value: float): 159 | callback.call(current_value) 160 | , 161 | from_value, 162 | to_value, 163 | _duration 164 | ) 165 | 166 | _current_tween.finished.connect(func(): 167 | OnAnimationFinish.emit()) 168 | 169 | 170 | # if _delay > 0: 171 | # _current_tween.set_delay(_delay) 172 | 173 | # if _yoyo: 174 | # _current_tween.set_parallel(false) 175 | # _current_tween.tween_method(func(delta: float): 176 | # callback.call(delta) 177 | # , 178 | # _duration, 179 | # 0.0, 180 | # _duration 181 | # ) 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GDScriptUI (Status Experimental) 2 | 3 | GDScriptUI is a declarative UI framework built on top of Godot Engine UI Nodes.
4 | Inspired on SwiftUI, this framework brings an elegant declarative approach to GDScript.
5 | It aims to create modular interfaces with a clean, functional syntax without managing Control Nodes directly from the Scene Tree. 6 | 7 | ### 🎨 Declarative Syntax 8 | Write your UI layouts in a clean, declarative way: 9 | 10 | ```gdscript 11 | body = [ 12 | VBox([ 13 | Label("Hello World") 14 | .fontSize(78), 15 | Button("Click Me") 16 | .onPressed(func(): print("Clicked!")) 17 | ]) 18 | .spacing(10) 19 | .background(Color.BLACK.lightened(0.2), 10) 20 | ] 21 | ``` 22 | 23 | ### 📦 Built-in Components 24 | - **Current Implemented Containers** 25 | - `HBox` - Horizontal container 26 | - `VBox` - Vertical container 27 | - `ForEach` - Dynamic list rendering 28 | 29 | - **Current Implemented Elements** 30 | - `Label` - Text display with auto-wrapping and alignment 31 | - `Button` - Customizable buttons with events 32 | - `Image` - Texture display with various sizing options 33 | - `TextEdit` - Text input fields 34 | - `Spacer` - Push View 35 | 36 | - **On the Roadmap** 37 | - `Grid` 38 | - `ScrollContainer` 39 | - `LineEdit` 40 | - `OptionButton` 41 | - `ItemList` 42 | - `Slider` 43 | - `TabButtons` 44 | 45 | ## Usage Example 46 | 47 | ```gdscript 48 | extends View 49 | class_name SimpleView 50 | 51 | var items = ["Sword", "Shield", "Potion"] 52 | 53 | func _ready(): 54 | body = [ 55 | #VBox is center aligned by default 56 | VBox([ 57 | Label("Inventory") 58 | .fontSize(78), 59 | 60 | ForEach(items, func(item): 61 | return Label(item).align(TextAlignment.CENTER)) 62 | .vertical() # makes the labels stack vertically 63 | ]) 64 | .spacing(10) 65 | .background(Color.BLACK.lightened(0.2), 10) 66 | ] 67 | ``` 68 | 69 | Screenshot 2025-03-27 at 12 23 26 70 | 71 | 72 | ## Reusable Custom Views 73 | 74 | GDScriptUI scans your project for View subclasses and dynamically generates factory methods, making your custom components instantly available with the same syntax as built-in elements. 75 | 76 | ```gdscript 77 | extends View 78 | class_name ContentView 79 | 80 | 81 | func _ready(): 82 | #Body is where all the UI elements are defined 83 | body = [ 84 | VBox([ 85 | Label("Hello World"), 86 | Button("Click me"), 87 | 88 | SimpleView() 89 | ]) 90 | ] 91 | ``` 92 | 93 | Screenshot 2025-03-27 at 13 15 17 94 | 95 | ## Reactive Properties 96 | 97 | The framework supports reactive properties that automatically trigger UI updates: 98 | 99 | ```gdscript 100 | extends View 101 | class_name PersonView 102 | 103 | var names: Array = ["Lucy", "Albert", "Niklas", "John", "Mariela"] 104 | 105 | #Observed property 106 | var person_name: String = "Lucy": 107 | set(value): 108 | person_name = value 109 | observe("person_name", person_name) 110 | 111 | func _ready() -> void: 112 | body = [ 113 | HBox([ 114 | 115 | Label(person_name) 116 | .fontSize(20), # ',' defines the end of the component 117 | 118 | Image("res://icon.svg") 119 | .expand_mode(ExpandMode.IGNORE_SIZE) 120 | .frame(50, 50) #We can define our own size 121 | .visible(person_name == "Lucy"), 122 | 123 | Button("Random Name") 124 | .onPressed(func(): person_name = names.pick_random()) 125 | ]) 126 | .spacing(5) 127 | ] 128 | ``` 129 | 130 | https://github.com/user-attachments/assets/c4614358-0c6e-46cb-b281-4589782293ea 131 | 132 | ## Key Features 133 | 134 | - **Function-Based Views**: Build UIs with simple calls like `Button()`, `Image()`,`Label()`, `VBox()`, `HBox()` and your custom views 135 | - **Reusable Custom Views**: Your View files automatically become available as custom views `SimpleView()`, `PersonView()` 136 | - **Composable Design**: Nest and combine views with a clean, readable syntax 137 | - **Reactive Updates**: UI automatically rebuilds when observable properties change (Subject to change) 138 | - **Flexible Layouts**: Powerful layout system with spacing, alignment, and padding 139 | - **Style Customization**: Easy styling with methods like `.background()`, `.fontSize()`, `.cornerRadius()` 140 | - **Modifier Propagation**: Containers propagate style modifiers to childs `.fontSize()` 141 | - **Event Handling**: Simple callback system for user interactions `Button("Delete").onPress(deleteItem)`, `Label("Delete").onTap(deleteItem)` 142 | - **Responsive Design**: Support for flexible and fixed sizing with `Infinity` and `FitContent` constants 143 | - **Label FitContent**: Label support FitContent and wrapping by default 144 | 145 | 146 | ### 🛠 Layout Modifiers 147 | Common modifiers available for components: 148 | - `.frame()` - Set component dimensions FitContent or Infinity for expansion 149 | - `.padding()` - Add padding around components 150 | - `.background()` - Set background color and corner radius 151 | - `.spacing()` - Control space between items in containers 152 | - `.alignment()` - Control alignment of items BoxContainerAlignment.CENTER by default, other options (.BEGGIN, END) 153 | - `.visible(value)` - Toggle visibility 154 | 155 | ## Architecture 156 | 157 | - **View**: Super class for all UI views 158 | - **Builders**: Component builders that handle the creation and modification of UI elements 159 | - **UIRoot**: Manages the UI tree and handles property changes 160 | 161 | ## Standard Setup 162 | 163 | - Instantiate UIRoot in your level or in your CanvasLayer: 164 | This is a node that extends from panel container. Adapt its size as you need. 165 | 166 | - Make a script that inherits from View. 167 | Define your UI inside of the Body 168 | 169 | - Add your View as a child of UIRoot and add a reference to the inspector (This might change in the future) 170 | 171 | 172 | ## Requirements 173 | 174 | - Godot 4.x 175 | - GDScript 176 | 177 | ## License 178 | This project is licensed under the [MIT License](LICENSE). 179 | -------------------------------------------------------------------------------- /framework/core/View.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name View 3 | 4 | signal property_changed(property_name, new_value) 5 | var body: BaseBuilder 6 | var nestedViews: Dictionary = {} 7 | 8 | ## Does emit property_change Signal 9 | func observe(property_name: String, value): 10 | property_changed.emit(property_name, value) 11 | 12 | 13 | func to_parent(parent): 14 | if body != null: 15 | body._in_node(parent) 16 | 17 | # Only for Mobile, parent is expect to be MarginContainer acting as a SafeArea space 18 | if parent is not MarginContainer: 19 | return 20 | 21 | if body._explicit_modifiers.get("ignore_safe_area") == true: 22 | # body._frame(Infinity, Infinity) 23 | parent.add_theme_constant_override("margin_left", 0) 24 | parent.add_theme_constant_override("margin_right", 0) 25 | parent.add_theme_constant_override("margin_top", 0) 26 | parent.add_theme_constant_override("margin_bottom", 0) 27 | 28 | 29 | func build_ui(parent) -> ContainerBuilder: 30 | if not body: 31 | return null 32 | 33 | body.view_owner = self 34 | return body 35 | 36 | # Factory methods for container creation 37 | func HBox(children: Array = [], description: String = "") -> ContainerBuilder: 38 | var builder = ContainerBuilder.new(children) 39 | return builder.horizontal(description) 40 | 41 | func VBox(children: Array = [], description: String = "") -> ContainerBuilder: 42 | var builder = ContainerBuilder.new(children) 43 | return builder.vertical(description) 44 | 45 | func ZStack(children: Array = [], _description: String = "") -> ZStackBuilder: 46 | var builder = ZStackBuilder.new(children) 47 | return builder 48 | 49 | # Button cant define icon because icon sizing doesnt work properly as TextureRect sizing 50 | # For adding icon inside of a Button is better to wrap a Image and Button inside of a BoxContainer 51 | ## Button from GDScriptUI 52 | func Button(_text: String, action: Callable = Callable()) -> ButtonBuilder: 53 | return ButtonBuilder.new(_text, action) 54 | 55 | func ForEach(items, action: Callable) -> ContainerBuilder: 56 | # In order to bind the UI node we have to build it first 57 | var hbox = HBox([]) 58 | 59 | # This kind of looks efficient 60 | # What is not efficient is the way the property ContainerBuilder.children 61 | # Works, we are deleting child nodes and create them again. 62 | # Other frameworks like SwiftUI will work with ID 63 | if items is ObserveArray: 64 | # Bind to the container builder's children property 65 | items.bind(hbox._content_node, "children", func(new_items: Array): 66 | var new_elements = [] 67 | for item in new_items: 68 | var element = action.call(item) 69 | if element != null: 70 | new_elements.append(element) 71 | hbox.children = new_elements 72 | ) 73 | else: 74 | var result = [] 75 | for item in items: 76 | var element = action.call(item) 77 | if element != null: 78 | result.append(element) 79 | 80 | return hbox 81 | 82 | 83 | func Image(texture: String = "") -> TextureRectBuilder: 84 | return TextureRectBuilder.new(texture) 85 | 86 | func Label(text) -> LabelBuilder: 87 | return LabelBuilder.new(text) 88 | 89 | ##Editor for Text 90 | func TextEdit(text, place_holder: String) -> TextEditBuilder: 91 | return TextEditBuilder.new(text, place_holder) 92 | 93 | func Spacer() -> SpacerBuilder: 94 | return SpacerBuilder.new() 95 | 96 | func ColorView(color: Color) -> ColorBuilder: 97 | return ColorBuilder.new(color) 98 | 99 | ## Creates a gradient view with specified colors. 100 | ## - startPoint (0,0) -> (top-left corner) 101 | ## - endPoint (1,1) -> (bottom-right corner) 102 | func GradientView(colors: Array[Color], startPoint: Vector2 = Vector2(0, 0), endPoint: Vector2 = Vector2(1, 1)) -> ColorBuilder: 103 | var texture = GradientTexture2D.new() 104 | texture.fill_from = startPoint 105 | texture.fill_to = endPoint 106 | texture.gradient = Gradient.new() 107 | texture.gradient.colors = colors 108 | 109 | var off_set = [] 110 | for element in range(colors.size()): 111 | if element == 0: 112 | off_set.append(element) 113 | continue 114 | 115 | off_set.append(1.0 / element) 116 | texture.gradient.offsets = off_set 117 | return ColorBuilder.new(texture) 118 | 119 | func bind(initial_value) -> Binding: 120 | return Binding.new(initial_value) 121 | 122 | 123 | # Custom enums that mirror TextureRect's enums for better readability 124 | enum ExpandMode { 125 | ## The minimum size will be equal to texture size, TextureRect can't be smaller than the texture 126 | KEEP_SIZE = 0, 127 | ## The size of the texture won't be considered for minimum size calculation 128 | IGNORE_SIZE = 1, 129 | ## The height of the texture will be ignored; useful for horizontal layouts 130 | FIT_WIDTH_PROPORTIONAL = 3, 131 | ## The width of the texture will be ignored; useful for vertical layouts 132 | FIT_HEIGHT = 4, 133 | ## Same as FIT_HEIGHT but keeps texture's aspect ratio 134 | FIT_HEIGHT_PROPORTIONAL = 5, 135 | } 136 | 137 | enum StretchMode { 138 | ## Scale (stretch) to fit the node's bounding rectangle 139 | SCALE = 0, 140 | ## Tile the image inside the node's bounding rectangle 141 | TILE = 1, 142 | ## The texture keeps its original size, is not counted for minimum size calculation inside the container 143 | KEEP = 2, 144 | ## As with [Keep] the texture keeps its original size, is not counted for minimum size calculation inside the container 145 | KEEP_ASPECT = 4, # Scale the texture to fit the bounding rectangle while maintaining aspect ratio 146 | ## Scale to fit, center it and maintain aspect ratio 147 | KEEP_ASPECT_CENTERED = 5, 148 | ## Cover the entire bounding rectangle, aspect ratio is not maintained, the texture may be cropped 149 | KEEP_ASPECT_COVERED = 6, 150 | } 151 | 152 | ##The SizeFlags constants goes like this, you could also use integers values 153 | ## SizeFlags { 154 | ## 0 = SHRINK_BEGIN 155 | ## 1 = FILL 156 | ## 2 = EXPAND 157 | ## 3 = EXPAND_FILL 158 | ## 4 = SHRINK_CENTER 159 | ## 5 = SHRINK_END 160 | ## } 161 | enum SizeFlags { 162 | SHRINK_BEGIN = Control.SIZE_SHRINK_BEGIN, 163 | FILL = Control.SIZE_FILL, 164 | EXPAND = Control.SIZE_EXPAND, 165 | EXPAND_FILL = Control.SIZE_EXPAND_FILL, 166 | SHRINK_CENTER = Control.SIZE_SHRINK_CENTER, 167 | SHRINK_END = Control.SIZE_SHRINK_END, 168 | } 169 | 170 | enum TextAlignment { 171 | LEADING = 0, 172 | CENTER = 1, 173 | TRAILING = 2, 174 | # TOP = 0, 175 | # BOTTOM = 2, 176 | } 177 | 178 | enum BoxContainerAlignment { 179 | BEGIN = 0, 180 | CENTER = 1, 181 | END = 2, 182 | } 183 | 184 | 185 | const Infinity = -1 186 | const FitContent = -2 187 | 188 | func set_nested_view(viewName: String, view: View): 189 | if not nestedViews.has(viewName): 190 | nestedViews.set(viewName, view) 191 | 192 | func get_nested_view(viewName: String) -> View: 193 | return nestedViews.get(viewName) 194 | 195 | func build_nested_view(viewName: String, view: View, parent: Node) -> ContainerBuilder: 196 | set_nested_view(viewName, view) 197 | return get_nested_view(viewName).build_ui(parent) 198 | 199 | 200 | # REFACTOR HOW UI ARE BUILD 201 | # What we really need is to retun the View, and construct it inside of the Builder 202 | # This way we can 203 | 204 | # BEGIN GENERATED VIEW FUNCTIONS 205 | 206 | func HomeView(): 207 | var element = load("res://examples/mobile app/HomeView.gd").new() 208 | element.configure() #constructor call 209 | return build_nested_view("HomeView", element, self) 210 | 211 | 212 | func SheetTest(isPresented): 213 | var element = load("res://examples/mobile app/SheetTest.gd").new() 214 | element.configure(isPresented) #constructor call 215 | return build_nested_view("SheetTest", element, self) 216 | 217 | # END GENERATED VIEW FUNCTIONS 218 | -------------------------------------------------------------------------------- /framework/builders/containers/ContainerBuilder.gd: -------------------------------------------------------------------------------- 1 | extends BaseBuilder 2 | class_name ContainerBuilder 3 | 4 | #Builders Array, each builder construct itself from inside 5 | var _children: Array = [] 6 | var _max_child_stretch_ratio: float = 1.0 7 | 8 | var children: Array: 9 | set(new_children): 10 | _children = new_children 11 | for child in _content_node.get_children(): 12 | child.queue_free() 13 | _add_children_to_container() 14 | _check_explicit_modifier() 15 | _check_custom_stretch_ratio() 16 | _check_ignore_safe_area_modifier() 17 | get: 18 | return _children 19 | 20 | func _init(children: Array = []): 21 | _children = children # Store children for later use 22 | _margin_node = MarginContainer.new() 23 | _content_node = BoxContainer.new() 24 | _margin_node.add_child(_content_node) 25 | _with_margin(true) 26 | 27 | _add_children_to_container() 28 | 29 | func _add_children_to_container(): 30 | for child in _children: 31 | #This lines where made with the intention of replace HBox or Vbox on BoxContainer 32 | if child._get_parent_node().get_parent(): 33 | child._get_parent_node().get_parent().remove_child(child._get_parent_node()) 34 | _content_node.add_child(child._get_parent_node()) 35 | child.direct_container_builder = self 36 | 37 | # By Default Godot UI works with fill content. 38 | # GDscriptUI intent to work as Fit Content (ShrinkCenter at content size) 39 | # this method checks if the child has requested the parent to expand with the .frame(width: Infinity, height: Infinity) 40 | # inside .frame() modifier we set true or false on _explicit_modifiers("expand_horizontal" or "expand_vertical") that means 41 | # on this ContainerBuilder based on child requirement we set a explicit modifier when calling frame(value, value) 42 | # with that making a chain of propagation from bottom to top 43 | func _check_explicit_modifier(): 44 | for child in _children: 45 | var expand_horizontal_requested = child._explicit_modifiers.get("expand_horizontal", false) 46 | var expand_vertical_requested = child._explicit_modifiers.get("expand_vertical", false) 47 | 48 | var should_expand = false 49 | 50 | # Default sizing on GDscriptUI 51 | var horizontal_value = View.FitContent 52 | var vertical_value = View.FitContent 53 | 54 | # _get_parent_node() is a helper function that retuns the outermost node of the Builder 55 | var is_parent_already_expanded_horizontally = _get_parent_node().size_flags_horizontal == View.SizeFlags.EXPAND_FILL 56 | var is_parent_already_expanded_vertically = _get_parent_node().size_flags_vertical == View.SizeFlags.EXPAND_FILL 57 | 58 | # if Outermost node in this container is not expanded this means we need to expand this container on width 59 | if expand_horizontal_requested and not is_parent_already_expanded_horizontally: 60 | should_expand = true 61 | horizontal_value = View.Infinity 62 | 63 | #if Outermost node in this container is not expanded this means we need to expand this container on height 64 | if expand_vertical_requested and not is_parent_already_expanded_vertically: 65 | should_expand = true 66 | vertical_value = View.Infinity 67 | 68 | # If custom sizing was defined on this container we drop propagation by setting should expand to false 69 | if _get_parent_node().custom_minimum_size != Vector2.ZERO: 70 | should_expand = false 71 | 72 | # Perform chain of propagation 73 | if should_expand: 74 | frame(horizontal_value, vertical_value) 75 | 76 | 77 | func _check_ignore_safe_area_modifier(): 78 | for child in _children: 79 | if child._explicit_modifiers.get("ignore_safe_area"): 80 | _explicit_modifiers.set("ignore_safe_area", true) 81 | 82 | func horizontal(description: String = "") -> ContainerBuilder: 83 | _content_node.vertical = false 84 | _content_node.alignment = View.BoxContainerAlignment.BEGIN 85 | _margin_node.name = description + " Margin Container" 86 | _content_node.name = description + " HBox Container" 87 | 88 | #spacing 89 | _content_node.set("theme_override_constants/separation", 8) 90 | 91 | _margin_node.add_theme_constant_override("margin_left", 8) 92 | _margin_node.add_theme_constant_override("margin_right", 8) 93 | _margin_node.add_theme_constant_override("margin_top", 8) 94 | _margin_node.add_theme_constant_override("margin_bottom", 8) 95 | 96 | _check_explicit_modifier() 97 | _check_custom_stretch_ratio() 98 | _check_ignore_safe_area_modifier() 99 | return self 100 | 101 | func vertical(description: String = "") -> ContainerBuilder: 102 | _content_node.vertical = true 103 | _content_node.alignment = View.BoxContainerAlignment.BEGIN 104 | 105 | _margin_node.name = description + " Margin Container" 106 | _content_node.name = description + " VBox Container" 107 | 108 | #spacing 109 | _content_node.set("theme_override_constants/separation", 8) 110 | 111 | #padding 112 | _margin_node.add_theme_constant_override("margin_left", 8) 113 | _margin_node.add_theme_constant_override("margin_right", 8) 114 | _margin_node.add_theme_constant_override("margin_top", 8) 115 | _margin_node.add_theme_constant_override("margin_bottom", 8) 116 | 117 | # _add_children_to_container() 118 | _check_explicit_modifier() 119 | _check_custom_stretch_ratio() 120 | _check_ignore_safe_area_modifier() 121 | return self 122 | 123 | func spacing(value: int = 8) -> ContainerBuilder: 124 | _content_node.set("theme_override_constants/separation", value) 125 | return self 126 | 127 | func alignment(alignment: View.BoxContainerAlignment) -> ContainerBuilder: 128 | _content_node.set("alignment", alignment) 129 | return self 130 | 131 | func fontSize(font_size: int) -> ContainerBuilder: 132 | for child in _children: 133 | # if a child has already set a fontSize modifier, we skip it 134 | if child._has_explicit_modifier("fontSize"): 135 | continue 136 | 137 | # If one child is also a container, we make a recursive call to propagate down 138 | if child is ContainerBuilder and child._children.size() > 0: 139 | child.fontSize(font_size) 140 | 141 | if child.has_method("fontSize"): 142 | child.fontSize(font_size) 143 | return self 144 | 145 | func background(color: Color, radius: int = 0) -> ContainerBuilder: 146 | super.background(color, radius) 147 | 148 | _check_explicit_modifier() 149 | 150 | return self 151 | 152 | # Needs a Binding to reflect the change 153 | func changeToVertical(value: bool) -> ContainerBuilder: 154 | if value: 155 | return self.vertical() 156 | else: 157 | return self.horizontal() 158 | 159 | 160 | func _check_custom_stretch_ratio() -> ContainerBuilder: 161 | var highest_ratio = 1.0 162 | 163 | # Find highest ratio from any label 164 | for child in _children: 165 | if child._has_explicit_modifier("ratio"): 166 | highest_ratio = max(highest_ratio, child._explicit_modifiers.get("ratio")) 167 | 168 | # Apply to explicit label expansions 169 | for child in _children: 170 | # Because all labels expand by default because of (autowrap behaviour and fit content behaviour), if a label wants 171 | # to expand, because it is already expanded, in order to fight for space we need to set his aspect equal to the biggest 172 | # brother in the container. 173 | if child is LabelBuilder: 174 | if child._has_explicit_modifier("label_expand_ratio"): 175 | # the stretch_ratio needs to also be applied to padding, backgroun, margin 176 | child._aspect_ratio_based_on_label_siblings(highest_ratio) 177 | 178 | else: 179 | continue 180 | 181 | # # NEW: Also apply to any element requesting expansion 182 | # We are obtaining the highest ratio and apply it even on containers that should not be applied a different ratio that what they already have 183 | # This code is adjusting child builders per container container builder, 184 | elif (child._has_explicit_modifier("expand_horizontal") or child._has_explicit_modifier("expand_vertical")): 185 | child._aspect_ratio_based_on_label_siblings(highest_ratio) 186 | 187 | return self 188 | -------------------------------------------------------------------------------- /framework/builders/BaseBuilder.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name BaseBuilder 3 | 4 | # strict modifiers that will not be overwriten by container modifiers 5 | var view_owner: View 6 | var _explicit_modifiers = {} 7 | 8 | var _content_node: Control # The actual control being built (Button, Label, etc.) 9 | var _margin_node: MarginContainer # Optional margin wrapper 10 | var _panel_node: PanelContainer # Optional panel background wrapper 11 | var _panel_margin_node: MarginContainer # Optional panel and margin wrapper 12 | var _use_margin: bool = false # Flag to determine if we're using margin 13 | var _use_panel: bool = false # Flag to determine if we're using panel background 14 | var _use_panel_margin: bool = false # Flag to determine if we're using panel and margin 15 | var node_parent: Control 16 | var direct_container_builder: ContainerBuilder 17 | 18 | func _init(): 19 | pass 20 | 21 | # Creates and configures the margin container if needed 22 | func _setup_margin_if_needed(): 23 | if _use_margin and not _margin_node: 24 | _margin_node = MarginContainer.new() 25 | _margin_node.size_flags_horizontal = View.SizeFlags.FILL 26 | _margin_node.size_flags_vertical = View.SizeFlags.FILL 27 | 28 | if _content_node.get_parent(): 29 | var parent = _content_node.get_parent() 30 | parent.remove_child(_content_node) 31 | parent.add_child(_margin_node) 32 | _margin_node.add_child(_content_node) 33 | else: 34 | _margin_node.add_child(_content_node) 35 | 36 | func _setup_panel_margin_if_needed(): 37 | if _use_panel_margin and not _panel_margin_node: 38 | _panel_margin_node = MarginContainer.new() 39 | _panel_margin_node.size_flags_horizontal = View.SizeFlags.FILL 40 | _panel_margin_node.size_flags_vertical = View.SizeFlags.FILL 41 | 42 | # Now reparent the panel into the panel margin 43 | if _panel_node.get_parent(): 44 | var parent = _panel_node.get_parent() 45 | parent.remove_child(_panel_node) 46 | parent.add_child(_panel_margin_node) 47 | _panel_margin_node.add_child(_panel_node) 48 | else: 49 | _panel_margin_node.add_child(_panel_node) 50 | 51 | 52 | # Creates and configures the panel container if needed 53 | func _setup_panel_if_needed(): 54 | if _use_panel and not _panel_node: 55 | _panel_node = PanelContainer.new() 56 | _panel_node.size_flags_horizontal = View.SizeFlags.SHRINK_CENTER 57 | _panel_node.size_flags_vertical = View.SizeFlags.SHRINK_CENTER 58 | 59 | # If we already have a margin, place the panel outside the margin 60 | if _use_margin and _margin_node: 61 | if _margin_node.get_parent(): 62 | var parent = _margin_node.get_parent() 63 | parent.remove_child(_margin_node) 64 | parent.add_child(_panel_node) 65 | _panel_node.add_child(_margin_node) 66 | else: 67 | _panel_node.add_child(_margin_node) 68 | # Otherwise, place the panel directly around the content 69 | elif _content_node.get_parent(): 70 | var parent = _content_node.get_parent() 71 | parent.remove_child(_content_node) 72 | parent.add_child(_panel_node) 73 | _panel_node.add_child(_content_node) 74 | else: 75 | _panel_node.add_child(_content_node) 76 | 77 | ## Gets the node that should be added to the parent 78 | func _get_parent_node() -> Control: 79 | if _use_panel_margin: 80 | return _panel_margin_node 81 | elif _use_panel: 82 | return _panel_node 83 | elif _use_margin: 84 | return _margin_node 85 | else: 86 | return _content_node 87 | 88 | func _with_panel_margin(enable: bool = true) -> BaseBuilder: 89 | _use_panel_margin = enable 90 | _setup_panel_margin_if_needed() 91 | return self 92 | 93 | # Toggle margin container usage 94 | func _with_margin(enable: bool = true) -> BaseBuilder: 95 | _use_margin = enable 96 | _setup_margin_if_needed() 97 | return self 98 | 99 | # Add to parent node 100 | func _in_node(parent: Node) -> BaseBuilder: 101 | parent.add_child(_get_parent_node()) 102 | node_parent = parent 103 | return self 104 | 105 | # Visibility control 106 | func visible(value) -> BaseBuilder: 107 | if value is Binding: 108 | value.bind(_get_parent_node(), "visible", func(is_visible): 109 | _get_parent_node().visible = is_visible) 110 | else: 111 | _get_parent_node().visible = value 112 | return self 113 | 114 | ## Not implemented correctly yet 115 | func _size(width, height) -> BaseBuilder: 116 | if width is Binding: 117 | width.bind(_get_parent_node(), "size.x", func(new_value): 118 | _get_parent_node().size = Vector2(new_value, new_value)) 119 | else: 120 | _get_parent_node().size = Vector2(width, height) 121 | 122 | return self 123 | 124 | #END OF TESTING 125 | 126 | # Name setting (applies to both the main node and the margin if present) 127 | func name(value: String) -> BaseBuilder: 128 | _content_node.name = value 129 | if _use_margin and _margin_node: 130 | _margin_node.name = value + " Container" 131 | if _use_panel and _panel_node: 132 | _panel_node.name = value + " Panel" 133 | return self 134 | 135 | # Focus control 136 | func focusable(value: bool = true) -> BaseBuilder: 137 | _content_node.focus_mode = Control.FOCUS_ALL if value else Control.FOCUS_NONE 138 | return self 139 | 140 | # Tooltip 141 | func tooltip(text: String) -> BaseBuilder: 142 | _content_node.tooltip_text = text 143 | return self 144 | 145 | # Mouse filter 146 | func mouseFilter(filter: Control.MouseFilter) -> BaseBuilder: 147 | _content_node.mouse_filter = filter 148 | return self 149 | 150 | 151 | #amount: int = 8 152 | func padding(amount = 8) -> BaseBuilder: 153 | if _use_panel and not _use_panel_margin: 154 | # Automatically enable panel margin if padding is requested after background 155 | _use_panel_margin = true 156 | _setup_panel_margin_if_needed() 157 | # _with_panel_margin() 158 | 159 | 160 | if amount is Binding: 161 | amount.bind(_panel_margin_node, "_panel_margin_node", func(new_value): 162 | _panel_margin_node.add_theme_constant_override("margin_left", new_value) 163 | _panel_margin_node.add_theme_constant_override("margin_right", new_value) 164 | _panel_margin_node.add_theme_constant_override("margin_top", new_value) 165 | _panel_margin_node.add_theme_constant_override("margin_bottom", new_value)) 166 | else: 167 | _panel_margin_node.add_theme_constant_override("margin_left", amount) 168 | _panel_margin_node.add_theme_constant_override("margin_right", amount) 169 | _panel_margin_node.add_theme_constant_override("margin_top", amount) 170 | _panel_margin_node.add_theme_constant_override("margin_bottom", amount) 171 | 172 | _outer_frame_if_needed() 173 | 174 | if _has_explicit_modifier("label_expand_ratio"): 175 | _panel_margin_node.size_flags_stretch_ratio = _explicit_modifiers.get("label_expand_ratio") 176 | 177 | return self 178 | 179 | # Regular padding 180 | _with_margin(true) 181 | 182 | if amount is Binding: 183 | amount.bind(_margin_node, "margin", func(new_value): 184 | _margin_node.add_theme_constant_override("margin_left", new_value) 185 | _margin_node.add_theme_constant_override("margin_right", new_value) 186 | _margin_node.add_theme_constant_override("margin_top", new_value) 187 | _margin_node.add_theme_constant_override("margin_bottom", new_value)) 188 | else: 189 | _margin_node.add_theme_constant_override("margin_left", amount) 190 | _margin_node.add_theme_constant_override("margin_right", amount) 191 | _margin_node.add_theme_constant_override("margin_top", amount) 192 | _margin_node.add_theme_constant_override("margin_bottom", amount) 193 | 194 | _outer_frame_if_needed() 195 | 196 | if _has_explicit_modifier("label_expand_ratio"): 197 | _margin_node.size_flags_stretch_ratio = _explicit_modifiers.get("label_expand_ratio") 198 | 199 | return self 200 | 201 | # Set specific padding values 202 | func paddingSpecific(left: int = 0, top: int = 0, right: int = 0, bottom: int = 0) -> BaseBuilder: 203 | if _use_panel and not _use_panel_margin: 204 | # Automatically enable panel margin if padding is requested after background 205 | _use_panel_margin = true 206 | _setup_panel_margin_if_needed() 207 | 208 | _panel_margin_node.add_theme_constant_override("margin_left", left) 209 | _panel_margin_node.add_theme_constant_override("margin_right", right) 210 | _panel_margin_node.add_theme_constant_override("margin_top", top) 211 | _panel_margin_node.add_theme_constant_override("margin_bottom", bottom) 212 | 213 | if _explicit_modifiers.has("expand_horizontal") and _explicit_modifiers.get("expand_horizontal"): 214 | _panel_margin_node.size_flags_horizontal = View.SizeFlags.FILL 215 | 216 | if _explicit_modifiers.has("expand_vertical") and _explicit_modifiers.get("expand_vertical"): 217 | _panel_margin_node.size_flags_vertical = View.SizeFlags.FILL 218 | 219 | return self 220 | 221 | if not _use_margin: 222 | # Automatically enable margin if padding is requested 223 | _with_margin(true) 224 | 225 | _margin_node.add_theme_constant_override("margin_left", left) 226 | _margin_node.add_theme_constant_override("margin_top", top) 227 | _margin_node.add_theme_constant_override("margin_right", right) 228 | _margin_node.add_theme_constant_override("margin_bottom", bottom) 229 | 230 | if _explicit_modifiers.has("expand_horizontal") and _explicit_modifiers.get("expand_horizontal"): 231 | _margin_node.size_flags_horizontal = View.SizeFlags.FILL 232 | 233 | if _explicit_modifiers.has("expand_vertical") and _explicit_modifiers.get("expand_vertical"): 234 | _margin_node.size_flags_vertical = View.SizeFlags.FILL 235 | 236 | return self 237 | 238 | func _padding_theme_override(control_node: MarginContainer, amount: int): 239 | control_node.add_theme_constant_override("margin_left", amount) 240 | control_node.add_theme_constant_override("margin_top", amount) 241 | control_node.add_theme_constant_override("margin_right", amount) 242 | control_node.add_theme_constant_override("margin_bottom", amount) 243 | 244 | func _sizeFlags(size_flags_h := View.SizeFlags.FILL, size_flags_v := View.SizeFlags.FILL) -> BaseBuilder: 245 | _get_parent_node().size_flags_horizontal = size_flags_h 246 | _get_parent_node().size_flags_vertical = size_flags_v 247 | return self 248 | 249 | # Customize panel background color 250 | func background(color: Color, corner_radius: int = 0) -> BaseBuilder: 251 | if not _use_panel: 252 | _use_panel = true 253 | _setup_panel_if_needed() 254 | 255 | var style = StyleBoxFlat.new() 256 | style.bg_color = color 257 | #corner_radius 258 | style.corner_radius_top_left = corner_radius 259 | style.corner_radius_top_right = corner_radius 260 | style.corner_radius_bottom_left = corner_radius 261 | style.corner_radius_bottom_right = corner_radius 262 | _panel_node.add_theme_stylebox_override("panel", style) 263 | 264 | _outer_frame_if_needed() 265 | 266 | if _has_explicit_modifier("label_expand_ratio"): 267 | _panel_node.size_flags_stretch_ratio = _explicit_modifiers.get("label_expand_ratio") 268 | 269 | return self 270 | 271 | #TODO: Test frame modifier for each control View 272 | func frame(width: int = View.FitContent, height: int = View.FitContent) -> BaseBuilder: 273 | # Fit content is by default so no need to change anything. 274 | if width == View.FitContent and height == View.FitContent: 275 | return self 276 | 277 | # if either width or height is View.Infinity we then expand and set explicit modifier for parent to respect this sizing. 278 | if width == View.Infinity: 279 | _content_node.size_flags_horizontal = View.SizeFlags.FILL 280 | 281 | if _margin_node: 282 | _margin_node.size_flags_horizontal = View.SizeFlags.FILL 283 | 284 | if _panel_node: 285 | _panel_node.size_flags_horizontal = View.SizeFlags.FILL 286 | 287 | if _panel_margin_node: 288 | _panel_node.size_flags_horizontal = View.SizeFlags.FILL 289 | 290 | _get_parent_node().size_flags_horizontal = View.SizeFlags.EXPAND_FILL 291 | _explicit_modifiers["expand_horizontal"] = true 292 | 293 | if height == View.Infinity: 294 | _content_node.size_flags_vertical = View.SizeFlags.FILL 295 | 296 | if _margin_node: 297 | _margin_node.size_flags_vertical = View.SizeFlags.FILL 298 | 299 | if _panel_node: 300 | _panel_node.size_flags_vertical = View.SizeFlags.FILL 301 | 302 | if _panel_margin_node: 303 | _panel_node.size_flags_vertical = View.SizeFlags.FILL 304 | 305 | _get_parent_node().size_flags_vertical = View.SizeFlags.EXPAND_FILL 306 | _explicit_modifiers["expand_vertical"] = true 307 | 308 | # custom size only modify the element that call frame, and because we are following FitContent (shrink_center) design 309 | # the parent will grow based on the custom_minimum_size of the childs 310 | if width >= 0: 311 | _get_parent_node().size_flags_horizontal = View.SizeFlags.SHRINK_CENTER 312 | _get_parent_node().custom_minimum_size.x = width 313 | 314 | if height >= 0: 315 | _get_parent_node().size_flags_vertical = View.SizeFlags.SHRINK_CENTER 316 | _get_parent_node().custom_minimum_size.y = height 317 | 318 | return self 319 | 320 | func _frame(width: int = View.FitContent, height: int = View.FitContent): 321 | # Fit content is by default the so no need to change anything. 322 | if width == View.FitContent and height == View.FitContent: 323 | return 324 | 325 | # if either width or height is View.Infinity we then expand and set explicit modifier for parent to respect this sizing. 326 | if width == View.Infinity: 327 | _content_node.size_flags_horizontal = View.SizeFlags.FILL 328 | 329 | if _margin_node: 330 | _margin_node.size_flags_horizontal = View.SizeFlags.FILL 331 | 332 | if _panel_node: 333 | _panel_node.size_flags_horizontal = View.SizeFlags.FILL 334 | 335 | if _panel_margin_node: 336 | _panel_node.size_flags_horizontal = View.SizeFlags.FILL 337 | 338 | _get_parent_node().size_flags_horizontal = View.SizeFlags.EXPAND_FILL 339 | _explicit_modifiers["expand_horizontal"] = true 340 | 341 | if height == View.Infinity: 342 | _content_node.size_flags_vertical = View.SizeFlags.FILL 343 | 344 | if _margin_node: 345 | _margin_node.size_flags_vertical = View.SizeFlags.FILL 346 | 347 | if _panel_node: 348 | _panel_node.size_flags_vertical = View.SizeFlags.FILL 349 | 350 | if _panel_margin_node: 351 | _panel_node.size_flags_vertical = View.SizeFlags.FILL 352 | 353 | _get_parent_node().size_flags_vertical = View.SizeFlags.EXPAND_FILL 354 | _explicit_modifiers["expand_vertical"] = true 355 | 356 | # custom size only modify the element that call frame, and because we are following FitContent (shrink_center) design 357 | # the parent will grow based on the custom_minimum_size of the childs 358 | if width >= 0: 359 | _get_parent_node().size_flags_horizontal = View.SizeFlags.SHRINK_CENTER 360 | _get_parent_node().custom_minimum_size.x = width 361 | 362 | if height >= 0: 363 | _get_parent_node().size_flags_vertical = View.SizeFlags.SHRINK_CENTER 364 | _get_parent_node().custom_minimum_size.y = height 365 | 366 | func _outer_frame_if_needed(): 367 | if _explicit_modifiers.has("expand_horizontal") or _explicit_modifiers.has("expand_vertical"): 368 | var should_expand = false 369 | var should_expand_horizontal = _explicit_modifiers.get("expand_horizontal", false) 370 | var should_expand_vertical = _explicit_modifiers.get("expand_vertical", false) 371 | var horizontal_value = View.FitContent 372 | var vertical_value = View.FitContent 373 | 374 | if should_expand_horizontal: 375 | should_expand = true 376 | horizontal_value = View.Infinity 377 | 378 | if should_expand_vertical: 379 | should_expand = true 380 | vertical_value = View.Infinity 381 | 382 | if should_expand: 383 | frame(horizontal_value, vertical_value) 384 | 385 | func onMouseEntered(callback: Callable) -> BaseBuilder: 386 | _content_node.mouse_entered.connect(callback) 387 | return self 388 | 389 | func onMouseExited(callback: Callable) -> BaseBuilder: 390 | _content_node.mouse_exited.connect(callback) 391 | return self 392 | 393 | func onPressed(callback: Callable) -> BaseBuilder: 394 | _content_node.mouse_filter = Control.MOUSE_FILTER_STOP 395 | 396 | if _content_node is Label: 397 | _content_node.add_theme_color_override("font_color", Color(0.0, 0.0, 0.933).lightened(0.6)) 398 | 399 | var press_handler = func(event: InputEvent): 400 | if event is InputEventMouseButton: 401 | if event.button_index == MOUSE_BUTTON_LEFT and event.pressed == false: 402 | # Only trigger on release inside the control 403 | callback.call() 404 | 405 | _content_node.gui_input.connect(press_handler) 406 | return self 407 | 408 | func _has_explicit_modifier(modifier: String) -> bool: 409 | return _explicit_modifiers.has(modifier) 410 | 411 | func _add_explicit_modifier(modifier: String, value: bool) -> BaseBuilder: 412 | _explicit_modifiers[modifier] = value 413 | return self 414 | 415 | func _is_binding(value: Variant): 416 | return value is Binding 417 | 418 | 419 | func _aspect_ratio_based_on_label_siblings(ratio): 420 | _content_node.size_flags_stretch_ratio = ratio 421 | if _margin_node != null: 422 | _margin_node.size_flags_stretch_ratio = ratio 423 | 424 | 425 | if _panel_node != null: 426 | _panel_node.size_flags_stretch_ratio = ratio 427 | 428 | if _panel_margin_node != null: 429 | _panel_margin_node.size_flags_stretch_ratio = ratio 430 | 431 | func ignoreSafeArea() -> BaseBuilder: 432 | _explicit_modifiers["ignore_safe_area"] = true 433 | return self 434 | 435 | 436 | func opacity(value) -> BaseBuilder: 437 | if value is Binding: 438 | value.bind(_get_parent_node(), "modulate", func(new_value): 439 | _get_parent_node().modulate.a = new_value) 440 | else: 441 | _get_parent_node().modulate.a = value 442 | 443 | return self 444 | 445 | func scale(value) -> BaseBuilder: 446 | if value is Binding: 447 | value.bind(_get_parent_node(), "scale", func(new_value): 448 | _get_parent_node().scale = Vector2(new_value, new_value)) 449 | else: 450 | _get_parent_node().scale = Vector2(value) 451 | 452 | return self 453 | 454 | func animation(flow: Flow, binding: Binding) -> BaseBuilder: 455 | binding.animation(flow) 456 | return self 457 | 458 | #TODO: This color rect needs to be tracked by the builder 459 | #TODO: 460 | func blur(value) -> BaseBuilder: 461 | var material = load("res://framework/shaders/SimpleBlurMat.tres") 462 | var color_rect = ColorRect.new() 463 | color_rect.material = material 464 | 465 | if value is Binding: 466 | value.bind(color_rect, "lod", func(new_value): 467 | color_rect.material.set_shader_parameter("lod", new_value)) 468 | else: 469 | color_rect.material.set_shader_parameter("lod", value) 470 | 471 | 472 | if _use_panel_margin: 473 | _set_blur(_panel_margin_node, color_rect, material) 474 | 475 | elif _use_panel: 476 | _set_blur(_panel_node, color_rect, material) 477 | 478 | elif _use_margin: 479 | _set_blur(_margin_node, color_rect, material) 480 | else: 481 | _set_blur(_content_node, color_rect, material) 482 | 483 | return self 484 | 485 | func _set_blur(target_node: Control, color_rect: ColorRect, material): 486 | var buffery = BackBufferCopy.new() 487 | buffery.set("rect", Rect2(target_node.position.x, target_node.position.y, target_node.size.x, target_node.size.y)) 488 | target_node.add_child(color_rect) 489 | target_node.add_child(buffery) 490 | target_node.move_child(color_rect, 0) 491 | buffery.show_behind_parent = true 492 | color_rect.custom_minimum_size = target_node.size 493 | 494 | # isPresented: Binding, content: BaseBuilder 495 | func sheet(isPresented: Binding, content: BaseBuilder) -> BaseBuilder: 496 | var ui_root = Engine.get_main_loop().root.get_node("UIRoot") 497 | 498 | if view_owner == null: 499 | view_owner = content.view_owner 500 | 501 | isPresented.bind(_content_node, "isPresented", func(new_value: bool): 502 | if new_value: 503 | var sheet = Sheet.new() 504 | sheet.view_content = view_owner.body 505 | sheet.is_presented = isPresented 506 | ui_root.modals.append(sheet) 507 | ui_root.add_child(sheet) 508 | sheet.present() 509 | else: 510 | # sheet.dismiss() 511 | if ui_root.modals.is_empty(): 512 | print_debug("Nothing to dismiss") 513 | return 514 | 515 | ui_root.dismiss() 516 | 517 | ) 518 | return self 519 | --------------------------------------------------------------------------------