diff --git a/Assets/Icons/checkbox.svg b/Assets/Icons/checkbox.svg
new file mode 100644
index 0000000..7a84b76
--- /dev/null
+++ b/Assets/Icons/checkbox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Assets/Icons/checkbox.svg.import b/Assets/Icons/checkbox.svg.import
new file mode 100644
index 0000000..3955cd8
--- /dev/null
+++ b/Assets/Icons/checkbox.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dn4dxn8hkrd64"
+path="res://.godot/imported/checkbox.svg-b131741b9ad567d1cf024db4c0f11166.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/checkbox.svg"
+dest_files=["res://.godot/imported/checkbox.svg-b131741b9ad567d1cf024db4c0f11166.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=2.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/Assets/Icons/checkbox_disabled.svg b/Assets/Icons/checkbox_disabled.svg
new file mode 100644
index 0000000..6950b25
--- /dev/null
+++ b/Assets/Icons/checkbox_disabled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Assets/Icons/checkbox_disabled.svg.import b/Assets/Icons/checkbox_disabled.svg.import
new file mode 100644
index 0000000..8bcef9c
--- /dev/null
+++ b/Assets/Icons/checkbox_disabled.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bap17ryrkcyey"
+path="res://.godot/imported/checkbox_disabled.svg-d293c35e32232bb898ac1dc4c01f3d34.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/checkbox_disabled.svg"
+dest_files=["res://.godot/imported/checkbox_disabled.svg-d293c35e32232bb898ac1dc4c01f3d34.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/Assets/Icons/checkbox_pressed.svg b/Assets/Icons/checkbox_pressed.svg
new file mode 100644
index 0000000..b0f6aed
--- /dev/null
+++ b/Assets/Icons/checkbox_pressed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Assets/Icons/checkbox_pressed.svg.import b/Assets/Icons/checkbox_pressed.svg.import
new file mode 100644
index 0000000..395100b
--- /dev/null
+++ b/Assets/Icons/checkbox_pressed.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c5scqw224r2e5"
+path="res://.godot/imported/checkbox_pressed.svg-7864c501a8fa0fa8194946d708755b56.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/checkbox_pressed.svg"
+dest_files=["res://.godot/imported/checkbox_pressed.svg-7864c501a8fa0fa8194946d708755b56.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/Assets/Icons/checkbox_pressed_grayscale.svg b/Assets/Icons/checkbox_pressed_grayscale.svg
new file mode 100644
index 0000000..c33cd38
--- /dev/null
+++ b/Assets/Icons/checkbox_pressed_grayscale.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Assets/Icons/checkbox_pressed_grayscale.svg.import b/Assets/Icons/checkbox_pressed_grayscale.svg.import
new file mode 100644
index 0000000..8313616
--- /dev/null
+++ b/Assets/Icons/checkbox_pressed_grayscale.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b6ucuyluuw43"
+path="res://.godot/imported/checkbox_pressed_grayscale.svg-45c89d5e4123fde397a9e7b9084c1a59.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/checkbox_pressed_grayscale.svg"
+dest_files=["res://.godot/imported/checkbox_pressed_grayscale.svg-45c89d5e4123fde397a9e7b9084c1a59.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/README.md b/README.md
index a6c9928..e8ce9e3 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@ TODO:
5. **Store** tab containers so switching tabs won't erase previous tab.
6. **GIF** support
7. **Video** support via [GDE GoZen](https://github.com/VoylinsGamedevJourney/gde_gozen)
+8. **More input types** (password, email, number, etc.)
Issues:
1. **< br />** counts as 1 element in **WebsiteContainer**, therefore despite being (0,0) in size, it counts as double in spacing.
\ No newline at end of file
diff --git a/Scenes/Styles/BrowserText.tres b/Scenes/Styles/BrowserText.tres
index 6f49502..5a24c0f 100644
--- a/Scenes/Styles/BrowserText.tres
+++ b/Scenes/Styles/BrowserText.tres
@@ -1,4 +1,53 @@
-[gd_resource type="Theme" load_steps=7 format=3 uid="uid://bn6rbmdy60lhr"]
+[gd_resource type="Theme" load_steps=17 format=3 uid="uid://bn6rbmdy60lhr"]
+
+[ext_resource type="Texture2D" uid="uid://dn4dxn8hkrd64" path="res://Assets/Icons/checkbox.svg" id="1_75mhk"]
+[ext_resource type="Texture2D" uid="uid://b6ucuyluuw43" path="res://Assets/Icons/checkbox_pressed_grayscale.svg" id="2_2abar"]
+[ext_resource type="Texture2D" uid="uid://c5scqw224r2e5" path="res://Assets/Icons/checkbox_pressed.svg" id="2_3k2jb"]
+[ext_resource type="Texture2D" uid="uid://bap17ryrkcyey" path="res://Assets/Icons/checkbox_disabled.svg" id="4_c32on"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_c32on"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7g0pl"]
+bg_color = Color(0.168627, 0.168627, 0.168627, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c32on"]
+bg_color = Color(0.105882, 0.105882, 0.105882, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_75mhk"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jecr6"]
+content_margin_left = 5.0
+bg_color = Color(0.6, 0.6, 0.6, 0)
+border_width_left = 3
+border_width_top = 3
+border_width_right = 3
+border_width_bottom = 3
+border_color = Color(0, 0, 0, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_75mhk"]
+content_margin_left = 5.0
+bg_color = Color(0.6, 0.6, 0.6, 0)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0, 0, 0, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
[sub_resource type="SystemFont" id="SystemFont_jecr6"]
font_names = PackedStringArray("Serif")
@@ -23,6 +72,20 @@ font_names = PackedStringArray("Serif")
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_jecr6"]
[resource]
+Button/styles/focus = SubResource("StyleBoxEmpty_c32on")
+Button/styles/hover = SubResource("StyleBoxFlat_7g0pl")
+Button/styles/normal = SubResource("StyleBoxFlat_c32on")
+CheckBox/icons/checked = ExtResource("2_3k2jb")
+CheckBox/icons/checked_disabled = ExtResource("2_2abar")
+CheckBox/icons/unchecked = ExtResource("1_75mhk")
+CheckBox/icons/unchecked_disabled = ExtResource("4_c32on")
+CheckBox/styles/focus = SubResource("StyleBoxEmpty_75mhk")
+LineEdit/colors/caret_color = Color(0, 0, 0, 1)
+LineEdit/colors/font_color = Color(0, 0, 0, 1)
+LineEdit/colors/font_placeholder_color = Color(0, 0, 0, 0.6)
+LineEdit/styles/focus = SubResource("StyleBoxFlat_jecr6")
+LineEdit/styles/normal = SubResource("StyleBoxFlat_75mhk")
+LineEdit/styles/read_only = null
RichTextLabel/colors/font_selected_color = Color(1, 1, 1, 1)
RichTextLabel/colors/selection_color = Color(0.313726, 0.403922, 0.8, 1)
RichTextLabel/fonts/bold_font = SubResource("SystemFont_jecr6")
diff --git a/Scenes/Tags/button.tscn b/Scenes/Tags/button.tscn
new file mode 100644
index 0000000..bc1656d
--- /dev/null
+++ b/Scenes/Tags/button.tscn
@@ -0,0 +1,20 @@
+[gd_scene load_steps=3 format=3 uid="uid://dhqqt4hg21w8d"]
+
+[ext_resource type="Script" uid="uid://cks35eudcm1wj" path="res://Scripts/Tags/button.gd" id="1_button"]
+[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_theme"]
+
+[node name="Button" type="Control"]
+custom_minimum_size = Vector2(100, 30)
+layout_mode = 3
+anchors_preset = 0
+script = ExtResource("1_button")
+
+[node name="ButtonNode" type="Button" parent="."]
+layout_mode = 1
+offset_right = 100.0
+offset_bottom = 30.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_default_cursor_shape = 2
+theme = ExtResource("2_theme")
+text = "Button"
diff --git a/Scenes/Tags/form.tscn b/Scenes/Tags/form.tscn
new file mode 100644
index 0000000..f8a08ce
--- /dev/null
+++ b/Scenes/Tags/form.tscn
@@ -0,0 +1,10 @@
+[gd_scene load_steps=2 format=3 uid="uid://bw8h5k6j2m3n4"]
+
+[ext_resource type="Script" uid="uid://cn2iolk6biupv" path="res://Scripts/Tags/form.gd" id="1_form"]
+
+[node name="Form" type="VBoxContainer"]
+anchors_preset = 10
+anchor_right = 1.0
+grow_horizontal = 2
+theme_override_constants/separation = 22
+script = ExtResource("1_form")
diff --git a/Scenes/Tags/input.tscn b/Scenes/Tags/input.tscn
new file mode 100644
index 0000000..d15529c
--- /dev/null
+++ b/Scenes/Tags/input.tscn
@@ -0,0 +1,34 @@
+[gd_scene load_steps=3 format=3 uid="uid://c7yay102a3b4c"]
+
+[ext_resource type="Script" uid="uid://kv6ebscarj2e" path="res://Scripts/Tags/input.gd" id="1_input"]
+[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_theme"]
+
+[node name="Input" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = 400.0
+offset_bottom = 31.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+script = ExtResource("1_input")
+
+[node name="LineEdit" type="LineEdit" parent="."]
+layout_mode = 1
+offset_right = 400.0
+offset_bottom = 35.0
+theme = ExtResource("2_theme")
+placeholder_text = "Enter text..."
+caret_blink = true
+
+[node name="CheckBox" type="CheckBox" parent="."]
+visible = false
+layout_mode = 0
+offset_right = 31.0
+offset_bottom = 31.0
+theme = ExtResource("2_theme")
+theme_override_constants/icon_max_width = 24
+flat = true
diff --git a/Scenes/Tags/span.tscn b/Scenes/Tags/span.tscn
index 259b813..5973bf5 100644
--- a/Scenes/Tags/span.tscn
+++ b/Scenes/Tags/span.tscn
@@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://bkj3x5y2m8qrl"]
-[ext_resource type="Script" uid="uid://cjk4x6y8m9wts" path="res://Scripts/Tags/span.gd" id="1_span"]
+[ext_resource type="Script" uid="uid://4pbphta3r67k" path="res://Scripts/Tags/span.gd" id="1_span"]
[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_theme"]
[node name="SPAN" type="VBoxContainer"]
@@ -17,5 +17,6 @@ mouse_default_cursor_shape = 1
theme = ExtResource("2_theme")
theme_override_colors/default_color = Color(0, 0, 0, 1)
bbcode_enabled = true
+text = "Placeholder"
fit_content = true
selection_enabled = true
diff --git a/Scenes/main.tscn b/Scenes/main.tscn
index 58a0eb5..0eec7bc 100644
--- a/Scenes/main.tscn
+++ b/Scenes/main.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=23 format=3 uid="uid://bytm7bt2s4ak8"]
+[gd_scene load_steps=27 format=3 uid="uid://bytm7bt2s4ak8"]
[ext_resource type="Script" uid="uid://bg5iqnwic1rio" path="res://Scripts/main.gd" id="1_8q3xr"]
[ext_resource type="Texture2D" uid="uid://df1m4j7uxi63v" path="res://Assets/Icons/chevron-down.svg" id="2_6bp64"]
@@ -9,6 +9,8 @@
[ext_resource type="PackedScene" uid="uid://sqhcxhcre081" path="res://Scenes/Tab.tscn" id="4_344ge"]
[ext_resource type="Texture2D" uid="uid://cu4hjoba6etf" path="res://Assets/Icons/rotate-cw.svg" id="5_344ge"]
[ext_resource type="Texture2D" uid="uid://cehbtwq6gq0cn" path="res://Assets/Icons/plus.svg" id="5_ynf5e"]
+[ext_resource type="Script" uid="uid://bgqglerkcylxx" path="res://addons/SmoothScroll/SmoothScrollContainer.gd" id="10_d1ilt"]
+[ext_resource type="Script" uid="uid://b7h0k2h2qwlqv" path="res://addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd" id="11_6iyac"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_344ge"]
@@ -61,6 +63,18 @@ expand_margin_left = 40.0
LineEdit/styles/focus = SubResource("StyleBoxEmpty_8gbba")
LineEdit/styles/normal = SubResource("StyleBoxFlat_8gbba")
+[sub_resource type="Resource" id="Resource_fdnlq"]
+script = ExtResource("11_6iyac")
+friction = 4.0
+minimum_velocity = 0.4
+rebound_strength = 7.0
+
+[sub_resource type="Resource" id="Resource_jkdf5"]
+script = ExtResource("11_6iyac")
+friction = 4.0
+minimum_velocity = 0.4
+rebound_strength = 7.0
+
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_white"]
bg_color = Color(1, 1, 1, 1)
@@ -190,14 +204,22 @@ stretch_mode = 5
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
-[node name="WebsiteContainer" type="VBoxContainer" parent="VBoxContainer"]
-unique_name_in_owner = true
+[node name="SmoothScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
-theme_override_constants/separation = 22
+script = ExtResource("10_d1ilt")
+wheel_scroll_damper = SubResource("Resource_fdnlq")
+dragging_scroll_damper = SubResource("Resource_jkdf5")
+drag_with_mouse = false
+allow_overdragging = false
+metadata/_custom_type_script = "uid://bgqglerkcylxx"
-[node name="Control" type="Control" parent="VBoxContainer/WebsiteContainer"]
+[node name="WebsiteContainer" type="VBoxContainer" parent="VBoxContainer/SmoothScrollContainer"]
+unique_name_in_owner = true
layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 22
[node name="WebsiteBackground" type="Panel" parent="."]
z_index = -1
@@ -213,18 +235,24 @@ theme_override_styles/panel = SubResource("StyleBoxFlat_white")
[node name="Panel" type="Panel" parent="."]
z_index = -5
-layout_mode = 2
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
offset_top = 58.0
-offset_right = 1920.0
-offset_bottom = 125.0
+grow_horizontal = 2
+grow_vertical = 2
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_6iyac")
[node name="Panel2" type="Panel" parent="."]
z_index = -6
-layout_mode = 2
-offset_right = 1920.0
-offset_bottom = 125.0
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_21xkr")
diff --git a/Scripts/B9/HTMLParser.gd b/Scripts/B9/HTMLParser.gd
index d99a853..6b91fdf 100644
--- a/Scripts/B9/HTMLParser.gd
+++ b/Scripts/B9/HTMLParser.gd
@@ -80,9 +80,6 @@ class HTMLElement:
func is_inline_element() -> bool:
return tag_name in ["b", "i", "u", "small", "mark", "code", "span", "a"]
-
- func is_heading_element() -> bool:
- return tag_name in ["h1", "h2", "h3", "h4", "h5", "h6"]
class ParseResult:
var root: HTMLElement
diff --git a/Scripts/TabContainer.gd b/Scripts/TabContainer.gd
index 82f388f..a86ca4e 100644
--- a/Scripts/TabContainer.gd
+++ b/Scripts/TabContainer.gd
@@ -86,7 +86,7 @@ func create_tab() -> void:
# WARNING: temporary
main.render()
-func _input(event: InputEvent) -> void:
+func _input(_event: InputEvent) -> void:
if Input.is_action_just_pressed("NewTab"):
create_tab()
if Input.is_action_just_pressed("CloseTab"):
diff --git a/Scripts/Tags/br.gd b/Scripts/Tags/br.gd
index e372935..d590451 100644
--- a/Scripts/Tags/br.gd
+++ b/Scripts/Tags/br.gd
@@ -1,4 +1,4 @@
extends Control
-func init(element: HTMLParser.HTMLElement) -> void:
+func init(_element: HTMLParser.HTMLElement) -> void:
pass
diff --git a/Scripts/Tags/button.gd b/Scripts/Tags/button.gd
new file mode 100644
index 0000000..051ffd8
--- /dev/null
+++ b/Scripts/Tags/button.gd
@@ -0,0 +1,18 @@
+extends Control
+
+func init(element: HTMLParser.HTMLElement) -> void:
+ var button_node: Button = $ButtonNode
+
+ var button_text = element.text_content.strip_edges()
+ if button_text.length() == 0:
+ button_text = element.get_bbcode_formatted_text()
+
+ if button_text.length() > 0:
+ button_node.text = button_text
+
+ button_node.custom_minimum_size = button_node.get_theme_default_font().get_string_size(
+ button_node.text,
+ HORIZONTAL_ALIGNMENT_LEFT,
+ -1,
+ button_node.get_theme_default_font_size()
+ ) + Vector2(20, 10) # Add padding
diff --git a/Scripts/Tags/button.gd.uid b/Scripts/Tags/button.gd.uid
new file mode 100644
index 0000000..485c216
--- /dev/null
+++ b/Scripts/Tags/button.gd.uid
@@ -0,0 +1 @@
+uid://cks35eudcm1wj
diff --git a/Scripts/Tags/form.gd b/Scripts/Tags/form.gd
new file mode 100644
index 0000000..fd81a25
--- /dev/null
+++ b/Scripts/Tags/form.gd
@@ -0,0 +1,4 @@
+extends VBoxContainer
+
+func init(_element: HTMLParser.HTMLElement) -> void:
+ pass
diff --git a/Scripts/Tags/form.gd.uid b/Scripts/Tags/form.gd.uid
new file mode 100644
index 0000000..375a87d
--- /dev/null
+++ b/Scripts/Tags/form.gd.uid
@@ -0,0 +1 @@
+uid://cn2iolk6biupv
diff --git a/Scripts/Tags/input.gd b/Scripts/Tags/input.gd
new file mode 100644
index 0000000..76bd305
--- /dev/null
+++ b/Scripts/Tags/input.gd
@@ -0,0 +1,23 @@
+extends Control
+
+func init(element: HTMLParser.HTMLElement) -> void:
+ var line_edit: LineEdit = $LineEdit
+ var check_box: CheckBox = $CheckBox
+
+ var input_type = element.get_attribute("type").to_lower()
+ var placeholder = element.get_attribute("placeholder")
+ var value = element.get_attribute("value")
+
+ match input_type:
+ "checkbox":
+ line_edit.visible = false
+ check_box.visible = true
+ if value and value == "true": check_box.button_pressed = true
+ custom_minimum_size = check_box.size
+ _: # Default to text input
+ line_edit.visible = true
+ check_box.visible = false
+ custom_minimum_size = line_edit.size
+
+ if placeholder: line_edit.placeholder_text = placeholder
+ if value: line_edit.text = value
diff --git a/Scripts/Tags/input.gd.uid b/Scripts/Tags/input.gd.uid
new file mode 100644
index 0000000..ec2969e
--- /dev/null
+++ b/Scripts/Tags/input.gd.uid
@@ -0,0 +1 @@
+uid://kv6ebscarj2e
diff --git a/Scripts/main.gd b/Scripts/main.gd
index 523c13a..beb86b3 100644
--- a/Scripts/main.gd
+++ b/Scripts/main.gd
@@ -19,6 +19,16 @@ const H3 = preload("res://Scenes/Tags/h3.tscn")
const H4 = preload("res://Scenes/Tags/h4.tscn")
const H5 = preload("res://Scenes/Tags/h5.tscn")
const H6 = preload("res://Scenes/Tags/h6.tscn")
+const FORM = preload("res://Scenes/Tags/form.tscn")
+const INPUT = preload("res://Scenes/Tags/input.tscn")
+const BUTTON = preload("res://Scenes/Tags/button.tscn")
+
+const MIN_SIZE = Vector2i(750, 200)
+
+func _ready():
+ ProjectSettings.set_setting("display/window/size/min_width", MIN_SIZE.x)
+ ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
+ DisplayServer.window_set_min_size(MIN_SIZE)
func render():
# Clear existing content
@@ -64,6 +74,17 @@ both spaces and
line breaks
+
+
@@ -176,6 +197,24 @@ line breaks
var separator = SEPARATOR.instantiate()
separator.init(element)
website_container.add_child(separator)
+ "form":
+ var form = FORM.instantiate()
+ form.init(element)
+ website_container.add_child(form)
+
+ # Render form children
+ for child_element in element.children:
+ var child_node = create_element_node(child_element)
+ if child_node:
+ form.add_child(child_node)
+ "input":
+ var input = INPUT.instantiate()
+ input.init(element)
+ website_container.add_child(input)
+ "button":
+ var button = BUTTON.instantiate()
+ button.init(element)
+ website_container.add_child(button)
"span":
var span = SPAN.instantiate()
span.init(element)
@@ -213,3 +252,20 @@ func contains_hyperlink(element: HTMLParser.HTMLElement) -> bool:
return true
return false
+
+func create_element_node(element: HTMLParser.HTMLElement) -> Control:
+ match element.tag_name:
+ "input":
+ var input = INPUT.instantiate()
+ input.init(element)
+ return input
+ "button":
+ var button = BUTTON.instantiate()
+ button.init(element)
+ return button
+ "span":
+ var span = SPAN.instantiate()
+ span.init(element)
+ return span
+ _:
+ return null
diff --git a/addons/SmoothScroll/SmoothScrollContainer.gd b/addons/SmoothScroll/SmoothScrollContainer.gd
new file mode 100644
index 0000000..4c3c9ce
--- /dev/null
+++ b/addons/SmoothScroll/SmoothScrollContainer.gd
@@ -0,0 +1,916 @@
+## Smooth scroll functionality for ScrollContainer
+##
+## Applies velocity based momentum and "overdrag"
+## functionality to a ScrollContainer
+@tool
+extends ScrollContainer
+class_name SmoothScrollContainer
+
+@export_group("Mouse Wheel")
+## Drag impact for one scroll input
+@export_range(0, 10, 0.01, "or_greater", "hide_slider")
+var speed := 1000.0
+## ScrollDamper for wheel scrolling
+@export
+var wheel_scroll_damper: ScrollDamper = ExpoScrollDamper.new()
+
+@export_group("Dragging")
+## ScrollDamper for dragging
+@export
+var dragging_scroll_damper: ScrollDamper = ExpoScrollDamper.new()
+### Allow dragging with mouse or not
+@export
+var drag_with_mouse := true
+## Allow dragging with touch or not
+@export
+var drag_with_touch := true
+
+@export_group("Container")
+## Below this value, snap content to boundary
+@export
+var just_snap_under := 0.4
+## Margin of the currently focused element
+@export_range(0, 50)
+var follow_focus_margin := 20
+## Makes the container scrollable vertically
+@export
+var allow_vertical_scroll := true
+## Makes the container scrollable horizontally
+@export
+var allow_horizontal_scroll := true
+## Makes the container only scrollable where the content has overflow
+@export
+var auto_allow_scroll := true
+## Whether the content of this container should be allowed to overshoot at the ends
+## before interpolating back to its bounds
+@export
+var allow_overdragging := true
+
+@export_group("Scroll Bar")
+## Hides scrollbar as long as not hovered or interacted with
+@export
+var hide_scrollbar_over_time := false:
+ set(val): hide_scrollbar_over_time = _set_hide_scrollbar_over_time(val)
+## Time after scrollbar starts to fade out when 'hide_scrollbar_over_time' is true
+@export
+var scrollbar_hide_time := 5.0
+## Fadein time for scrollbar when 'hide_scrollbar_over_time' is true
+@export
+var scrollbar_fade_in_time := 0.2
+## Fadeout time for scrollbar when 'hide_scrollbar_over_time' is true
+@export
+var scrollbar_fade_out_time := 0.5
+
+@export_group("Input")
+## If true sets the input event as handled with set_input_as_handled()
+@export
+var handle_input := true
+
+@export_group("Debug")
+## Adds debug information
+@export
+var debug_mode := false
+
+## Current velocity of the `content_node`
+var velocity := Vector2(0,0)
+## Control node to move when scrolling
+var content_node: Control
+## Current position of `content_node`
+var pos := Vector2(0, 0)
+## Current ScrollDamper to use, recording to last input type
+var scroll_damper: ScrollDamper
+## When true, `content_node`'s position is only set by dragging the h scroll bar
+var h_scrollbar_dragging := false
+## When true, `content_node`'s position is only set by dragging the v scroll bar
+var v_scrollbar_dragging := false
+## When ture, `content_node` follows drag position
+var content_dragging := false
+## When ture, `content_node` has moved by dragging
+var content_dragging_moved := false
+## Timer for hiding scroll bar
+var scrollbar_hide_timer := Timer.new()
+## Tween for showing scroll bar
+var scrollbar_show_tween: Tween
+## Tween for hiding scroll bar
+var scrollbar_hide_tween: Tween
+## Tween for scroll x to
+var scroll_x_to_tween: Tween
+## Tween for scroll y to
+var scroll_y_to_tween: Tween
+## [0,1] Mouse or touch's relative movement accumulation when overdrag[br]
+## [2,3] Position where dragging starts[br]
+## [4,5,6,7] Left_distance, right_distance, top_distance, bottom_distance
+var drag_temp_data := []
+## Whether touch point is in deadzone.
+var is_in_deadzone := false
+## Whether mouse is on h or v scroll bar
+var mouse_on_scrollbar := false
+
+## If content is being scrolled
+var is_scrolling := false:
+ set(val):
+ if is_scrolling != val:
+ if val:
+ emit_signal("scroll_started")
+ else:
+ emit_signal("scroll_ended")
+ is_scrolling = val
+
+## Last type of input used to scroll
+enum SCROLL_TYPE {WHEEL, BAR, DRAG}
+var last_scroll_type: SCROLL_TYPE
+
+#region Virtual Functions
+
+func _ready() -> void:
+ if debug_mode:
+ setup_debug_drawing()
+ # Initialize variables
+ scroll_damper = wheel_scroll_damper
+
+ get_v_scroll_bar().gui_input.connect(_scrollbar_input.bind(true))
+ get_h_scroll_bar().gui_input.connect(_scrollbar_input.bind(false))
+ get_v_scroll_bar().mouse_entered.connect(_mouse_on_scroll_bar.bind(true))
+ get_v_scroll_bar().mouse_exited.connect(_mouse_on_scroll_bar.bind(false))
+ get_h_scroll_bar().mouse_entered.connect(_mouse_on_scroll_bar.bind(true))
+ get_h_scroll_bar().mouse_exited.connect(_mouse_on_scroll_bar.bind(false))
+ get_viewport().gui_focus_changed.connect(_on_focus_changed)
+
+ for c in get_children():
+ if not c is ScrollBar:
+ content_node = c
+
+ add_child(scrollbar_hide_timer)
+ scrollbar_hide_timer.one_shot = true
+ scrollbar_hide_timer.timeout.connect(_scrollbar_hide_timer_timeout)
+ if hide_scrollbar_over_time:
+ scrollbar_hide_timer.start(scrollbar_hide_time)
+ get_tree().node_added.connect(_on_node_added)
+
+func _process(delta: float) -> void:
+ if Engine.is_editor_hint(): return
+ scroll(true, velocity.y, pos.y, delta)
+ scroll(false, velocity.x, pos.x, delta)
+ update_scrollbars()
+ update_is_scrolling()
+
+ if debug_mode:
+ queue_redraw()
+
+# Detecting mouse entering and exiting scroll bar
+func _mouse_on_scroll_bar(entered: bool) -> void:
+ mouse_on_scrollbar = entered
+
+# Forwarding scroll inputs from scrollbar
+func _scrollbar_input(event: InputEvent, vertical: bool) -> void:
+ if event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_WHEEL_DOWN\
+ or event.button_index == MOUSE_BUTTON_WHEEL_UP\
+ or event.button_index == MOUSE_BUTTON_WHEEL_LEFT\
+ or event.button_index == MOUSE_BUTTON_WHEEL_RIGHT:
+ _gui_input(event)
+
+ if event.button_index == MOUSE_BUTTON_LEFT:
+ if event.pressed:
+ if vertical:
+ v_scrollbar_dragging = true
+ last_scroll_type = SCROLL_TYPE.BAR
+ kill_scroll_to_tweens()
+ else:
+ h_scrollbar_dragging = true
+ last_scroll_type = SCROLL_TYPE.BAR
+ kill_scroll_to_tweens()
+ else:
+ if vertical:
+ v_scrollbar_dragging = false
+ else:
+ h_scrollbar_dragging = false
+
+ if event is InputEventScreenTouch:
+ if event.pressed:
+ if vertical:
+ v_scrollbar_dragging = true
+ last_scroll_type = SCROLL_TYPE.BAR
+ kill_scroll_to_tweens()
+ else:
+ h_scrollbar_dragging = true
+ last_scroll_type = SCROLL_TYPE.BAR
+ kill_scroll_to_tweens()
+ else:
+ if vertical:
+ v_scrollbar_dragging = false
+ else:
+ h_scrollbar_dragging = false
+
+func _gui_input(event: InputEvent) -> void:
+ # Show scroll bars when mouse moves
+ if hide_scrollbar_over_time and event is InputEventMouseMotion:
+ show_scrollbars()
+
+ if event is InputEventMouseButton:
+ match event.button_index:
+ MOUSE_BUTTON_WHEEL_DOWN:
+ if event.pressed:
+ last_scroll_type = SCROLL_TYPE.WHEEL
+ if event.shift_pressed or not should_scroll_vertical():
+ if should_scroll_horizontal():
+ velocity.x -= speed * event.factor
+ else:
+ if should_scroll_vertical():
+ velocity.y -= speed * event.factor
+ scroll_damper = wheel_scroll_damper
+ kill_scroll_to_tweens()
+ MOUSE_BUTTON_WHEEL_UP:
+ if event.pressed:
+ last_scroll_type = SCROLL_TYPE.WHEEL
+ if event.shift_pressed or not should_scroll_vertical():
+ if should_scroll_horizontal():
+ velocity.x += speed * event.factor
+ else:
+ if should_scroll_vertical():
+ velocity.y += speed * event.factor
+ scroll_damper = wheel_scroll_damper
+ kill_scroll_to_tweens()
+ MOUSE_BUTTON_WHEEL_LEFT:
+ if event.pressed:
+ last_scroll_type = SCROLL_TYPE.WHEEL
+ if event.shift_pressed:
+ if should_scroll_vertical():
+ velocity.y -= speed * event.factor
+ else:
+ if should_scroll_horizontal():
+ velocity.x += speed * event.factor
+ scroll_damper = wheel_scroll_damper
+ kill_scroll_to_tweens()
+ MOUSE_BUTTON_WHEEL_RIGHT:
+ if event.pressed:
+ last_scroll_type = SCROLL_TYPE.WHEEL
+ if event.shift_pressed:
+ if should_scroll_vertical():
+ velocity.y += speed * event.factor
+ else:
+ if should_scroll_horizontal():
+ velocity.x -= speed * event.factor
+ scroll_damper = wheel_scroll_damper
+ kill_scroll_to_tweens()
+ MOUSE_BUTTON_LEFT:
+ if event.pressed:
+ if !drag_with_mouse: return
+ content_dragging = true
+ is_in_deadzone = true
+ scroll_damper = dragging_scroll_damper
+ last_scroll_type = SCROLL_TYPE.DRAG
+ init_drag_temp_data()
+ kill_scroll_to_tweens()
+ else:
+ content_dragging = false
+ is_in_deadzone = false
+
+ if (event is InputEventScreenDrag and drag_with_touch) \
+ or (event is InputEventMouseMotion and drag_with_mouse):
+ if content_dragging:
+ if should_scroll_horizontal():
+ drag_temp_data[0] += event.relative.x
+ if should_scroll_vertical():
+ drag_temp_data[1] += event.relative.y
+ remove_all_children_focus(self)
+ handle_content_dragging()
+
+ if event is InputEventPanGesture:
+ if should_scroll_horizontal():
+ velocity.x = -event.delta.x * speed
+ kill_scroll_to_tweens()
+ if should_scroll_vertical():
+ velocity.y = -event.delta.y * speed
+ kill_scroll_to_tweens()
+
+ if event is InputEventScreenTouch:
+ if event.pressed:
+ if !drag_with_touch: return
+ content_dragging = true
+ is_in_deadzone = true
+ scroll_damper = dragging_scroll_damper
+ last_scroll_type = SCROLL_TYPE.DRAG
+ init_drag_temp_data()
+ kill_scroll_to_tweens()
+ else:
+ content_dragging = false
+ is_in_deadzone = false
+ # Handle input if handle_input is true
+ if handle_input:
+ get_tree().get_root().set_input_as_handled()
+
+# Scroll to new focused element
+func _on_focus_changed(control: Control) -> void:
+ if follow_focus:
+ self.ensure_control_visible(control)
+
+func _draw() -> void:
+ if debug_mode:
+ draw_debug()
+
+# Sets default mouse filter for SmoothScroll children to MOUSE_FILTER_PASS
+func _on_node_added(node: Node) -> void:
+ if node is Control and Engine.is_editor_hint():
+ if is_ancestor_of(node):
+ node.mouse_filter = Control.MOUSE_FILTER_PASS
+
+func _scrollbar_hide_timer_timeout() -> void:
+ if !any_scroll_bar_dragged():
+ hide_scrollbars()
+
+func _set_hide_scrollbar_over_time(value: bool) -> bool:
+ if value == false:
+ if scrollbar_hide_timer != null:
+ scrollbar_hide_timer.stop()
+ if scrollbar_show_tween != null:
+ scrollbar_show_tween.kill()
+ if scrollbar_hide_tween != null:
+ scrollbar_hide_tween.kill()
+ get_h_scroll_bar().modulate = Color.WHITE
+ get_v_scroll_bar().modulate = Color.WHITE
+ else:
+ if scrollbar_hide_timer != null and scrollbar_hide_timer.is_inside_tree():
+ scrollbar_hide_timer.start(scrollbar_hide_time)
+ return value
+
+func _get(property: StringName) -> Variant:
+ match property:
+ "scroll_horizontal":
+ if !content_node: return 0
+ return -int(content_node.position.x)
+ "scroll_vertical":
+ if !content_node: return 0
+ return -int(content_node.position.y)
+ _:
+ return null
+
+func _set(property: StringName, value: Variant) -> bool:
+ match property:
+ "scroll_horizontal":
+ if !content_node:
+ scroll_horizontal = 0
+ return true
+ scroll_horizontal = value
+ kill_scroll_x_to_tween()
+ velocity.x = 0.0
+ pos.x = clampf(
+ -value as float,
+ -get_child_size_x_diff(content_node, true),
+ 0.0
+ )
+ return true
+ "scroll_vertical":
+ if !content_node:
+ scroll_vertical = 0
+ return true
+ scroll_vertical = value
+ kill_scroll_y_to_tween()
+ velocity.y = 0.0
+ pos.y = clampf(
+ -value as float,
+ -get_child_size_y_diff(content_node, true),
+ 0.0
+ )
+ return true
+ _:
+ return false
+
+#endregion
+
+#region Scrolling Logic
+
+func scroll(vertical: bool, axis_velocity: float, axis_pos: float, delta: float):
+ # If no scroll needed, don't apply forces
+ if vertical:
+ if not should_scroll_vertical():
+ return
+ else:
+ if not should_scroll_horizontal():
+ return
+ if !scroll_damper: return
+ # Applies counterforces when overdragging
+ if not content_dragging:
+ axis_velocity = handle_overdrag(vertical, axis_velocity, axis_pos, delta)
+ # Move content node by applying velocity
+ var slide_result = scroll_damper.slide(axis_velocity, delta)
+ axis_velocity = slide_result[0]
+ axis_pos += slide_result[1]
+ # Snap to boundary if close enough
+ var snap_result = snap(vertical, axis_velocity, axis_pos)
+ axis_velocity = snap_result[0]
+ axis_pos = snap_result[1]
+ else:
+ # Preserve dragging velocity for 1 frame
+ # in case no movement event while releasing dragging with touch
+ if content_dragging_moved:
+ content_dragging_moved = false
+ else:
+ axis_velocity = 0.0
+ # If using scroll bar dragging, set the content_node's
+ # position by using the scrollbar position
+ if handle_scrollbar_drag():
+ return
+
+ if vertical:
+ if not allow_overdragging:
+ # Clamp if calculated position is beyond boundary
+ if is_outside_top_boundary(axis_pos):
+ axis_pos = 0.0
+ axis_velocity = 0.0
+ elif is_outside_bottom_boundary(axis_pos):
+ axis_pos = -get_child_size_y_diff(content_node, true)
+ axis_velocity = 0.0
+
+ content_node.position.y = axis_pos
+ pos.y = axis_pos
+ velocity.y = axis_velocity
+ else:
+ if not allow_overdragging:
+ # Clamp if calculated position is beyond boundary
+ if is_outside_left_boundary(axis_pos):
+ axis_pos = 0.0
+ axis_velocity = 0.0
+ elif is_outside_right_boundary(axis_pos):
+ axis_pos = -get_child_size_x_diff(content_node, true)
+ axis_velocity = 0.0
+
+ content_node.position.x = axis_pos
+ pos.x = axis_pos
+ velocity.x = axis_velocity
+
+func handle_overdrag(vertical: bool, axis_velocity: float, axis_pos: float, delta: float) -> float:
+ if !scroll_damper: return 0.0
+ # Calculate the size difference between this container and content_node
+ var size_diff = get_child_size_y_diff(content_node, true) \
+ if vertical else get_child_size_x_diff(content_node, true)
+ # Calculate distance to left and right or top and bottom
+ var dist1 = get_child_top_dist(axis_pos, size_diff) \
+ if vertical else get_child_left_dist(axis_pos, size_diff)
+ var dist2 = get_child_bottom_dist(axis_pos, size_diff) \
+ if vertical else get_child_right_dist(axis_pos, size_diff)
+ # Calculate velocity to left and right or top and bottom
+ var target_vel1 = scroll_damper._calculate_velocity_to_dest(dist1, 0.0)
+ var target_vel2 = scroll_damper._calculate_velocity_to_dest(dist2, 0.0)
+ # Bounce when out of boundary. When velocity is not fast enough to go back,
+ # apply a opposite force and get a new velocity. If the new velocity is too fast,
+ # apply a velocity that makes it scroll back exactly.
+ if axis_pos > 0.0:
+ if axis_velocity > target_vel1:
+ axis_velocity = scroll_damper.attract(
+ dist1,
+ 0.0,
+ axis_velocity,
+ delta
+ )
+ if axis_pos < -size_diff:
+ if axis_velocity < target_vel2:
+ axis_velocity = scroll_damper.attract(
+ dist2,
+ 0.0,
+ axis_velocity,
+ delta
+ )
+
+ return axis_velocity
+
+# Snap to boundary if close enough in next frame
+func snap(vertical: bool, axis_velocity: float, axis_pos: float) -> Array:
+ # Calculate the size difference between this container and content_node
+ var size_diff = get_child_size_y_diff(content_node, true) \
+ if vertical else get_child_size_x_diff(content_node, true)
+ # Calculate distance to left and right or top and bottom
+ var dist1 = get_child_top_dist(axis_pos, size_diff) \
+ if vertical else get_child_left_dist(axis_pos, size_diff)
+ var dist2 = get_child_bottom_dist(axis_pos, size_diff) \
+ if vertical else get_child_right_dist(axis_pos, size_diff)
+ if (
+ dist1 > 0.0 \
+ and abs(dist1) < just_snap_under \
+ and abs(axis_velocity) < just_snap_under \
+ ):
+ axis_pos -= dist1
+ axis_velocity = 0.0
+ elif (
+ dist2 < 0.0 \
+ and abs(dist2) < just_snap_under \
+ and abs(axis_velocity) < just_snap_under \
+ ):
+ axis_pos -= dist2
+ axis_velocity = 0.0
+
+ return [axis_velocity, axis_pos]
+
+## Returns true when scrollbar was dragged
+func handle_scrollbar_drag() -> bool:
+ if h_scrollbar_dragging:
+ velocity.x = 0.0
+ pos.x = -get_h_scroll_bar().value
+ return true
+
+ if v_scrollbar_dragging:
+ velocity.y = 0.0
+ pos.y = -get_v_scroll_bar().value
+ return true
+ return false
+
+func handle_content_dragging() -> void:
+ if !dragging_scroll_damper: return
+
+ if(
+ Vector2(drag_temp_data[0], drag_temp_data[1]).length() < scroll_deadzone \
+ and is_in_deadzone
+ ):
+ return
+ elif is_in_deadzone == true:
+ is_in_deadzone = false
+ drag_temp_data[0] = 0.0
+ drag_temp_data[1] = 0.0
+
+ content_dragging_moved = true
+
+ var calculate_dest = func(delta: float, damping: float) -> float:
+ if delta >= 0.0:
+ return delta / (1 + delta * damping * 0.00001)
+ else:
+ return delta
+
+ var calculate_position = func(
+ temp_dist1: float, # Temp distance
+ temp_dist2: float,
+ temp_relative: float # Event's relative movement accumulation
+ ) -> float:
+ if temp_relative + temp_dist1 > 0.0:
+ var delta = min(temp_relative, temp_relative + temp_dist1)
+ var dest = calculate_dest.call(delta, dragging_scroll_damper._attract_factor)
+ return dest - min(0.0, temp_dist1)
+ elif temp_relative + temp_dist2 < 0.0:
+ var delta = max(temp_relative, temp_relative + temp_dist2)
+ var dest = -calculate_dest.call(-delta, dragging_scroll_damper._attract_factor)
+ return dest - max(0.0, temp_dist2)
+ else: return temp_relative
+
+ if should_scroll_vertical():
+ var y_pos = calculate_position.call(
+ drag_temp_data[6], # Temp top_distance
+ drag_temp_data[7], # Temp bottom_distance
+ drag_temp_data[1] # Temp y relative accumulation
+ ) + drag_temp_data[3]
+ velocity.y = (y_pos - pos.y) / get_process_delta_time()
+ pos.y = y_pos
+ if should_scroll_horizontal():
+ var x_pos = calculate_position.call(
+ drag_temp_data[4], # Temp left_distance
+ drag_temp_data[5], # Temp right_distance
+ drag_temp_data[0] # Temp x relative accumulation
+ ) + drag_temp_data[2]
+ velocity.x = (x_pos - pos.x) / get_process_delta_time()
+ pos.x = x_pos
+
+func remove_all_children_focus(node: Node) -> void:
+ if node is Control:
+ var control = node as Control
+ control.release_focus()
+
+ for child in node.get_children():
+ remove_all_children_focus(child)
+
+func update_is_scrolling() -> void:
+ if(
+ (content_dragging and not is_in_deadzone)
+ or any_scroll_bar_dragged()
+ or velocity != Vector2.ZERO
+ ):
+ is_scrolling = true
+ else:
+ is_scrolling = false
+
+func update_scrollbars() -> void:
+ # Update vertical scroll bar
+ if get_v_scroll_bar().value != -pos.y:
+ get_v_scroll_bar().set_value_no_signal(-pos.y)
+ get_v_scroll_bar().queue_redraw()
+ # Update horizontal scroll bar
+ if get_h_scroll_bar().value != -pos.x:
+ get_h_scroll_bar().set_value_no_signal(-pos.x)
+ get_h_scroll_bar().queue_redraw()
+
+ # Always show sroll bars when scrolling or mouse is on any scroll bar
+ if hide_scrollbar_over_time and (is_scrolling or mouse_on_scrollbar):
+ show_scrollbars()
+
+func init_drag_temp_data() -> void:
+ # Calculate the size difference between this container and content_node
+ var content_node_size_diff = get_child_size_diff(content_node, true, true)
+ # Calculate distance to left, right, top and bottom
+ var content_node_boundary_dist = get_child_boundary_dist(
+ content_node.position,
+ content_node_size_diff
+ )
+ drag_temp_data = [
+ 0.0,
+ 0.0,
+ content_node.position.x,
+ content_node.position.y,
+ content_node_boundary_dist.x,
+ content_node_boundary_dist.y,
+ content_node_boundary_dist.z,
+ content_node_boundary_dist.w,
+ ]
+
+# Get container size x without v scroll bar 's width
+func get_spare_size_x() -> float:
+ var size_x = size.x
+ if get_v_scroll_bar().visible:
+ size_x -= get_v_scroll_bar().size.x
+ return max(size_x, 0.0)
+
+# Get container size y without h scroll bar 's height
+func get_spare_size_y() -> float:
+ var size_y = size.y
+ if get_h_scroll_bar().visible:
+ size_y -= get_h_scroll_bar().size.y
+ return max(size_y, 0.0)
+
+# Get container size without scroll bars' size
+func get_spare_size() -> Vector2:
+ return Vector2(get_spare_size_x(), get_spare_size_y())
+
+# Calculate the size x difference between this container and child node
+func get_child_size_x_diff(child: Control, clamp: bool) -> float:
+ var child_size_x = child.size.x * child.scale.x
+ # Falsify the size of the child node to avoid errors
+ # when its size is smaller than this container 's
+ if clamp:
+ child_size_x = max(child_size_x, get_spare_size_x())
+ return child_size_x - get_spare_size_x()
+
+# Calculate the size y difference between this container and child node
+func get_child_size_y_diff(child: Control, clamp: bool) -> float:
+ var child_size_y = child.size.y * child.scale.y
+ # Falsify the size of the child node to avoid errors
+ # when its size is smaller than this container 's
+ if clamp:
+ child_size_y = max(child_size_y, get_spare_size_y())
+ return child_size_y - get_spare_size_y()
+
+# Calculate the size difference between this container and child node
+func get_child_size_diff(child: Control, clamp_x: bool, clamp_y: bool) -> Vector2:
+ return Vector2(
+ get_child_size_x_diff(child, clamp_x),
+ get_child_size_y_diff(child, clamp_y)
+ )
+
+# Calculate distance to left
+func get_child_left_dist(child_pos_x: float, child_size_diff_x: float) -> float:
+ return child_pos_x
+
+# Calculate distance to right
+func get_child_right_dist(child_pos_x: float, child_size_diff_x: float) -> float:
+ return child_pos_x + child_size_diff_x
+
+# Calculate distance to top
+func get_child_top_dist(child_pos_y: float, child_size_diff_y: float) -> float:
+ return child_pos_y
+
+# Calculate distance to bottom
+func get_child_bottom_dist(child_pos_y: float, child_size_diff_y: float) -> float:
+ return child_pos_y + child_size_diff_y
+
+# Calculate distance to left, right, top and bottom
+func get_child_boundary_dist(child_pos: Vector2, child_size_diff: Vector2) -> Vector4:
+ return Vector4(
+ get_child_left_dist(child_pos.x, child_size_diff.x),
+ get_child_right_dist(child_pos.x, child_size_diff.x),
+ get_child_top_dist(child_pos.y, child_size_diff.y),
+ get_child_bottom_dist(child_pos.y, child_size_diff.y),
+ )
+
+func kill_scroll_x_to_tween() -> void:
+ if scroll_x_to_tween: scroll_x_to_tween.kill()
+
+func kill_scroll_y_to_tween() -> void:
+ if scroll_y_to_tween: scroll_y_to_tween.kill()
+
+func kill_scroll_to_tweens() -> void:
+ kill_scroll_x_to_tween()
+ kill_scroll_y_to_tween()
+
+#endregion
+
+#region Debug Drawing
+
+var debug_gradient := Gradient.new()
+
+func setup_debug_drawing() -> void:
+ debug_gradient.set_color(0.0, Color.GREEN)
+ debug_gradient.set_color(1.0, Color.RED)
+
+func draw_debug() -> void:
+ # Calculate the size difference between this container and content_node
+ var size_diff = get_child_size_diff(content_node, false, false)
+ # Calculate distance to left, right, top and bottom
+ var boundary_dist = get_child_boundary_dist(
+ content_node.position,
+ size_diff
+ )
+ var bottom_distance = boundary_dist.w
+ var top_distance = boundary_dist.z
+ var right_distance = boundary_dist.y
+ var left_distance = boundary_dist.x
+ # Overdrag lines
+ # Top + Bottom
+ draw_line(Vector2(0.0, 0.0), Vector2(0.0, top_distance), debug_gradient.sample(clamp(top_distance / size.y, 0.0, 1.0)), 5.0)
+ draw_line(Vector2(0.0, size.y), Vector2(0.0, size.y+bottom_distance), debug_gradient.sample(clamp(-bottom_distance / size.y, 0.0, 1.0)), 5.0)
+ # Left + Right
+ draw_line(Vector2(0.0, size.y), Vector2(left_distance, size.y), debug_gradient.sample(clamp(left_distance / size.y, 0.0, 1.0)), 5.0)
+ draw_line(Vector2(size.x, size.y), Vector2(size.x+right_distance, size.y), debug_gradient.sample(clamp(-right_distance / size.y, 0.0, 1.0)), 5.0)
+
+ # Velocity lines
+ var origin := Vector2(5.0, size.y/2)
+ draw_line(origin, origin + Vector2(0.0, velocity.y*0.01), debug_gradient.sample(clamp(velocity.y*2 / size.y, 0.0, 1.0)), 5.0)
+ draw_line(origin, origin + Vector2(0.0, velocity.x*0.01), debug_gradient.sample(clamp(velocity.x*2 / size.x, 0.0, 1.0)), 5.0)
+
+#endregion
+
+#region API Functions
+
+## Scrolls to specific x position
+func scroll_x_to(x_pos: float, duration := 0.5) -> void:
+ if not should_scroll_horizontal(): return
+ if content_dragging: return
+ velocity.x = 0.0
+ var size_x_diff = get_child_size_x_diff(content_node, true)
+ x_pos = clampf(x_pos, -size_x_diff, 0.0)
+ kill_scroll_x_to_tween()
+ scroll_x_to_tween = create_tween()
+ var tweener = scroll_x_to_tween.tween_property(self, "pos:x", x_pos, duration)
+ tweener.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUINT)
+
+## Scrolls to specific y position
+func scroll_y_to(y_pos: float, duration := 0.5) -> void:
+ if not should_scroll_vertical(): return
+ if content_dragging: return
+ velocity.y = 0.0
+ var size_y_diff = get_child_size_y_diff(content_node, true)
+ y_pos = clampf(y_pos, -size_y_diff, 0.0)
+ kill_scroll_y_to_tween()
+ scroll_y_to_tween = create_tween()
+ var tweener = scroll_y_to_tween.tween_property(self, "pos:y", y_pos, duration)
+ tweener.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUINT)
+
+## Scrolls up a page
+func scroll_page_up(duration := 0.5) -> void:
+ var destination = content_node.position.y + get_spare_size_y()
+ scroll_y_to(destination, duration)
+
+## Scrolls down a page
+func scroll_page_down(duration := 0.5) -> void:
+ var destination = content_node.position.y - get_spare_size_y()
+ scroll_y_to(destination, duration)
+
+## Scrolls left a page
+func scroll_page_left(duration := 0.5) -> void:
+ var destination = content_node.position.x + get_spare_size_x()
+ scroll_x_to(destination, duration)
+
+## Scrolls right a page
+func scroll_page_right(duration := 0.5) -> void:
+ var destination = content_node.position.x - get_spare_size_x()
+ scroll_x_to(destination, duration)
+
+## Adds velocity to the vertical scroll
+func scroll_vertically(amount: float) -> void:
+ velocity.y -= amount
+
+## Adds velocity to the horizontal scroll
+func scroll_horizontally(amount: float) -> void:
+ velocity.x -= amount
+
+## Scrolls to top
+func scroll_to_top(duration := 0.5) -> void:
+ scroll_y_to(0.0, duration)
+
+## Scrolls to bottom
+func scroll_to_bottom(duration := 0.5) -> void:
+ scroll_y_to(get_spare_size_y() - content_node.size.y, duration)
+
+## Scrolls to left
+func scroll_to_left(duration := 0.5) -> void:
+ scroll_x_to(0.0, duration)
+
+## Scrolls to right
+func scroll_to_right(duration := 0.5) -> void:
+ scroll_x_to(get_spare_size_x() - content_node.size.x, duration)
+
+func is_outside_top_boundary(y_pos: float = pos.y) -> bool:
+ var size_y_diff = get_child_size_y_diff(content_node,true)
+ var top_dist = get_child_top_dist(y_pos, size_y_diff)
+ return top_dist > 0.0
+
+func is_outside_bottom_boundary(y_pos: float = pos.y) -> bool:
+ var size_y_diff = get_child_size_y_diff(content_node,true)
+ var bottom_dist = get_child_bottom_dist(y_pos, size_y_diff)
+ return bottom_dist < 0.0
+
+func is_outside_left_boundary(x_pos: float = pos.x) -> bool:
+ var size_x_diff = get_child_size_x_diff(content_node,true)
+ var left_dist = get_child_left_dist(x_pos, size_x_diff)
+ return left_dist > 0.0
+
+func is_outside_right_boundary(x_pos: float = pos.x) -> bool:
+ var size_x_diff = get_child_size_x_diff(content_node,true)
+ var right_dist = get_child_right_dist(x_pos, size_x_diff)
+ return right_dist < 0.0
+
+## Returns true if any scroll bar is being dragged
+func any_scroll_bar_dragged() -> bool:
+ return h_scrollbar_dragging or v_scrollbar_dragging
+
+## Returns true if there is enough content height to scroll
+func should_scroll_vertical() -> bool:
+ var disable_scroll = (not allow_vertical_scroll) \
+ or (auto_allow_scroll and get_child_size_y_diff(content_node, false) <= 0) \
+ or !scroll_damper
+ if disable_scroll:
+ velocity.y = 0.0
+ return false
+ else:
+ return true
+
+## Returns true if there is enough content width to scroll
+func should_scroll_horizontal() -> bool:
+ var disable_scroll = (not allow_horizontal_scroll) \
+ or (auto_allow_scroll and get_child_size_x_diff(content_node, false) <= 0) \
+ or !scroll_damper
+ if disable_scroll:
+ velocity.x = 0.0
+ return false
+ else:
+ return true
+
+## Fades out scrollbars within given [param time].[br]
+## Default for [param time] is current [member scrollbar_fade_out_time]
+func hide_scrollbars(time: float = scrollbar_fade_out_time) -> void:
+ # Kill scrollbar_show_tween to avoid animation conflict
+ if scrollbar_show_tween != null and scrollbar_show_tween.is_valid():
+ scrollbar_show_tween.kill()
+ # Create new tweens if needed
+ if (
+ get_v_scroll_bar().modulate != Color.TRANSPARENT \
+ or get_h_scroll_bar().modulate != Color.TRANSPARENT
+ ):
+ if scrollbar_hide_tween and !scrollbar_hide_tween.is_running():
+ scrollbar_hide_tween.kill()
+ if scrollbar_hide_tween == null or !scrollbar_hide_tween.is_valid():
+ scrollbar_hide_tween = create_tween()
+ scrollbar_hide_tween.set_parallel(true)
+ scrollbar_hide_tween.tween_property(get_v_scroll_bar(), 'modulate', Color.TRANSPARENT, time)
+ scrollbar_hide_tween.tween_property(get_h_scroll_bar(), 'modulate', Color.TRANSPARENT, time)
+
+## Fades in scrollbars within given [param time].[br]
+## Default for [param time] is current [member scrollbar_fade_in_time]
+func show_scrollbars(time: float = scrollbar_fade_in_time) -> void:
+ # Restart timer
+ scrollbar_hide_timer.start(scrollbar_hide_time)
+ # Kill scrollbar_hide_tween to avoid animation conflict
+ if scrollbar_hide_tween != null and scrollbar_hide_tween.is_valid():
+ scrollbar_hide_tween.kill()
+ # Create new tweens if needed
+ if (
+ get_v_scroll_bar().modulate != Color.WHITE \
+ or get_h_scroll_bar().modulate != Color.WHITE \
+ ):
+ if scrollbar_show_tween and !scrollbar_show_tween.is_running():
+ scrollbar_show_tween.kill()
+ if scrollbar_show_tween == null or !scrollbar_show_tween.is_valid():
+ scrollbar_show_tween = create_tween()
+ scrollbar_show_tween.set_parallel(true)
+ scrollbar_show_tween.tween_property(get_v_scroll_bar(), 'modulate', Color.WHITE, time)
+ scrollbar_show_tween.tween_property(get_h_scroll_bar(), 'modulate', Color.WHITE, time)
+
+## Scroll to position to ensure the given control node is visible
+func ensure_control_visible(control: Control) -> void:
+ if !content_node: return
+ if !content_node.is_ancestor_of(control): return
+ if !scroll_damper: return
+
+ var size_diff = (
+ control.get_global_rect().size - get_global_rect().size
+ ) / (get_global_rect().size / size)
+ var boundary_dist = get_child_boundary_dist(
+ (control.global_position - global_position) \
+ / (get_global_rect().size / size),
+ size_diff
+ )
+ var content_node_position = content_node.position
+ if boundary_dist.x < 0 + follow_focus_margin:
+ scroll_x_to(content_node_position.x - boundary_dist.x + follow_focus_margin)
+ elif boundary_dist.y > 0 - follow_focus_margin:
+ scroll_x_to(content_node_position.x - boundary_dist.y - follow_focus_margin)
+ if boundary_dist.z < 0 + follow_focus_margin:
+ scroll_y_to(content_node_position.y - boundary_dist.z + follow_focus_margin)
+ elif boundary_dist.w > 0 - follow_focus_margin:
+ scroll_y_to(content_node_position.y - boundary_dist.w - follow_focus_margin)
+
+#endregion
diff --git a/addons/SmoothScroll/SmoothScrollContainer.gd.uid b/addons/SmoothScroll/SmoothScrollContainer.gd.uid
new file mode 100644
index 0000000..305c994
--- /dev/null
+++ b/addons/SmoothScroll/SmoothScrollContainer.gd.uid
@@ -0,0 +1 @@
+uid://bgqglerkcylxx
diff --git a/addons/SmoothScroll/class-icon.svg b/addons/SmoothScroll/class-icon.svg
new file mode 100644
index 0000000..30862c1
--- /dev/null
+++ b/addons/SmoothScroll/class-icon.svg
@@ -0,0 +1,19 @@
+
+
+
diff --git a/addons/SmoothScroll/class-icon.svg.import b/addons/SmoothScroll/class-icon.svg.import
new file mode 100644
index 0000000..488e0a4
--- /dev/null
+++ b/addons/SmoothScroll/class-icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dorhyoghxkay6"
+path="res://.godot/imported/class-icon.svg-c17de51589a7d30572bf401526524f64.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/SmoothScroll/class-icon.svg"
+dest_files=["res://.godot/imported/class-icon.svg-c17de51589a7d30572bf401526524f64.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/SmoothScroll/debug_font.tres b/addons/SmoothScroll/debug_font.tres
new file mode 100644
index 0000000..3871a65
--- /dev/null
+++ b/addons/SmoothScroll/debug_font.tres
@@ -0,0 +1,3 @@
+[gd_resource type="SystemFont" format=3 uid="uid://cw8c0p3b5mv5y"]
+
+[resource]
diff --git a/addons/SmoothScroll/icon.afdesign b/addons/SmoothScroll/icon.afdesign
new file mode 100644
index 0000000..1b54598
Binary files /dev/null and b/addons/SmoothScroll/icon.afdesign differ
diff --git a/addons/SmoothScroll/plugin.cfg b/addons/SmoothScroll/plugin.cfg
new file mode 100644
index 0000000..2c42343
--- /dev/null
+++ b/addons/SmoothScroll/plugin.cfg
@@ -0,0 +1,8 @@
+[plugin]
+
+name="SmoothScroll"
+description="""This plugin adds a new scroll container class
+with additional smooth scroll options."""
+author="Fabian Keßler (SpyrexDE)"
+version="1.3"
+script="plugin.gd"
diff --git a/addons/SmoothScroll/plugin.gd b/addons/SmoothScroll/plugin.gd
new file mode 100644
index 0000000..de14735
--- /dev/null
+++ b/addons/SmoothScroll/plugin.gd
@@ -0,0 +1,12 @@
+@tool
+extends EditorPlugin
+
+
+func _enter_tree():
+ add_custom_type("ScrollDamper", "Resource", preload("scroll_damper/scroll_damper.gd"), preload("scroll_damper/icon.svg"))
+ add_custom_type("SmoothScrollContainer", "ScrollContainer", preload("SmoothScrollContainer.gd"), preload("class-icon.svg"))
+
+
+func _exit_tree():
+ remove_custom_type("ScrollDamper")
+ remove_custom_type("SmoothScrollContainer")
diff --git a/addons/SmoothScroll/plugin.gd.uid b/addons/SmoothScroll/plugin.gd.uid
new file mode 100644
index 0000000..ca7fc96
--- /dev/null
+++ b/addons/SmoothScroll/plugin.gd.uid
@@ -0,0 +1 @@
+uid://b7svd7w4nwpch
diff --git a/addons/SmoothScroll/scroll_damper/cubic_scroll_damper.gd b/addons/SmoothScroll/scroll_damper/cubic_scroll_damper.gd
new file mode 100644
index 0000000..3c40815
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/cubic_scroll_damper.gd
@@ -0,0 +1,33 @@
+extends ScrollDamper
+class_name CubicScrollDamper
+
+
+## Friction, not physical.
+## The higher the value, the more obvious the deceleration.
+@export_range(0.001, 10000.0, 0.001, "or_greater", "hide_slider")
+var friction := 4.0:
+ set(val):
+ friction = max(val, 0.001)
+ _factor = pow(10.0, friction) - 1.0
+
+## Factor to use in formula
+var _factor := 10000.0:
+ set(val): _factor = max(val, 0.000000000001)
+
+
+func _calculate_velocity_by_time(time: float) -> float:
+ if time <= 0.0: return 0.0
+ return time*time*time * _factor
+
+
+func _calculate_time_by_velocity(velocity: float) -> float:
+ return pow(abs(velocity) / _factor, 1.0/3.0)
+
+
+func _calculate_offset_by_time(time: float) -> float:
+ time = max(time, 0.0)
+ return 1.0/4.0 * _factor * time*time*time*time
+
+
+func _calculate_time_by_offset(offset: float) -> float:
+ return pow(abs(offset) * 4.0 / _factor, 1.0/4.0)
diff --git a/addons/SmoothScroll/scroll_damper/cubic_scroll_damper.gd.uid b/addons/SmoothScroll/scroll_damper/cubic_scroll_damper.gd.uid
new file mode 100644
index 0000000..5917f98
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/cubic_scroll_damper.gd.uid
@@ -0,0 +1 @@
+uid://dlagxcnf11lr4
diff --git a/addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd b/addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd
new file mode 100644
index 0000000..c6d9b9d
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd
@@ -0,0 +1,47 @@
+extends ScrollDamper
+class_name ExpoScrollDamper
+
+
+## Friction, not physical.
+## The higher the value, the more obvious the deceleration.
+@export_range(0.001, 10000.0, 0.001, "or_greater", "hide_slider")
+var friction := 4.0:
+ set(val):
+ friction = max(val, 0.001)
+ _factor = pow(10.0, friction)
+
+## Factor to use in formula
+var _factor := 10000.0:
+ set(val): _factor = max(val, 1.000000000001)
+
+## Minumun velocity.
+@export_range(0.001, 100000.0, 0.001, "or_greater", "hide_slider")
+var minimum_velocity := 0.4:
+ set(val): minimum_velocity = max(val, 0.001)
+
+
+func _calculate_velocity_by_time(time: float) -> float:
+ var minimum_time = _calculate_time_by_velocity(minimum_velocity)
+ if time <= minimum_time: return 0.0
+ return pow(_factor, time)
+
+
+func _calculate_time_by_velocity(velocity: float) -> float:
+ return log(abs(velocity)) / log(_factor)
+
+
+func _calculate_offset_by_time(time: float) -> float:
+ return pow(_factor, time) / log(_factor)
+
+
+func _calculate_time_by_offset(offset: float) -> float:
+ return log(offset * log(_factor)) / log(_factor)
+
+
+func _calculate_velocity_to_dest(from: float, to: float) -> float:
+ var dist = to - from
+ var min_time = _calculate_time_by_velocity(minimum_velocity)
+ var min_offset = _calculate_offset_by_time(min_time)
+ var time = _calculate_time_by_offset(abs(dist) + min_offset)
+ var vel = _calculate_velocity_by_time(time) * sign(dist)
+ return vel
diff --git a/addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd.uid b/addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd.uid
new file mode 100644
index 0000000..bd22621
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd.uid
@@ -0,0 +1 @@
+uid://b7h0k2h2qwlqv
diff --git a/addons/SmoothScroll/scroll_damper/icon.afdesign b/addons/SmoothScroll/scroll_damper/icon.afdesign
new file mode 100644
index 0000000..2bd817e
Binary files /dev/null and b/addons/SmoothScroll/scroll_damper/icon.afdesign differ
diff --git a/addons/SmoothScroll/scroll_damper/icon.svg b/addons/SmoothScroll/scroll_damper/icon.svg
new file mode 100644
index 0000000..54a8de1
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/icon.svg
@@ -0,0 +1,29 @@
+
+
+
diff --git a/addons/SmoothScroll/scroll_damper/icon.svg.import b/addons/SmoothScroll/scroll_damper/icon.svg.import
new file mode 100644
index 0000000..240f33d
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://4ok12qtgl7xq"
+path="res://.godot/imported/icon.svg-5b01e8115f19d6d63dc265815ce29c75.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/SmoothScroll/scroll_damper/icon.svg"
+dest_files=["res://.godot/imported/icon.svg-5b01e8115f19d6d63dc265815ce29c75.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/SmoothScroll/scroll_damper/linear_scroll_damper.gd b/addons/SmoothScroll/scroll_damper/linear_scroll_damper.gd
new file mode 100644
index 0000000..c563513
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/linear_scroll_damper.gd
@@ -0,0 +1,33 @@
+extends ScrollDamper
+class_name LinearScrollDamper
+
+
+## Friction, not physical.
+## The higher the value, the more obvious the deceleration.
+@export_range(0.001, 10000.0, 0.001, "or_greater", "hide_slider")
+var friction := 4.0:
+ set(val):
+ friction = max(val, 0.001)
+ _factor = pow(10.0, friction) - 1.0
+
+## Factor to use in formula
+var _factor := 10000.0:
+ set(val): _factor = max(val, 0.000000000001)
+
+
+func _calculate_velocity_by_time(time: float) -> float:
+ if time <= 0.0: return 0.0
+ return time * _factor
+
+
+func _calculate_time_by_velocity(velocity: float) -> float:
+ return abs(velocity) / _factor
+
+
+func _calculate_offset_by_time(time: float) -> float:
+ time = max(time, 0.0)
+ return 1.0/2.0 * _factor * time*time
+
+
+func _calculate_time_by_offset(offset: float) -> float:
+ return sqrt(abs(offset) * 2.0 / _factor)
diff --git a/addons/SmoothScroll/scroll_damper/linear_scroll_damper.gd.uid b/addons/SmoothScroll/scroll_damper/linear_scroll_damper.gd.uid
new file mode 100644
index 0000000..632707a
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/linear_scroll_damper.gd.uid
@@ -0,0 +1 @@
+uid://dvvc75dvopgcg
diff --git a/addons/SmoothScroll/scroll_damper/quad_scroll_damper.gd b/addons/SmoothScroll/scroll_damper/quad_scroll_damper.gd
new file mode 100644
index 0000000..308c0e0
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/quad_scroll_damper.gd
@@ -0,0 +1,33 @@
+extends ScrollDamper
+class_name QuadScrollDamper
+
+
+## Friction, not physical.
+## The higher the value, the more obvious the deceleration.
+@export_range(0.001, 10000.0, 0.001, "or_greater", "hide_slider")
+var friction := 4.0:
+ set(val):
+ friction = max(val, 0.001)
+ _factor = pow(10.0, friction) - 1.0
+
+## Factor to use in formula
+var _factor := 10000.0:
+ set(val): _factor = max(val, 0.000000000001)
+
+
+func _calculate_velocity_by_time(time: float) -> float:
+ if time <= 0.0: return 0.0
+ return time*time * _factor
+
+
+func _calculate_time_by_velocity(velocity: float) -> float:
+ return sqrt(abs(velocity) / _factor)
+
+
+func _calculate_offset_by_time(time: float) -> float:
+ time = max(time, 0.0)
+ return 1.0/3.0 * _factor * time*time*time
+
+
+func _calculate_time_by_offset(offset: float) -> float:
+ return pow(abs(offset) * 3.0 / _factor, 1.0/3.0)
diff --git a/addons/SmoothScroll/scroll_damper/quad_scroll_damper.gd.uid b/addons/SmoothScroll/scroll_damper/quad_scroll_damper.gd.uid
new file mode 100644
index 0000000..51ad970
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/quad_scroll_damper.gd.uid
@@ -0,0 +1 @@
+uid://ccgjrqkk3oksk
diff --git a/addons/SmoothScroll/scroll_damper/scroll_damper.gd b/addons/SmoothScroll/scroll_damper/scroll_damper.gd
new file mode 100644
index 0000000..448f0ee
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/scroll_damper.gd
@@ -0,0 +1,74 @@
+@icon("icon.svg")
+extends Resource
+class_name ScrollDamper
+
+## Abstract class
+
+## Rebound strength. The higher the value, the faster it attracts.
+@export_range(0.0, 1.0, 0.001, "or_greater", "hide_slider")
+var rebound_strength := 7.0:
+ set(val):
+ rebound_strength= max(val, 0.0)
+ _attract_factor = rebound_strength * rebound_strength * rebound_strength
+
+## Factor for attracting.
+var _attract_factor := 400.0:
+ set(val):
+ _attract_factor = max(val, 0.0)
+
+
+# Abstract method
+func _calculate_velocity_by_time(time: float) -> float:
+ return 0.0
+
+# Abstract method
+func _calculate_time_by_velocity(velocity: float) -> float:
+ return 0.0
+
+# Abstract method
+func _calculate_offset_by_time(time: float) -> float:
+ return 0.0
+
+# Abstract method
+func _calculate_time_by_offset(offset: float) -> float:
+ return 0.0
+
+
+func _calculate_velocity_to_dest(from: float, to: float) -> float:
+ var dist = to - from
+ var time = _calculate_time_by_offset(abs(dist))
+ var vel = _calculate_velocity_by_time(time) * sign(dist)
+ return vel
+
+
+func _calculate_next_velocity(present_time: float, delta_time: float) -> float:
+ return _calculate_velocity_by_time(present_time - delta_time)
+
+
+func _calculate_next_offset(present_time: float, delta_time: float) -> float:
+ return _calculate_offset_by_time(present_time) \
+ - _calculate_offset_by_time(present_time - delta_time)
+
+
+## Return the result of next velocity and position according to delta time
+func slide(velocity: float, delta_time: float) -> Array:
+ var present_time = _calculate_time_by_velocity(velocity)
+ return [
+ _calculate_next_velocity(present_time, delta_time) * sign(velocity),
+ _calculate_next_offset(present_time, delta_time) * sign(velocity)
+ ]
+
+
+## Emulate force that attracts something to destination.
+## Return the result of next velocity according to delta time
+func attract(from: float, to: float, velocity: float, delta_time: float) -> float:
+ var dist = to - from
+ var target_vel = _calculate_velocity_to_dest(from, to)
+ velocity += _attract_factor * dist * delta_time \
+ + _calculate_velocity_by_time(delta_time) * sign(dist)
+ if (
+ (dist > 0 and velocity >= target_vel) \
+ or (dist < 0 and velocity <= target_vel) \
+ ):
+ velocity = target_vel
+ return velocity
diff --git a/addons/SmoothScroll/scroll_damper/scroll_damper.gd.uid b/addons/SmoothScroll/scroll_damper/scroll_damper.gd.uid
new file mode 100644
index 0000000..8f446a0
--- /dev/null
+++ b/addons/SmoothScroll/scroll_damper/scroll_damper.gd.uid
@@ -0,0 +1 @@
+uid://bcy471w0pi8af
diff --git a/project.godot b/project.godot
index ed9f754..9315199 100644
--- a/project.godot
+++ b/project.godot
@@ -26,6 +26,10 @@ window/size/viewport_width=1920
window/size/viewport_height=1080
window/stretch/aspect="ignore"
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/SmoothScroll/plugin.cfg")
+
[file_customization]
folder_colors={