From a6e96328aec836bb18230933672606e19438a6cf Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:08:47 +0300 Subject: [PATCH] trace, console --- dns/frontend/script.lua | 2 +- dns/frontend/signup.lua | 2 +- flumi/Assets/Icons/eraser.svg | 1 + flumi/Assets/Icons/eraser.svg.import | 37 ++ flumi/Assets/Icons/funnel.svg | 1 + flumi/Assets/Icons/funnel.svg.import | 37 ++ flumi/Resources/LuaSyntaxHighlighter.tres | 16 + flumi/Scenes/DevTools.tscn | 243 +++++++++ flumi/Scripts/B9/Lua.gd | 9 +- flumi/Scripts/DevToolsConsole.gd | 574 ++++++++++++++++++++++ flumi/Scripts/DevToolsConsole.gd.uid | 1 + flumi/Scripts/LuaCodeEdit.gd.uid | 1 + flumi/Scripts/LuaSyntaxHighlighter.gd | 190 +++++++ flumi/Scripts/LuaSyntaxHighlighter.gd.uid | 1 + flumi/Scripts/Tab.gd | 39 +- flumi/Scripts/Trace.gd.uid | 1 + flumi/Scripts/Utils/Lua/DOM.gd | 2 +- flumi/Scripts/Utils/Lua/Print.gd | 5 +- flumi/Scripts/Utils/Lua/ThreadedVM.gd | 75 ++- flumi/Scripts/Utils/Lua/Trace.gd | 87 ++++ flumi/Scripts/Utils/Lua/Trace.gd.uid | 1 + flumi/Scripts/main.gd | 32 +- flumi/project.godot | 5 + tests/add-remove-child.html | 2 +- tests/attribute.html | 24 +- tests/canvas.html | 4 +- tests/clipboard.html | 4 +- tests/interval-and-network.html | 2 +- tests/lua-api.html | 8 +- tests/network-and-json.html | 2 +- tests/signal.html | 2 +- tests/snake.html | 2 +- tests/tween.html | 4 +- tests/websocket.html | 2 +- 34 files changed, 1353 insertions(+), 65 deletions(-) create mode 100644 flumi/Assets/Icons/eraser.svg create mode 100644 flumi/Assets/Icons/eraser.svg.import create mode 100644 flumi/Assets/Icons/funnel.svg create mode 100644 flumi/Assets/Icons/funnel.svg.import create mode 100644 flumi/Resources/LuaSyntaxHighlighter.tres create mode 100644 flumi/Scenes/DevTools.tscn create mode 100644 flumi/Scripts/DevToolsConsole.gd create mode 100644 flumi/Scripts/DevToolsConsole.gd.uid create mode 100644 flumi/Scripts/LuaCodeEdit.gd.uid create mode 100644 flumi/Scripts/LuaSyntaxHighlighter.gd create mode 100644 flumi/Scripts/LuaSyntaxHighlighter.gd.uid create mode 100644 flumi/Scripts/Trace.gd.uid create mode 100644 flumi/Scripts/Utils/Lua/Trace.gd create mode 100644 flumi/Scripts/Utils/Lua/Trace.gd.uid diff --git a/dns/frontend/script.lua b/dns/frontend/script.lua index 896f664..f0fe8e3 100644 --- a/dns/frontend/script.lua +++ b/dns/frontend/script.lua @@ -8,7 +8,7 @@ local password_input = gurt.select('#password') local log_output = gurt.select('#log-output') function addLog(message) - gurt.log(message) + trace.log(message) log_output.text = log_output.text .. message .. '\\n' end diff --git a/dns/frontend/signup.lua b/dns/frontend/signup.lua index 544c168..3c7d0ee 100644 --- a/dns/frontend/signup.lua +++ b/dns/frontend/signup.lua @@ -9,7 +9,7 @@ local confirm_password_input = gurt.select('#confirm-password') local log_output = gurt.select('#log-output') function addLog(message) - gurt.log(message) + trace.log(message) log_output.text = log_output.text .. message .. '\n' end diff --git a/flumi/Assets/Icons/eraser.svg b/flumi/Assets/Icons/eraser.svg new file mode 100644 index 0000000..9c52339 --- /dev/null +++ b/flumi/Assets/Icons/eraser.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/eraser.svg.import b/flumi/Assets/Icons/eraser.svg.import new file mode 100644 index 0000000..00f4e07 --- /dev/null +++ b/flumi/Assets/Icons/eraser.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://custohlvwclqs" +path="res://.godot/imported/eraser.svg-e6400a62a38066fbd6aaec113f0b7fb0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/eraser.svg" +dest_files=["res://.godot/imported/eraser.svg-e6400a62a38066fbd6aaec113f0b7fb0.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/flumi/Assets/Icons/funnel.svg b/flumi/Assets/Icons/funnel.svg new file mode 100644 index 0000000..933a54c --- /dev/null +++ b/flumi/Assets/Icons/funnel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/funnel.svg.import b/flumi/Assets/Icons/funnel.svg.import new file mode 100644 index 0000000..6420d50 --- /dev/null +++ b/flumi/Assets/Icons/funnel.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cqg4eny0nyojd" +path="res://.godot/imported/funnel.svg-cd3708ed9907c314e118f9c06364d430.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/funnel.svg" +dest_files=["res://.godot/imported/funnel.svg-cd3708ed9907c314e118f9c06364d430.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/flumi/Resources/LuaSyntaxHighlighter.tres b/flumi/Resources/LuaSyntaxHighlighter.tres new file mode 100644 index 0000000..7ab4c9b --- /dev/null +++ b/flumi/Resources/LuaSyntaxHighlighter.tres @@ -0,0 +1,16 @@ +[gd_resource type="SyntaxHighlighter" script_class="LuaSyntaxHighlighter" load_steps=2 format=3 uid="uid://d0aeuvwp0545i"] + +[ext_resource type="Script" uid="uid://qicpfnrmje1v" path="res://Scripts/LuaSyntaxHighlighter.gd" id="1_5xwa7"] + +[resource] +script = ExtResource("1_5xwa7") +font_color = Color(1, 1, 1, 1) +keyword_color = Color(1, 0.584314, 0.772549, 1) +gurt_globals_color = Color(0.584314, 1, 0.635294, 1) +function_color = Color(0.584314, 0.839216, 1, 1) +member_color = Color(1, 1, 0.584314, 1) +number_color = Color(0.772549, 0.584314, 1, 1) +string_color = Color(1, 0.772549, 0.584314, 1) +comment_color = Color(0.490196, 0.54902, 0.545098, 1) +symbol_color = Color(0.470588, 0.87451, 0.827451, 1) +metadata/_custom_type_script = "uid://qicpfnrmje1v" diff --git a/flumi/Scenes/DevTools.tscn b/flumi/Scenes/DevTools.tscn new file mode 100644 index 0000000..ae30000 --- /dev/null +++ b/flumi/Scenes/DevTools.tscn @@ -0,0 +1,243 @@ +[gd_scene load_steps=19 format=3 uid="uid://cgav3xl2xgupb"] + +[ext_resource type="Script" uid="uid://vrobqac6makc" path="res://Scripts/DevToolsConsole.gd" id="2_3m6n9"] +[ext_resource type="Texture2D" uid="uid://custohlvwclqs" path="res://Assets/Icons/eraser.svg" id="3_6hj4c"] +[ext_resource type="Texture2D" uid="uid://cqg4eny0nyojd" path="res://Assets/Icons/funnel.svg" id="4_ynqb1"] +[ext_resource type="SyntaxHighlighter" uid="uid://d0aeuvwp0545i" path="res://Resources/LuaSyntaxHighlighter.tres" id="5_xkykt"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_6hj4c"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6hj4c"] +content_margin_left = 2.0 +content_margin_top = 2.0 +content_margin_right = 2.0 +content_margin_bottom = 2.0 +bg_color = Color(0.105882, 0.105882, 0.105882, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qb7hm"] +content_margin_left = 15.0 +content_margin_top = 15.0 +content_margin_right = 15.0 +content_margin_bottom = 15.0 +bg_color = Color(0.137255, 0.137255, 0.137255, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ynqb1"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.105882, 0.105882, 0.105882, 1) +border_width_bottom = 1 +border_color = Color(0.247059, 0.466667, 0.807843, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8muo7"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.137255, 0.137255, 0.137255, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xkykt"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.105882, 0.105882, 0.105882, 1) +border_color = Color(0.8, 0.8, 1, 0) +corner_radius_top_left = 10 +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ynqb1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pqhy6"] +bg_color = Color(0.168627, 0.168627, 0.168627, 1) +corner_radius_top_left = 50 +corner_radius_top_right = 50 +corner_radius_bottom_right = 50 +corner_radius_bottom_left = 50 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_apu5o"] +bg_color = Color(0.6, 0.6, 0.6, 0) +draw_center = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dderp"] +bg_color = Color(0.6, 0.6, 0.6, 0) +draw_center = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ko37l"] +content_margin_left = 8.0 +content_margin_right = 8.0 +bg_color = Color(0.168627, 0.168627, 0.168627, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.247059, 0.466667, 0.807843, 1) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_65n6c"] +content_margin_left = 8.0 +content_margin_right = 8.0 +bg_color = Color(0.168627, 0.168627, 0.168627, 1) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_up43w"] +content_margin_left = 8.0 +content_margin_top = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.168627, 0.168627, 0.168627, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 +expand_margin_left = 4.0 +expand_margin_right = 4.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qb3ke"] +content_margin_left = 8.0 +content_margin_top = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.168627, 0.168627, 0.168627, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.247059, 0.466667, 0.807843, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 +expand_margin_left = 4.0 +expand_margin_right = 4.0 + +[node name="DevTools" type="VBoxContainer"] +custom_minimum_size = Vector2(450, 400) +size_flags_vertical = 3 + +[node name="TabContainer" type="TabContainer" parent="."] +custom_minimum_size = Vector2(300, 0) +layout_mode = 2 +size_flags_vertical = 3 +theme_override_colors/drop_mark_color = Color(0.247059, 0.466667, 0.807843, 1) +theme_override_styles/tab_focus = SubResource("StyleBoxEmpty_6hj4c") +theme_override_styles/tabbar_background = SubResource("StyleBoxFlat_6hj4c") +theme_override_styles/panel = SubResource("StyleBoxFlat_qb7hm") +theme_override_styles/tab_selected = SubResource("StyleBoxFlat_ynqb1") +theme_override_styles/tab_hovered = SubResource("StyleBoxFlat_8muo7") +theme_override_styles/tab_unselected = SubResource("StyleBoxFlat_xkykt") +tab_alignment = 1 +current_tab = 0 +drag_to_rearrange_enabled = true + +[node name="Console" type="VBoxContainer" parent="TabContainer"] +layout_mode = 2 +script = ExtResource("2_3m6n9") +metadata/_tab_index = 0 + +[node name="Toolbar" type="HBoxContainer" parent="TabContainer/Console"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="ClearButton" type="Button" parent="TabContainer/Console/Toolbar"] +custom_minimum_size = Vector2(32, 32) +layout_mode = 2 +theme_override_constants/icon_max_width = 20 +theme_override_styles/focus = SubResource("StyleBoxEmpty_ynqb1") +theme_override_styles/hover = SubResource("StyleBoxFlat_pqhy6") +theme_override_styles/pressed = SubResource("StyleBoxFlat_apu5o") +theme_override_styles/normal = SubResource("StyleBoxFlat_dderp") +icon = ExtResource("3_6hj4c") +icon_alignment = 1 + +[node name="LineEdit" type="LineEdit" parent="TabContainer/Console/Toolbar"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_styles/focus = SubResource("StyleBoxFlat_ko37l") +theme_override_styles/normal = SubResource("StyleBoxFlat_65n6c") +placeholder_text = "Filter" +right_icon = ExtResource("4_ynqb1") + +[node name="Spacer2" type="Control" parent="TabContainer/Console"] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/Console"] +custom_minimum_size = Vector2(0, 200) +layout_mode = 2 +size_flags_vertical = 3 + +[node name="LogContainer" type="VBoxContainer" parent="TabContainer/Console/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Spacer" type="Control" parent="TabContainer/Console"] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="InputContainer" type="HBoxContainer" parent="TabContainer/Console"] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="InputLine" type="CodeEdit" parent="TabContainer/Console/InputContainer"] +clip_contents = false +custom_minimum_size = Vector2(0, 35) +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_styles/normal = SubResource("StyleBoxFlat_up43w") +theme_override_styles/focus = SubResource("StyleBoxFlat_qb3ke") +text = "test" +placeholder_text = "Enter Lua code..." +scroll_fit_content_height = true +caret_blink = true +syntax_highlighter = ExtResource("5_xkykt") +highlight_all_occurrences = true +highlight_current_line = true +symbol_tooltip_on_hover = true +gutters_draw_line_numbers = true +auto_brace_completion_enabled = true +auto_brace_completion_highlight_matching = true + +[node name="PositioningTimer" type="Timer" parent="TabContainer/Console/InputContainer"] + +[node name="Elements" type="Label" parent="TabContainer"] +visible = false +layout_mode = 2 +text = "Elements tab - Coming soon" +horizontal_alignment = 1 +vertical_alignment = 1 +metadata/_tab_index = 1 + +[node name="Sources" type="Label" parent="TabContainer"] +visible = false +layout_mode = 2 +text = "Sources tab - Coming soon" +horizontal_alignment = 1 +vertical_alignment = 1 +metadata/_tab_index = 2 + +[node name="Network" type="Label" parent="TabContainer"] +visible = false +layout_mode = 2 +text = "Network tab - Coming soon" +horizontal_alignment = 1 +vertical_alignment = 1 +metadata/_tab_index = 3 + +[node name="Application" type="Label" parent="TabContainer"] +visible = false +layout_mode = 2 +text = "Application tab - Coming soon" +horizontal_alignment = 1 +vertical_alignment = 1 +metadata/_tab_index = 4 diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd index affbfab..b22f155 100644 --- a/flumi/Scripts/B9/Lua.gd +++ b/flumi/Scripts/B9/Lua.gd @@ -29,7 +29,7 @@ func _init(): timeout_manager = LuaTimeoutManager.new() threaded_vm = ThreadedLuaVM.new() threaded_vm.script_completed.connect(_on_threaded_script_completed) - threaded_vm.script_error.connect(func(e): print(e)) + threaded_vm.script_error.connect(_on_threaded_script_error) threaded_vm.dom_operation_request.connect(_handle_dom_operation) threaded_vm.print_output.connect(_on_print_output) @@ -645,8 +645,11 @@ func execute_lua_script(code: String): func _on_threaded_script_completed(_result: Dictionary): pass -func _on_print_output(message: String): - LuaPrintUtils.lua_print_direct(message) +func _on_threaded_script_error(error_message: String): + Trace.trace_error("RuntimeError: " + error_message) + +func _on_print_output(message: Dictionary): + Trace.get_instance().log_message.emit(message, "lua", Time.get_ticks_msec() / 1000.0) func kill_script_execution(): threaded_vm.stop_lua_thread() diff --git a/flumi/Scripts/DevToolsConsole.gd b/flumi/Scripts/DevToolsConsole.gd new file mode 100644 index 0000000..8f5b82a --- /dev/null +++ b/flumi/Scripts/DevToolsConsole.gd @@ -0,0 +1,574 @@ +class_name DevToolsConsole +extends VBoxContainer + +@onready var log_container: VBoxContainer = $ScrollContainer/LogContainer +@onready var input_line: CodeEdit = $InputContainer/InputLine +@onready var scroll_container: ScrollContainer = $ScrollContainer +@onready var clear_button: Button = $Toolbar/ClearButton +@onready var filter_input: LineEdit = $Toolbar/LineEdit + +var log_entries: Array[Dictionary] = [] +var current_filter: String = "" +var last_log_item: Control = null +var last_log_entry: Dictionary = {} +var input_history: Array[String] = [] +var history_index: int = -1 + +func _ready(): + connect_signals() + initialize_filter() + load_existing_logs() + + visibility_changed.connect(_on_visibility_changed) + +func initialize_filter() -> void: + filter_input.placeholder_text = "Filter" + current_filter = "" + +func connect_signals() -> void: + Trace.get_instance().log_message.connect(_on_trace_message) + clear_button.pressed.connect(_on_clear_pressed) + + input_line.gui_input.connect(_on_input_gui_input) + filter_input.text_changed.connect(_on_filter_changed) + +func load_existing_logs() -> void: + var existing_messages = Trace.get_all_messages() + for msg in existing_messages: + add_log_entry(msg.message, msg.level, msg.timestamp) + +func _on_trace_message(message: Variant, level: String, timestamp: float) -> void: + call_deferred("add_log_entry", message, level, timestamp) + +func _on_lua_print(message: String) -> void: + add_log_entry(message, "lua", Time.get_ticks_msec() / 1000.0) + +func add_log_entry(message: Variant, level: String, timestamp: float) -> void: + var entry = { + "message": message, + "level": level, + "timestamp": timestamp, + "count": 1 + } + + if can_group_with_last_entry(entry): + last_log_entry.count += 1 + last_log_entry.timestamp = timestamp + log_entries[log_entries.size() - 1] = last_log_entry + + update_log_item_display(last_log_item, last_log_entry) + return + + log_entries.append(entry) + last_log_entry = entry + + var should_add_separator = false + if level == "log" or level == "lua" or level == "input": + if log_container.get_child_count() > 0: + var last_displayed_entry = get_last_displayed_entry() + if last_displayed_entry and last_displayed_entry.level != "warning" and last_displayed_entry.level != "error": + should_add_separator = true + + if should_add_separator: + var separator = HSeparator.new() + separator.add_theme_color_override("separator", Color.GRAY * 0.3) + log_container.add_child(separator) + + var log_item = create_log_item(entry) + log_container.add_child(log_item) + last_log_item = log_item + + apply_filter_to_item(log_item, entry) + + call_deferred("_scroll_to_bottom") + +func create_log_item(entry: Dictionary) -> Control: + if entry.level == "input": + var message_code_edit = CodeEdit.new() + var input_display_text = get_display_text_for_entry(entry) + message_code_edit.text = input_display_text + message_code_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL + message_code_edit.scroll_fit_content_height = true + message_code_edit.editable = true + message_code_edit.context_menu_enabled = true + message_code_edit.shortcut_keys_enabled = true + message_code_edit.selecting_enabled = true + message_code_edit.deselect_on_focus_loss_enabled = true + message_code_edit.drag_and_drop_selection_enabled = false + message_code_edit.virtual_keyboard_enabled = false + message_code_edit.middle_mouse_paste_enabled = false + + var code_style_normal = StyleBoxFlat.new() + code_style_normal.bg_color = Color.TRANSPARENT + code_style_normal.border_width_left = 0 + code_style_normal.border_width_top = 0 + code_style_normal.border_width_right = 0 + code_style_normal.border_width_bottom = 0 + code_style_normal.content_margin_bottom = 8 + message_code_edit.add_theme_stylebox_override("normal", code_style_normal) + message_code_edit.add_theme_stylebox_override("focus", code_style_normal) + + message_code_edit.syntax_highlighter = input_line.syntax_highlighter.duplicate() + + message_code_edit.gui_input.connect(_on_log_code_edit_gui_input) + message_code_edit.focus_entered.connect(_on_log_code_edit_focus_entered.bind(message_code_edit)) + message_code_edit.text_changed.connect(_on_log_code_edit_text_changed.bind(message_code_edit, input_display_text)) + + return message_code_edit + + if entry.level == "lua" and entry.message is Dictionary and entry.message.has("parts"): + return create_structured_log_item(entry) + + var panel = PanelContainer.new() + var style_box = StyleBoxFlat.new() + + match entry.level: + "warning": + style_box.bg_color = Color.YELLOW + style_box.bg_color.a = 0.2 + style_box.corner_radius_top_left = 6 + style_box.corner_radius_top_right = 6 + style_box.corner_radius_bottom_left = 6 + style_box.corner_radius_bottom_right = 6 + style_box.content_margin_left = 8 + style_box.content_margin_right = 8 + style_box.content_margin_top = 4 + style_box.content_margin_bottom = 4 + "error": + style_box.bg_color = Color.RED + style_box.bg_color.a = 0.2 + style_box.corner_radius_top_left = 6 + style_box.corner_radius_top_right = 6 + style_box.corner_radius_bottom_left = 6 + style_box.corner_radius_bottom_right = 6 + style_box.content_margin_left = 8 + style_box.content_margin_right = 8 + style_box.content_margin_top = 4 + style_box.content_margin_bottom = 4 + _: + style_box.bg_color = Color.TRANSPARENT + + panel.add_theme_stylebox_override("panel", style_box) + + var container: Control + if entry.level == "warning" or entry.level == "error": + var margin_container = MarginContainer.new() + margin_container.add_child(panel) + container = margin_container + else: + container = panel + + var message_label = Label.new() + var display_text = get_display_text_for_entry(entry) + message_label.text = display_text + message_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + message_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + + match entry.level: + "warning": + message_label.add_theme_color_override("font_color", Color.YELLOW) + "error": + message_label.add_theme_color_override("font_color", Color.WHITE) + _: + message_label.add_theme_color_override("font_color", Color.WHITE) + + panel.add_child(message_label) + return container + +func _scroll_to_bottom() -> void: + if scroll_container: + scroll_container.scroll_vertical = int(scroll_container.get_v_scroll_bar().max_value) + +func _on_clear_pressed() -> void: + for child in log_container.get_children(): + child.queue_free() + log_entries.clear() + + last_log_item = null + last_log_entry = {} + + Trace.clear_messages() + +func _on_input_gui_input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed: + if event.keycode == KEY_ENTER and event.ctrl_pressed: + var text = input_line.text.strip_edges() + if not text.is_empty(): + _on_input_submitted(text) + input_line.text = "" + get_viewport().set_input_as_handled() + elif event.keycode == KEY_UP and not event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed: + if input_line.get_caret_column() == 0 and input_line.get_caret_line() == 0: + navigate_history_up() + get_viewport().set_input_as_handled() + elif event.keycode == KEY_DOWN and not event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed: + var caret_pos = input_line.get_caret_column() + var last_line = input_line.get_line_count() - 1 + var last_line_length = input_line.get_line(last_line).length() + if input_line.get_caret_line() == last_line and caret_pos == last_line_length: + navigate_history_down() + get_viewport().set_input_as_handled() + +func navigate_history_up() -> void: + if input_history.is_empty(): + return + if history_index == -1: + history_index = input_history.size() - 1 + elif history_index > 0: + history_index -= 1 + + if history_index >= 0 and history_index < input_history.size(): + input_line.text = input_history[history_index] + call_deferred("move_caret_to_end") + +func navigate_history_down() -> void: + if input_history.is_empty() or history_index == -1: + return + + history_index += 1 + + if history_index >= input_history.size(): + history_index = -1 + input_line.text = "" + else: + input_line.text = input_history[history_index] + call_deferred("move_caret_to_end") + +func move_caret_to_end() -> void: + var last_line = input_line.get_line_count() - 1 + var last_line_length = input_line.get_line(last_line).length() + input_line.set_caret_line(last_line) + input_line.set_caret_column(last_line_length) + +func _on_input_submitted(text: String) -> void: + if text.strip_edges().is_empty(): + return + + if input_history.is_empty() or input_history[input_history.size() - 1] != text: + input_history.append(text) + if input_history.size() > 100: + input_history.pop_front() + + history_index = -1 + + add_log_entry("> " + text, "input", Time.get_ticks_msec() / 1000.0) + + execute_lua_command(text) + +func execute_lua_command(code: String) -> void: + var main_scene = Engine.get_main_loop().current_scene + if main_scene and main_scene.has_method("get_active_tab"): + var active_tab = main_scene.get_active_tab() + if active_tab and active_tab.lua_apis.size() > 0: + var lua_api = active_tab.lua_apis[0] + if lua_api: + var is_expression = is_likely_expression(code) + if is_expression: + var wrapped_code = "print(" + code + ")" + lua_api.execute_lua_script(wrapped_code) + else: + lua_api.execute_lua_script(code) + return + + add_log_entry("No Lua context available", "error", Time.get_ticks_msec() / 1000.0) + +func is_likely_expression(code: String) -> bool: + var trimmed = code.strip_edges() + var statement_keywords = ["if", "for", "while", "do", "function", "local", "return", "break"] + for keyword in statement_keywords: + if trimmed.begins_with(keyword + " ") or trimmed.begins_with(keyword + "("): + return false + if "=" in trimmed and not ("==" in trimmed or "!=" in trimmed or ">=" in trimmed or "<=" in trimmed): + return false + if "print(" in trimmed or "console.log(" in trimmed or "_trace_" in trimmed: + return false + if trimmed.ends_with(")") or trimmed.ends_with("]") or not (" " in trimmed): + return true + return true + +func _on_filter_changed(new_text: String) -> void: + current_filter = new_text.strip_edges() + refresh_log_display() + +func refresh_log_display() -> void: + for i in range(log_container.get_child_count()): + var child = log_container.get_child(i) + if child is HSeparator: + child.visible = should_separator_be_visible(i) + else: + var entry_index = get_entry_index_for_child(i) + if entry_index >= 0 and entry_index < log_entries.size(): + var entry = log_entries[entry_index] + apply_filter_to_item(child, entry) + + call_deferred("_scroll_to_bottom") + +func apply_filter_to_item(item: Control, entry: Dictionary) -> void: + item.visible = entry_matches_filter(entry) + +func entry_matches_filter(entry: Dictionary) -> bool: + if current_filter.is_empty(): + return true + + var message_text = "" + if entry.message is Dictionary and entry.message.has("parts"): + message_text = get_display_text_for_entry(entry) + else: + message_text = str(entry.message) + + if current_filter.to_lower() in message_text.to_lower(): + return true + + if current_filter.to_lower() == entry.level.to_lower(): + return true + + return false + +func get_last_displayed_entry() -> Dictionary: + for i in range(log_entries.size() - 2, -1, -1): # Start from second-to-last entry + var entry = log_entries[i] + if entry_matches_filter(entry): + return entry + return {} + +func should_separator_be_visible(separator_index: int) -> bool: + var before_visible = false + var after_visible = false + + if separator_index > 0: + var before_child = log_container.get_child(separator_index - 1) + before_visible = before_child.visible + + + if separator_index < log_container.get_child_count() - 1: + var after_child = log_container.get_child(separator_index + 1) + after_visible = after_child.visible + + return before_visible and after_visible + +func get_entry_index_for_child(child_index: int) -> int: + var entry_index = 0 + for i in range(child_index): + var child = log_container.get_child(i) + if not child is HSeparator: + entry_index += 1 + return entry_index + +func can_group_with_last_entry(entry: Dictionary) -> bool: + if last_log_entry.is_empty() or last_log_item == null: + return false + + if entry.level != last_log_entry.level: + return false + + var current_message_text = "" + var last_message_text = "" + + if entry.message is Dictionary and entry.message.has("parts"): + current_message_text = get_display_text_for_entry(entry) + else: + current_message_text = str(entry.message) + + if last_log_entry.message is Dictionary and last_log_entry.message.has("parts"): + last_message_text = get_display_text_for_entry(last_log_entry) + else: + last_message_text = str(last_log_entry.message) + + if current_message_text != last_message_text: + return false + + if entry.level == "input": + return false + + return true + +func update_log_item_display(log_item: Control, entry: Dictionary) -> void: + if entry.level == "input": + var code_edit = log_item as CodeEdit + if code_edit: + code_edit.text = get_display_text_for_entry(entry) + else: + var label = find_message_label_in_item(log_item) + if label: + label.text = get_display_text_for_entry(entry) + +func find_message_code_edit_in_item(item: Control) -> CodeEdit: + if item is CodeEdit: + return item as CodeEdit + + if item is MarginContainer: + var panel = item.get_child(0) as PanelContainer + if panel: + return panel.get_child(0) as CodeEdit + elif item is PanelContainer: + return item.get_child(0) as CodeEdit + return null + +func find_message_label_in_item(item: Control) -> Label: + if item is MarginContainer: + var panel = item.get_child(0) as PanelContainer + if panel: + return panel.get_child(0) as Label + elif item is PanelContainer: + return item.get_child(0) as Label + return null + +func _on_log_code_edit_gui_input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed: + var key = event.keycode + if event.ctrl_pressed and (key == KEY_C or key == KEY_A): + return + if key in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END, KEY_PAGEUP, KEY_PAGEDOWN]: + return + if event.shift_pressed and key in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END]: + return + get_viewport().set_input_as_handled() + elif event is InputEventMouseButton: + return + +func _on_log_code_edit_focus_entered(code_edit: CodeEdit) -> void: + code_edit.release_focus() + +func _on_log_code_edit_text_changed(code_edit: CodeEdit, original_text: String) -> void: + if code_edit.text != original_text: + code_edit.text = original_text + +func get_display_text_for_entry(entry: Dictionary) -> String: + var count = entry.get("count", 1) + var message = entry.message + + var base_text = "" + if message is Dictionary and message.has("parts"): + var parts = message.parts + var text_parts = [] + for part in parts: + if part.type == "primitive": + text_parts.append(str(part.data)) + elif part.type == "table": + var key_count = part.data.keys().size() + text_parts.append("Object {" + str(key_count) + "}") + base_text = "\t".join(text_parts) + else: + base_text = str(message) + + if count > 1: + return "(" + str(count) + ") " + base_text + else: + return base_text + +func create_structured_log_item(entry) -> Control: + var container = VBoxContainer.new() + var parts = entry.message.parts + if parts.size() == 1 and parts[0].type == "primitive": + var simple_panel = PanelContainer.new() + var style_box = StyleBoxFlat.new() + style_box.bg_color = Color.TRANSPARENT + style_box.content_margin_left = 10 + style_box.content_margin_top = 5 + style_box.content_margin_right = 10 + style_box.content_margin_bottom = 5 + simple_panel.add_theme_stylebox_override("panel", style_box) + var label = Label.new() + label.text = str(parts[0].data) + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + simple_panel.add_child(label) + container.add_child(simple_panel) + return container + + for i in range(parts.size()): + var part = parts[i] + + if part.type == "primitive": + var text_panel = PanelContainer.new() + var style_box = StyleBoxFlat.new() + style_box.bg_color = Color.TRANSPARENT + style_box.content_margin_left = 10 + style_box.content_margin_top = 2 + style_box.content_margin_right = 10 + style_box.content_margin_bottom = 2 + text_panel.add_theme_stylebox_override("panel", style_box) + + var label = Label.new() + label.text = str(part.data) + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + text_panel.add_child(label) + container.add_child(text_panel) + + elif part.type == "table": + var table_item = create_expandable_table_item(part.data) + container.add_child(table_item) + + return container + +func create_expandable_table_item(table_data) -> Control: + var main_container = VBoxContainer.new() + + var header_container = HBoxContainer.new() + header_container.custom_minimum_size.y = 24 + + var chevron_button = Button.new() + chevron_button.text = "▶" + chevron_button.custom_minimum_size = Vector2(20, 20) + chevron_button.flat = true + chevron_button.focus_mode = Control.FOCUS_NONE + header_container.add_child(chevron_button) + + var summary_label = Label.new() + var key_count = table_data.keys().size() + if table_data.has("__type"): + summary_label.text = str(table_data.get("__type", "Object")) + " {" + str(key_count) + "}" + else: + summary_label.text = "Object {" + str(key_count) + "}" + summary_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + summary_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header_container.add_child(summary_label) + + main_container.add_child(header_container) + + var content_container = VBoxContainer.new() + content_container.visible = false + + for key in table_data.keys(): + if key == "__type": + continue + + var value = table_data[key] + var row_container = HBoxContainer.new() + + var key_label = Label.new() + key_label.text = str(key) + ": " + key_label.custom_minimum_size.x = 80 + key_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP + row_container.add_child(key_label) + + var value_label = Label.new() + if value is Dictionary: + var nested_count = value.keys().size() + value_label.text = "Object {" + str(nested_count) + "}" + value_label.modulate = Color.GRAY + elif value is Array: + value_label.text = "Array [" + str(value.size()) + "]" + value_label.modulate = Color.GRAY + else: + value_label.text = str(value) + + value_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + value_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP + value_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row_container.add_child(value_label) + + content_container.add_child(row_container) + + main_container.add_child(content_container) + + chevron_button.pressed.connect(func(): + content_container.visible = !content_container.visible + chevron_button.text = "▼" if content_container.visible else "▶" + ) + + return main_container + +func _on_visibility_changed() -> void: + if visible: + input_line.grab_focus() diff --git a/flumi/Scripts/DevToolsConsole.gd.uid b/flumi/Scripts/DevToolsConsole.gd.uid new file mode 100644 index 0000000..5dc4c11 --- /dev/null +++ b/flumi/Scripts/DevToolsConsole.gd.uid @@ -0,0 +1 @@ +uid://vrobqac6makc diff --git a/flumi/Scripts/LuaCodeEdit.gd.uid b/flumi/Scripts/LuaCodeEdit.gd.uid new file mode 100644 index 0000000..1f1a4ba --- /dev/null +++ b/flumi/Scripts/LuaCodeEdit.gd.uid @@ -0,0 +1 @@ +uid://cer1rniskhi24 diff --git a/flumi/Scripts/LuaSyntaxHighlighter.gd b/flumi/Scripts/LuaSyntaxHighlighter.gd new file mode 100644 index 0000000..1b0a922 --- /dev/null +++ b/flumi/Scripts/LuaSyntaxHighlighter.gd @@ -0,0 +1,190 @@ +@tool +class_name LuaSyntaxHighlighter +extends SyntaxHighlighter + +@export_group("Colors") +@export var font_color: Color = Color("#d4d4d4", Color.WHITE) +@export var keyword_color: Color = Color.from_string("#c586c0", Color.WHITE) +@export var gurt_globals_color: Color = Color.from_string("#569cd6", Color.WHITE) +@export var function_color: Color = Color.from_string("#dcdcaa", Color.WHITE) +@export var member_color: Color = Color.from_string("#9cdcfe", Color.WHITE) +@export var number_color: Color = Color.from_string("#b5cea8", Color.WHITE) +@export var string_color: Color = Color.from_string("#ce9178", Color.WHITE) +@export var comment_color: Color = Color.from_string("#6a9955", Color.WHITE) +@export var symbol_color: Color = Color.from_string("#c586c0", Color.WHITE) + +enum State { DEFAULT, IN_MULTILINE_COMMENT, IN_MULTILINE_STRING } +var _state_cache: Dictionary = {} + +var _keywords: Dictionary = {} +var _built_in_functions: Dictionary = {} +var _gurt_globals: Dictionary = {} +var _member_keywords: Dictionary = {} + +func _init() -> void: + for k in ["and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"]: + _keywords[k] = true + + for f in ["assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "load", "loadfile", "next", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "tonumber", "tostring", "type", "xpcall"]: + _built_in_functions[f] = true + + for g in ["gurt", "trace", "JSON", "Time", "WebSocket", "Clipboard", "Regex", "setTimeout", "setInterval", "clearTimeout", "clearInterval", "fetch", "urlEncode", "urlDecode"]: + _gurt_globals[g] = true + + var members = [ + "select", "selectAll", "create", "body", "location", "href", "reload", "goto", "query", "get", "has", "getAll", + "log", "warn", "error", "text", "value", "visible", "children", "parent", "nextSibling", "previousSibling", + "firstChild", "lastChild", "classList", "on", "append", "remove", "insertBefore", "insertAfter", "replace", + "clone", "getAttribute", "setAttribute", "show", "hide", "focus", "unfocus", "createTween", "unsubscribe", + "add", "remove", "toggle", "contains", "item", "length", "to", "duration", "easing", "transition", "play", + "pause", "stop", "currentTime", "volume", "loop", "src", "playing", "paused", "withContext", "fillRect", + "strokeRect", "clearRect", "drawCircle", "drawText", "setFont", "measureText", "beginPath", "moveTo", + "lineTo", "closePath", "stroke", "fill", "arc", "quadraticCurveTo", "bezierCurveTo", "setStrokeStyle", + "setFillStyle", "setLineWidth", "save", "translate", "rotate", "scale", "restore", "source", "ok", "json", + "status", "statusText", "headers", "stringify", "parse", "now", "format", "date", "sleep", "benchmark", + "timer", "delay", "elapsed", "reset", "complete", "remaining", "new", "send", "close", "readyState", + "test", "match", "tostring", "replace", "replaceAll", "trim", + ] + for m in members: + _member_keywords[m] = true + +func _is_whitespace(char: String) -> bool: + return char == " " or char == "\t" + + +func _clear_highlighting_cache(): + _state_cache.clear() + +func _get_initial_state() -> int: + return State.DEFAULT + +func _get_line_state(p_line: int, p_state: int) -> int: + var current_state: int = p_state + var line_text: String = get_text_edit().get_line(p_line) + var line_len: int = line_text.length() + var i := 0 + while i < line_len: + if current_state == State.DEFAULT: + if i + 3 < line_len and line_text.substr(i, 4) == "--[[": + current_state = State.IN_MULTILINE_COMMENT + i += 4 + continue + if i + 1 < line_len and line_text.substr(i, 2) == "[[": + current_state = State.IN_MULTILINE_STRING + i += 2 + continue + if line_text[i] == "'" or line_text[i] == "\"": + var quote = line_text[i] + i += 1 + while i < line_len: + if line_text[i] == "\\": i += 2; continue + if line_text[i] == quote: break + i += 1 + else: + var end_idx = line_text.find("]]", i) + if end_idx != -1: + current_state = State.DEFAULT + i = end_idx + 2 + continue + else: + i = line_len + i += 1 + _state_cache[p_line] = current_state + return current_state + +func _get_line_syntax_highlighting(p_line: int) -> Dictionary: + var color_map := {} + var start_state: int = _state_cache.get(p_line - 1, _get_initial_state()) + + var line_text: String = get_text_edit().get_line(p_line) + var line_len: int = line_text.length() + + color_map[0] = {"color": font_color} + + var i := 0 + if start_state != State.DEFAULT: + var end_idx = line_text.find("]]") + var region_color = comment_color if start_state == State.IN_MULTILINE_COMMENT else string_color + + color_map[0] = {"color": region_color} + if end_idx == -1: + return color_map + else: + i = end_idx + 2 + if i < line_len: + color_map[i] = {"color": font_color} + start_state = State.DEFAULT + while i < line_len: + var start_col: int = i + var current_char: String = line_text[i] + + if current_char == "-" and i + 1 < line_len and line_text[i+1] == "-": + if not (i + 3 < line_len and line_text.substr(i, 4) == "--[["): + color_map[i] = {"color": comment_color} + return color_map + + if current_char == "\"" or current_char == "'": + var quote = current_char + var string_start = i + i += 1 + while i < line_len: + if line_text[i] == "\\": i += 2; continue + if line_text[i] == quote: i += 1; break + i += 1 + var string_end = i + color_map[string_start] = {"color": string_color} + if string_end < line_len: + color_map[string_end] = {"color": font_color} + continue + + if current_char.is_valid_int() or (current_char == "." and i + 1 < line_len and line_text[i+1].is_valid_int()): + var is_hex = false + if current_char == "0" and i + 1 < line_len and line_text[i+1].to_lower() == "x": + i += 2; is_hex = true + while i < line_len: + var char = line_text[i] + if (is_hex and char.is_valid_hex_number(false)) or \ + (not is_hex and (char.is_valid_int() or char in "Ee.-+")): + i += 1 + else: + break + var number_end = i + color_map[start_col] = {"color": number_color} + if number_end < line_len: + color_map[number_end] = {"color": font_color} + continue + + if current_char.is_valid_identifier() and not current_char.is_valid_int(): + while i < line_len and line_text[i].is_valid_identifier(): + i += 1 + var word = line_text.substr(start_col, i - start_col) + + var color = font_color + if _keywords.has(word): color = keyword_color + elif _gurt_globals.has(word): color = gurt_globals_color + elif _built_in_functions.has(word): color = function_color + else: + var prev_char_idx = start_col - 1 + while prev_char_idx >= 0 and _is_whitespace(line_text[prev_char_idx]): + prev_char_idx -= 1 + var next_char_idx = i + while next_char_idx < line_len and _is_whitespace(line_text[next_char_idx]): + next_char_idx += 1 + + var is_member = prev_char_idx >= 0 and line_text[prev_char_idx] in [".", ":"] + var is_function_call = next_char_idx < line_len and line_text[next_char_idx] == "(" + + if is_member and _member_keywords.has(word): color = member_color + elif is_function_call: color = function_color + + if color != font_color: color_map[start_col] = {"color": color} + continue + + if not _is_whitespace(current_char): + color_map[i] = {"color": symbol_color} + if i + 1 < line_len: + color_map[i + 1] = {"color": font_color} + + i += 1 + + return color_map diff --git a/flumi/Scripts/LuaSyntaxHighlighter.gd.uid b/flumi/Scripts/LuaSyntaxHighlighter.gd.uid new file mode 100644 index 0000000..6e91b5b --- /dev/null +++ b/flumi/Scripts/LuaSyntaxHighlighter.gd.uid @@ -0,0 +1 @@ +uid://qicpfnrmje1v diff --git a/flumi/Scripts/Tab.gd b/flumi/Scripts/Tab.gd index df7832a..7ef3ec0 100644 --- a/flumi/Scripts/Tab.gd +++ b/flumi/Scripts/Tab.gd @@ -35,6 +35,9 @@ var loading_tween: Tween var scroll_container: ScrollContainer = null var website_container: VBoxContainer = null var background_panel: PanelContainer = null +var main_hbox: HBoxContainer = null +var dev_tools: Control = null +var dev_tools_visible: bool = false var lua_apis: Array[LuaAPI] = [] var current_url: String = "" var has_content: bool = false @@ -131,6 +134,9 @@ func _exit_tree(): background_panel.get_parent().remove_child(background_panel) background_panel.queue_free() + if dev_tools and is_instance_valid(dev_tools): + dev_tools.queue_free() + remove_from_group("tabs") func init_scene(parent_container: Control) -> void: @@ -144,9 +150,15 @@ func init_scene(parent_container: Control) -> void: style_box.bg_color = Color(1, 1, 1, 1) # White background background_panel.add_theme_stylebox_override("panel", style_box) + main_hbox = HBoxContainer.new() + main_hbox.name = "Tab_MainHBox_" + str(get_instance_id()) + main_hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL + main_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL + scroll_container = ScrollContainer.new() scroll_container.name = "Tab_ScrollContainer_" + str(get_instance_id()) scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL + scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Take 2/3 of space website_container = VBoxContainer.new() website_container.name = "Tab_WebsiteContainer_" + str(get_instance_id()) @@ -154,8 +166,15 @@ func init_scene(parent_container: Control) -> void: website_container.size_flags_vertical = Control.SIZE_EXPAND_FILL website_container.add_theme_constant_override("separation", 22) + var dev_tools_scene = preload("res://Scenes/DevTools.tscn") + dev_tools = dev_tools_scene.instantiate() + dev_tools.name = "Tab_DevTools_" + str(get_instance_id()) + dev_tools.visible = false + parent_container.call_deferred("add_child", background_panel) - background_panel.call_deferred("add_child", scroll_container) + background_panel.call_deferred("add_child", main_hbox) + main_hbox.call_deferred("add_child", scroll_container) + main_hbox.call_deferred("add_child", dev_tools) scroll_container.call_deferred("add_child", website_container) background_panel.visible = is_active @@ -215,3 +234,21 @@ func _on_close_button_pressed() -> void: await close_tween.finished tab_closed.emit() queue_free() + +func toggle_dev_tools() -> void: + if not dev_tools: + return + + dev_tools_visible = not dev_tools_visible + dev_tools.visible = dev_tools_visible + + if dev_tools_visible: + scroll_container.size_flags_stretch_ratio = 2.0 + dev_tools.size_flags_stretch_ratio = 1.0 + else: + scroll_container.size_flags_stretch_ratio = 1.0 + +func get_dev_tools_console() -> DevToolsConsole: + if dev_tools and dev_tools.has_method("get_console"): + return dev_tools.get_console() + return null diff --git a/flumi/Scripts/Trace.gd.uid b/flumi/Scripts/Trace.gd.uid new file mode 100644 index 0000000..bedf0a1 --- /dev/null +++ b/flumi/Scripts/Trace.gd.uid @@ -0,0 +1 @@ +uid://8tv4fx7krev1 diff --git a/flumi/Scripts/Utils/Lua/DOM.gd b/flumi/Scripts/Utils/Lua/DOM.gd index 2197d70..f6324d7 100644 --- a/flumi/Scripts/Utils/Lua/DOM.gd +++ b/flumi/Scripts/Utils/Lua/DOM.gd @@ -272,7 +272,7 @@ static func render_new_element(element: HTMLParser.HTMLElement, parent_node: Nod # Create the visual node for the element var element_node = await main_scene.create_element_node(element, dom_parser) if not element_node: - LuaPrintUtils.lua_print_direct("Failed to create visual node for element: " + str(element)) + Trace.trace_log("Failed to create visual node for element: " + str(element)) return # Set metadata so ul/ol can detect dynamically added li elements diff --git a/flumi/Scripts/Utils/Lua/Print.gd b/flumi/Scripts/Utils/Lua/Print.gd index c4f4fed..959e82b 100644 --- a/flumi/Scripts/Utils/Lua/Print.gd +++ b/flumi/Scripts/Utils/Lua/Print.gd @@ -10,12 +10,9 @@ static func lua_print(vm: LuauVM) -> int: message_parts.append(value_str) var final_message = "\t".join(message_parts) - lua_print_direct(final_message) + Trace.trace_log(final_message) return 0 -static func lua_print_direct(msg) -> void: - print("GURT LOG: ", msg) - static func lua_value_to_string(vm: LuauVM, index: int) -> String: var lua_type = vm.lua_type(index) diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd index cd03640..7b2fde5 100644 --- a/flumi/Scripts/Utils/Lua/ThreadedVM.gd +++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd @@ -3,7 +3,7 @@ extends RefCounted signal script_completed(result: Dictionary) signal script_error(error: String) -signal print_output(message: String) +signal print_output(print_data: Dictionary) signal dom_operation_request(operation: Dictionary) var lua_thread: Thread @@ -238,23 +238,26 @@ func _print_handler(vm: LuauVM) -> int: var num_args = vm.lua_gettop() for i in range(1, num_args + 1): - var arg_str = "" - if vm.lua_isstring(i): - arg_str = vm.lua_tostring(i) - elif vm.lua_isnumber(i): - arg_str = str(vm.lua_tonumber(i)) - elif vm.lua_isboolean(i): - arg_str = "true" if vm.lua_toboolean(i) else "false" - elif vm.lua_isnil(i): - arg_str = "nil" + var lua_type = vm.lua_type(i) + if lua_type == vm.LUA_TTABLE: + var table_data = vm.lua_todictionary(i) + message_parts.append({ + "type": "table", + "data": table_data + }) else: - arg_str = vm.lua_typename(vm.lua_type(i)) - - message_parts.append(arg_str) + var value_str = LuaPrintUtils.lua_value_to_string(vm, i) + message_parts.append({ + "type": "primitive", + "data": value_str + }) - var final_message = "\t".join(message_parts) + var print_data = { + "parts": message_parts, + "count": message_parts.size() + } - call_deferred("_emit_print_output", final_message) + call_deferred("_emit_print_output", print_data) return 0 @@ -267,10 +270,35 @@ func _time_sleep_handler(vm: LuauVM) -> int: return 0 +func _trace_log_handler(vm: LuauVM) -> int: + var message = vm.luaL_checkstring(1) + call_deferred("_emit_trace_message", message, "lua") + return 0 + +func _trace_warning_handler(vm: LuauVM) -> int: + var message = vm.luaL_checkstring(1) + call_deferred("_emit_trace_message", message, "warning") + return 0 + +func _trace_error_handler(vm: LuauVM) -> int: + var message = vm.luaL_checkstring(1) + call_deferred("_emit_trace_message", message, "error") + return 0 + func _setup_threaded_gurt_api(): lua_vm.lua_pushcallable(_print_handler, "print") lua_vm.lua_setglobal("print") + # Setup trace functions + lua_vm.lua_pushcallable(_trace_log_handler, "_trace_log") + lua_vm.lua_setglobal("_trace_log") + + lua_vm.lua_pushcallable(_trace_warning_handler, "_trace_warning") + lua_vm.lua_setglobal("_trace_warning") + + lua_vm.lua_pushcallable(_trace_error_handler, "_trace_error") + lua_vm.lua_setglobal("_trace_error") + LuaTimeUtils.setup_time_api(lua_vm) lua_vm.lua_getglobal("Time") @@ -281,9 +309,6 @@ func _setup_threaded_gurt_api(): lua_vm.lua_newtable() - lua_vm.lua_pushcallable(_print_handler, "gurt.log") - lua_vm.lua_setfield(-2, "log") - lua_vm.lua_pushcallable(_gurt_select_handler, "gurt.select") lua_vm.lua_setfield(-2, "select") @@ -357,6 +382,7 @@ func _setup_additional_lua_apis(): LuaCrumbsUtils.setup_crumbs_api(lua_vm) LuaRegexUtils.setup_regex_api(lua_vm) LuaURLUtils.setup_url_api(lua_vm) + Trace.setup_trace_api(lua_vm) func _table_tostring_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) @@ -370,8 +396,8 @@ func _emit_script_completed(result: Dictionary): func _emit_script_error(error: String): script_error.emit(error) -func _emit_print_output(message: String): - print_output.emit(message) +func _emit_print_output(print_data: Dictionary): + print_output.emit(print_data) func _gurt_select_all_handler(vm: LuauVM) -> int: var selector: String = vm.luaL_checkstring(1) @@ -567,3 +593,12 @@ func _create_threaded_interval(interval_id: int, delay_ms: int): timeout_info.timer = timer lua_api.add_child(timer) timer.start() + +func _emit_trace_message(message: String, level: String): + match level: + "lua", "log": + Trace.trace_log(message) + "warning": + Trace.trace_warning(message) + "error": + Trace.trace_error(message) diff --git a/flumi/Scripts/Utils/Lua/Trace.gd b/flumi/Scripts/Utils/Lua/Trace.gd new file mode 100644 index 0000000..f00c0cf --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Trace.gd @@ -0,0 +1,87 @@ +class_name Trace +extends RefCounted + +signal log_message(message: String, level: String, timestamp: float) + +enum LogLevel { + LOG, + WARNING, + ERROR +} + +static var _instance: Trace +static var _messages: Array[Dictionary] = [] + +static func get_instance() -> Trace: + if not _instance: + _instance = Trace.new() + return _instance + +static func trace_log(message: String) -> void: + _emit_message(message, "log") + +static func trace_warning(message: String) -> void: + _emit_message(message, "warning") + +static func trace_error(message: String) -> void: + _emit_message(message, "error") + +static func _emit_message(message: String, level: String) -> void: + var timestamp = Time.get_ticks_msec() / 1000.0 + var log_entry = { + "message": message, + "level": level, + "timestamp": timestamp + } + + _messages.append(log_entry) + get_instance().call_deferred("emit_signal", "log_message", message, level, timestamp) + + match level: + "log": + print("TRACE LOG: ", message) + "warning": + print("TRACE WARNING: ", message) + "error": + print("TRACE ERROR: ", message) + +static func get_all_messages() -> Array[Dictionary]: + return _messages.duplicate() + +static func clear_messages() -> void: + _messages.clear() + +static func _lua_trace_log_handler(vm: LuauVM) -> int: + var message = vm.luaL_checkstring(1) + vm.lua_getglobal("_trace_log") + vm.lua_pushstring(message) + vm.lua_call(1, 0) + return 0 + +static func _lua_trace_warn_handler(vm: LuauVM) -> int: + var message = vm.luaL_checkstring(1) + vm.lua_getglobal("_trace_warning") + vm.lua_pushstring(message) + vm.lua_call(1, 0) + return 0 + +static func _lua_trace_error_handler(vm: LuauVM) -> int: + var message = vm.luaL_checkstring(1) + vm.lua_getglobal("_trace_error") + vm.lua_pushstring(message) + vm.lua_call(1, 0) + return 0 + +static func setup_trace_api(vm: LuauVM) -> void: + vm.lua_newtable() + + vm.lua_pushcallable(_lua_trace_log_handler, "trace.log") + vm.lua_setfield(-2, "log") + + vm.lua_pushcallable(_lua_trace_warn_handler, "trace.warn") + vm.lua_setfield(-2, "warn") + + vm.lua_pushcallable(_lua_trace_error_handler, "trace.error") + vm.lua_setfield(-2, "error") + + vm.lua_setglobal("trace") diff --git a/flumi/Scripts/Utils/Lua/Trace.gd.uid b/flumi/Scripts/Utils/Lua/Trace.gd.uid new file mode 100644 index 0000000..c275c34 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Trace.gd.uid @@ -0,0 +1 @@ +uid://k87sfbjp7myx diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 532b2f3..1e07ef1 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -61,6 +61,16 @@ func _ready(): call_deferred("render") +func _input(event: InputEvent) -> void: + if Input.is_action_just_pressed("DevTools"): + _toggle_dev_tools() + get_viewport().set_input_as_handled() + +func _toggle_dev_tools() -> void: + var active_tab = get_active_tab() + if active_tab: + active_tab.toggle_dev_tools() + func resolve_url(href: String) -> String: return URLUtils.resolve_url(current_domain, href) @@ -280,12 +290,16 @@ func render_content(html_bytes: PackedByteArray) -> void: parser.register_dom_node(body, target_container) var scripts = parser.find_all("script") - var lua_api = null - if scripts.size() > 0: - lua_api = LuaAPI.new() - add_child(lua_api) - if active_tab: - active_tab.lua_apis.append(lua_api) + + var lua_api = LuaAPI.new() + add_child(lua_api) + if active_tab: + active_tab.lua_apis.append(lua_api) + + lua_api.dom_parser = parser + + if lua_api.threaded_vm: + lua_api.threaded_vm.dom_parser = parser var i = 0 if body: @@ -694,3 +708,9 @@ func get_active_website_container() -> Control: if active_tab: return active_tab.website_container return website_container # fallback to original container + +func get_dev_tools_console() -> DevToolsConsole: + var active_tab = get_active_tab() + if active_tab: + return active_tab.get_dev_tools_console() + return null diff --git a/flumi/project.godot b/flumi/project.godot index df340cc..97c3a98 100644 --- a/flumi/project.godot +++ b/flumi/project.godot @@ -72,3 +72,8 @@ FocusSearch={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":76,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +DevTools={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} diff --git a/tests/add-remove-child.html b/tests/add-remove-child.html index 0f340a8..ddd22e4 100644 --- a/tests/add-remove-child.html +++ b/tests/add-remove-child.html @@ -19,7 +19,7 @@ local list = gurt.select('#item-list') local counter = 1 - gurt.log('List manipulation script started.') + trace.log('List manipulation script started.') add_button:on('click', function() local new_item = gurt.create('li', { diff --git a/tests/attribute.html b/tests/attribute.html index b59ba0c..57da9c2 100644 --- a/tests/attribute.html +++ b/tests/attribute.html @@ -32,7 +32,7 @@ local infoBox = gurt.select('#info-box') local clickCounter = gurt.select('#click-counter') - gurt.log('Button attribute demo script started.') + trace.log('Button attribute demo script started.') local clickCount = 0 @@ -64,7 +64,7 @@ targetButton:on('click', function() clickCount = clickCount + 1 clickCounter.text = 'Button clicked ' .. clickCount .. ' times!' - gurt.log('Target button clicked! Count:', clickCount) + trace.log('Target button clicked! Count:', clickCount) updateStatus() end) @@ -72,7 +72,7 @@ enableBtn:on('click', function() targetButton:setAttribute('disabled', '') -- Remove disabled attribute targetButton:setAttribute('data-value', 'enabled') - gurt.log('Target button enabled via setAttribute') + trace.log('Target button enabled via setAttribute') updateStatus() end) @@ -80,7 +80,7 @@ disableBtn:on('click', function() targetButton:setAttribute('disabled', 'true') targetButton:setAttribute('data-value', 'disabled') - gurt.log('Target button disabled via setAttribute') + trace.log('Target button disabled via setAttribute') updateStatus() end) @@ -92,12 +92,12 @@ -- Currently disabled, so enable it targetButton:setAttribute('disabled', '') targetButton:setAttribute('data-value', 'toggled-enabled') - gurt.log('Target button toggled to enabled state') + trace.log('Target button toggled to enabled state') else -- Currently enabled, so disable it targetButton:setAttribute('disabled', 'true') targetButton:setAttribute('data-value', 'toggled-disabled') - gurt.log('Target button toggled to disabled state') + trace.log('Target button toggled to disabled state') end updateStatus() @@ -109,12 +109,12 @@ local type = targetButton:getAttribute('type') local dataValue = targetButton:getAttribute('data-value') - gurt.log('=== BUTTON STATUS CHECK ===') - gurt.log('Disabled attribute:', disabled or 'not set') - gurt.log('Type attribute:', type or 'not set') - gurt.log('Data-value attribute:', dataValue or 'not set') - gurt.log('Click count:', clickCount) - gurt.log('===========================') + trace.log('=== BUTTON STATUS CHECK ===') + trace.log('Disabled attribute:', disabled or 'not set') + trace.log('Type attribute:', type or 'not set') + trace.log('Data-value attribute:', dataValue or 'not set') + trace.log('Click count:', clickCount) + trace.log('===========================') -- Demonstrate style setAttribute local randomColors = {'bg-red-500', 'bg-green-500', 'bg-purple-500', 'bg-orange-500', 'bg-pink-500'} diff --git a/tests/canvas.html b/tests/canvas.html index f6f906e..924ad96 100644 --- a/tests/canvas.html +++ b/tests/canvas.html @@ -18,7 +18,7 @@ diff --git a/tests/clipboard.html b/tests/clipboard.html index ff46aad..9383a0f 100644 --- a/tests/clipboard.html +++ b/tests/clipboard.html @@ -17,12 +17,12 @@ diff --git a/tests/interval-and-network.html b/tests/interval-and-network.html index 5735c67..535e3a1 100644 --- a/tests/interval-and-network.html +++ b/tests/interval-and-network.html @@ -26,7 +26,7 @@ local loadImageBtn = gurt.select('#load-image-btn') local imageContainer = gurt.select('#image-container') - gurt.log('setInterval & Network Image demo script started.') + trace.log('setInterval & Network Image demo script started.') local logMessages = {} local counter = 0 diff --git a/tests/lua-api.html b/tests/lua-api.html index 687afea..9633505 100644 --- a/tests/lua-api.html +++ b/tests/lua-api.html @@ -17,7 +17,7 @@ local mouse = gurt.select('#mouse') local btnmouse = gurt.select('#btnmouse') - gurt.log('Starting Lua script execution...') + trace.log('Starting Lua script execution...') gurt.body:on('keypress', function(el) typing.text = table.tostring(el) @@ -68,13 +68,13 @@ subscription:unsubscribe() end) - gurt.log('Event listener attached to button with subscription ID') + trace.log('Event listener attached to button with subscription ID') else - gurt.log('Could not find button or event log element') + trace.log('Could not find button or event log element') end -- DOM Manipulation Demo - gurt.log('Testing DOM manipulation...') + trace.log('Testing DOM manipulation...') -- Create a new div with styling local new_div = gurt.create('div', { style = 'bg-red-500 p-4 rounded-lg mb-4' }) diff --git a/tests/network-and-json.html b/tests/network-and-json.html index 2abc129..62e8d41 100644 --- a/tests/network-and-json.html +++ b/tests/network-and-json.html @@ -28,7 +28,7 @@ local urlInput = gurt.select('#url-input') local jsonInput = gurt.select('#json-input') - gurt.log('Network & JSON API demo script started.') + trace.log('Network & JSON API demo script started.') local logMessages = {} diff --git a/tests/signal.html b/tests/signal.html index 22c8e38..148e799 100644 --- a/tests/signal.html +++ b/tests/signal.html @@ -38,7 +38,7 @@ local fireDataBtn = gurt.select('#fire-data-btn') local clearLogBtn = gurt.select('#clear-log-btn') - gurt.log('Signal API demo script started.') + trace.log('Signal API demo script started.') local logMessages = {} local connectionCount = 0 diff --git a/tests/snake.html b/tests/snake.html index d55e96c..1bb15ec 100644 --- a/tests/snake.html +++ b/tests/snake.html @@ -201,7 +201,7 @@ math.randomseed(Time.now()) render() - gurt.log('Snake game initialized!') + trace.log('Snake game initialized!') diff --git a/tests/tween.html b/tests/tween.html index bfe9ccf..9fdd23c 100644 --- a/tests/tween.html +++ b/tests/tween.html @@ -53,7 +53,7 @@ local moveBox = gurt.select('#move-box') local comboBox = gurt.select('#combo-box') - gurt.log('Tween animation demo started!') + trace.log('Tween animation demo started!') -- Fade Animation gurt.select('#fade-btn'):on('click', function() @@ -143,7 +143,7 @@ -- Callback example gurt.select('#callback-btn'):on('click', function() fadeBox:createTween():to('opacity', 0.3):duration(1.0):easing('inout'):callback(function() - gurt.log('Fade animation completed!') + trace.log('Fade animation completed!') end):play() end) diff --git a/tests/websocket.html b/tests/websocket.html index 0244340..08983d3 100644 --- a/tests/websocket.html +++ b/tests/websocket.html @@ -33,7 +33,7 @@ local urlInput = gurt.select('#url-input') local messageInput = gurt.select('#message-input') - gurt.log('WebSocket API demo script started.') + trace.log('WebSocket API demo script started.') local logMessages = {} local socket = nil