From 1576ef146575a3a5d93c237b4500a9acb7e0b43e Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Fri, 25 Jul 2025 18:55:28 +0300 Subject: [PATCH] textarea --- Assets/Icons/resize-handle.svg | 5 ++ Assets/Icons/resize-handle.svg.import | 37 +++++++++++++ Scenes/Styles/BrowserText.tres | 50 +++++++++++++++++- Scenes/Tags/textarea.tscn | 27 ++++++++++ Scripts/ResizableTextEdit.gd | 64 +++++++++++++++++++++++ Scripts/ResizableTextEdit.gd.uid | 1 + Scripts/Tags/select.gd | 1 - Scripts/Tags/textarea.gd | 75 +++++++++++++++++++++++++++ Scripts/Tags/textarea.gd.uid | 1 + Scripts/main.gd | 12 +++++ 10 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 Assets/Icons/resize-handle.svg create mode 100644 Assets/Icons/resize-handle.svg.import create mode 100644 Scenes/Tags/textarea.tscn create mode 100644 Scripts/ResizableTextEdit.gd create mode 100644 Scripts/ResizableTextEdit.gd.uid create mode 100644 Scripts/Tags/textarea.gd create mode 100644 Scripts/Tags/textarea.gd.uid diff --git a/Assets/Icons/resize-handle.svg b/Assets/Icons/resize-handle.svg new file mode 100644 index 0000000..e58754d --- /dev/null +++ b/Assets/Icons/resize-handle.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/Assets/Icons/resize-handle.svg.import b/Assets/Icons/resize-handle.svg.import new file mode 100644 index 0000000..49464f8 --- /dev/null +++ b/Assets/Icons/resize-handle.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://tb7e8bcs8kqv" +path="res://.godot/imported/resize-handle.svg-433a7f0ff4889b64a8919f3af66061e6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/resize-handle.svg" +dest_files=["res://.godot/imported/resize-handle.svg-433a7f0ff4889b64a8919f3af66061e6.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/Scenes/Styles/BrowserText.tres b/Scenes/Styles/BrowserText.tres index 7d6c026..f0b4990 100644 --- a/Scenes/Styles/BrowserText.tres +++ b/Scenes/Styles/BrowserText.tres @@ -1,4 +1,4 @@ -[gd_resource type="Theme" load_steps=44 format=3 uid="uid://bn6rbmdy60lhr"] +[gd_resource type="Theme" load_steps=47 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"] @@ -184,6 +184,45 @@ corner_radius_top_right = 4 corner_radius_bottom_right = 4 corner_radius_bottom_left = 4 +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_n33pi"] +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_rmpy5"] +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="StyleBoxFlat" id="StyleBoxFlat_8uupr"] +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 + [resource] Button/styles/focus = SubResource("StyleBoxEmpty_c32on") Button/styles/hover = SubResource("StyleBoxFlat_0v877") @@ -248,3 +287,12 @@ RichTextLabel/styles/focus = SubResource("StyleBoxEmpty_jecr6") TabContainer/styles/tab_hovered = SubResource("StyleBoxFlat_1vd2l") TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_4akvr") TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_r011l") +TextEdit/colors/caret_color = Color(0, 0, 0, 1) +TextEdit/colors/font_color = Color(0, 0, 0, 1) +TextEdit/colors/font_placeholder_color = Color(0, 0, 0, 0.6) +TextEdit/colors/font_readonly_color = Color(0.196078, 0.196078, 0.196078, 1) +TextEdit/colors/font_selected_color = Color(1, 1, 1, 1) +TextEdit/colors/selection_color = Color(0.313726, 0.403922, 0.8, 1) +TextEdit/styles/focus = SubResource("StyleBoxFlat_n33pi") +TextEdit/styles/normal = SubResource("StyleBoxFlat_rmpy5") +TextEdit/styles/read_only = SubResource("StyleBoxFlat_8uupr") diff --git a/Scenes/Tags/textarea.tscn b/Scenes/Tags/textarea.tscn new file mode 100644 index 0000000..fb25c55 --- /dev/null +++ b/Scenes/Tags/textarea.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=4 format=3 uid="uid://kfm80txc5t8m"] + +[ext_resource type="Script" uid="uid://bmcx7mr4nye6a" path="res://Scripts/Tags/textarea.gd" id="1_textarea"] +[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_theme"] +[ext_resource type="Script" uid="uid://c5xpoyqcg1p8k" path="res://Scripts/ResizableTextEdit.gd" id="3_kbdk1"] + +[node name="Textarea" type="Control"] +layout_mode = 3 +anchors_preset = 10 +anchor_right = 1.0 +grow_horizontal = 2 +script = ExtResource("1_textarea") + +[node name="TextEdit" type="TextEdit" parent="."] +layout_mode = 1 +offset_right = 341.0 +offset_bottom = 128.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme = ExtResource("2_theme") +placeholder_text = "Enter text here..." +context_menu_enabled = false +emoji_menu_enabled = false +middle_mouse_paste_enabled = false +wrap_mode = 1 +caret_blink = true +script = ExtResource("3_kbdk1") diff --git a/Scripts/ResizableTextEdit.gd b/Scripts/ResizableTextEdit.gd new file mode 100644 index 0000000..bb30aea --- /dev/null +++ b/Scripts/ResizableTextEdit.gd @@ -0,0 +1,64 @@ +extends TextEdit + +@onready var resize_handle = TextureRect.new() +var is_resizing = false +var resize_start_pos = Vector2() +var original_size = Vector2() + +var min_size = Vector2(100, 50) + +func _ready(): + # Create resize handle as TextureRect child of TextEdit + resize_handle.texture = load("res://Assets/Icons/resize-handle.svg") + resize_handle.size = Vector2(32, 32) + resize_handle.mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE + resize_handle.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + add_child(resize_handle) + + # Position handle in bottom-right corner + _update_handle_position() + + # Connect signals + resize_handle.gui_input.connect(_on_resize_handle_input) + resized.connect(_update_handle_position) + +func _gui_input(event): + if event is InputEventMouseButton and get_global_rect().has_point(get_viewport().get_mouse_position()): + if event.button_index == MOUSE_BUTTON_WHEEL_UP: + set_v_scroll(get_v_scroll() - 2) + accept_event() + elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: + set_v_scroll(get_v_scroll() + 2) + accept_event() + +func _update_handle_position(): + if resize_handle: + resize_handle.position = Vector2(size.x - 32, size.y - 32) + +func _on_resize_handle_input(event): + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT: + if event.pressed: + is_resizing = true + resize_start_pos = event.global_position + original_size = size + else: + is_resizing = false + + elif event is InputEventMouseMotion and is_resizing: + var delta = event.global_position - resize_start_pos + var new_size = original_size + delta + new_size.x = max(new_size.x, min_size.x) + new_size.y = max(new_size.y, min_size.y) + + size = new_size + + # Sync parent Control size + var parent_control = get_parent() as Control + if parent_control: + parent_control.size = new_size + parent_control.custom_minimum_size = new_size + if parent_control: + parent_control.size = new_size + parent_control.custom_minimum_size = new_size + diff --git a/Scripts/ResizableTextEdit.gd.uid b/Scripts/ResizableTextEdit.gd.uid new file mode 100644 index 0000000..6fab09d --- /dev/null +++ b/Scripts/ResizableTextEdit.gd.uid @@ -0,0 +1 @@ +uid://c5xpoyqcg1p8k diff --git a/Scripts/Tags/select.gd b/Scripts/Tags/select.gd index ee0c4f8..86afae7 100644 --- a/Scripts/Tags/select.gd +++ b/Scripts/Tags/select.gd @@ -35,5 +35,4 @@ func init(element: HTMLParser.HTMLElement) -> void: if selected_index >= 0: option_button.selected = selected_index - add_child(option_button) custom_minimum_size = option_button.size diff --git a/Scripts/Tags/textarea.gd b/Scripts/Tags/textarea.gd new file mode 100644 index 0000000..119e6d2 --- /dev/null +++ b/Scripts/Tags/textarea.gd @@ -0,0 +1,75 @@ +extends Control + +const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres") + +func init(element: HTMLParser.HTMLElement) -> void: + var text_edit: TextEdit = $TextEdit + + var placeholder = element.get_attribute("placeholder") + var value = element.get_attribute("value") + var rows = element.get_attribute("rows") + var cols = element.get_attribute("cols") + var maxlength = element.get_attribute("maxlength") + var readonly = element.get_attribute("readonly") + var disabled = element.get_attribute("disabled") + + # Set placeholder text + text_edit.placeholder_text = placeholder + + # Set initial value + if value.length() > 0: + text_edit.text = value + elif element.text_content.length() > 0: + text_edit.text = element.text_content + + # We assume to fit $rows amount of new lines + var line_height = text_edit.get_theme_default_font().get_height(text_edit.get_theme_default_font_size()) + # We assume the biggest letter typed is "M" (77), and optimize to fit $cols amount of "M" + var char_width = text_edit.get_theme_default_font().get_char_size(77, text_edit.get_theme_default_font_size()).x + + var min_height = line_height * (rows.to_int() if rows.length() > 0 else 4) + 26 # padding + var min_width = char_width * (cols.to_int() if cols.length() > 0 else 50) + 16 # padding + + text_edit.custom_minimum_size = Vector2(min_width, min_height) + text_edit.size = Vector2(min_width, min_height) + text_edit.min_size = Vector2(min_width, min_height) + + # Sync Control size with TextEdit + custom_minimum_size = text_edit.custom_minimum_size + size = text_edit.size + + # Set readonly state + if readonly.length() > 0: + text_edit.editable = false + + # Set disabled state + if disabled.length() > 0: + text_edit.editable = false + var stylebox = StyleBoxFlat.new() + stylebox.bg_color = Color(0.8, 0.8, 0.8, 1.0) + stylebox.border_color = Color(0, 0, 0, 1.0) + stylebox.border_width_bottom = 1 + stylebox.border_width_top = 1 + stylebox.border_width_left = 1 + stylebox.border_width_right = 1 + stylebox.corner_radius_bottom_left = 3 + stylebox.corner_radius_bottom_right = 3 + stylebox.corner_radius_top_left = 3 + stylebox.corner_radius_top_right = 3 + text_edit.add_theme_stylebox_override("normal", stylebox) + text_edit.add_theme_stylebox_override("focus", stylebox) + text_edit.add_theme_stylebox_override("readonly", stylebox) + + # Handle maxlength + if maxlength.length() > 0 and maxlength.is_valid_int(): + var max_len = maxlength.to_int() + text_edit.text_changed.connect(_on_text_changed.bind(max_len)) + +func _on_text_changed(max_length: int) -> void: + var text_edit = $TextEdit as TextEdit + if text_edit.text.length() > max_length: + var cursor_pos = text_edit.get_caret_column() + var line_pos = text_edit.get_caret_line() + text_edit.text = text_edit.text.substr(0, max_length) + text_edit.set_caret_line(line_pos) + text_edit.set_caret_column(min(cursor_pos, text_edit.get_line(line_pos).length())) diff --git a/Scripts/Tags/textarea.gd.uid b/Scripts/Tags/textarea.gd.uid new file mode 100644 index 0000000..5bd821a --- /dev/null +++ b/Scripts/Tags/textarea.gd.uid @@ -0,0 +1 @@ +uid://bmcx7mr4nye6a diff --git a/Scripts/main.gd b/Scripts/main.gd index a2d12bf..e5df1c9 100644 --- a/Scripts/main.gd +++ b/Scripts/main.gd @@ -25,6 +25,7 @@ const OL = preload("res://Scenes/Tags/ol.tscn") const LI = preload("res://Scenes/Tags/li.tscn") const SELECT = preload("res://Scenes/Tags/select.tscn") const OPTION = preload("res://Scenes/Tags/option.tscn") +const TEXTAREA = preload("res://Scenes/Tags/textarea.tscn") const MIN_SIZE = Vector2i(750, 200) @@ -85,6 +86,14 @@ line breaks + + + + + + + +