URL UI, history UI

This commit is contained in:
Face
2025-08-12 21:30:41 +03:00
parent fd545885e9
commit c61167b834
48 changed files with 1514 additions and 126 deletions

View File

@@ -151,7 +151,7 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = "
styles[property] = inline_parsed[property]
# Inherit certain properties from parent elements
var inheritable_properties = ["width", "height", "font-size", "color", "font-family", "cursor"]
var inheritable_properties = ["width", "height", "font-size", "color", "font-family", "cursor", "font-bold", "font-italic", "underline"]
var parent_element = element.parent
while parent_element:
var parent_styles = get_element_styles_internal(parent_element, event)

View File

@@ -28,6 +28,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.dom_operation_request.connect(_handle_dom_operation)
threaded_vm.print_output.connect(_on_print_output)

View File

@@ -2794,4 +2794,108 @@ audio.play()
# Set the active HTML content to use the audio demo
func _ready():
HTML_CONTENT = HTML_CONTENT_AUDIO_TEST
HTML_CONTENT = """<head>
<title>Login</title>
<icon src="https://cdn-icons-png.flaticon.com/512/295/295128.png">
<meta name="theme-color" content="#1b1b1b">
<meta name="description" content="Login to your account">
<font name="roboto" src="https://fonts.gstatic.com/s/roboto/v48/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2" />
<style>
body { bg-[#1b1b1b] text-[#ffffff] font-roboto flex items-center justify-center p-4 }
.login-card { bg-[#2a2a2a] rounded-lg p-8 shadow-2xl mx-0 }
h1 { text-3xl font-bold text-center mb-6 text-[#ffffff] }
input {
bg-[#3b3b3b]
border-none
rounded-md
p-3
w-full
text-[#ffffff]
placeholder:text-[#999999]
outline-none
focus:ring-2
focus:ring-[#5b5b5b]
mb-4
}
button {
bg-[#4ade80]
text-[#1b1b1b]
font-bold
p-3
rounded-md
w-full
hover:bg-[#22c55e]
active:bg-[#15803d]
cursor-pointer
}
a { text-[#4ade80] hover:text-[#22c55e] cursor-pointer }
#log-output { text-white p-4 rounded-md mt-4 font-mono max-h-40 }
</style>
<script>
local submitBtn = gurt.select('#submit')
local username_input = gurt.select('#username')
local password_input = gurt.select('#password')
local log_output = gurt.select('#log-output')
function addLog(message)
gurt.log(message)
log_output.text = log_output.text .. message .. '\\n'
end
submitBtn:on('submit', function(event)
local username = event.data.username
local password = event.data.password
local request_body = JSON.stringify({
username = username,
password = password
})
print(request_body)
local url = 'http://localhost:8080/auth/login'
local headers = {
['Content-Type'] = 'application/json'
}
addLog('Attempting to log in with username: ' .. username)
log_output.text = ''
local response = fetch(url, {
method = 'POST',
headers = headers,
body = request_body
})
addLog('Response Status: ' .. response.status .. ' ' .. response.statusText)
if response:ok() then
addLog('Login successful!')
local jsonData = response:json()
if jsonData then
addLog('Logged in as user: ' .. jsonData.user.username)
addLog('Token: ' .. jsonData.token:sub(1, 20) .. '...')
end
else
addLog('Request failed with status: ' .. response.status)
local error_data = response:text()
addLog('Error response: ' .. error_data)
end
end)
</script>
</head>
<body>
<div style="login-card">
<h1>Login</h1>
<form id="login-form">
<input id="username" type="text" placeholder="Username" required="true" />
<input id="password" type="password" placeholder="Password" required="true" />
<button type="submit" id="submit">Log In</button>
</form>
<p style="text-center mt-4 text-[#999999] text-base">Don't have an account? <a href="#">Register here</a></p>
<p id="log-output" style="min-h-24"></p>
</div>
</body>""".to_utf8_buffer()

View File

@@ -0,0 +1,198 @@
extends RefCounted
class_name GurtProtocol
const DNS_API_URL = "http://localhost:8080"
static func is_gurt_domain(url: String) -> bool:
if url.begins_with("gurt://"):
return true
var parts = url.split(".")
return parts.size() == 2 and not url.contains("://")
static func parse_gurt_domain(url: String) -> Dictionary:
print("Parsing URL: ", url)
var domain_part = url
if url.begins_with("gurt://"):
domain_part = url.substr(7) # Remove "gurt://"
var parts = domain_part.split(".")
if parts.size() != 2:
print("Invalid domain format: ", domain_part)
return {}
print("Parsed domain - name: ", parts[0], ", tld: ", parts[1])
return {
"name": parts[0],
"tld": parts[1],
"display_url": domain_part
}
static func fetch_domain_info(name: String, tld: String) -> Dictionary:
print("Fetching domain info for: ", name, ".", tld)
var http_request = HTTPRequest.new()
var tree = Engine.get_main_loop()
tree.current_scene.add_child(http_request)
http_request.timeout = 5.0
var url = DNS_API_URL + "/domain/" + name + "/" + tld
print("DNS API URL: ", url)
var error = http_request.request(url)
if error != OK:
print("HTTP request failed with error: ", error)
http_request.queue_free()
return {"error": "Failed to make DNS request"}
var response = await http_request.request_completed
http_request.queue_free()
if response[1] == 0 and response[3].size() == 0:
print("DNS API request timed out")
return {"error": "DNS server is not responding"}
var http_code = response[1]
var body = response[3]
print("DNS API response code: ", http_code)
print("DNS API response body: ", body.get_string_from_utf8())
if http_code != 200:
return {"error": "Domain not found or not approved"}
var json = JSON.new()
var parse_result = json.parse(body.get_string_from_utf8())
if parse_result != OK:
print("JSON parse error: ", parse_result)
return {"error": "Invalid JSON response from DNS server"}
print("Domain info retrieved: ", json.data)
return json.data
static func fetch_index_html(ip: String) -> String:
print("Fetching index.html from IP: ", ip)
var http_request = HTTPRequest.new()
var tree = Engine.get_main_loop()
tree.current_scene.add_child(http_request)
http_request.timeout = 5.0
var url = "http://" + ip + "/index.html"
print("Fetching from URL: ", url)
var error = http_request.request(url)
if error != OK:
print("HTTP request to IP failed with error: ", error)
http_request.queue_free()
return ""
var response = await http_request.request_completed
http_request.queue_free()
if response[1] == 0 and response[3].size() == 0:
print("Index.html request timed out")
return ""
var http_code = response[1]
var body = response[3]
print("IP response code: ", http_code)
if http_code != 200:
print("Failed to fetch index.html, HTTP code: ", http_code)
return ""
var html_content = body.get_string_from_utf8()
print("Successfully fetched HTML content (", html_content.length(), " characters)")
return html_content
static func handle_gurt_domain(url: String) -> Dictionary:
print("Handling GURT domain: ", url)
var parsed = parse_gurt_domain(url)
if parsed.is_empty():
return {"error": "Invalid domain format. Use: domain.tld", "html": create_error_page("Invalid domain format. Use: domain.tld")}
var domain_info = await fetch_domain_info(parsed.name, parsed.tld)
if domain_info.has("error"):
return {"error": domain_info.error, "html": create_error_page(domain_info.error)}
var html_content = await fetch_index_html(domain_info.ip)
if html_content.is_empty():
var error_msg = "Failed to fetch index.html from " + domain_info.ip
return {"error": error_msg, "html": create_error_page(error_msg)}
return {"html": html_content, "display_url": parsed.display_url}
static func get_error_type(error_message: String) -> Dictionary:
if "DNS server is not responding" in error_message or "Domain not found" in error_message:
return {"code": "ERR_NAME_NOT_RESOLVED", "title": "This site can't be reached", "icon": "🌐"}
elif "timeout" in error_message.to_lower() or "timed out" in error_message.to_lower():
return {"code": "ERR_CONNECTION_TIMED_OUT", "title": "This site can't be reached", "icon": ""}
elif "Failed to fetch" in error_message or "HTTP request failed" in error_message:
return {"code": "ERR_CONNECTION_REFUSED", "title": "This site can't be reached", "icon": "🚫"}
elif "Invalid domain format" in error_message:
return {"code": "ERR_INVALID_URL", "title": "This page isn't working", "icon": "⚠️"}
else:
return {"code": "ERR_UNKNOWN", "title": "Something went wrong", "icon": ""}
static func create_error_page(error_message: String) -> String:
var error_info = get_error_type(error_message)
return """<head>
<title>""" + error_info.title + """ - GURT</title>
<meta name="theme-color" content="#f8f9fa">
<style>
body { bg-[#ffffff] text-[#202124] font-sans p-0 m-0 }
.error-container { flex flex-col items-center justify-center max-w-[600px] mx-auto px-6 text-center }
.error-icon { text-6xl mb-6 opacity-60 w-32 h-32 }
.error-title { text-[#202124] text-2xl font-normal mb-4 line-height-1.3 }
.error-subtitle { text-[#5f6368] text-base mb-6 line-height-1.4 }
.error-code { bg-[#f8f9fa] text-[#5f6368] px-3 py-2 rounded-md font-mono text-sm inline-block mb-6 }
.suggestions { text-left max-w-[400px] w-[500px] }
.suggestion-title { text-[#202124] text-lg font-normal mb-3 }
.suggestion-list { text-[#5f6368] text-sm line-height-1.6 }
.suggestion-item { mb-2 pl-4 relative }
.suggestion-item:before { content-"" absolute left-0 top-0 text-[#5f6368] }
.retry-button { bg-[#1a73e8] text-[#ffffff] px-6 py-3 rounded-md font-medium text-sm hover:bg-[#1557b0] active:bg-[#1246a0] cursor-pointer border-none mt-4 }
.details-section { mt-8 pt-6 border-t border-[#e8eaed] }
.details-toggle { text-[#1a73e8] text-sm cursor-pointer hover:underline }
.details-content { bg-[#f8f9fa] text-[#5f6368] text-xs font-mono p-4 rounded-md mt-3 text-left display-none }
</style>
<script>
gurt.select("#reload"):on("click", function()
gurt.location.reload()
end)
</script>
</head>
<body>
<div style="error-container">
<p style="error-icon">""" + error_info.icon + """</p>
<h1 style="error-title">""" + error_info.title + """</h1>
<p style="error-subtitle">""" + error_message + """</p>
<div style="error-code">""" + error_info.code + """</div>
<div style="suggestions">
<h2 style="suggestion-title">Try:</h2>
<ul style="suggestion-list">
<li style="suggestion-item">Checking if the domain is correctly registered</li>
<li style="suggestion-item">Verifying your DNS server is running</li>
<li style="suggestion-item">Checking your internet connection</li>
</ul>
</div>
<button style="retry-button" id="reload">Reload</button>
</div>
</body>"""

View File

@@ -0,0 +1 @@
uid://clhivwjs3eujk

View File

@@ -0,0 +1,18 @@
extends Button
@onready var tab_container: TabManager = $"../../TabContainer"
@onready var website_background: Panel = %WebsiteBackground
func _on_pressed() -> void:
%OptionsMenu.show()
func _on_options_menu_id_pressed(id: int) -> void:
if id == 0: # new tab
tab_container.create_tab()
if id == 1: # new window
OS.create_process(OS.get_executable_path(), [])
if id == 2: # new ingonito window
# TODO: handle incognito
OS.create_process(OS.get_executable_path(), ["--incognito"])
if id == 4: # history
website_background.modulate = Constants.SECONDARY_COLOR

View File

@@ -0,0 +1 @@
uid://vjjhljlftlbk

View File

@@ -318,16 +318,23 @@ static func apply_margin_wrapper(node: Control, styles: Dictionary) -> Control:
var margin_container = MarginContainer.new()
margin_container.name = "MarginWrapper_" + node.name
# Copy size flags from the original node
margin_container.size_flags_horizontal = node.size_flags_horizontal
margin_container.size_flags_vertical = node.size_flags_vertical
var has_explicit_width = styles.has("width")
var has_explicit_height = styles.has("height")
if has_explicit_width:
margin_container.size_flags_horizontal = node.size_flags_horizontal
else:
margin_container.size_flags_horizontal = node.size_flags_horizontal
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if has_explicit_height:
margin_container.size_flags_vertical = node.size_flags_vertical
else:
margin_container.size_flags_vertical = node.size_flags_vertical
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
apply_margin_styles_to_container(margin_container, styles)
# Reset the original node's size flags since they're now handled by the wrapper
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
# Handle reparenting properly
var original_parent = node.get_parent()
if original_parent:
@@ -542,9 +549,22 @@ static func parse_radius(radius_str: String) -> int:
static func apply_font_to_label(label: RichTextLabel, font_resource: Font) -> void:
label.add_theme_font_override("normal_font", font_resource)
label.add_theme_font_override("bold_font", font_resource)
label.add_theme_font_override("italics_font", font_resource)
label.add_theme_font_override("bold_italics_font", font_resource)
var bold_font = SystemFont.new()
bold_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
bold_font.font_weight = 700 # Bold weight
label.add_theme_font_override("bold_font", bold_font)
var italic_font = SystemFont.new()
italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
italic_font.font_italic = true
label.add_theme_font_override("italics_font", italic_font)
var bold_italic_font = SystemFont.new()
bold_italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
bold_italic_font.font_weight = 700 # Bold weight
bold_italic_font.font_italic = true
label.add_theme_font_override("bold_italics_font", bold_italic_font)
static func apply_font_to_button(button: Button, styles: Dictionary) -> void:
if styles.has("font-family"):

View File

@@ -47,6 +47,10 @@ func set_icon(new_icon: Texture) -> void:
icon.rotation = 0
func update_icon_from_url(icon_url: String) -> void:
if icon_url.is_empty():
stop_loading()
return
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
loading_tween = create_tween()
@@ -68,9 +72,7 @@ func update_icon_from_url(icon_url: String) -> void:
# Only update if tab still exists
if is_instance_valid(self):
set_icon(icon_resource)
if loading_tween:
loading_tween.kill()
loading_tween = null
stop_loading()
func _on_button_mouse_entered() -> void:
mouse_over_tab = true
@@ -82,6 +84,27 @@ func _on_button_mouse_exited() -> void:
if is_active: return
gradient_texture.texture = TAB_GRADIENT_DEFAULT
func start_loading() -> void:
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
stop_loading()
loading_tween = create_tween()
set_icon(LOADER_CIRCLE)
loading_tween.set_loops()
icon.pivot_offset = Vector2(11.5, 11.5)
loading_tween.tween_method(func(angle):
if !is_instance_valid(icon):
if loading_tween: loading_tween.kill()
return
icon.rotation = angle
, 0.0, TAU, 1.0)
func stop_loading() -> void:
if loading_tween:
loading_tween.kill()
loading_tween = null
func _exit_tree():
if loading_tween:
loading_tween.kill()

View File

@@ -189,10 +189,23 @@ func apply_padding_to_stylebox(style_box: StyleBoxFlat, styles: Dictionary) -> v
func apply_size_and_flags(ctrl: Control, width: Variant, height: Variant) -> void:
if width != null or height != null:
ctrl.custom_minimum_size = Vector2(
width if width != null else 0,
height if height != null else 0
)
var new_width = 0
var new_height = 0
if width != null:
if SizingUtils.is_percentage(width):
new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
else:
new_width = width
if height != null:
if SizingUtils.is_percentage(height):
new_height = SizingUtils.calculate_percentage_size(height, SizingUtils.DEFAULT_VIEWPORT_HEIGHT)
else:
new_height = height
ctrl.custom_minimum_size = Vector2(new_width, new_height)
if width != null:
ctrl.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
if height != null:

View File

@@ -404,10 +404,22 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
if active_child:
if width or height:
# Explicit sizing from CSS
var new_child_size = Vector2(
width if width else active_child.custom_minimum_size.x,
height if height else max(active_child.custom_minimum_size.y, active_child.size.y)
)
var new_width = active_child.custom_minimum_size.x
var new_height = max(active_child.custom_minimum_size.y, active_child.size.y)
if width:
if SizingUtils.is_percentage(width):
new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
else:
new_width = width
if height:
if SizingUtils.is_percentage(height):
new_height = SizingUtils.calculate_percentage_size(height, SizingUtils.DEFAULT_VIEWPORT_HEIGHT)
else:
new_height = height
var new_child_size = Vector2(new_width, new_height)
active_child.custom_minimum_size = new_child_size

View File

@@ -7,9 +7,38 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
# Allow mouse events to pass through to parent containers for hover effects while keeping text selection
mouse_filter = Control.MOUSE_FILTER_PASS
# NOTE: estimate width/height because FlexContainer removes our anchor preset (sets 0 width)
var plain_text = element.get_collapsed_text()
var estimated_height = 30
var estimated_width = min(200, max(100, plain_text.length() * 12))
autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
custom_minimum_size = Vector2(estimated_width, estimated_height)
call_deferred("_auto_resize_to_content")
size_flags_horizontal = Control.SIZE_EXPAND_FILL
set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
func _auto_resize_to_content():
if not is_inside_tree():
await tree_entered
var min_width = 20
var max_width = 800
var min_height = 30
fit_content = true
var original_autowrap = autowrap_mode
autowrap_mode = TextServer.AUTOWRAP_OFF
await get_tree().process_frame
var natural_width = size.x
var desired_width = clampf(natural_width, min_width, max_width)
autowrap_mode = original_autowrap
await get_tree().process_frame
var content_height = get_content_height()
var explicit_height = custom_minimum_size.y if custom_minimum_size.y > 0 else null
var final_height = explicit_height if explicit_height != null else max(content_height, min_height)
custom_minimum_size = Vector2(desired_width, final_height)
queue_redraw()

39
flumi/Scripts/history.gd Normal file
View File

@@ -0,0 +1,39 @@
extends MarginContainer
@onready var history_entry_container: VBoxContainer = $Main/PanelContainer2/ScrollContainer/HistoryEntryContainer
@onready var delete_menu: PanelContainer = $Main/DeleteMenu
@onready var line_edit: LineEdit = $Main/LineEdit
@onready var entries_label: RichTextLabel = $Main/DeleteMenu/HBoxContainer/RichTextLabel
@onready var cancel_button: Button = $Main/DeleteMenu/HBoxContainer/CancelButton
var toggled_entries = []
func _ready():
for entry in history_entry_container.get_children():
entry.connect("checkbox_toggle", history_toggle.bind(entry))
func history_toggle(toggled: bool, entry) -> void:
print('toggling ', entry, ' to :', toggled)
if toggled:
toggled_entries.append(entry)
else:
toggled_entries.remove_at(toggled_entries.find(entry))
entries_label.text = str(toggled_entries.size()) + " selected"
if toggled_entries.size() != 0:
delete_menu.show()
line_edit.hide()
else:
delete_menu.hide()
line_edit.show()
func _on_cancel_button_pressed() -> void:
var entries_to_reset = toggled_entries.duplicate()
toggled_entries.clear()
for entry in entries_to_reset:
entry.reset()
delete_menu.hide()
line_edit.show()

View File

@@ -0,0 +1 @@
uid://ektopbvnhfga

View File

@@ -0,0 +1,10 @@
extends HBoxContainer
signal checkbox_toggle
@onready var check_box: CheckBox = $CheckBox
func reset() -> void:
check_box.set_pressed_no_signal(false)
func _on_check_box_toggled(toggled_on: bool) -> void:
checkbox_toggle.emit(toggled_on)

View File

@@ -0,0 +1 @@
uid://bw5pr4wrf780h

View File

@@ -4,6 +4,8 @@ extends Control
@onready var website_container: Control = %WebsiteContainer
@onready var website_background: Control = %WebsiteBackground
@onready var tab_container: TabManager = $VBoxContainer/TabContainer
@onready var search_bar: LineEdit = $VBoxContainer/HBoxContainer/LineEdit
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
const AUTO_SIZING_FLEX_CONTAINER = preload("res://Scripts/AutoSizingFlexContainer.gd")
@@ -53,6 +55,8 @@ func _ready():
DisplayServer.window_set_min_size(MIN_SIZE)
get_viewport().size_changed.connect(_on_viewport_size_changed)
call_deferred("render")
func _on_viewport_size_changed():
recalculate_percentage_elements(website_container)
@@ -64,7 +68,49 @@ func recalculate_percentage_elements(node: Node):
for child in node.get_children():
recalculate_percentage_elements(child)
var current_domain = "" # Store current domain for display
func _on_search_submitted(url: String) -> void:
print("Search submitted: ", url)
if GurtProtocol.is_gurt_domain(url):
print("Processing as GURT domain")
var tab = tab_container.tabs[tab_container.active_tab]
tab.start_loading()
var result = await GurtProtocol.handle_gurt_domain(url)
if result.has("error"):
print("GURT domain error: ", result.error)
const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
tab.stop_loading()
tab.set_icon(GLOBE_ICON)
var html_bytes = result.html.to_utf8_buffer()
render_content(html_bytes)
if result.has("display_url"):
current_domain = result.display_url
if not search_bar.has_focus():
search_bar.text = current_domain
else:
print("Non-GURT URL entered: ", url)
func _on_search_focus_entered() -> void:
if not current_domain.is_empty():
search_bar.text = "gurt://" + current_domain
func _on_search_focus_exited() -> void:
if not current_domain.is_empty():
search_bar.text = current_domain
func render() -> void:
render_content(Constants.HTML_CONTENT)
func render_content(html_bytes: PackedByteArray) -> void:
# Clear existing content
for child in website_container.get_children():
child.queue_free()
@@ -73,8 +119,6 @@ func render() -> void:
FontManager.clear_fonts()
FontManager.set_refresh_callback(refresh_fonts)
var html_bytes = Constants.HTML_CONTENT
var parser: HTMLParser = HTMLParser.new(html_bytes)
var parse_result = parser.parse()
@@ -109,57 +153,59 @@ func render() -> void:
add_child(lua_api)
var i = 0
while i < body.children.size():
var element: HTMLParser.HTMLElement = body.children[i]
if should_group_as_inline(element):
# Create an HBoxContainer for consecutive inline elements
var inline_elements: Array[HTMLParser.HTMLElement] = []
while i < body.children.size() and should_group_as_inline(body.children[i]):
inline_elements.append(body.children[i])
i += 1
var hbox = HBoxContainer.new()
hbox.add_theme_constant_override("separation", 4)
for inline_element in inline_elements:
var inline_node = await create_element_node(inline_element, parser)
if inline_node:
# Input elements register their own DOM nodes in their init() function
if inline_element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
parser.register_dom_node(inline_element, inline_node)
safe_add_child(hbox, inline_node)
# Handle hyperlinks for all inline elements
if contains_hyperlink(inline_element) and inline_node is RichTextLabel:
inline_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
else:
print("Failed to create inline element node: ", inline_element.tag_name)
safe_add_child(website_container, hbox)
continue
var element_node = await create_element_node(element, parser)
if element_node:
# Input elements register their own DOM nodes in their init() function
if element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
parser.register_dom_node(element, element_node)
if body:
while i < body.children.size():
var element: HTMLParser.HTMLElement = body.children[i]
# ul/ol handle their own adding
if element.tag_name != "ul" and element.tag_name != "ol":
safe_add_child(website_container, element_node)
if should_group_as_inline(element):
# Create an HBoxContainer for consecutive inline elements
var inline_elements: Array[HTMLParser.HTMLElement] = []
# Handle hyperlinks for all elements
if contains_hyperlink(element):
if element_node is RichTextLabel:
element_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
elif element_node.has_method("get") and element_node.get("rich_text_label"):
element_node.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
else:
print("Couldn't parse unsupported HTML tag \"%s\"" % element.tag_name)
i += 1
while i < body.children.size() and should_group_as_inline(body.children[i]):
inline_elements.append(body.children[i])
i += 1
var hbox = HBoxContainer.new()
hbox.add_theme_constant_override("separation", 4)
for inline_element in inline_elements:
var inline_node = await create_element_node(inline_element, parser)
if inline_node:
# Input elements register their own DOM nodes in their init() function
if inline_element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
parser.register_dom_node(inline_element, inline_node)
safe_add_child(hbox, inline_node)
# Handle hyperlinks for all inline elements
if contains_hyperlink(inline_element) and inline_node is RichTextLabel:
inline_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
else:
print("Failed to create inline element node: ", inline_element.tag_name)
safe_add_child(website_container, hbox)
continue
var element_node = await create_element_node(element, parser)
if element_node:
# Input elements register their own DOM nodes in their init() function
if element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
parser.register_dom_node(element, element_node)
# ul/ol handle their own adding
if element.tag_name != "ul" and element.tag_name != "ol":
safe_add_child(website_container, element_node)
if contains_hyperlink(element):
if element_node is RichTextLabel:
element_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
elif element_node.has_method("get") and element_node.get("rich_text_label"):
element_node.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
else:
print("Couldn't parse unsupported HTML tag \"%s\"" % element.tag_name)
i += 1
if scripts.size() > 0 and lua_api:
parser.process_scripts(lua_api, null)
@@ -381,6 +427,9 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
var p_node = P.instantiate()
p_node.init(element, parser)
var div_styles = parser.get_element_styles_with_inheritance(element, "", [])
StyleManager.apply_styles_to_label(p_node, div_styles, element, parser)
var container_for_children = node
if node is PanelContainer and node.get_child_count() > 0:
container_for_children = node.get_child(0) # The VBoxContainer inside