add search engine - ringle
This commit is contained in:
@@ -12,8 +12,11 @@ func _resort() -> void:
|
||||
if has_meta("should_fill_horizontal"):
|
||||
size_flags_horizontal = Control.SIZE_FILL
|
||||
else:
|
||||
if not has_meta("size_flags_set_by_style_manager"):
|
||||
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
if size_flags_horizontal == Control.SIZE_EXPAND_FILL and has_meta("size_flags_set_by_style_manager"):
|
||||
pass
|
||||
else:
|
||||
if not has_meta("size_flags_set_by_style_manager"):
|
||||
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
|
||||
# Check if we should fill vertically (for h-full)
|
||||
if has_meta("should_fill_vertical"):
|
||||
|
||||
@@ -6,8 +6,9 @@ class CSSRule:
|
||||
var event_prefix: String = ""
|
||||
var properties: Dictionary = {}
|
||||
var specificity: int = 0
|
||||
var selector_type: String = "simple" # simple, descendant, child, adjacent_sibling, general_sibling, attribute
|
||||
var selector_parts: Array = [] # For complex selectors
|
||||
var selector_type: String = "simple" # simple, descendant, child, adjacent_sibling, general_sibling, attribute
|
||||
var selector_parts: Array = [] # For complex selectors
|
||||
var is_user_css: bool = false
|
||||
|
||||
func init(sel: String = ""):
|
||||
selector = sel
|
||||
@@ -52,9 +53,9 @@ class CSSRule:
|
||||
func calculate_specificity():
|
||||
specificity = 1
|
||||
if selector.begins_with("."):
|
||||
specificity += 10
|
||||
specificity += 20
|
||||
if selector.contains("["):
|
||||
specificity += 10 # Attribute selectors
|
||||
specificity += 10
|
||||
match selector_type:
|
||||
"child":
|
||||
specificity += 8
|
||||
@@ -68,6 +69,10 @@ class CSSRule:
|
||||
specificity += 4
|
||||
if event_prefix.length() > 0:
|
||||
specificity += 10
|
||||
|
||||
if is_user_css:
|
||||
specificity += 100
|
||||
|
||||
|
||||
class CSSStylesheet:
|
||||
var rules: Array[CSSRule] = []
|
||||
@@ -287,7 +292,7 @@ func init(css_content: String = ""):
|
||||
stylesheet = CSSStylesheet.new()
|
||||
css_text = css_content
|
||||
|
||||
func parse() -> void:
|
||||
func parse(is_user_css: bool = false) -> void:
|
||||
if css_text.is_empty():
|
||||
return
|
||||
|
||||
@@ -295,7 +300,7 @@ func parse() -> void:
|
||||
var rules = extract_rules(cleaned_css)
|
||||
|
||||
for rule_data in rules:
|
||||
var rule = parse_rule(rule_data)
|
||||
var rule = parse_rule(rule_data, is_user_css)
|
||||
if rule:
|
||||
stylesheet.add_rule(rule)
|
||||
|
||||
@@ -355,9 +360,10 @@ func find_matching_brace(css: String, start_pos: int) -> int:
|
||||
|
||||
return -1
|
||||
|
||||
func parse_rule(rule_data: Dictionary) -> CSSRule:
|
||||
func parse_rule(rule_data: Dictionary, is_user_css: bool = false) -> CSSRule:
|
||||
var rule = CSSRule.new()
|
||||
rule.selector = rule_data.selector
|
||||
rule.is_user_css = is_user_css
|
||||
rule.init(rule.selector)
|
||||
var properties_text = rule_data.properties
|
||||
|
||||
@@ -397,6 +403,7 @@ func parse_utility_class(rule: CSSRule, utility_name: String) -> void:
|
||||
pseudo_rule.event_prefix = pseudo
|
||||
pseudo_rule.selector_type = rule.selector_type
|
||||
pseudo_rule.selector_parts = rule.selector_parts.duplicate()
|
||||
pseudo_rule.is_user_css = rule.is_user_css
|
||||
pseudo_rule.calculate_specificity()
|
||||
pseudo_rule.specificity += 100
|
||||
|
||||
|
||||
@@ -25,20 +25,20 @@ class HTMLElement:
|
||||
return get_attribute("id")
|
||||
|
||||
func get_collapsed_text() -> String:
|
||||
var collapsed = text_content.strip_edges()
|
||||
var collapsed = HTMLParser.unescape_html_entities(text_content).strip_edges()
|
||||
# Replace multiple whitespace characters with single space
|
||||
var regex = RegEx.new()
|
||||
regex.compile("\\s+")
|
||||
return regex.sub(collapsed, " ", true)
|
||||
|
||||
func get_preserved_text() -> String:
|
||||
return text_content
|
||||
return HTMLParser.unescape_html_entities(text_content)
|
||||
|
||||
func get_bbcode_formatted_text(parser: HTMLParser) -> String:
|
||||
var styles = {}
|
||||
if parser != null:
|
||||
styles = parser.get_element_styles_with_inheritance(self, "", [])
|
||||
return HTMLParser.get_bbcode_with_styles(self, styles, parser)
|
||||
return HTMLParser.get_bbcode_with_styles(self, styles, parser, [])
|
||||
|
||||
func is_inline_element() -> bool:
|
||||
return tag_name in ["b", "i", "u", "small", "mark", "code", "span", "a", "input"]
|
||||
@@ -68,10 +68,69 @@ var bitcode: PackedByteArray
|
||||
var parse_result: ParseResult
|
||||
|
||||
func _init(data: PackedByteArray):
|
||||
bitcode = data
|
||||
var html_string = data.get_string_from_utf8()
|
||||
html_string = preprocess_html_entities(html_string)
|
||||
bitcode = html_string.to_utf8_buffer()
|
||||
xml_parser = XMLParser.new()
|
||||
parse_result = ParseResult.new()
|
||||
|
||||
static func unescape_html_entities(text: String) -> String:
|
||||
return text.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'").replace("&", "&")
|
||||
|
||||
static func preprocess_html_entities(html: String) -> String:
|
||||
var result = ""
|
||||
var i = 0
|
||||
var in_tag = false
|
||||
|
||||
while i < html.length():
|
||||
var char = html[i]
|
||||
|
||||
if char == "<":
|
||||
# Check if this starts a valid HTML tag
|
||||
var tag_end = html.find(">", i)
|
||||
if tag_end != -1:
|
||||
var potential_tag = html.substr(i, tag_end - i + 1)
|
||||
# Simple check for valid tag pattern
|
||||
if is_valid_tag_pattern(potential_tag):
|
||||
result += potential_tag
|
||||
i = tag_end + 1
|
||||
continue
|
||||
# If not a valid tag, escape it
|
||||
result += "<"
|
||||
elif char == ">":
|
||||
# Escape standalone > that's not part of a tag
|
||||
result += ">"
|
||||
else:
|
||||
result += char
|
||||
|
||||
i += 1
|
||||
|
||||
return result
|
||||
|
||||
static func is_valid_tag_pattern(tag: String) -> bool:
|
||||
if tag.length() < 3: # Minimum: <x>
|
||||
return false
|
||||
|
||||
if not tag.begins_with("<") or not tag.ends_with(">"):
|
||||
return false
|
||||
|
||||
var inner = tag.substr(1, tag.length() - 2).strip_edges()
|
||||
|
||||
if inner.begins_with("/"):
|
||||
inner = inner.substr(1).strip_edges()
|
||||
|
||||
# Handle self-closing tags
|
||||
if inner.ends_with("/"):
|
||||
inner = inner.substr(0, inner.length() - 1).strip_edges()
|
||||
|
||||
# Extract tag name (first part before space or attributes)
|
||||
var tag_name = inner.split(" ")[0].split("\t")[0]
|
||||
|
||||
# Valid tag names contain only letters, numbers, and hyphens
|
||||
var regex = RegEx.new()
|
||||
regex.compile("^[a-zA-Z][a-zA-Z0-9-]*$")
|
||||
return regex.search(tag_name) != null
|
||||
|
||||
# Main parsing function
|
||||
func parse() -> ParseResult:
|
||||
xml_parser.open_buffer(bitcode)
|
||||
@@ -408,7 +467,7 @@ func apply_element_styles(node: Control, element: HTMLElement, parser: HTMLParse
|
||||
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||
if node.get("rich_text_label"):
|
||||
var label = node.rich_text_label
|
||||
var text = HTMLParser.get_bbcode_with_styles(element, styles, parser)
|
||||
var text = HTMLParser.get_bbcode_with_styles(element, styles, parser, [])
|
||||
label.text = text
|
||||
|
||||
static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictionary, content: String, parser: HTMLParser = null) -> String:
|
||||
@@ -478,7 +537,13 @@ static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictio
|
||||
|
||||
return formatted_content
|
||||
|
||||
static func get_bbcode_with_styles(element: HTMLElement, styles: Dictionary, parser: HTMLParser) -> String:
|
||||
static func get_bbcode_with_styles(element: HTMLElement, styles: Dictionary, parser: HTMLParser, visited_elements: Array = []) -> String:
|
||||
if element in visited_elements:
|
||||
return ""
|
||||
|
||||
var new_visited = visited_elements.duplicate()
|
||||
new_visited.append(element)
|
||||
|
||||
var text = ""
|
||||
if element.text_content.length() > 0:
|
||||
text += element.get_collapsed_text()
|
||||
@@ -486,8 +551,8 @@ static func get_bbcode_with_styles(element: HTMLElement, styles: Dictionary, par
|
||||
for child in element.children:
|
||||
var child_styles = styles
|
||||
if parser != null:
|
||||
child_styles = parser.get_element_styles_with_inheritance(child, "", [])
|
||||
var child_content = HTMLParser.get_bbcode_with_styles(child, child_styles, parser)
|
||||
child_styles = parser.get_element_styles_with_inheritance(child, "", new_visited)
|
||||
var child_content = HTMLParser.get_bbcode_with_styles(child, child_styles, parser, new_visited)
|
||||
child_content = apply_element_bbcode_formatting(child, child_styles, child_content, parser)
|
||||
text += child_content
|
||||
|
||||
|
||||
@@ -642,8 +642,6 @@ func execute_lua_script(code: String, vm: LuauVM):
|
||||
script_start_time = Time.get_ticks_msec() / 1000.0
|
||||
threaded_vm.execute_script_async(code)
|
||||
|
||||
|
||||
|
||||
func _on_threaded_script_completed(result: Dictionary):
|
||||
var execution_time = (Time.get_ticks_msec() / 1000.0) - script_start_time
|
||||
|
||||
@@ -689,6 +687,10 @@ func _handle_dom_operation(operation: Dictionary):
|
||||
LuaDOMUtils.handle_insert_after(operation, dom_parser, self)
|
||||
"replace_child":
|
||||
LuaDOMUtils.handle_replace_child(operation, dom_parser, self)
|
||||
"focus_element":
|
||||
_handle_element_focus(operation)
|
||||
"unfocus_element":
|
||||
_handle_element_unfocus(operation)
|
||||
_:
|
||||
pass # Unknown operation type, ignore
|
||||
|
||||
@@ -786,6 +788,53 @@ func _handle_text_getting(operation: Dictionary):
|
||||
return element.text_content
|
||||
return ""
|
||||
|
||||
func _handle_element_focus(operation: Dictionary):
|
||||
var element_id: String = operation.element_id
|
||||
|
||||
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||
if not dom_node:
|
||||
return
|
||||
|
||||
var focusable_control = _find_focusable_control(dom_node)
|
||||
if focusable_control and focusable_control.has_method("grab_focus"):
|
||||
focusable_control.call_deferred("grab_focus")
|
||||
|
||||
func _handle_element_unfocus(operation: Dictionary):
|
||||
var element_id: String = operation.element_id
|
||||
|
||||
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||
if not dom_node:
|
||||
return
|
||||
|
||||
var focusable_control = _find_focusable_control(dom_node)
|
||||
if focusable_control and focusable_control.has_method("release_focus"):
|
||||
focusable_control.call_deferred("release_focus")
|
||||
|
||||
func _find_focusable_control(node: Node) -> Control:
|
||||
if not node:
|
||||
return null
|
||||
|
||||
if node is Control and node.focus_mode != Control.FOCUS_NONE and node.has_method("grab_focus"):
|
||||
return node
|
||||
|
||||
if node.has_method("get_children"):
|
||||
for child in node.get_children():
|
||||
if child.visible and child is Control:
|
||||
if child is LineEdit or child is TextEdit or child is SpinBox or child is OptionButton:
|
||||
if child.focus_mode != Control.FOCUS_NONE:
|
||||
return child
|
||||
|
||||
if child is SpinBox:
|
||||
var line_edit = child.get_line_edit()
|
||||
if line_edit and line_edit.focus_mode != Control.FOCUS_NONE:
|
||||
return line_edit
|
||||
|
||||
var focusable_child = _find_focusable_control(child)
|
||||
if focusable_child:
|
||||
return focusable_child
|
||||
|
||||
return null
|
||||
|
||||
func _handle_body_event_registration(operation: Dictionary):
|
||||
var event_name: String = operation.event_name
|
||||
var callback_ref: int = operation.callback_ref
|
||||
|
||||
@@ -21,6 +21,7 @@ code { text-xl font-mono }
|
||||
a { text-[#1a0dab] }
|
||||
pre { text-xl font-mono }
|
||||
|
||||
img { object-fill }
|
||||
button { text-[16px] bg-[#1b1b1b] rounded-md text-white hover:bg-[#2a2a2a] active:bg-[#101010] px-3 py-1.5 }
|
||||
button[disabled] { bg-[#666666] text-[#999999] cursor-not-allowed }
|
||||
|
||||
@@ -29,7 +30,7 @@ select:active { text-[#000000] border-[3px] border-[#000000] }
|
||||
|
||||
input[type="color"] { w-32 }
|
||||
input[type="range"] { w-32 }
|
||||
input[type="text"] { w-64 }
|
||||
input[type="text"] { text-[16px] w-64 }
|
||||
input[type="number"] { w-32 text-[16px] bg-transparent border border-[#000000] rounded-[3px] text-[#000000] hover:border-[3px] hover:border-[#000000] px-3 py-1.5 }
|
||||
input[type="date"] { w-28 text-[16px] bg-[#1b1b1b] rounded-md text-white hover:bg-[#2a2a2a] active:bg-[#101010] px-3 py-1.5 }
|
||||
"""
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
extends RefCounted
|
||||
class_name GurtProtocol
|
||||
|
||||
const DNS_API_URL = "gurt://localhost:8877"
|
||||
|
||||
# DNS resolution cache: domain.tld -> IP address
|
||||
static var _dns_cache: Dictionary = {}
|
||||
const DNS_SERVER_IP: String = "135.125.163.131"
|
||||
const DNS_SERVER_PORT: int = 8877
|
||||
|
||||
static func is_gurt_domain(url: String) -> bool:
|
||||
if url.begins_with("gurt://"):
|
||||
@@ -16,65 +14,13 @@ static func is_gurt_domain(url: String) -> bool:
|
||||
|
||||
return false
|
||||
|
||||
static func parse_gurt_domain(url: String) -> Dictionary:
|
||||
var domain_part = url
|
||||
var path = "/"
|
||||
static func is_direct_address(domain: String) -> bool:
|
||||
# Check if it's already an IP address or localhost
|
||||
if domain.contains(":"):
|
||||
var parts = domain.split(":")
|
||||
domain = parts[0]
|
||||
|
||||
if url.begins_with("gurt://"):
|
||||
domain_part = url.substr(7)
|
||||
|
||||
# Extract path from domain_part (e.g., "test.dawg/script.lua" -> "test.dawg" + "/script.lua")
|
||||
var path_start = domain_part.find("/")
|
||||
if path_start != -1:
|
||||
path = domain_part.substr(path_start)
|
||||
domain_part = domain_part.substr(0, path_start)
|
||||
|
||||
# Check if domain is cached (resolved before)
|
||||
var domain_key = domain_part
|
||||
if _dns_cache.has(domain_key):
|
||||
return {
|
||||
"direct_address": _dns_cache[domain_key],
|
||||
"display_url": domain_part + path,
|
||||
"is_direct": true,
|
||||
"path": path,
|
||||
"full_domain": domain_part
|
||||
}
|
||||
|
||||
if domain_part.contains(":") or domain_part.begins_with("127.0.0.1") or domain_part.begins_with("localhost") or is_ip_address(domain_part):
|
||||
return {
|
||||
"direct_address": domain_part,
|
||||
"display_url": domain_part + path,
|
||||
"is_direct": true,
|
||||
"path": path,
|
||||
"full_domain": domain_part
|
||||
}
|
||||
|
||||
var parts = domain_part.split(".")
|
||||
if parts.size() < 2:
|
||||
return {}
|
||||
|
||||
# Support subdomains (e.g., api.blog.example.com)
|
||||
if parts.size() == 2:
|
||||
return {
|
||||
"name": parts[0],
|
||||
"tld": parts[1],
|
||||
"display_url": domain_part + path,
|
||||
"is_direct": false,
|
||||
"path": path,
|
||||
"full_domain": domain_part,
|
||||
"is_subdomain": false
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"name": parts[parts.size() - 2], # The domain name part
|
||||
"tld": parts[parts.size() - 1], # The TLD part
|
||||
"display_url": domain_part + path,
|
||||
"is_direct": false,
|
||||
"path": path,
|
||||
"full_domain": domain_part,
|
||||
"is_subdomain": true,
|
||||
"subdomain_parts": parts.slice(0, parts.size() - 2)
|
||||
}
|
||||
return domain == "localhost" or domain == "127.0.0.1" or is_ip_address(domain)
|
||||
|
||||
static func is_ip_address(address: String) -> bool:
|
||||
var parts = address.split(".")
|
||||
@@ -90,329 +36,25 @@ static func is_ip_address(address: String) -> bool:
|
||||
|
||||
return true
|
||||
|
||||
static func fetch_domain_info(name: String, tld: String) -> Dictionary:
|
||||
var request_data = JSON.stringify({"name": name, "tld": tld})
|
||||
var result = await fetch_dns_post_working("localhost:8877", "/resolve", request_data)
|
||||
static func resolve_gurt_domain(domain: String) -> String:
|
||||
if is_direct_address(domain):
|
||||
if domain == "localhost":
|
||||
return "127.0.0.1"
|
||||
return domain
|
||||
|
||||
if result.has("error"):
|
||||
return {"error": result.error}
|
||||
|
||||
if not result.has("content"):
|
||||
return {"error": "No content in DNS response"}
|
||||
|
||||
var content_str = result.content.get_string_from_utf8()
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(content_str)
|
||||
|
||||
if parse_result != OK:
|
||||
return {"error": "Invalid JSON in DNS response"}
|
||||
|
||||
return json.data
|
||||
|
||||
static func fetch_full_domain_info(full_domain: String, record_type: String = "") -> Dictionary:
|
||||
var request_data = {"domain": full_domain}
|
||||
if not record_type.is_empty():
|
||||
request_data["record_type"] = record_type
|
||||
|
||||
var json_data = JSON.stringify(request_data)
|
||||
var result = await fetch_dns_post_working("localhost:8877", "/resolve-full", json_data)
|
||||
|
||||
if result.has("error"):
|
||||
return {"error": result.error}
|
||||
|
||||
if not result.has("content"):
|
||||
return {"error": "No content in DNS response"}
|
||||
|
||||
var content_str = result.content.get_string_from_utf8()
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(content_str)
|
||||
|
||||
if parse_result != OK:
|
||||
return {"error": "Invalid JSON in DNS response"}
|
||||
|
||||
return json.data
|
||||
|
||||
static func fetch_dns_post_working(server: String, path: String, json_data: String) -> Dictionary:
|
||||
var shared_result = {"finished": false}
|
||||
var thread = Thread.new()
|
||||
var mutex = Mutex.new()
|
||||
|
||||
var thread_func = func():
|
||||
var local_result = {}
|
||||
var client = GurtProtocolClient.new()
|
||||
|
||||
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||
client.add_ca_certificate(ca_cert)
|
||||
|
||||
if not client.create_client(10):
|
||||
local_result = {"error": "Failed to create client"}
|
||||
else:
|
||||
var url = "gurt://" + server + path
|
||||
|
||||
# Prepare request options
|
||||
var options = {
|
||||
"method": "POST",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"body": json_data
|
||||
}
|
||||
|
||||
var response = client.request(url, options)
|
||||
|
||||
client.disconnect()
|
||||
|
||||
if not response:
|
||||
local_result = {"error": "No response from server"}
|
||||
elif not response.is_success:
|
||||
local_result = {"error": "Server error: " + str(response.status_code) + " " + str(response.status_message)}
|
||||
else:
|
||||
local_result = {"content": response.body}
|
||||
|
||||
mutex.lock()
|
||||
shared_result.clear()
|
||||
for key in local_result:
|
||||
shared_result[key] = local_result[key]
|
||||
shared_result["finished"] = true
|
||||
mutex.unlock()
|
||||
|
||||
thread.start(thread_func)
|
||||
|
||||
# Non-blocking wait
|
||||
while not shared_result.get("finished", false):
|
||||
await Engine.get_main_loop().process_frame
|
||||
|
||||
thread.wait_to_finish()
|
||||
|
||||
mutex.lock()
|
||||
var final_result = {}
|
||||
for key in shared_result:
|
||||
if key != "finished":
|
||||
final_result[key] = shared_result[key]
|
||||
mutex.unlock()
|
||||
|
||||
return final_result
|
||||
|
||||
static func fetch_content_via_gurt(ip: String, path: String = "/") -> Dictionary:
|
||||
var client = GurtProtocolClient.new()
|
||||
|
||||
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||
client.add_ca_certificate(ca_cert)
|
||||
|
||||
if not client.create_client(30):
|
||||
return {"error": "Failed to create GURT client"}
|
||||
|
||||
var gurt_url = "gurt://" + ip + ":4878" + path
|
||||
|
||||
var response = client.request(gurt_url, {"method": "GET"})
|
||||
|
||||
client.disconnect()
|
||||
|
||||
if not response:
|
||||
return {"error": "No response from GURT server"}
|
||||
|
||||
if not response.is_success:
|
||||
var error_msg = "Server returned status " + str(response.status_code) + ": " + response.status_message
|
||||
return {"error": error_msg}
|
||||
|
||||
var content = response.body
|
||||
return {"content": content, "headers": response.headers}
|
||||
|
||||
static func fetch_content_via_gurt_direct(address: String, path: String = "/") -> Dictionary:
|
||||
var shared_result = {"finished": false}
|
||||
var thread = Thread.new()
|
||||
var mutex = Mutex.new()
|
||||
|
||||
var thread_func = func():
|
||||
var local_result = {}
|
||||
var client = GurtProtocolClient.new()
|
||||
|
||||
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||
client.add_ca_certificate(ca_cert)
|
||||
|
||||
if not client.create_client(10):
|
||||
local_result = {"error": "Failed to create GURT client"}
|
||||
else:
|
||||
var gurt_url: String
|
||||
if address.contains(":"):
|
||||
gurt_url = "gurt://" + address + path
|
||||
else:
|
||||
gurt_url = "gurt://" + address + ":4878" + path
|
||||
|
||||
var response = client.request(gurt_url, {"method": "GET"})
|
||||
|
||||
client.disconnect()
|
||||
|
||||
if not response:
|
||||
local_result = {"error": "No response from GURT server"}
|
||||
else:
|
||||
var content = response.body
|
||||
|
||||
if not response.is_success:
|
||||
var error_msg = "Server returned status " + str(response.status_code) + ": " + response.status_message
|
||||
local_result = {"error": error_msg, "content": content, "headers": response.headers}
|
||||
else:
|
||||
local_result = {"content": content, "headers": response.headers}
|
||||
|
||||
mutex.lock()
|
||||
shared_result.clear()
|
||||
for key in local_result:
|
||||
shared_result[key] = local_result[key]
|
||||
shared_result["finished"] = true
|
||||
mutex.unlock()
|
||||
|
||||
thread.start(thread_func)
|
||||
|
||||
# Non-blocking wait using signals instead of polling
|
||||
while not shared_result.get("finished", false):
|
||||
await Engine.get_main_loop().process_frame
|
||||
# Yield control back to the main thread without blocking delays
|
||||
|
||||
thread.wait_to_finish()
|
||||
|
||||
mutex.lock()
|
||||
var final_result = {}
|
||||
for key in shared_result:
|
||||
if key != "finished":
|
||||
final_result[key] = shared_result[key]
|
||||
mutex.unlock()
|
||||
|
||||
return final_result
|
||||
|
||||
static func handle_gurt_domain(url: String) -> Dictionary:
|
||||
var parsed = parse_gurt_domain(url)
|
||||
if parsed.is_empty():
|
||||
return {"error": "Invalid domain format. Use: domain.tld or IP:port", "html": create_error_page("Invalid domain format. Use: domain.tld or IP:port")}
|
||||
|
||||
var target_address: String
|
||||
var path = parsed.get("path", "/")
|
||||
|
||||
if parsed.get("is_direct", false):
|
||||
target_address = parsed.direct_address
|
||||
else:
|
||||
var domain_info: Dictionary
|
||||
|
||||
# Use the new full domain resolution for subdomains
|
||||
if parsed.get("is_subdomain", false):
|
||||
domain_info = await fetch_full_domain_info(parsed.full_domain)
|
||||
else:
|
||||
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)}
|
||||
|
||||
# Process DNS records to find target address
|
||||
var target_result = await resolve_target_address(domain_info, parsed.full_domain)
|
||||
if target_result.has("error"):
|
||||
return {"error": target_result.error, "html": create_error_page(target_result.error)}
|
||||
|
||||
target_address = target_result.address
|
||||
|
||||
# Cache the resolved address
|
||||
var domain_key = parsed.full_domain
|
||||
_dns_cache[domain_key] = target_address
|
||||
|
||||
var content_result = await fetch_content_via_gurt_direct(target_address, path)
|
||||
if content_result.has("error"):
|
||||
var error_msg = "Failed to fetch content from " + target_address + path + " via GURT protocol - " + content_result.error
|
||||
if content_result.has("content") and not content_result.content.is_empty():
|
||||
return {"html": content_result.content, "display_url": parsed.display_url}
|
||||
return {"error": error_msg, "html": create_error_page(error_msg)}
|
||||
|
||||
if not content_result.has("content"):
|
||||
var error_msg = "No content received from " + target_address + path
|
||||
return {"error": error_msg, "html": create_error_page(error_msg)}
|
||||
|
||||
var html_content = content_result.content
|
||||
if html_content.is_empty():
|
||||
var error_msg = "Empty content received from " + target_address + path
|
||||
return {"error": error_msg, "html": create_error_page(error_msg)}
|
||||
|
||||
return {"html": html_content, "display_url": parsed.display_url}
|
||||
|
||||
static func resolve_target_address(domain_info: Dictionary, original_domain: String) -> Dictionary:
|
||||
if not domain_info.has("records") or domain_info.records == null:
|
||||
return {"error": "No DNS records found for domain"}
|
||||
|
||||
var records = domain_info.records
|
||||
var max_cname_depth = 5 # Prevent infinite CNAME loops
|
||||
var cname_depth = 0
|
||||
|
||||
# First pass: Look for direct A/AAAA records
|
||||
var a_records = []
|
||||
var aaaa_records = []
|
||||
var cname_records = []
|
||||
var ns_records = []
|
||||
|
||||
for record in records:
|
||||
if not record.has("type") or not record.has("value"):
|
||||
continue
|
||||
|
||||
match record.type:
|
||||
"A":
|
||||
a_records.append(record.value)
|
||||
"AAAA":
|
||||
aaaa_records.append(record.value)
|
||||
"CNAME":
|
||||
cname_records.append(record.value)
|
||||
"NS":
|
||||
ns_records.append(record.value)
|
||||
|
||||
# If we have direct A records, use the first one
|
||||
if not a_records.is_empty():
|
||||
return {"address": a_records[0]}
|
||||
|
||||
# If we have IPv6 AAAA records and no A records, we need to handle this
|
||||
if not aaaa_records.is_empty() and a_records.is_empty():
|
||||
return {"error": "Only IPv6 (AAAA) records found, but IPv4 required for GURT protocol"}
|
||||
|
||||
# Follow CNAME chain
|
||||
if not cname_records.is_empty():
|
||||
var current_cname = cname_records[0]
|
||||
|
||||
while cname_depth < max_cname_depth:
|
||||
cname_depth += 1
|
||||
|
||||
# Try to resolve the CNAME target
|
||||
var cname_info = await fetch_full_domain_info(current_cname, "A")
|
||||
if cname_info.has("error"):
|
||||
return {"error": "Failed to resolve CNAME target: " + current_cname + " - " + cname_info.error}
|
||||
|
||||
if not cname_info.has("records") or cname_info.records == null:
|
||||
return {"error": "No records found for CNAME target: " + current_cname}
|
||||
|
||||
# Look for A records in the CNAME target
|
||||
var found_next_cname = false
|
||||
for record in cname_info.records:
|
||||
if record.has("type") and record.type == "A" and record.has("value"):
|
||||
return {"address": record.value}
|
||||
elif record.has("type") and record.type == "CNAME" and record.has("value"):
|
||||
# Another CNAME, continue the chain
|
||||
current_cname = record.value
|
||||
found_next_cname = true
|
||||
break
|
||||
|
||||
if not found_next_cname:
|
||||
# No more CNAMEs found, but also no A record
|
||||
return {"error": "CNAME chain ended without A record for: " + current_cname}
|
||||
|
||||
return {"error": "CNAME chain too deep (max " + str(max_cname_depth) + " levels)"}
|
||||
|
||||
# If we have NS records, this indicates delegation
|
||||
if not ns_records.is_empty():
|
||||
return {"error": "Domain is delegated to nameservers: " + str(ns_records) + ". Cannot resolve directly."}
|
||||
|
||||
return {"error": "No A record found for domain"}
|
||||
return domain
|
||||
|
||||
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": "🌐"}
|
||||
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": "⏰"}
|
||||
return {"code": "ERR_CONNECTION_TIMED_OUT", "title": "This site can't be reached", "icon": "...?"}
|
||||
elif "Failed to fetch" in error_message or "No response" in error_message:
|
||||
return {"code": "ERR_CONNECTION_REFUSED", "title": "This site can't be reached", "icon": "🚫"}
|
||||
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": "⚠️"}
|
||||
return {"code": "ERR_INVALID_URL", "title": "This page isn't working", "icon": ":|"}
|
||||
else:
|
||||
return {"code": "ERR_UNKNOWN", "title": "Something went wrong", "icon": "❌"}
|
||||
return {"code": "ERR_UNKNOWN", "title": "Something went wrong", "icon": ">_<"}
|
||||
|
||||
static func create_error_page(error_message: String) -> PackedByteArray:
|
||||
var error_info = get_error_type(error_message)
|
||||
|
||||
@@ -112,13 +112,40 @@ func fetch_gurt_resource(url: String) -> String:
|
||||
if not GurtProtocol.is_gurt_domain(url):
|
||||
return ""
|
||||
|
||||
var result = await GurtProtocol.handle_gurt_domain(url)
|
||||
var gurt_url = url
|
||||
if not gurt_url.begins_with("gurt://"):
|
||||
gurt_url = "gurt://" + gurt_url
|
||||
|
||||
if result.has("error"):
|
||||
print("GURT resource error: ", result.error)
|
||||
if gurt_url.contains("localhost"):
|
||||
gurt_url = gurt_url.replace("localhost", "127.0.0.1")
|
||||
|
||||
var client = GurtProtocolClient.new()
|
||||
|
||||
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||
client.add_ca_certificate(ca_cert)
|
||||
|
||||
if not client.create_client(30):
|
||||
print("GURT resource error: Failed to create client")
|
||||
return ""
|
||||
|
||||
if result.has("html"):
|
||||
return result.html.get_string_from_utf8()
|
||||
var host_domain = gurt_url
|
||||
if host_domain.begins_with("gurt://"):
|
||||
host_domain = host_domain.substr(7)
|
||||
var slash_pos = host_domain.find("/")
|
||||
if slash_pos != -1:
|
||||
host_domain = host_domain.substr(0, slash_pos)
|
||||
|
||||
return ""
|
||||
var response = client.request(gurt_url, {
|
||||
"method": "GET",
|
||||
"headers": {"Host": host_domain}
|
||||
})
|
||||
client.disconnect()
|
||||
|
||||
if not response or not response.is_success:
|
||||
var error_msg = "Failed to load GURT resource"
|
||||
if response:
|
||||
error_msg += ": " + str(response.status_code) + " " + response.status_message
|
||||
print("GURT resource error: ", error_msg)
|
||||
return ""
|
||||
|
||||
return response.body.get_string_from_utf8()
|
||||
|
||||
@@ -67,6 +67,7 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
|
||||
if width is String and width == "100%":
|
||||
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
node.custom_minimum_size.x = 0
|
||||
node.set_meta("size_flags_set_by_style_manager", true)
|
||||
else:
|
||||
node.custom_minimum_size.x = width
|
||||
node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
@@ -75,6 +76,7 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
|
||||
if height is String and height == "100%":
|
||||
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
node.custom_minimum_size.y = 0
|
||||
node.set_meta("size_flags_set_by_style_manager", true)
|
||||
else:
|
||||
node.custom_minimum_size.y = height
|
||||
node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
|
||||
@@ -33,6 +33,3 @@ func load_image_async(src: String, element: HTMLParser.HTMLElement, parser: HTML
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
custom_minimum_size = Vector2.ZERO
|
||||
else:
|
||||
custom_minimum_size = Vector2(1, 1)
|
||||
size = Vector2(100, 100) # StyleManager will handle this
|
||||
|
||||
@@ -444,6 +444,16 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
var placeholder_color = Color(text_color.r, text_color.g, text_color.b, text_color.a * 0.6)
|
||||
line_edit.add_theme_color_override("font_placeholder_color", placeholder_color)
|
||||
|
||||
if styles.has("font-size"):
|
||||
var font_size = int(styles["font-size"])
|
||||
if active_child is LineEdit:
|
||||
active_child.add_theme_font_size_override("font_size", font_size)
|
||||
elif active_child is SpinBox:
|
||||
active_child.add_theme_font_size_override("font_size", font_size)
|
||||
var line_edit = active_child.get_line_edit()
|
||||
if line_edit:
|
||||
line_edit.add_theme_font_size_override("font_size", font_size)
|
||||
|
||||
# Apply stylebox for borders, background, padding, etc.
|
||||
if BackgroundUtils.needs_background_wrapper(styles) or active_child is SpinBox:
|
||||
apply_stylebox_to_input(active_child, styles)
|
||||
@@ -464,7 +474,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
var new_height = max(active_child.custom_minimum_size.y, active_child.size.y)
|
||||
|
||||
if width:
|
||||
if typeof(width) == TYPE_STRING and width == "100%":
|
||||
if typeof(width) == TYPE_STRING and (width == "100%" or width == "full"):
|
||||
active_child.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
new_width = 0
|
||||
@@ -483,7 +493,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
|
||||
active_child.custom_minimum_size = new_child_size
|
||||
|
||||
if width and not (typeof(width) == TYPE_STRING and width == "100%"):
|
||||
if width and not (typeof(width) == TYPE_STRING and (width == "100%" or width == "full")):
|
||||
active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
if height:
|
||||
active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
@@ -494,7 +504,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
custom_minimum_size = new_child_size
|
||||
|
||||
# Root Control adjusts size flags to match child
|
||||
if width and not (typeof(width) == TYPE_STRING and width == "100%"):
|
||||
if width and not (typeof(width) == TYPE_STRING and (width == "100%" or width == "full")):
|
||||
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
else:
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
@@ -4,18 +4,19 @@ extends HBoxContainer
|
||||
var _element: HTMLParser.HTMLElement
|
||||
var _parser: HTMLParser
|
||||
|
||||
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
||||
const BROWSER_THEME = preload("res://Scenes/Styles/BrowserText.tres")
|
||||
|
||||
func init(element, parser: HTMLParser) -> void:
|
||||
_element = element
|
||||
_parser = parser
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
|
||||
mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
|
||||
if get_child_count() > 0:
|
||||
return
|
||||
|
||||
var content_parts = []
|
||||
var current_text = ""
|
||||
|
||||
var element_text = element.text_content
|
||||
var child_texts = []
|
||||
|
||||
@@ -27,7 +28,7 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
||||
parent_only_text = parent_only_text.replace(child_text, "")
|
||||
|
||||
if not parent_only_text.strip_edges().is_empty():
|
||||
var parent_label = create_styled_label(parent_only_text.strip_edges(), element, parser)
|
||||
create_styled_label(parent_only_text.strip_edges(), element, parser)
|
||||
|
||||
for child in element.children:
|
||||
var child_label = create_styled_label(child.get_bbcode_formatted_text(parser), element, parser)
|
||||
@@ -35,13 +36,30 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
||||
if contains_hyperlink(child):
|
||||
child_label.meta_clicked.connect(_on_meta_clicked)
|
||||
|
||||
func create_styled_label(text: String, element: HTMLParser.HTMLElement, parser: HTMLParser) -> RichTextLabel:
|
||||
func create_styled_label(text: String, element, parser: HTMLParser) -> RichTextLabel:
|
||||
var label = RichTextLabel.new()
|
||||
|
||||
label.theme = BROWSER_THEME
|
||||
label.focus_mode = Control.FOCUS_ALL
|
||||
label.add_theme_color_override("default_color", Color.BLACK)
|
||||
label.bbcode_enabled = true
|
||||
label.fit_content = true
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.selection_enabled = true
|
||||
|
||||
var parent_cursor_shape = Control.CURSOR_IBEAM
|
||||
if element.parent:
|
||||
var parent_styles = parser.get_element_styles_with_inheritance(element.parent, "", [])
|
||||
if parent_styles.has("cursor"):
|
||||
parent_cursor_shape = StyleManager.get_cursor_shape_from_type(parent_styles["cursor"])
|
||||
|
||||
label.mouse_default_cursor_shape = parent_cursor_shape
|
||||
label.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
label.bbcode_enabled = true
|
||||
|
||||
add_child(label)
|
||||
|
||||
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||
@@ -51,12 +69,17 @@ func create_styled_label(text: String, element: HTMLParser.HTMLElement, parser:
|
||||
return label
|
||||
|
||||
func _apply_auto_resize_to_label(label: RichTextLabel):
|
||||
if not is_instance_valid(label) or not is_instance_valid(self):
|
||||
return
|
||||
|
||||
if not label.is_inside_tree():
|
||||
await label.tree_entered
|
||||
|
||||
if not is_instance_valid(label) or not is_instance_valid(self):
|
||||
return
|
||||
|
||||
var min_width = 20
|
||||
var max_width = 800
|
||||
var min_height = 30
|
||||
|
||||
label.fit_content = true
|
||||
|
||||
@@ -65,8 +88,11 @@ func _apply_auto_resize_to_label(label: RichTextLabel):
|
||||
|
||||
await get_tree().process_frame
|
||||
|
||||
if not is_instance_valid(label) or not is_instance_valid(self):
|
||||
return
|
||||
|
||||
var natural_width = label.size.x
|
||||
natural_width *= 1.0 # font weight multiplier simplified
|
||||
natural_width *= 1.0
|
||||
|
||||
var desired_width = clampf(natural_width, min_width, max_width)
|
||||
|
||||
@@ -74,6 +100,9 @@ func _apply_auto_resize_to_label(label: RichTextLabel):
|
||||
|
||||
await get_tree().process_frame
|
||||
|
||||
if not is_instance_valid(label) or not is_instance_valid(self):
|
||||
return
|
||||
|
||||
label.custom_minimum_size = Vector2(desired_width, 0)
|
||||
|
||||
label.queue_redraw()
|
||||
@@ -110,18 +139,35 @@ func set_text(new_text: String) -> void:
|
||||
child.queue_free()
|
||||
|
||||
if _element and _parser:
|
||||
var label = create_styled_label(new_text, _element, _parser)
|
||||
create_styled_label(new_text, _element, _parser)
|
||||
else:
|
||||
var label = create_label(new_text)
|
||||
create_label(new_text)
|
||||
|
||||
func create_label(text: String) -> RichTextLabel:
|
||||
var label = RichTextLabel.new()
|
||||
label.text = text
|
||||
|
||||
label.theme = BROWSER_THEME
|
||||
label.focus_mode = Control.FOCUS_ALL
|
||||
label.add_theme_color_override("default_color", Color.BLACK)
|
||||
label.bbcode_enabled = true
|
||||
label.fit_content = true
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.selection_enabled = true
|
||||
|
||||
var parent_cursor_shape = Control.CURSOR_IBEAM
|
||||
if _element and _parser and _element.parent:
|
||||
var parent_styles = _parser.get_element_styles_with_inheritance(_element.parent, "", [])
|
||||
if parent_styles.has("cursor"):
|
||||
parent_cursor_shape = StyleManager.get_cursor_shape_from_type(parent_styles["cursor"])
|
||||
|
||||
label.mouse_default_cursor_shape = parent_cursor_shape
|
||||
label.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
|
||||
label.text = text
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
label.bbcode_enabled = true
|
||||
|
||||
add_child(label)
|
||||
call_deferred("_apply_auto_resize_to_label", label)
|
||||
return label
|
||||
|
||||
@@ -195,12 +195,42 @@ static func setup_panel_hover_support(panel: PanelContainer, normal_styles: Dict
|
||||
panel.set_meta("hover_stylebox", hover_stylebox)
|
||||
panel.set_meta("normal_styles", normal_styles.duplicate(true))
|
||||
panel.set_meta("hover_styles", merged_hover_styles.duplicate(true))
|
||||
panel.set_meta("is_hovering", false)
|
||||
|
||||
# Connect mouse events
|
||||
panel.mouse_entered.connect(_on_panel_mouse_entered.bind(panel))
|
||||
panel.mouse_exited.connect(_on_panel_mouse_exited.bind(panel))
|
||||
panel.mouse_exited.connect(_on_panel_mouse_exited_with_delay.bind(panel))
|
||||
|
||||
_setup_child_hover_listeners(panel)
|
||||
|
||||
static func _setup_child_hover_listeners(panel: PanelContainer):
|
||||
for child in panel.get_children():
|
||||
_connect_child_hover_events(child, panel)
|
||||
|
||||
panel.child_entered_tree.connect(_on_child_added.bind(panel))
|
||||
|
||||
static func _connect_child_hover_events(child: Node, panel: PanelContainer):
|
||||
if child is Control:
|
||||
# Only connect if not already connected
|
||||
if not child.mouse_entered.is_connected(_on_child_mouse_entered.bind(panel)):
|
||||
child.mouse_entered.connect(_on_child_mouse_entered.bind(panel))
|
||||
if not child.mouse_exited.is_connected(_on_child_mouse_exited.bind(panel)):
|
||||
child.mouse_exited.connect(_on_child_mouse_exited.bind(panel))
|
||||
|
||||
for grandchild in child.get_children():
|
||||
_connect_child_hover_events(grandchild, panel)
|
||||
|
||||
static func _on_child_added(child: Node, panel: PanelContainer):
|
||||
_connect_child_hover_events(child, panel)
|
||||
|
||||
static func _on_child_mouse_entered(panel: PanelContainer):
|
||||
_on_panel_mouse_entered(panel)
|
||||
|
||||
static func _on_child_mouse_exited(panel: PanelContainer):
|
||||
panel.get_tree().create_timer(0.01).timeout.connect(func(): _check_panel_hover(panel))
|
||||
|
||||
static func _on_panel_mouse_entered(panel: PanelContainer):
|
||||
panel.set_meta("is_hovering", true)
|
||||
if panel.has_meta("hover_stylebox"):
|
||||
var hover_stylebox = panel.get_meta("hover_stylebox")
|
||||
panel.add_theme_stylebox_override("panel", hover_stylebox)
|
||||
@@ -210,15 +240,27 @@ static func _on_panel_mouse_entered(panel: PanelContainer):
|
||||
var transform_target = find_transform_target_for_panel(panel)
|
||||
StyleManager.apply_transform_properties_direct(transform_target, hover_styles)
|
||||
|
||||
static func _on_panel_mouse_exited(panel: PanelContainer):
|
||||
if panel.has_meta("normal_stylebox"):
|
||||
var normal_stylebox = panel.get_meta("normal_stylebox")
|
||||
panel.add_theme_stylebox_override("panel", normal_stylebox)
|
||||
static func _on_panel_mouse_exited_with_delay(panel: PanelContainer):
|
||||
panel.get_tree().create_timer(0.01).timeout.connect(func(): _check_panel_hover(panel))
|
||||
|
||||
static func _check_panel_hover(panel: PanelContainer):
|
||||
if not panel or not is_instance_valid(panel):
|
||||
return
|
||||
|
||||
if panel.has_meta("normal_styles"):
|
||||
var normal_styles = panel.get_meta("normal_styles")
|
||||
var transform_target = find_transform_target_for_panel(panel)
|
||||
StyleManager.apply_transform_properties_direct(transform_target, normal_styles)
|
||||
var mouse_pos = panel.get_global_mouse_position()
|
||||
var panel_rect = panel.get_global_rect()
|
||||
var is_mouse_over = panel_rect.has_point(mouse_pos)
|
||||
|
||||
if not is_mouse_over and panel.get_meta("is_hovering", false):
|
||||
panel.set_meta("is_hovering", false)
|
||||
if panel.has_meta("normal_stylebox"):
|
||||
var normal_stylebox = panel.get_meta("normal_stylebox")
|
||||
panel.add_theme_stylebox_override("panel", normal_stylebox)
|
||||
|
||||
if panel.has_meta("normal_styles"):
|
||||
var normal_styles = panel.get_meta("normal_styles")
|
||||
var transform_target = find_transform_target_for_panel(panel)
|
||||
StyleManager.apply_transform_properties_direct(transform_target, normal_styles)
|
||||
|
||||
static func find_transform_target_for_panel(panel: PanelContainer) -> Control:
|
||||
var parent = panel.get_parent()
|
||||
|
||||
@@ -64,6 +64,7 @@ static func apply_flex_container_properties(node, styles: Dictionary) -> void:
|
||||
if width_val == "full" or width_val == "100%":
|
||||
# For flex containers, w-full should expand to fill parent
|
||||
node.set_meta("should_fill_horizontal", true)
|
||||
node.set_meta("size_flags_set_by_style_manager", true)
|
||||
elif typeof(width_val) == TYPE_STRING and width_val.ends_with("%"):
|
||||
node.set_meta("custom_css_width_percentage", width_val)
|
||||
else:
|
||||
@@ -73,6 +74,7 @@ static func apply_flex_container_properties(node, styles: Dictionary) -> void:
|
||||
if height_val == "full":
|
||||
# For flex containers, h-full should expand to fill parent
|
||||
node.set_meta("should_fill_vertical", true)
|
||||
node.set_meta("size_flags_set_by_style_manager", true)
|
||||
elif typeof(height_val) == TYPE_STRING and height_val.ends_with("%"):
|
||||
node.set_meta("custom_css_height_percentage", height_val)
|
||||
else:
|
||||
|
||||
@@ -278,7 +278,7 @@ static func update_div_hover_styles(dom_node: Control, element: HTMLParser.HTMLE
|
||||
|
||||
if dom_node.mouse_entered.is_connected(BackgroundUtils._on_panel_mouse_entered):
|
||||
dom_node.mouse_entered.disconnect(BackgroundUtils._on_panel_mouse_entered)
|
||||
if dom_node.mouse_exited.is_connected(BackgroundUtils._on_panel_mouse_exited):
|
||||
dom_node.mouse_exited.disconnect(BackgroundUtils._on_panel_mouse_exited)
|
||||
if dom_node.mouse_exited.is_connected(BackgroundUtils._on_panel_mouse_exited_with_delay):
|
||||
dom_node.mouse_exited.disconnect(BackgroundUtils._on_panel_mouse_exited_with_delay)
|
||||
|
||||
update_element_text_content(dom_node, element, dom_parser)
|
||||
|
||||
@@ -640,6 +640,12 @@ static func add_element_methods(vm: LuauVM, lua_api: LuaAPI) -> void:
|
||||
vm.lua_pushcallable(LuaDOMUtils._element_hide_wrapper, "element.hide")
|
||||
vm.lua_setfield(-2, "hide")
|
||||
|
||||
vm.lua_pushcallable(LuaDOMUtils._element_focus_wrapper, "element.focus")
|
||||
vm.lua_setfield(-2, "focus")
|
||||
|
||||
vm.lua_pushcallable(LuaDOMUtils._element_unfocus_wrapper, "element.unfocus")
|
||||
vm.lua_setfield(-2, "unfocus")
|
||||
|
||||
_add_classlist_support(vm, lua_api)
|
||||
|
||||
vm.lua_newtable()
|
||||
@@ -1420,6 +1426,48 @@ static func _element_hide_wrapper(vm: LuauVM) -> int:
|
||||
|
||||
return 0
|
||||
|
||||
static func _element_focus_wrapper(vm: LuauVM) -> int:
|
||||
var lua_api = vm.get_meta("lua_api") as LuaAPI
|
||||
if not lua_api:
|
||||
vm.lua_pushboolean(false)
|
||||
return 1
|
||||
|
||||
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
||||
|
||||
vm.lua_getfield(1, "_element_id")
|
||||
var element_id: String = vm.lua_tostring(-1)
|
||||
vm.lua_pop(1)
|
||||
|
||||
var operation = {
|
||||
"type": "focus_element",
|
||||
"element_id": element_id
|
||||
}
|
||||
|
||||
emit_dom_operation(lua_api, operation)
|
||||
vm.lua_pushboolean(true)
|
||||
return 1
|
||||
|
||||
static func _element_unfocus_wrapper(vm: LuauVM) -> int:
|
||||
var lua_api = vm.get_meta("lua_api") as LuaAPI
|
||||
if not lua_api:
|
||||
vm.lua_pushboolean(false)
|
||||
return 1
|
||||
|
||||
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
||||
|
||||
vm.lua_getfield(1, "_element_id")
|
||||
var element_id: String = vm.lua_tostring(-1)
|
||||
vm.lua_pop(1)
|
||||
|
||||
var operation = {
|
||||
"type": "unfocus_element",
|
||||
"element_id": element_id
|
||||
}
|
||||
|
||||
emit_dom_operation(lua_api, operation)
|
||||
vm.lua_pushboolean(true)
|
||||
return 1
|
||||
|
||||
static func _element_create_tween_wrapper(vm: LuauVM) -> int:
|
||||
var lua_api = vm.get_meta("lua_api") as LuaAPI
|
||||
if not lua_api:
|
||||
|
||||
@@ -118,6 +118,12 @@ static func string_replace_all_handler(vm: LuauVM) -> int:
|
||||
|
||||
return 1
|
||||
|
||||
static func string_trim_handler(vm: LuauVM) -> int:
|
||||
var subject: String = vm.luaL_checkstring(1)
|
||||
var trimmed = subject.strip_edges()
|
||||
vm.lua_pushstring(trimmed)
|
||||
return 1
|
||||
|
||||
static func setup_regex_api(vm: LuauVM) -> void:
|
||||
vm.lua_newtable()
|
||||
|
||||
@@ -139,4 +145,7 @@ static func setup_regex_api(vm: LuauVM) -> void:
|
||||
vm.lua_pushcallable(string_replace_all_handler, "string.replaceAll")
|
||||
vm.lua_setfield(-2, "replaceAll")
|
||||
|
||||
vm.lua_pushcallable(string_trim_handler, "string.trim")
|
||||
vm.lua_setfield(-2, "trim")
|
||||
|
||||
vm.lua_pop(1)
|
||||
|
||||
@@ -53,6 +53,7 @@ func stop_lua_thread():
|
||||
while lua_thread.is_alive() and (Time.get_ticks_msec() - timeout_start) < 500:
|
||||
OS.delay_msec(10)
|
||||
|
||||
lua_thread.wait_to_finish()
|
||||
lua_thread = null
|
||||
|
||||
func execute_script_async(script_code: String):
|
||||
@@ -356,6 +357,7 @@ func _setup_additional_lua_apis():
|
||||
LuaAudioUtils.setup_audio_api(lua_vm)
|
||||
LuaCrumbsUtils.setup_crumbs_api(lua_vm)
|
||||
LuaRegexUtils.setup_regex_api(lua_vm)
|
||||
LuaURLUtils.setup_url_api(lua_vm)
|
||||
|
||||
func _table_tostring_handler(vm: LuauVM) -> int:
|
||||
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
||||
|
||||
21
flumi/Scripts/Utils/Lua/URL.gd
Normal file
21
flumi/Scripts/Utils/Lua/URL.gd
Normal file
@@ -0,0 +1,21 @@
|
||||
class_name LuaURLUtils
|
||||
extends RefCounted
|
||||
|
||||
static func url_encode_handler(vm: LuauVM) -> int:
|
||||
var input: String = vm.luaL_checkstring(1)
|
||||
var encoded = input.uri_encode()
|
||||
vm.lua_pushstring(encoded)
|
||||
return 1
|
||||
|
||||
static func url_decode_handler(vm: LuauVM) -> int:
|
||||
var input: String = vm.luaL_checkstring(1)
|
||||
var decoded = input.uri_decode()
|
||||
vm.lua_pushstring(decoded)
|
||||
return 1
|
||||
|
||||
static func setup_url_api(vm: LuauVM) -> void:
|
||||
vm.lua_pushcallable(url_encode_handler, "urlEncode")
|
||||
vm.lua_setglobal("urlEncode")
|
||||
|
||||
vm.lua_pushcallable(url_decode_handler, "urlDecode")
|
||||
vm.lua_setglobal("urlDecode")
|
||||
1
flumi/Scripts/Utils/Lua/URL.gd.uid
Normal file
1
flumi/Scripts/Utils/Lua/URL.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bjiiw0qfqg2he
|
||||
@@ -83,33 +83,78 @@ func _on_search_submitted(url: String) -> void:
|
||||
var tab = tab_container.tabs[tab_container.active_tab]
|
||||
tab.start_loading()
|
||||
|
||||
var result = await GurtProtocol.handle_gurt_domain(url)
|
||||
var gurt_url = url
|
||||
if not gurt_url.begins_with("gurt://"):
|
||||
gurt_url = "gurt://" + gurt_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)
|
||||
return
|
||||
|
||||
var html_bytes = result.html
|
||||
|
||||
if result.has("display_url"):
|
||||
current_domain = result.display_url
|
||||
if not current_domain.begins_with("gurt://"):
|
||||
current_domain = "gurt://" + current_domain
|
||||
if not search_bar.has_focus():
|
||||
search_bar.text = result.display_url # Show clean version in search bar
|
||||
else:
|
||||
current_domain = url
|
||||
|
||||
render_content(html_bytes)
|
||||
|
||||
# Stop loading spinner after successful render
|
||||
tab.stop_loading()
|
||||
await fetch_gurt_content_async(gurt_url, tab, url)
|
||||
else:
|
||||
print("Non-GURT URL entered: ", url)
|
||||
|
||||
func fetch_gurt_content_async(gurt_url: String, tab: Tab, original_url: String) -> void:
|
||||
var thread = Thread.new()
|
||||
var request_data = {"gurt_url": gurt_url}
|
||||
|
||||
thread.start(_perform_gurt_request_threaded.bind(request_data))
|
||||
|
||||
while thread.is_alive():
|
||||
await get_tree().process_frame
|
||||
|
||||
var result = thread.wait_to_finish()
|
||||
|
||||
_handle_gurt_result(result, tab, original_url, gurt_url)
|
||||
|
||||
func _perform_gurt_request_threaded(request_data: Dictionary) -> Dictionary:
|
||||
var gurt_url: String = request_data.gurt_url
|
||||
var client = GurtProtocolClient.new()
|
||||
|
||||
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||
client.add_ca_certificate(ca_cert)
|
||||
|
||||
if not client.create_client_with_dns(30, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT):
|
||||
client.disconnect()
|
||||
return {"success": false, "error": "Failed to connect to GURT DNS server"}
|
||||
|
||||
var response = client.request(gurt_url, {
|
||||
"method": "GET"
|
||||
})
|
||||
client.disconnect()
|
||||
|
||||
if not response or not response.is_success:
|
||||
var error_msg = "Connection failed"
|
||||
if response:
|
||||
error_msg = "GURT %d: %s" % [response.status_code, response.status_message]
|
||||
elif not response:
|
||||
error_msg = "Request timed out or connection failed"
|
||||
return {"success": false, "error": error_msg}
|
||||
|
||||
return {"success": true, "html_bytes": response.body}
|
||||
|
||||
func _handle_gurt_result(result: Dictionary, tab: Tab, original_url: String, gurt_url: String) -> void:
|
||||
if not result.success:
|
||||
print("GURT request failed: ", result.error)
|
||||
handle_gurt_error(result.error, tab)
|
||||
return
|
||||
|
||||
var html_bytes = result.html_bytes
|
||||
|
||||
current_domain = gurt_url
|
||||
if not search_bar.has_focus():
|
||||
search_bar.text = original_url # Show the original input in search bar
|
||||
|
||||
render_content(html_bytes)
|
||||
|
||||
tab.stop_loading()
|
||||
|
||||
func handle_gurt_error(error_message: String, tab: Tab) -> void:
|
||||
var error_html = GurtProtocol.create_error_page(error_message)
|
||||
|
||||
render_content(error_html)
|
||||
|
||||
const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
|
||||
tab.stop_loading()
|
||||
tab.set_icon(GLOBE_ICON)
|
||||
|
||||
func _on_search_focus_entered() -> void:
|
||||
if not current_domain.is_empty():
|
||||
search_bar.text = current_domain
|
||||
@@ -298,7 +343,7 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
|
||||
if is_grid_container:
|
||||
if element.tag_name == "div":
|
||||
if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0:
|
||||
if BackgroundUtils.needs_background_wrapper(styles) or BackgroundUtils.needs_background_wrapper(hover_styles):
|
||||
final_node = BackgroundUtils.create_panel_container_with_background(styles, hover_styles)
|
||||
var grid_container = GridContainer.new()
|
||||
grid_container.name = "Grid_" + element.tag_name
|
||||
@@ -316,21 +361,24 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
elif is_flex_container:
|
||||
# The element's primary identity IS a flex container.
|
||||
if element.tag_name == "div":
|
||||
if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0:
|
||||
if BackgroundUtils.needs_background_wrapper(styles) or BackgroundUtils.needs_background_wrapper(hover_styles):
|
||||
final_node = BackgroundUtils.create_panel_container_with_background(styles, hover_styles)
|
||||
var flex_container = AUTO_SIZING_FLEX_CONTAINER.new()
|
||||
flex_container.name = "Flex_" + element.tag_name
|
||||
var vbox = final_node.get_child(0) as VBoxContainer
|
||||
vbox.add_child(flex_container)
|
||||
container_for_children = flex_container
|
||||
FlexUtils.apply_flex_container_properties(flex_container, styles)
|
||||
else:
|
||||
final_node = AUTO_SIZING_FLEX_CONTAINER.new()
|
||||
final_node.name = "Flex_" + element.tag_name
|
||||
container_for_children = final_node
|
||||
FlexUtils.apply_flex_container_properties(final_node, styles)
|
||||
else:
|
||||
final_node = AUTO_SIZING_FLEX_CONTAINER.new()
|
||||
final_node.name = "Flex_" + element.tag_name
|
||||
container_for_children = final_node
|
||||
FlexUtils.apply_flex_container_properties(final_node, styles)
|
||||
|
||||
# For FLEX ul/ol elements, we need to create the li children directly in the flex container
|
||||
if element.tag_name == "ul" or element.tag_name == "ol":
|
||||
@@ -351,7 +399,8 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
# If the element itself has text (like <span style="flex">TEXT</span>)
|
||||
elif not element.text_content.is_empty():
|
||||
var new_node = await create_element_node_internal(element, parser)
|
||||
container_for_children.add_child(new_node)
|
||||
if new_node:
|
||||
container_for_children.add_child(new_node)
|
||||
# For flex divs, we're done - no additional node creation needed
|
||||
elif element.tag_name == "div":
|
||||
pass
|
||||
@@ -369,26 +418,6 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
# Applies background, size, etc. to the FlexContainer (top-level node)
|
||||
final_node = StyleManager.apply_element_styles(final_node, element, parser)
|
||||
|
||||
# Apply flex CONTAINER properties if it's a flex container
|
||||
if is_flex_container:
|
||||
var flex_container_node = final_node
|
||||
|
||||
if final_node is FlexContainer:
|
||||
# Direct FlexContainer
|
||||
flex_container_node = final_node
|
||||
elif final_node is MarginContainer and final_node.get_child_count() > 0:
|
||||
var first_child = final_node.get_child(0)
|
||||
if first_child is FlexContainer:
|
||||
flex_container_node = first_child
|
||||
elif final_node is PanelContainer and final_node.get_child_count() > 0:
|
||||
var vbox = final_node.get_child(0)
|
||||
if vbox is VBoxContainer and vbox.get_child_count() > 0:
|
||||
var potential_flex = vbox.get_child(0)
|
||||
if potential_flex is FlexContainer:
|
||||
flex_container_node = potential_flex
|
||||
|
||||
if flex_container_node is FlexContainer:
|
||||
FlexUtils.apply_flex_container_properties(flex_container_node, styles)
|
||||
|
||||
if is_grid_container:
|
||||
var grid_container_node = final_node
|
||||
@@ -528,7 +557,7 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
|
||||
return null
|
||||
|
||||
# Create div container
|
||||
if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0:
|
||||
if BackgroundUtils.needs_background_wrapper(styles) or BackgroundUtils.needs_background_wrapper(hover_styles):
|
||||
node = BackgroundUtils.create_panel_container_with_background(styles, hover_styles)
|
||||
else:
|
||||
node = DIV.instantiate()
|
||||
|
||||
Reference in New Issue
Block a user