├── 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 |
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 |
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 |
--------------------------------------------------------------------------------