move wayfinder files to Flumi folder

This commit is contained in:
Face
2025-08-02 14:18:14 +03:00
parent 8fbabdd01a
commit e575974721
442 changed files with 6 additions and 188 deletions

View File

@@ -0,0 +1,204 @@
@tool
class_name AutoSizingFlexContainer
extends FlexContainer
signal flex_resized
var content_size: Vector2 = Vector2.ZERO
# This is the overridden layout logic for the auto-sizing container
func _resort() -> void:
# Check if we should fill horizontally (for w-full)
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_CENTER
# Check if we should fill vertically (for h-full)
if has_meta("should_fill_vertical"):
size_flags_vertical = Control.SIZE_FILL
else:
if not has_meta("size_flags_set_by_style_manager"):
size_flags_vertical = Control.SIZE_SHRINK_CENTER
if debug_draw:
_draw_rects.clear()
var child_count = get_child_count()
var valid_child_index = 0
for i in range(child_count):
var c = get_child(i)
if not c is Control or c.is_set_as_top_level():
continue
# Skip background panel from flex calculations
if BackgroundUtils.is_background_panel(c):
continue
var cid = c.get_instance_id()
var target_index = _find_index_from_flex_list(_flex_list, cid)
var flexbox: Flexbox
# If the child is not visible, remove its corresponding flexbox node
if not c.is_visible_in_tree():
if target_index != -1:
_root.remove_child_at(target_index)
_flex_list.remove_at(target_index)
continue
# Find, swap, or create a new flexbox node for the child
if target_index != -1:
var old_flex_data = _flex_list[valid_child_index]
var new_flex_data = _flex_list[target_index]
flexbox = new_flex_data[FlexDataType.FLEXBOX]
if old_flex_data[FlexDataType.CID] != cid:
_root.swap_child(valid_child_index, target_index)
_flex_list[target_index] = old_flex_data
_flex_list[valid_child_index] = new_flex_data
else:
flexbox = Flexbox.new()
_root.insert_child(flexbox, valid_child_index)
_flex_list.insert(valid_child_index, [cid, flexbox, c])
# Set the minimum size and apply flex properties for the child
_set_control_min_size(c, flexbox)
var flex_metas = c.get_meta("flex_metas", {})
if flex_metas.size():
apply_flex_meta(flexbox, flex_metas)
if flex_metas.has("padding"):
padding_wrapper(c, flex_metas.get("padding"))
valid_child_index += 1
# Clean up any flexbox nodes for children that were removed
child_count = valid_child_index
if child_count != _flex_list.size():
for i in range(_flex_list.size() - 1, child_count - 1, -1):
_root.remove_child_at(i)
_flex_list.resize(child_count)
_root.mark_dirty_and_propogate()
var auto_size_width = not has_meta("custom_css_width") and not has_meta("should_fill_horizontal") and not has_meta("custom_css_width_percentage")
var auto_size_height = not has_meta("custom_css_height") and not has_meta("should_fill_vertical") and not has_meta("custom_css_height_percentage")
var available_width = NAN
var available_height = NAN
if not auto_size_width:
available_width = calculate_available_dimension(true)
if not auto_size_height:
available_height = calculate_available_dimension(false)
_root.calculate_layout(available_width, available_height, 1) # 1 = LTR direction
# Get the size computed by Yoga
var computed_size = Vector2(
_root.get_computed_width(),
_root.get_computed_height()
)
# Respect any explicit width/height set via metadata
var custom_w = calculate_custom_dimension(true)
var custom_h = calculate_custom_dimension(false)
var needed_size = Vector2(
max(custom_w, computed_size.x),
max(custom_h, computed_size.y)
)
# Store the actual content size for background drawing
content_size = needed_size
# Construct the new minimum size for this container
var new_min_size = custom_minimum_size
if auto_size_width:
new_min_size.x = needed_size.x
else:
# For w-full, ensure minimum size matches the needed size
new_min_size.x = needed_size.x
if auto_size_height:
new_min_size.y = needed_size.y
else:
# For h-full, ensure minimum size matches the needed size
new_min_size.y = needed_size.y
if not custom_minimum_size.is_equal_approx(new_min_size):
custom_minimum_size = new_min_size
# For w-full/h-full, also force the actual size if SIZE_FILL isn't working
if has_meta("should_fill_horizontal") and size.x < new_min_size.x:
size.x = new_min_size.x
if has_meta("should_fill_vertical") and size.y < new_min_size.y:
size.y = new_min_size.y
# Apply the calculated layout to each child control
for flex_data in _flex_list:
var flexbox = flex_data[FlexDataType.FLEXBOX]
var c = flex_data[FlexDataType.CONTROL]
var offset = Vector2(flexbox.get_computed_left(), flexbox.get_computed_top())
var rect_size = Vector2(flexbox.get_computed_width(), flexbox.get_computed_height())
_fit_child_in_rect(c, Rect2(offset, rect_size))
if debug_draw:
_draw_debug_rect(Rect2(offset, rect_size), Color(1, 0, 0, 0.8))
# Update background panel if needed
BackgroundUtils.update_background_panel(self)
emit_signal("flex_resized")
func calculate_available_dimension(is_width: bool) -> float:
var percentage_key = "custom_css_width_percentage" if is_width else "custom_css_height_percentage"
var fill_key = "should_fill_horizontal" if is_width else "should_fill_vertical"
if has_meta(fill_key):
return get_parent_or_fallback_size(is_width)
elif has_meta(percentage_key):
var percentage_str = get_meta(percentage_key)
var percentage = float(percentage_str.replace("%", "")) / 100.0
var parent_size = get_parent_size(is_width)
return parent_size * percentage if parent_size > 0 else (custom_minimum_size.x if is_width else custom_minimum_size.y)
else:
return size.x if is_width else size.y
func calculate_custom_dimension(is_width: bool) -> float:
var dimension_key = "custom_css_width" if is_width else "custom_css_height"
var percentage_key = "custom_css_width_percentage" if is_width else "custom_css_height_percentage"
var fill_key = "should_fill_horizontal" if is_width else "should_fill_vertical"
if has_meta(dimension_key):
return float(get_meta(dimension_key))
elif has_meta(percentage_key):
var percentage_str = get_meta(percentage_key)
var percentage = float(percentage_str.replace("%", "")) / 100.0
var parent_size = get_parent_size(is_width)
if parent_size > 0:
return parent_size * percentage
elif (size.x if is_width else size.y) > 0:
return (size.x if is_width else size.y) * percentage
else:
return 0.0
elif has_meta(fill_key):
return get_parent_or_fallback_size(is_width)
else:
return 0.0
func get_parent_size(is_width: bool) -> float:
var parent_container = get_parent()
if parent_container:
return parent_container.size.x if is_width else parent_container.size.y
return 0.0
func get_parent_or_fallback_size(is_width: bool) -> float:
var parent_container = get_parent()
if parent_container and (parent_container.size.x if is_width else parent_container.size.y) > 0:
return parent_container.size.x if is_width else parent_container.size.y
elif (size.x if is_width else size.y) > 0:
return size.x if is_width else size.y
else:
var fallback = custom_minimum_size.x if is_width else custom_minimum_size.y
return fallback if fallback > 0 else NAN

View File

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

View File

@@ -0,0 +1,828 @@
class_name CSSParser
extends RefCounted
class CSSRule:
var selector: String
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
func init(sel: String = ""):
selector = sel
parse_selector()
calculate_specificity()
func parse_selector():
if selector.contains(":"):
var parts = selector.split(":", false, 1)
if parts.size() == 2:
selector = parts[0]
event_prefix = parts[1]
# Parse complex selectors
if selector.contains(" > "):
selector_type = "child"
selector_parts = selector.split(" > ")
elif selector.contains(" + "):
selector_type = "adjacent_sibling"
selector_parts = selector.split(" + ")
elif selector.contains(" ~ "):
selector_type = "general_sibling"
selector_parts = selector.split(" ~ ")
elif selector.contains("["):
selector_type = "attribute"
parse_attribute_selector()
elif selector.contains(" "):
selector_type = "descendant"
selector_parts = selector.split(" ")
else:
selector_type = "simple"
selector_parts = [selector]
func parse_attribute_selector():
var bracket_start = selector.find("[")
var bracket_end = selector.find("]")
if bracket_start != -1 and bracket_end != -1:
var element_part = selector.substr(0, bracket_start)
var attribute_part = selector.substr(bracket_start + 1, bracket_end - bracket_start - 1)
selector_parts = [element_part, attribute_part]
func calculate_specificity():
specificity = 1
if selector.begins_with("."):
specificity += 10
if selector.contains("["):
specificity += 10 # Attribute selectors
match selector_type:
"child":
specificity += 8
"adjacent_sibling":
specificity += 7
"attribute":
specificity += 6
"general_sibling":
specificity += 5
"descendant":
specificity += 4
if event_prefix.length() > 0:
specificity += 10
class CSSStylesheet:
var rules: Array[CSSRule] = []
func add_rule(rule: CSSRule):
rules.append(rule)
func get_styles_for_element(tag_name: String, event: String = "", class_names: Array[String] = [], element: HTMLParser.HTMLElement = null) -> Dictionary:
var styles = {}
# Sort rules by specificity
var applicable_rules: Array[CSSRule] = []
for rule in rules:
if selector_matches(rule, tag_name, event, class_names, element):
applicable_rules.append(rule)
applicable_rules.sort_custom(func(a, b): return a.specificity < b.specificity)
# Apply styles in order of specificity
for rule in applicable_rules:
for property in rule.properties:
styles[property] = rule.properties[property]
return styles
func selector_matches(rule: CSSRule, tag_name: String, event: String = "", cls_names: Array[String] = [], element: HTMLParser.HTMLElement = null) -> bool:
if rule.event_prefix.length() > 0:
if rule.event_prefix != event:
return false
elif event.length() > 0:
return false
match rule.selector_type:
"simple":
return matches_simple_selector(rule.selector_parts[0], tag_name, cls_names)
"descendant":
return matches_descendant_selector(rule.selector_parts, element)
"child":
return matches_child_selector(rule.selector_parts, element)
"adjacent_sibling":
return matches_adjacent_sibling_selector(rule.selector_parts, element)
"general_sibling":
return matches_general_sibling_selector(rule.selector_parts, element)
"attribute":
return matches_attribute_selector(rule.selector_parts, tag_name, cls_names, element)
return false
func matches_simple_selector(selector: String, tag_name: String, cls_names: Array[String]) -> bool:
if selector.begins_with("."):
var cls = selector.substr(1)
return cls in cls_names
else:
return selector == tag_name
func matches_descendant_selector(parts: Array, element: HTMLParser.HTMLElement) -> bool:
if not element or parts.size() < 2:
return false
# Last part should match current element
var last_part = parts[-1].strip_edges()
if not matches_simple_selector(last_part, element.tag_name, get_element_class_names(element)):
return false
# Check ancestors for remaining parts
var current_element = element.parent
var part_index = parts.size() - 2
while current_element and part_index >= 0:
var part = parts[part_index].strip_edges()
if matches_simple_selector(part, current_element.tag_name, get_element_class_names(current_element)):
part_index -= 1
if part_index < 0:
return true
current_element = current_element.parent
return false
func matches_child_selector(parts: Array, element: HTMLParser.HTMLElement) -> bool:
if not element or not element.parent or parts.size() != 2:
return false
var child_part = parts[1].strip_edges()
var parent_part = parts[0].strip_edges()
# Element must match the child part
if not matches_simple_selector(child_part, element.tag_name, get_element_class_names(element)):
return false
# Parent must match the parent part
return matches_simple_selector(parent_part, element.parent.tag_name, get_element_class_names(element.parent))
func matches_adjacent_sibling_selector(parts: Array, element: HTMLParser.HTMLElement) -> bool:
if not element or not element.parent or parts.size() != 2:
return false
var second_part = parts[1].strip_edges()
var first_part = parts[0].strip_edges()
if not matches_simple_selector(second_part, element.tag_name, get_element_class_names(element)):
return false
# Find previous sibling
var siblings = element.parent.children
var element_index = siblings.find(element)
if element_index <= 0:
return false
var prev_sibling = siblings[element_index - 1]
return matches_simple_selector(first_part, prev_sibling.tag_name, get_element_class_names(prev_sibling))
func matches_general_sibling_selector(parts: Array, element: HTMLParser.HTMLElement) -> bool:
if not element or not element.parent or parts.size() != 2:
return false
var second_part = parts[1].strip_edges()
var first_part = parts[0].strip_edges()
if not matches_simple_selector(second_part, element.tag_name, get_element_class_names(element)):
return false
# Check all previous siblings
var siblings = element.parent.children
var element_index = siblings.find(element)
for i in range(element_index):
var sibling = siblings[i]
if matches_simple_selector(first_part, sibling.tag_name, get_element_class_names(sibling)):
return true
return false
func matches_attribute_selector(parts: Array, tag_name: String, cls_names: Array[String], element: HTMLParser.HTMLElement) -> bool:
if not element or parts.size() != 2:
return false
var element_part = parts[0].strip_edges()
var attribute_part = parts[1].strip_edges()
# Check if element matches
if element_part != "" and not matches_simple_selector(element_part, tag_name, cls_names):
return false
# Parse attribute condition
if attribute_part.contains("="):
var parsed = {}
var element_value = ""
if attribute_part.contains("^="):
# Starts with
parsed = parse_attribute_value(attribute_part, "^=")
element_value = element.get_attribute(parsed.name)
return element_value.begins_with(parsed.value)
elif attribute_part.contains("$="):
# Ends with
parsed = parse_attribute_value(attribute_part, "$=")
element_value = element.get_attribute(parsed.name)
return element_value.ends_with(parsed.value)
elif attribute_part.contains("*="):
# Contains
parsed = parse_attribute_value(attribute_part, "*=")
element_value = element.get_attribute(parsed.name)
return element_value.contains(parsed.value)
else:
# Exact match
parsed = parse_attribute_value(attribute_part, "=")
return element.get_attribute(parsed.name) == parsed.value
else:
# Just check if attribute exists
return element.has_attribute(attribute_part)
func parse_attribute_value(attribute_part: String, operator: String) -> Dictionary:
var attr_parts = attribute_part.split(operator)
var attr_name = attr_parts[0].strip_edges()
var attr_value = attr_parts[1].strip_edges()
# Remove quotes
if attr_value.begins_with('"') and attr_value.ends_with('"'):
attr_value = attr_value.substr(1, attr_value.length() - 2)
elif attr_value.begins_with("'") and attr_value.ends_with("'"):
attr_value = attr_value.substr(1, attr_value.length() - 2)
return {"name": attr_name, "value": attr_value}
func get_element_class_names(element: HTMLParser.HTMLElement) -> Array[String]:
var class_names: Array[String] = []
var class_attr = element.get_attribute("class")
if class_attr.length() > 0:
var classes = class_attr.split(" ")
for cls in classes:
cls = cls.strip_edges()
if cls.length() > 0:
class_names.append(cls)
return class_names
var stylesheet: CSSStylesheet
var css_text: String
func init(css_content: String = ""):
stylesheet = CSSStylesheet.new()
css_text = css_content
func parse() -> void:
if css_text.is_empty():
return
var cleaned_css = preprocess_css(css_text)
var rules = extract_rules(cleaned_css)
for rule_data in rules:
var rule = parse_rule(rule_data)
if rule:
stylesheet.add_rule(rule)
func preprocess_css(css: String) -> String:
# Remove comments
var regex = RegEx.new()
regex.compile("/\\*.*?\\*/")
css = regex.sub(css, "", true)
# Normalize whitespace
regex.compile("\\s+")
css = regex.sub(css, " ", true)
return css.strip_edges()
func extract_rules(css: String) -> Array:
var rules = []
var current_pos = 0
while current_pos < css.length():
var brace_start = css.find("{", current_pos)
if brace_start == -1:
break
var brace_end = find_matching_brace(css, brace_start)
if brace_end == -1:
break
var selector_part = css.substr(current_pos, brace_start - current_pos).strip_edges()
var properties_part = css.substr(brace_start + 1, brace_end - brace_start - 1).strip_edges()
# Handle multiple selectors separated by commas
var selectors = selector_part.split(",")
for selector in selectors:
rules.append({
"selector": selector.strip_edges(),
"properties": properties_part
})
current_pos = brace_end + 1
return rules
func find_matching_brace(css: String, start_pos: int) -> int:
var brace_count = 0
var pos = start_pos
while pos < css.length():
match css[pos]:
"{":
brace_count += 1
"}":
brace_count -= 1
if brace_count == 0:
return pos
pos += 1
return -1
func parse_rule(rule_data: Dictionary) -> CSSRule:
var rule = CSSRule.new()
rule.selector = rule_data.selector
rule.init(rule.selector)
var properties_text = rule_data.properties
var utility_classes = properties_text.split(" ")
for utility_name in utility_classes:
utility_name = utility_name.strip_edges()
if utility_name.is_empty():
continue
parse_utility_class(rule, utility_name)
return rule
func parse_utility_class(rule: CSSRule, utility_name: String) -> void:
var pseudo_classes = ["hover", "active"]
for pseudo in pseudo_classes:
var prefix = pseudo + ":"
if utility_name.begins_with(prefix):
var actual_utility = utility_name.substr(prefix.length())
var pseudo_rule = CSSRule.new()
pseudo_rule.selector = rule.selector + ":" + pseudo
pseudo_rule.init(pseudo_rule.selector)
parse_utility_class_internal(pseudo_rule, actual_utility)
stylesheet.add_rule(pseudo_rule)
return
# Fallback to normal parsing
parse_utility_class_internal(rule, utility_name)
# Parses a utility class (e.g. "text-red-500") and adds properties to the rule (e.g. "color: red")
# Used as a translation layer for Tailwind-like utility classes, as it becomes easier to manage these programmatically
static func parse_utility_class_internal(rule: CSSRule, utility_name: String) -> void:
# Handle color classes like text-[#ff0000]
if utility_name.begins_with("text-[") and utility_name.ends_with("]"):
var color_value = SizeUtils.extract_bracket_content(utility_name, 5) # after 'text-'
rule.properties["color"] = ColorUtils.parse_color(color_value)
return
# Handle standard text color classes like text-white, text-black, etc.
# But exclude text alignment classes
if utility_name.begins_with("text-") and not utility_name in ["text-left", "text-center", "text-right", "text-justify"]:
var color_name = utility_name.substr(5) # after 'text-'
var color = ColorUtils.get_color(color_name)
if color != null:
rule.properties["color"] = color
return
# Handle background color classes like bg-[#ff0000]
if utility_name.begins_with("bg-[") and utility_name.ends_with("]"):
var color_value = SizeUtils.extract_bracket_content(utility_name, 3) # after 'bg-'
var color = ColorUtils.parse_color(color_value)
rule.properties["background-color"] = color
return
# Handle standard background color classes like bg-white, bg-black, etc.
if utility_name.begins_with("bg-"):
var color_name = utility_name.substr(3) # after 'bg-'
var color = ColorUtils.get_color(color_name)
if color != null:
rule.properties["background-color"] = color
return
# e.g. max-w-[123px], w-[50%], h-[2rem]
if utility_name.match("^max-w-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 6)
rule.properties["max-width"] = val
return
if utility_name.match("^max-h-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 6)
rule.properties["max-height"] = val
return
if utility_name.match("^min-w-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 6)
rule.properties["min-width"] = val
return
if utility_name.match("^min-h-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 6)
rule.properties["min-height"] = val
return
if utility_name.match("^w-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 2)
rule.properties["width"] = val
return
if utility_name.match("^h-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 2)
rule.properties["height"] = val
return
# Handle font weight
if utility_name == "font-bold":
rule.properties["font-bold"] = true
return
# Handle font family
if utility_name == "font-sans":
rule.properties["font-family"] = "sans-serif"
return
if utility_name == "font-serif":
rule.properties["font-family"] = "serif"
return
if utility_name == "font-mono":
rule.properties["font-family"] = "monospace"
rule.properties["font-mono"] = true
return
var reserved_font_styles = ["font-sans", "font-serif", "font-mono", "font-bold", "font-italic"]
# Handle custom font families like font-roboto
if utility_name.begins_with("font-") and not utility_name in reserved_font_styles:
var font_name = utility_name.substr(5) # after 'font-'
rule.properties["font-family"] = font_name
return
# Handle font style italic
if utility_name == "font-italic":
rule.properties["font-italic"] = true
return
# Handle underline
if utility_name == "underline":
rule.properties["underline"] = true
return
# Handle text size classes
match utility_name:
"text-xs": rule.properties["font-size"] = 12
"text-sm": rule.properties["font-size"] = 14
"text-base": rule.properties["font-size"] = 16
"text-lg": rule.properties["font-size"] = 18
"text-xl": rule.properties["font-size"] = 20
"text-2xl": rule.properties["font-size"] = 24
"text-3xl": rule.properties["font-size"] = 30
"text-4xl": rule.properties["font-size"] = 36
"text-5xl": rule.properties["font-size"] = 48
"text-6xl": rule.properties["font-size"] = 60
# Handle text alignment classes
"text-left": rule.properties["text-align"] = "left"
"text-center": rule.properties["text-align"] = "center"
"text-right": rule.properties["text-align"] = "right"
"text-justify": rule.properties["text-align"] = "justify"
# Width
if utility_name.begins_with("w-"):
var val = utility_name.substr(2)
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
rule.properties["width"] = SizeUtils.parse_size(val)
return
# Height
if utility_name.begins_with("h-"):
var val = utility_name.substr(2)
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
rule.properties["height"] = SizeUtils.parse_size(val)
return
# Min width
if utility_name.begins_with("min-w-"):
var val = utility_name.substr(6)
rule.properties["min-width"] = SizeUtils.parse_size(val)
return
# Min height
if utility_name.begins_with("min-h-"):
var val = utility_name.substr(6)
rule.properties["min-height"] = SizeUtils.parse_size(val)
return
# Max width
if utility_name.begins_with("max-w-"):
var val = utility_name.substr(6)
rule.properties["max-width"] = SizeUtils.parse_size(val)
return
# Max height
if utility_name.begins_with("max-h-"):
var val = utility_name.substr(6)
rule.properties["max-height"] = SizeUtils.parse_size(val)
return
# Flex container
if utility_name == "flex":
rule.properties["display"] = "flex"
return
if utility_name == "inline-flex":
rule.properties["display"] = "inline-flex"
return
# Flex direction
match utility_name:
"flex-row": rule.properties["flex-direction"] = "row"; return
"flex-row-reverse": rule.properties["flex-direction"] = "row-reverse"; return
"flex-col": rule.properties["flex-direction"] = "column"; return
"flex-col-reverse": rule.properties["flex-direction"] = "column-reverse"; return
# Flex wrap
match utility_name:
"flex-nowrap": rule.properties["flex-wrap"] = "nowrap"; return
"flex-wrap": rule.properties["flex-wrap"] = "wrap"; return
"flex-wrap-reverse": rule.properties["flex-wrap"] = "wrap-reverse"; return
# Justify content
match utility_name:
"justify-start": rule.properties["justify-content"] = "flex-start"; return
"justify-end": rule.properties["justify-content"] = "flex-end"; return
"justify-center": rule.properties["justify-content"] = "center"; return
"justify-between": rule.properties["justify-content"] = "space-between"; return
"justify-around": rule.properties["justify-content"] = "space-around"; return
"justify-evenly": rule.properties["justify-content"] = "space-evenly"; return
# Align items
match utility_name:
"items-start": rule.properties["align-items"] = "flex-start"; return
"items-end": rule.properties["align-items"] = "flex-end"; return
"items-center": rule.properties["align-items"] = "center"; return
"items-baseline": rule.properties["align-items"] = "baseline"; return
"items-stretch": rule.properties["align-items"] = "stretch"; return
# Align content
match utility_name:
"content-start": rule.properties["align-content"] = "flex-start"; return
"content-end": rule.properties["align-content"] = "flex-end"; return
"content-center": rule.properties["align-content"] = "center"; return
"content-between": rule.properties["align-content"] = "space-between"; return
"content-around": rule.properties["align-content"] = "space-around"; return
"content-stretch": rule.properties["align-content"] = "stretch"; return
# Gap
if utility_name.begins_with("gap-"):
var val = utility_name.substr(4)
rule.properties["gap"] = SizeUtils.parse_size(val)
return
if utility_name.begins_with("row-gap-"):
var val = utility_name.substr(8)
rule.properties["row-gap"] = SizeUtils.parse_size(val)
return
if utility_name.begins_with("col-gap-"):
var val = utility_name.substr(8)
rule.properties["column-gap"] = SizeUtils.parse_size(val)
return
# FLEX ITEM PROPERTIES
if utility_name.begins_with("flex-grow-"):
var val = utility_name.substr(10)
rule.properties["flex-grow"] = val.to_float()
return
if utility_name.begins_with("flex-shrink-"):
var val = utility_name.substr(12)
rule.properties["flex-shrink"] = val.to_float()
return
if utility_name.begins_with("basis-"):
var val = utility_name.substr(6)
rule.properties["flex-basis"] = SizeUtils.parse_size(val)
return
# Align self
match utility_name:
"self-auto": rule.properties["align-self"] = "auto"; return
"self-start": rule.properties["align-self"] = "flex-start"; return
"self-end": rule.properties["align-self"] = "flex-end"; return
"self-center": rule.properties["align-self"] = "center"; return
"self-stretch": rule.properties["align-self"] = "stretch"; return
"self-baseline": rule.properties["align-self"] = "baseline"; return
# Order
if utility_name.begins_with("order-"):
var val = utility_name.substr(6)
rule.properties["order"] = val.to_int()
return
if utility_name == "rounded":
rule.properties["border-radius"] = "4px" # Default rounded
return
# Handle padding classes like p-8, px-4, py-2, etc.
if utility_name.begins_with("p-"):
var val = utility_name.substr(2)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding"] = padding_value
return
if utility_name.begins_with("px-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-left"] = padding_value
rule.properties["padding-right"] = padding_value
return
if utility_name.begins_with("py-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-top"] = padding_value
rule.properties["padding-bottom"] = padding_value
return
if utility_name.begins_with("pt-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-top"] = padding_value
return
if utility_name.begins_with("pr-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-right"] = padding_value
return
if utility_name.begins_with("pb-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-bottom"] = padding_value
return
if utility_name.begins_with("pl-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-left"] = padding_value
return
# Handle border radius classes like rounded, rounded-lg, rounded-[12px]
if utility_name == "rounded":
rule.properties["border-radius"] = "4px"
return
if utility_name == "rounded-none":
rule.properties["border-radius"] = "0px"
return
if utility_name == "rounded-sm":
rule.properties["border-radius"] = "2px"
return
if utility_name == "rounded-md":
rule.properties["border-radius"] = "6px"
return
if utility_name == "rounded-lg":
rule.properties["border-radius"] = "8px"
return
if utility_name == "rounded-xl":
rule.properties["border-radius"] = "12px"
return
if utility_name == "rounded-2xl":
rule.properties["border-radius"] = "16px"
return
if utility_name == "rounded-3xl":
rule.properties["border-radius"] = "24px"
return
if utility_name == "rounded-full":
rule.properties["border-radius"] = "9999px"
return
# Handle custom border radius like rounded-[12px]
if utility_name.begins_with("rounded-[") and utility_name.ends_with("]"):
var radius_value = SizeUtils.extract_bracket_content(utility_name, 8) # after 'rounded-'
rule.properties["border-radius"] = radius_value
return
# Handle numeric border radius classes like rounded-8, rounded-12, etc.
if utility_name.begins_with("rounded-"):
var val = utility_name.substr(8)
if val.is_valid_int():
rule.properties["border-radius"] = str(int(val)) + "px"
return
# Handle margin auto classes for centering
if utility_name == "mx-auto":
rule.properties["mx-auto"] = true
return
if utility_name == "my-auto":
rule.properties["my-auto"] = true
return
if utility_name == "m-auto":
rule.properties["mx-auto"] = true
rule.properties["my-auto"] = true
return
# Apply border properties
var apply_border = func(side: String, width: String = "", color = null, style: String = "solid"):
var prefix = "border" + ("-" + side if side != "" else "")
if width != "":
rule.properties[prefix + "-width"] = width
if color != null:
rule.properties[prefix + "-color"] = color
if style != "":
rule.properties[prefix + "-style"] = style
# Handle border utilities
if utility_name == "border":
apply_border.call("", "1px", Color.BLACK)
return
if utility_name == "border-none":
rule.properties["border-style"] = "none"
return
# Individual border sides - pattern: border-{side}-{value}
var border_sides = ["t", "r", "b", "l"]
var side_map = {"t": "top", "r": "right", "b": "bottom", "l": "left"}
for side in border_sides:
var short_side = side
var full_side = side_map[side]
# Basic side border (e.g., border-t)
if utility_name == "border-" + short_side:
apply_border.call(full_side, "1px")
return
# Side with value (e.g., border-t-2, border-t-red-500)
if utility_name.begins_with("border-" + short_side + "-"):
var val = utility_name.substr(9) # after "border-X-"
# Check for bracket notation first
if utility_name.begins_with("border-" + short_side + "-[") and utility_name.ends_with("]"):
var value = SizeUtils.extract_bracket_content(utility_name, 9)
if value.begins_with("#") or ColorUtils.parse_color(value) != null:
apply_border.call(full_side, "", ColorUtils.parse_color(value))
else:
apply_border.call(full_side, value)
return
# Check if it's a numeric width
if val.is_valid_int():
apply_border.call(full_side, str(int(val)) + "px")
return
# Check if it's a color
var color = ColorUtils.get_color(val)
if color != null:
apply_border.call(full_side, "", color)
return
# General border width (e.g., border-2)
if utility_name.begins_with("border-"):
var val = utility_name.substr(7)
# Custom border width like border-[2px]
if utility_name.begins_with("border-[") and utility_name.ends_with("]"):
var value = SizeUtils.extract_bracket_content(utility_name, 7)
if value.begins_with("#"):
apply_border.call("", "", ColorUtils.parse_color(value))
else:
apply_border.call("", value)
return
# Numeric width
if val.is_valid_int():
apply_border.call("", str(int(val)) + "px")
return
# Color name
var color = ColorUtils.get_color(val)
if color != null:
apply_border.call("", "", color)
return
# Handle cursor classes like cursor-pointer, cursor-default, cursor-text, etc.
if utility_name.begins_with("cursor-"):
var cursor_type = utility_name.substr(7) # after 'cursor-'
rule.properties["cursor"] = cursor_type
return
# Handle z-index classes like z-10, z-50, z-[999]
if utility_name.begins_with("z-"):
var val = utility_name.substr(2)
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
rule.properties["z-index"] = val.to_int()
return
# Handle opacity classes like opacity-50, opacity-75, opacity-[0.5]
if utility_name.begins_with("opacity-"):
var val = utility_name.substr(8) # after 'opacity-'
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
rule.properties["opacity"] = val.to_float()
elif val.is_valid_int():
# Convert percentage (0-100) to decimal (0.0-1.0)
rule.properties["opacity"] = val.to_int() / 100.0
return
# Handle more utility classes as needed
# Add more cases here for other utilities
static func parse_inline_style(style_string: String) -> Dictionary:
var rule = CSSRule.new()
rule.selector = ""
rule.init(rule.selector)
var utility_classes = style_string.split(" ")
for utility_name in utility_classes:
utility_name = utility_name.strip_edges()
if utility_name.is_empty():
continue
parse_utility_class_internal(rule, utility_name)
return rule.properties

View File

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

View File

@@ -0,0 +1,405 @@
class_name HTMLParser
extends Node
class HTMLElement:
var tag_name: String
var attributes: Dictionary = {}
var text_content: String = ""
var children: Array[HTMLElement] = []
var parent: HTMLElement = null
var is_self_closing: bool = false
func _init(tag: String = ""):
tag_name = tag
func get_attribute(name_: String, default: String = "") -> String:
return attributes.get(name_, default)
func has_attribute(name_: String) -> bool:
return attributes.has(name_)
func get_class_name() -> String:
return get_attribute("class")
func get_id() -> String:
return get_attribute("id")
func get_collapsed_text() -> String:
var collapsed = 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
func get_bbcode_formatted_text(parser: HTMLParser = null) -> String:
var styles = {}
if parser != null:
styles = parser.get_element_styles_with_inheritance(self, "", [])
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"]
class ParseResult:
var root: HTMLElement
var all_elements: Array[HTMLElement] = []
var errors: Array[String] = []
var css_parser: CSSParser = null
var inline_styles: Dictionary = {}
func _init():
root = HTMLElement.new("document")
# Properties
var xml_parser: XMLParser
var bitcode: PackedByteArray
var parse_result: ParseResult
func _init(data: PackedByteArray):
bitcode = data
xml_parser = XMLParser.new()
parse_result = ParseResult.new()
# Main parsing function
func parse() -> ParseResult:
xml_parser.open_buffer(bitcode)
var element_stack: Array[HTMLElement] = [parse_result.root]
while xml_parser.read() != ERR_FILE_EOF:
match xml_parser.get_node_type():
XMLParser.NODE_ELEMENT:
var element = create_element()
var current_parent = element_stack.back()
element.parent = current_parent
current_parent.children.append(element)
parse_result.all_elements.append(element)
if element.tag_name == "style":
handle_style_element(element)
if not element.is_self_closing:
element_stack.append(element)
XMLParser.NODE_ELEMENT_END:
if element_stack.size() > 1:
element_stack.pop_back()
XMLParser.NODE_TEXT:
var text = xml_parser.get_node_data().strip_edges()
if text.length() > 0 and element_stack.size() > 0:
element_stack.back().text_content += text
return parse_result
func handle_style_element(style_element: HTMLElement) -> void:
# Check if it's an external stylesheet
var src = style_element.get_attribute("src")
if src.length() > 0:
# TODO: Handle external CSS loading when Network module is available
print("External CSS not yet supported: " + src)
return
# Handle inline CSS - we'll get the text content when parsing is complete
# For now, create a parser that will be populated later
if not parse_result.css_parser:
parse_result.css_parser = CSSParser.new()
parse_result.css_parser.init()
func process_styles() -> void:
if not parse_result.css_parser:
return
# Collect all style element content
var css_content = Constants.DEFAULT_CSS
var style_elements = find_all("style")
for style_element in style_elements:
if style_element.get_attribute("src").is_empty():
css_content += style_element.text_content + "\n"
print("Processing CSS: ", css_content)
# Parse CSS if we have any
if css_content.length() > 0:
parse_result.css_parser.css_text = css_content
parse_result.css_parser.parse()
for child: CSSParser.CSSRule in parse_result.css_parser.stylesheet.rules:
print("INFO: for selector \"%s\" we have props: %s" % [child.selector, child.properties])
func get_element_styles_with_inheritance(element: HTMLElement, event: String = "", visited_elements: Array = []) -> Dictionary:
# Prevent infinite recursion
if element in visited_elements:
return {}
visited_elements.append(element)
var styles = {}
var class_names = get_css_class_names(element)
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names, element))
# Apply inline styles (higher priority) - force override CSS rules
var inline_style = element.get_attribute("style")
if inline_style.length() > 0:
var inline_parsed = CSSParser.parse_inline_style(inline_style)
for property in inline_parsed:
styles[property] = inline_parsed[property]
# Inherit certain properties from parent elements
var inheritable_properties = ["width", "height", "font-size", "color", "font-family"]
var parent_element = element.parent
while parent_element:
var parent_styles = get_element_styles_internal(parent_element, event)
for property in inheritable_properties:
# Only inherit if child doesn't already have this property
if not styles.has(property) and parent_styles.has(property):
styles[property] = parent_styles[property]
parent_element = parent_element.parent
return styles
func get_element_styles_internal(element: HTMLElement, event: String = "") -> Dictionary:
var styles = {}
# Apply CSS rules
if parse_result.css_parser:
var class_names = get_css_class_names(element)
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names, element))
# Apply inline styles (higher priority) - force override CSS rules
var inline_style = element.get_attribute("style")
if inline_style.length() > 0:
var inline_parsed = parse_inline_style_with_event(inline_style, event)
for property in inline_parsed:
styles[property] = inline_parsed[property] # Force override
return styles
func parse_inline_style_with_event(style_string: String, event: String = "") -> Dictionary:
var properties = {}
# Split style string into individual utility classes
var utility_classes = style_string.split(" ") # e.g. ["bg-red-500, "text-lg", "hover:bg-blue-500"]
for utility_name in utility_classes:
utility_name = utility_name.strip_edges() # e.g. "bg-red-500"
if utility_name.is_empty():
continue
# Check if this utility is for the requested event
if event.length() > 0:
if utility_name.begins_with(event + ":"): # e.g. "hover:bg-blue-500"
var actual_utility = utility_name.substr(event.length() + 1) # bg-blue-500
var rule = CSSParser.CSSRule.new()
CSSParser.parse_utility_class_internal(rule, actual_utility)
for property in rule.properties:
properties[property] = rule.properties[property]
else:
if not utility_name.contains(":"):
var rule = CSSParser.CSSRule.new()
CSSParser.parse_utility_class_internal(rule, utility_name)
for property in rule.properties:
properties[property] = rule.properties[property]
return properties
func get_css_class_names(element: HTMLElement) -> Array[String]:
var class_names: Array[String] = []
var class_attr = element.get_attribute("class")
if class_attr.length() > 0:
var classes = class_attr.split(" ")
for cls in classes:
cls = cls.strip_edges()
if cls.length() > 0:
class_names.append(cls)
return class_names
func extract_class_names_from_style(element: HTMLElement) -> Array[String]:
var class_names: Array[String] = []
var style_attr = element.get_attribute("style")
if style_attr.length() > 0:
var style_tokens = style_attr.split(" ")
for token in style_tokens:
token = token.strip_edges()
if token.length() > 0 and not UtilityClassValidator.is_utility_class(token):
class_names.append(token)
return class_names
# Creates element from CURRENT xml parser node
func create_element() -> HTMLElement:
var element = HTMLElement.new(xml_parser.get_node_name())
element.is_self_closing = xml_parser.is_empty()
# Parse attributes
for i in range(xml_parser.get_attribute_count()):
var attr_name = xml_parser.get_attribute_name(i)
var attr_value = xml_parser.get_attribute_value(i)
element.attributes[attr_name] = attr_value
return element
# Utility functions
func find_all(tag: String, attribute: String = "") -> Array[HTMLElement]:
if parse_result.all_elements.is_empty():
parse()
var results: Array[HTMLElement] = []
for element in parse_result.all_elements:
if element.tag_name == tag:
if attribute.is_empty() or element.has_attribute(attribute):
results.append(element)
return results
func find_all_by_class(tag: String, the_class_name: String) -> Array[HTMLElement]:
if parse_result.all_elements.is_empty():
parse()
var results: Array[HTMLElement] = []
for element in parse_result.all_elements:
if element.tag_name == tag and element.get_class_name() == the_class_name:
results.append(element)
return results
func find_by_id(element_id: String) -> HTMLElement:
if parse_result.all_elements.is_empty():
parse()
for element in parse_result.all_elements:
if element.get_id() == element_id:
return element
return null
func find_first(tag: String, attribute: String = "") -> HTMLElement:
var results = find_all(tag, attribute)
return results[0] if results.size() > 0 else null
# Extract attribute values
func get_attribute_values(tag: String, attribute: String) -> Array[String]:
var elements = find_all(tag, attribute)
var values: Array[String] = []
for element in elements:
var value = element.get_attribute(attribute)
if value.length() > 0:
values.append(value)
return values
func get_attribute_values_by_class(tag: String, the_class_name: String, attribute: String) -> Array[String]:
var elements = find_all_by_class(tag, the_class_name)
var values: Array[String] = []
for element in elements:
var value = element.get_attribute(attribute)
if value.length() > 0:
values.append(value)
return values
# Misc
func get_title() -> String:
var title_element = find_first("title")
return title_element.text_content if title_element != null else ""
func get_icon() -> String:
var icon_element = find_first("icon")
return icon_element.get_attribute("src") if icon_element != null else ""
func process_fonts() -> void:
var font_elements = find_all("font")
for font_element in font_elements:
var name = font_element.get_attribute("name")
var src = font_element.get_attribute("src")
var weight = font_element.get_attribute("weight", "400")
if name and src:
FontManager.register_font(name, src, weight)
func get_meta_content(name_: String) -> String:
var meta_elements = find_all("meta", "name")
for element in meta_elements:
if element.get_attribute("name") == name_:
return element.get_attribute("content")
return ""
func get_all_links() -> Array[String]:
return get_attribute_values("a", "href")
func get_all_images() -> Array[String]:
return get_attribute_values("img", "src")
func get_all_scripts() -> Array[String]:
return get_attribute_values("script", "src")
func get_all_stylesheets() -> Array[String]:
return get_attribute_values("style", "src")
func apply_element_styles(node: Control, element: HTMLElement, parser: HTMLParser) -> void:
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)
label.text = text
static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictionary, content: String) -> String:
match element.tag_name:
"b":
if styles.has("font-bold") and styles["font-bold"]:
return "[b]" + content + "[/b]"
"i":
if styles.has("font-italic") and styles["font-italic"]:
return "[i]" + content + "[/i]"
"u":
if styles.has("underline") and styles["underline"]:
return "[u]" + content + "[/u]"
"small":
if styles.has("font-size"):
return "[font_size=%d]%s[/font_size]" % [styles["font-size"], content]
else:
return "[font_size=20]%s[/font_size]" % content
"mark":
if styles.has("bg"):
var color = styles["bg"]
if typeof(color) == TYPE_COLOR:
color = color.to_html(false)
return "[bgcolor=#%s]%s[/bgcolor]" % [color, content]
else:
return "[bgcolor=#FFFF00]%s[/bgcolor]" % content
"code":
if styles.has("font-size"):
return "[font_size=%d][code]%s[/code][/font_size]" % [styles["font-size"], content]
else:
return "[font_size=20][code]%s[/code][/font_size]" % content
"a":
var href = element.get_attribute("href")
var color = "#1a0dab"
if styles.has("color"):
var c = styles["color"]
if typeof(c) == TYPE_COLOR:
color = "#" + c.to_html(false)
else:
color = str(c)
if href.length() > 0:
return "[color=%s][url=%s]%s[/url][/color]" % [color, href, content]
return content
static func get_bbcode_with_styles(element: HTMLElement, styles: Dictionary, parser: HTMLParser) -> String:
var text = ""
if element.text_content.length() > 0:
text += element.get_collapsed_text()
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_content = apply_element_bbcode_formatting(child, child_styles, child_content)
text += child_content
# Apply formatting to the current element itself
text = apply_element_bbcode_formatting(element, styles, text)
return text

View File

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

592
flumi/Scripts/Constants.gd Normal file
View File

@@ -0,0 +1,592 @@
extends Node
const MAIN_COLOR = Color(27/255.0, 27/255.0, 27/255.0, 1)
const SECONDARY_COLOR = Color(43/255.0, 43/255.0, 43/255.0, 1)
const HOVER_COLOR = Color(0, 0, 0, 1)
const DEFAULT_CSS = """
body { text-base text-[#000000] text-left }
h1 { text-5xl font-bold }
h2 { text-4xl font-bold }
h3 { text-3xl font-bold }
h4 { text-2xl font-bold }
h5 { text-xl font-bold }
b { font-bold }
i { font-italic }
u { underline }
small { text-xl }
mark { bg-[#FFFF00] }
code { text-xl font-mono }
a { text-[#1a0dab] }
pre { text-xl font-mono }
button { bg-[#1b1b1b] rounded-md text-white hover:bg-[#2a2a2a] active:bg-[#101010] }
button[disabled] { bg-[#666666] text-[#999999] cursor-not-allowed }
"""
var HTML_CONTENT2 = """<head>
<title>My Custom Dashboard</title>
<icon src="https://cdn-icons-png.flaticon.com/512/1828/1828774.png">
<meta name="theme-color" content="#1a202c">
<meta name="description" content="A stylish no-script dashboard">
<font name="roboto" src="https://fonts.gstatic.com/s/roboto/v48/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2" />
<style>
h1 { text-[#ffffff] text-3xl font-bold }
h2 { text-[#cbd5e1] text-xl }
p { text-[#94a3b8] text-base }
button { bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e] active:bg-[#15803d] }
.card { bg-[#1e293b] text-[#f8fafc] rounded-xl p-4 shadow-lg }
</style>
</head>
<body style="bg-[#0f172a] p-8 text-white font-roboto">
<h1 style="text-center mb-4">📊 My Dashboard</h1>
<!-- Top Summary Cards -->
<div style="flex flex-row gap-4 justify-center flex-wrap">
<div style="card w-48 h-24 flex flex-col justify-center items-center">
<h2 style="text-red-500">Users</h2>
<p>1,240</p>
</div>
<div style="card w-48 h-24 flex flex-col justify-center items-center">
<h2>Sales</h2>
<p>$9,842</p>
</div>
<div style="card w-48 h-24 flex flex-col justify-center items-center">
<h2>Visitors</h2>
<p>3,590</p>
</div>
</div>
<separator direction="horizontal" />
<!-- User Info Panel -->
<h2 style="text-center mt-6">👤 User Panel</h2>
<div style="flex flex-row gap-4 justify-center mt-2">
<div style="card w-64">
<p>Name: Jane Doe</p>
<p>Email: jane@example.com</p>
<p>Status: <span style="text-[#22c55e]">Active</span></p>
</div>
<div style="card w-64">
<p>Plan: Pro</p>
<p>Projects: 8</p>
<p>Tasks: 42</p>
</div>
</div>
<separator direction="horizontal" />
<!-- Recent Activity Log -->
<h2 style="text-center mt-6">📝 Recent Activity</h2>
<ul style="w-[80%] mt-2 flex justify-center flex-column gap-2">
<li style="bg-[#334155] px-4 py-2 rounded-xl mb-1">✅ Task "Update UI" marked as complete</li>
<li style="bg-[#334155] px-4 py-2 rounded-xl mb-1">🔔 New comment on "Bug Fix #224"</li>
<li style="bg-[#334155] px-4 py-2 rounded-xl mb-1">📤 Exported report "Q2 Metrics"</li>
</ul>
<separator direction="horizontal" />
<!-- Action Buttons -->
<h2 style="text-center mt-6">🔧 Actions</h2>
<div style="flex flex-row gap-2 justify-center mt-2">
<button style="rounded-lg px-4 py-2">Create Report</button>
<button style="rounded-lg px-4 py-2 bg-[#3b82f6] hover:bg-[#2563eb] active:bg-[#1e40af]">Invite User</button>
<button style="rounded-lg px-4 py-2 bg-[#facc15] text-[#000] hover:bg-[#eab308] active:bg-[#ca8a04]">Upgrade Plan</button>
</div>
</body>
""".to_utf8_buffer()
var HTML_CONTENT = """<head>
<title>My cool web</title>
<icon src=\"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/768px-Google_%22G%22_logo.svg.png\">
<meta name=\"theme-color\" content=\"#000000\">
<meta name=\"description\" content=\"My cool web\">
<font name="roboto" src="https://fonts.gstatic.com/s/roboto/v48/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2" />
<style>
h1 { text-[#ff0000] font-italic hover:text-[#00ff00] }
p { text-[#333333] text-2xl }
button { hover:bg-[#FF6B35] hover:text-[#FFFFFF] active:bg-[#CC5429] active:text-[#F0F0F0] }
</style>
<style src=\"styles.css\">
<script src=\"script.lua\" />
</head>
<body>
<h1>Header 1</h1>
<h2>Header 2</h2>
<h3>Header 3</h3>
<h4>Header 4</h4>
<h5>Header 5</h5>
<h6>Header 6</h6>
<separator />
<p>Normal font</p>
<p style="font-mono">Mono font</p>
<p style="font-sans">Sans font</p>
<p style="font-roboto">Custom font - Roboto</p>
<p>Hey there! this is a test</p>
<b>This is bold</b>
<i>This is italic <mark>actually, and it's pretty <u>cool</u></mark></i>
<u>This is underline</u>
<small>this is small</small>
<mark>this is marked</mark>
<code>this is code<span> THIS IS A SPAN AND SHOULDNT BE ANY DIFFERENT</span></code>
<p>
<a href="https://youtube.com">Hello gang</a>
</p>
<pre>
Text in a pre element
is displayed in a fixed-width
font, and it preserves
both spaces and
line breaks
</pre>
<p style="text-center w-32 h-32">
So
</p>
<div>
<button style="rounded-lg px-4 py-2 cursor-pointer">Create Report</button>
<button style="rounded-lg px-4 py-2 bg-[#3b82f6] hover:bg-[#2563eb] active:bg-[#1e40af] cursor-pointer">Invite User</button>
<button style="rounded-lg px-4 py-2 bg-[#facc15] text-[#000] hover:bg-[#eab308] active:bg-[#ca8a04] cursor-pointer">Upgrade Plan</button>
</div>
<button disabled="true">Disabled Button</button>
<button>Normal Button</button>
<separator direction="horizontal" />
<!-- Test CSS Properties -->
<h2 style="text-center mt-6">🧪 CSS Properties Test</h2>
<div style="flex flex-col gap-2 justify-center items-center mt-2">
<div style="bg-[#ef4444] text-white p-4 rounded-lg opacity-75 z-10 cursor-pointer">
<p>Opacity 75% with cursor pointer and z-index 10 - Text should show pointer cursor, not I-beam</p>
</div>
<div style="bg-[#10b981] text-white p-4 rounded-lg opacity-50 z-20 cursor-text">
<p>Opacity 50% with cursor text and z-index 20 - Text should show I-beam cursor</p>
</div>
<div style="bg-[#8b5cf6] text-white p-4 rounded-lg opacity-[0.25] z-[999] cursor-default">
<p>Custom opacity 0.25 with cursor default and z-index 999 - Text should show arrow cursor</p>
</div>
<div style="bg-[#f59e0b] text-white p-2 rounded cursor-move">
<p>Cursor move - Text should show move cursor</p>
</div>
<div style="bg-[#06b6d4] text-white p-2 rounded cursor-crosshair">
<p>Cursor crosshair - Text should show crosshair cursor</p>
</div>
<div style="bg-[#84cc16] text-white p-2 rounded cursor-help">
<p>Cursor help - Text should show help cursor</p>
</div>
<div style="bg-[#ec4899] text-white p-2 rounded cursor-not-allowed">
<p>Cursor not-allowed - Text should show forbidden cursor</p>
</div>
</div>
<separator direction="horizontal" />
<!-- Test cursor inheritance -->
<h2 style="text-center mt-6">🖱️ Cursor Inheritance Test</h2>
<div style="cursor-pointer bg-[#1e293b] p-4 rounded-lg">
<p>This paragraph is inside a div with cursor-pointer.</p>
<p>Both paragraphs should show pointer cursor instead of default I-beam.</p>
<div style="bg-[#334155] p-2 rounded mt-2">
<p>This nested paragraph should also inherit the pointer cursor.</p>
</div>
</div>
<!-- Border examples -->
<div style="border p-2 mb-2">border</div>
<div style="border-2 p-2 mb-2">border-2</div>
<div style="border-4 p-2 mb-2">border-4</div>
<div style="border-2 border-red-500 p-2 mb-2">border-2 border-red-500</div>
<div style="border p-2 mb-2">border-solid</div>
<div style="border border-dashed p-2 mb-2">border-dashed</div>
<div style="border border-dotted p-2 mb-2">border-dotted</div>
<div style="border-none p-2 mb-2">border-none</div>
<div style="border-t p-2 mb-2">border-t</div>
<div style="border-r p-2 mb-2">border-r</div>
<div style="border-b p-2 mb-2">border-b</div>
<div style="border-l p-2 mb-2">border-l</div>
<div style="border-t-4 p-2 mb-2">border-t-4</div>
<div style="border-b-2 p-2 mb-2">border-b-2</div>
<div style="border-l-6 p-2 mb-2">border-l-6</div>
<div style="border-t-3 border-green-500 p-2 mb-2">border-t-3 border-green-500</div>
<div style="border border-white p-2 mb-2">border-white</div>
<div style="border border-black p-2 mb-2">border-black</div>
<div style="border border-transparent p-2 mb-2">border-transparent</div>
<div style="border border-gray-400 p-2 mb-2">border-gray-400</div>
<div style="border border-slate-700 p-2 mb-2">border-slate-700</div>
<div style="border border-red-500 p-2 mb-2">border-red-500</div>
<div style="border border-green-600 p-2 mb-2">border-green-600</div>
<div style="border border-blue-400 p-2 mb-2">border-blue-400</div>
<div style="border border-yellow-300 p-2 mb-2">border-yellow-300</div>
<select style=\"text-center max-w-5 max-h-32\">
<option value=\"test1\">Test 1</option>
<option value=\"test2\" selected=\"true\">Test 2</option>
<option value=\"test3\">Test 3</option>
<option value=\"test4\" disabled=\"true\">Test 4</option>
<option value=\"test5\">Test 5</option>
</select>
<textarea />
<textarea cols=\"30\" />
<textarea rows=\"2\" />
<textarea maxlength=\"20\" />
<textarea readonly=\"true\">le skibidi le toilet</textarea>
<textarea disabled=\"true\" value=\"DISABLED\" />
<textarea placeholder=\"this is a placeholder...\" />
<!-- action, method, and type=submit are for when we implement Lua -->
<form action=\"/submit\" method=\"POST\">
<span>Name:</span>
<input type=\"text\" placeholder=\"First name\" value=\"John\" maxlength=\"20\" minlength=\"3\" />
<span>Email regex:</span>
<input type=\"text\" placeholder=\"Last name\" value=\"Doe\" pattern=\"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$\" />
<span>Smart:</span>
<input type=\"checkbox\" />
<input type=\"checkbox\" value=\"true\" />
<p>favorite food</p>
<input type=\"radio\" group=\"food\" />
<span>Pizza</span>
<input type=\"radio\" group=\"food\" />
<span>Berry</span>
<input type=\"radio\" group=\"food\" />
<span>Gary</span>
<h2>Color</h2>
<input type=\"color\" value=\"#ff0000\" />
<h2>Date</h2>
<input type=\"date\" value=\"2018-07-22\" />
<h2>Range Slider</h2>
<input style=\"max-w-2 max-h-2\" type=\"range\" min=\"0\" max=\"100\" step=\"5\" value=\"50\" />
<h2>Number Input</h2>
<input type=\"number\" min=\"1\" max=\"10\" step=\"0.5\" value=\"5\" placeholder=\"Enter number\" />
<h2>File Upload</h2>
<input type=\"file\" accept=\".txt,.pdf,image/*\" />
</form>
<separator direction=\"horizontal\" />
# Ordered list
<ol>
<li>hello gang</li>
<li>this</li>
<li>is</li>
</ol>
<ol type=\"zero-lead\">
<li>hello gang</li>
<li>this</li>
<li>is</li>
<li>a test</li>
</ol>
<ol type=\"lower-alpha\">
<li>hello gang</li>
<li>this</li>
<li>is</li>
<li>a test</li>
</ol>
<ol type=\"upper-alpha\">
<li>hello gang</li>
<li>this</li>
<li>is</li>
<li>a test</li>
</ol>
<ol type=\"lower-roman\">
<li>hello gang</li>
<li>this</li>
<li>is</li>
<li>a test</li>
</ol>
<ol type=\"upper-roman\">
<li>hello gang</li>
<li>this</li>
<li>is</li>
<li>a test</li>
</ol>
<ul>
<li>hello gang</li>
<li>this</li>
<li>is</li>
<li>a test</li>
</ul>
<ul type=\"circle\">
<li>hello gang</li>
<li>this</li>
<li>is</li>
<li>a test</li>
</ul>
<ul type=\"none\">
<li>hello gang</li>
<li>this</li>
<li>is</li>
<li>a test</li>
</ul>
<ul type=\"square\">
<li>hello gang</li>
<li>this</li>
<li>is</li>
<li>a test</li>
</ul>
<img style=\"text-center max-w-24 max-h-24\" src=\"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMNUPIKabszX0Js_c0kfa4cz_JQYKfGTuBUA&s\" />
<separator direction=\"vertical\" />
<!-- FLEXBOX EXAMPLES -->
<h2>Flex Row (gap, justify-between, items-center)</h2>
<div style=\"flex flex-row gap-4 justify-between items-center w-64 h-16 bg-[#f0f0f0]\">
<span style=\"bg-[#ffaaaa] w-16 h-8 flex items-center justify-center\">A</span>
<span style=\"bg-[#aaffaa] w-16 h-8 flex items-center justify-center\">B</span>
<span style=\"bg-[#aaaaff] w-16 h-8 flex items-center justify-center\">C</span>
</div>
<h2>Flex Column (gap, items-center, content-center)</h2>
<div style=\"flex flex-col gap-2 items-center content-center h-32 w-32 bg-[#e0e0e0]\">
<span style=\"bg-[#ffaaaa] w-16 h-6 flex items-center justify-center\">1</span>
<span style=\"bg-[#aaffaa] w-16 h-6 flex items-center justify-center\">2</span>
<span style=\"bg-[#aaaaff] w-16 h-6 flex items-center justify-center\">3</span>
</div>
<h2>Flex Wrap (row, wrap, gap)</h2>
<div style=\"flex flex-row flex-wrap gap-2 w-40 bg-[#f8f8f8]\">
<span style=\"bg-[#ffaaaa] w-16 h-6 flex items-center justify-center\">X</span>
<span style=\"bg-[#aaffaa] w-16 h-6 flex items-center justify-center\">Y</span>
<span style=\"bg-[#aaaaff] w-16 h-6 flex items-center justify-center\">Z</span>
<span style=\"bg-[#ffffaa] w-16 h-6 flex items-center justify-center\">W</span>
</div>
<h2>Flex Grow/Shrink/Basis</h2>
<div style=\"flex flex-row gap-2 w-64 bg-[#f0f0f0]\">
<span style=\"bg-[#ffaaaa] flex-grow-1 h-8 flex items-center justify-center\">Grow 1</span>
<span style=\"bg-[#aaffaa] flex-grow-2 h-8 flex items-center justify-center\">Grow 2</span>
<span style=\"bg-[#aaaaff] flex-shrink-0 w-8 h-8 flex items-center justify-center\">No Shrink</span>
</div>
<h2>Align Self</h2>
<div style=\"flex flex-row h-24 bg-[#f0f0f0] items-stretch gap-2 w-64\">
<span style=\"bg-[#ffaaaa] w-12 h-8 self-start flex items-center justify-center\">Start</span>
<span style=\"bg-[#aaffaa] w-12 h-8 self-center flex items-center justify-center\">Center</span>
<span style=\"bg-[#aaaaff] w-12 h-8 self-end flex items-center justify-center\">End</span>
<span style=\"bg-[#ffffaa] w-12 h-8 self-stretch flex items-center justify-center\">Stretch</span>
</div>
</body>""".to_utf8_buffer()
var HTML_CONTENT_S = """<head>
<title>CSS Selector Tests</title>
<style>
/* Descendant selectors */
div p { text-[#663399] }
.container span { bg-[#ffeeaa] }
/* Direct child selectors */
.outer-div > p { font-bold }
.parent > button { bg-[#44cc88] }
/* Adjacent sibling selectors */
h1 + p { text-[#ff0000] font-bold }
h2 + div { bg-[#eeffee] }
/* General sibling selectors */
h1 ~ p { text-[#0000ff] }
h1 ~ .second-p { text-[#0000ff] }
h3 ~ span { bg-[#ffdddd] }
/* Attribute selectors */
input[type="text"] { border border-[#cccccc] bg-[#f9f9f9] }
a[href^="https"] { text-[#008000] font-bold }
button[disabled] { bg-[#888888] text-[#cccccc] }
input[placeholder*="email"] { border-2 border-[#0066cc] bg-[#ffffff] }
div[class$="special"] { bg-[#ffffaa] }
</style>
</head>
<body>
<h1>CSS Selector Test Page</h1>
<p>This paragraph should be red and bold (h1 + p)</p>
<p class="second-p">This paragraph should be blue (h1 ~ p)</p>
<h2>Descendant vs Child Selectors</h2>
<div class="outer-div">
<p>This paragraph should be purple and bold (div p and .outer-div > p)</p>
<div>
<p>This paragraph should be purple but not bold (div p only)</p>
</div>
</div>
<h3>Attribute Selectors</h3>
<input type="text" placeholder="Enter your name" />
<input type="text" placeholder="Enter your email address" />
<input type="password" placeholder="Enter password" />
<br />
<a href="http://example.com">HTTP Link (normal)</a>
<br />
<a href="https://secure.com">HTTPS Link (green and bold)</a>
<br />
<button>Normal Button</button>
<button disabled="true">Disabled Button (gray)</button>
<h3>Sibling Selectors</h3>
<div style="bg-[#eeffee]">This div should have light green bg (h2 + div)</div>
<span>This span should have light red bg (h3 ~ span)</span>
<span>This span should also have light red bg (h3 ~ span)</span>
<div class="container">
<span>This span should have yellow bg (.container span)</span>
<p>Regular paragraph in container</p>
</div>
<div class="parent">
<button>This button should be green (.parent > button)</button>
<div>
<button>This button should be normal (not direct child)</button>
</div>
</div>
<div class="item-special">This div should have yellow bg (class ends with 'special')</div>
<div class="special-item">This div should be normal</div>
</body>
""".to_utf8_buffer()
var HTML_CONTENT3 = """<head>
<title>Task Manager</title>
<icon src="https://cdn-icons-png.flaticon.com/512/126/126472.png">
<meta name="theme-color" content="#1e1e2f">
<meta name="description" content="Manage your tasks easily.">
<style>
h1 { text-[#4ade80] text-3xl font-bold }
p { text-[#94a3b8] text-lg }
input { border border-[#cbd5e1] px-2 py-1 rounded }
</style>
<script src="logic.lua" />
</head>
<body>
<h1 style="text-center">📝 My Task Manager</h1>
<p style="text-center mb-4">Keep track of your to-do list</p>
<!-- Task List -->
<div style="flex flex-col gap-2 w-80 mx-auto bg-[#f8fafc] p-4 rounded">
<span style="flex justify-between items-center bg-[#e2e8f0] px-2 py-1 rounded">
<span>✅ Finish homework</span>
<button style="bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e]">Delete</button>
</span>
<span style="flex justify-between items-center bg-[#e2e8f0] px-2 py-1 rounded">
<span>✍️ Write blog post</span>
<button style="bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e]">Delete</button>
</span>
<span style="flex justify-between items-center bg-[#e2e8f0] px-2 py-1 rounded">
<span>💪 Gym workout</span>
<button style="bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e]">Delete</button>
</span>
</div>
<separator direction="horizontal" />
<!-- Add New Task -->
<h2 style="text-center mt-4">Add a New Task</h2>
<form action="/add-task" method="POST" style="flex flex-col gap-2 w-80 mx-auto">
<input type="text" placeholder="Enter task..." minlength="3" required="true" />
<input type="date" />
<button type="submit" style="bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e]">Add Task</button>
</form>
<separator direction="horizontal" />
<h2 style="text-center">Task Categories</h2>
<div style="flex flex-row gap-2 justify-center items-center w-full">
<span style="bg-[#fef3c7] px-4 py-2 rounded">📚 Study</span>
<span style="bg-[#d1fae5] px-4 py-2 rounded">💼 Work</span>
<span style="bg-[#e0e7ff] px-4 py-2 rounded">🏋️ Health</span>
</div>
<form>
<input type=\"password\" placeholder=\"your password...\" />
<button type=\"submit\" style=\"bg-[#4CAF50] rounded-lg text-[#FFFFFF]\">Submit</button>
<button style=\"bg-[#2196F3] rounded-xl text-[#FFFFFF]\">Blue Button</button>
<button style=\"bg-[#FF5722] rounded-full text-[#FFFFFF]\">Orange Pill</button>
<button style=\"bg-[#9C27B0] rounded-[20px] text-[#FFFFFF]\">Purple Custom</button>
<button style=\"bg-[#FFD700] rounded text-[#000000] hover:bg-[#FFA500] hover:text-[#FFFFFF]\">Hover Test</button>
</form>
<h2>Button Style Tests</h2>
<button>Normal, no-styling button.</button>
<h3>Corner Radius Variants</h3>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-none\">No Radius</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-sm\">Small (2px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded\">Default (4px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-md\">Medium (6px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-lg\">Large (8px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-xl\">Extra Large (12px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-2xl\">2XL (16px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-3xl\">3XL (24px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-full\">Full (Pill)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-[30px]\">Custom 30px</button>
<h3>Color Combinations</h3>
<button style=\"bg-[#FF6B6B] text-[#FFFFFF] rounded-lg\">Red Background</button>
<button style=\"bg-[#4ECDC4] text-[#2C3E50] rounded-lg\">Teal & Dark Text</button>
<button style=\"bg-[#45B7D1] text-[#FFFFFF] rounded-lg\">Sky Blue</button>
<button style=\"bg-[#96CEB4] text-[#2C3E50] rounded-lg\">Mint Green</button>
<button style=\"bg-[#FFEAA7] text-[#2D3436] rounded-lg\">Yellow Cream</button>
<button style=\"bg-[#DDA0DD] text-[#FFFFFF] rounded-lg\">Plum Purple</button>
<button style=\"bg-[#98D8C8] text-[#2C3E50] rounded-lg\">Seafoam</button>
<h3>Hover Effects</h3>
<button style=\"bg-[#3498DB] text-[#FFFFFF] rounded-lg hover:bg-[#2980B9] hover:text-[#F8F9FA]\">Blue Hover</button>
<button style=\"bg-[#E67E22] text-[#FFFFFF] rounded-xl hover:bg-[#D35400] hover:text-[#ECF0F1]\">Orange Hover</button>
<button style=\"bg-[#9B59B6] text-[#FFFFFF] rounded-full hover:bg-[#8E44AD] hover:text-[#F4F4F4]\">Purple Pill Hover</button>
<button style=\"bg-[#1ABC9C] text-[#FFFFFF] rounded-2xl hover:bg-[#16A085]\">Turquoise Hover</button>
<h3>Advanced Hover Combinations</h3>
<button style=\"bg-[#34495E] text-[#ECF0F1] rounded hover:bg-[#E74C3C] hover:text-[#FFFFFF]\">Dark to Red</button>
<button style=\"bg-[#F39C12] text-[#2C3E50] rounded-lg hover:bg-[#27AE60] hover:text-[#FFFFFF]\">Gold to Green</button>
<button style=\"bg-[#FFFFFF] text-[#2C3E50] rounded-xl hover:bg-[#2C3E50] hover:text-[#FFFFFF]\">Light to Dark</button>
<h3>Text Color Focus</h3>
<button style=\"text-[#E74C3C] rounded-lg\">Red Text Only</button>
<button style=\"text-[#27AE60] rounded-lg\">Green Text Only</button>
<button style=\"text-[#3498DB] rounded-lg\">Blue Text Only</button>
<button style=\"text-[#9B59B6] rounded-full\">Purple Text Pill</button>
<h3>Mixed Styles</h3>
<button style=\"bg-[#FF7675] text-[#FFFFFF] rounded-[15px] hover:bg-[#FD79A8] hover:text-[#2D3436]\">Custom Mix 1</button>
<button style=\"bg-[#6C5CE7] text-[#DDD] rounded-3xl hover:bg-[#A29BFE] hover:text-[#2D3436]\">Custom Mix 2</button>
<button style=\"bg-[#00B894] text-[#FFFFFF] rounded-[25px] hover:bg-[#00CEC9] hover:text-[#2D3436]\">Custom Mix 3</button>
<button style=\"bg-[#0000ff] text-[#FFFFFF] rounded-[25px] hover:bg-[#ff0000] hover:text-[#2D3436]\">Blue normal, red hover</button>
<h3>Active State Tests</h3>
<button style=\"bg-[#3498DB] text-[#FFFFFF] rounded-lg hover:bg-[#2980B9] active:bg-[#1F618D] active:text-[#F8F9FA]\">Blue with Active</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-xl hover:bg-[#C0392B] active:bg-[#A93226] active:text-[#ECF0F1]\">Red with Active</button>
<button style=\"bg-[#27AE60] text-[#FFFFFF] rounded-full hover:bg-[#229954] active:bg-[#1E8449] active:text-[#D5DBDB]\">Green Pill Active</button>
<button style=\"bg-[#F39C12] text-[#2C3E50] rounded hover:bg-[#E67E22] hover:text-[#FFFFFF] active:bg-[#D35400] active:text-[#F7F9FC]\">Gold Multi-State</button>
<button style=\"bg-[#9B59B6] text-[#FFFFFF] rounded-2xl active:bg-[#7D3C98] active:text-[#E8DAEF]\">Purple Active Only</button>
</body>
""".to_utf8_buffer()

View File

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

View File

@@ -0,0 +1,91 @@
class_name FontManager
extends RefCounted
static var loaded_fonts: Dictionary = {}
static var font_requests: Array = []
static var refresh_callback: Callable
static func register_font(name: String, src: String, weight: String = "400") -> void:
var font_info = {
"name": name,
"src": src,
"weight": weight,
"font_resource": null
}
font_requests.append(font_info)
static func load_all_fonts() -> void:
if font_requests.size() == 0:
return
for font_info in font_requests:
load_font(font_info)
static func load_font(font_info: Dictionary) -> void:
var src = font_info["src"]
if src.begins_with("http://") or src.begins_with("https://"):
load_web_font(font_info)
static func load_web_font(font_info: Dictionary) -> void:
var src = font_info["src"]
var name = font_info["name"]
var http_request = HTTPRequest.new()
var temp_parent = Node.new()
Engine.get_main_loop().root.add_child(temp_parent)
temp_parent.add_child(http_request)
http_request.timeout = 30.0
http_request.request_completed.connect(func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
if response_code == 200:
if body.size() > 0:
var font = FontFile.new()
font.data = body
font_info["font_resource"] = font
loaded_fonts[name] = font
# Trigger font refresh if callback is available
if refresh_callback.is_valid():
refresh_callback.call(name)
else:
print("FontManager: Empty font data received for ", name)
else:
print("FontManager: Failed to load font ", name, " - HTTP ", response_code)
if is_instance_valid(temp_parent):
temp_parent.queue_free()
)
http_request.request(src)
static func get_font(family_name: String) -> Font:
if family_name == "sans-serif":
var sys_font = SystemFont.new()
sys_font.font_names = ["sans-serif"]
return sys_font
elif family_name == "serif":
var sys_font = SystemFont.new()
sys_font.font_names = ["serif"]
return sys_font
elif family_name == "monospace":
var sys_font = SystemFont.new()
sys_font.font_names = ["monospace"]
return sys_font
elif loaded_fonts.has(family_name):
return loaded_fonts[family_name]
else:
# Fallback to system font
var sys_font = SystemFont.new()
sys_font.font_names = [family_name]
return sys_font
static func clear_fonts() -> void:
loaded_fonts.clear()
font_requests.clear()
static func set_refresh_callback(callback: Callable) -> void:
refresh_callback = callback

View File

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

View File

@@ -0,0 +1,68 @@
@tool
class_name MaxSizeControl
extends Control
@export var max_size: Vector2 = Vector2(-1, -1):
set(value):
max_size = value
_enforce_size_limits()
var content_node: Control
func _ready():
# Auto-detect content node
if get_child_count() > 0:
setup_content_node(get_child(0))
# Connect to our own resize
resized.connect(_on_resized)
func setup_content_node(node: Control):
content_node = node
if content_node:
# Make content fill the container initially
content_node.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
# Connect to content changes
if not content_node.minimum_size_changed.is_connected(_on_content_changed):
content_node.minimum_size_changed.connect(_on_content_changed)
_enforce_size_limits()
func _on_content_changed():
_enforce_size_limits()
func _on_resized():
_enforce_size_limits()
func _enforce_size_limits():
if not content_node:
return
var target_width = max_size.x if max_size.x > 0 else content_node.get_combined_minimum_size().x
var target_height = max_size.y if max_size.y > 0 else content_node.get_combined_minimum_size().y
custom_minimum_size = Vector2(target_width, target_height)
# Set children's minimum size to match the constrained size
for child in get_children():
if child is Control:
child.custom_minimum_size = Vector2(target_width, target_height)
# Force the content to fit within our bounds and enable clipping
content_node.size = Vector2(target_width, target_height)
content_node.position = Vector2.ZERO
# Always enable clipping if max_size is set
var needs_clipping = max_size.x > 0 or max_size.y > 0
content_node.clip_contents = needs_clipping
clip_contents = true
func _get_minimum_size() -> Vector2:
# Only use max_size, ignore content's natural size
var final_size = Vector2.ZERO
if max_size.x > 0:
final_size.x = max_size.x
if max_size.y > 0:
final_size.y = max_size.y
return final_size

View File

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

61
flumi/Scripts/Network.gd Normal file
View File

@@ -0,0 +1,61 @@
extends Node
func fetch_image(url: String) -> ImageTexture:
var http_request = HTTPRequest.new()
add_child(http_request)
var error = http_request.request(url)
if error != OK:
print("Error making HTTP request: ", error)
http_request.queue_free()
return null
var response = await http_request.request_completed
var result = response[0] # HTTPClient.Result
var response_code = response[1] # int
var headers = response[2] # PackedStringArray
var body = response[3] # PackedByteArray
http_request.queue_free()
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
print("Failed to fetch image. Result: ", result, " Response code: ", response_code)
return null
# Get content type from headers
var content_type = ""
for header in headers:
if header.to_lower().begins_with("content-type:"):
content_type = header.split(":")[1].strip_edges().to_lower()
break
var image: Image = Image.new()
var load_error
# Load image based on content type
if content_type.contains("png") or url.to_lower().ends_with(".png"):
load_error = image.load_png_from_buffer(body)
elif content_type.contains("jpeg") or content_type.contains("jpg") or url.to_lower().ends_with(".jpg") or url.to_lower().ends_with(".jpeg"):
load_error = image.load_jpg_from_buffer(body)
elif content_type.contains("webp") or url.to_lower().ends_with(".webp"):
load_error = image.load_webp_from_buffer(body)
elif content_type.contains("bmp"):
load_error = image.load_bmp_from_buffer(body)
elif content_type.contains("tga"):
load_error = image.load_tga_from_buffer(body)
else:
print("Unknown or missing content-type. Attempting bruteforce converting across PNG, JPG and WebP...")
load_error = image.load_png_from_buffer(body)
if load_error != OK:
load_error = image.load_jpg_from_buffer(body)
if load_error != OK:
load_error = image.load_webp_from_buffer(body)
if load_error != OK:
print("Failed to load image from buffer. Content-Type: ", content_type, " Error: ", load_error)
return null
var texture = ImageTexture.create_from_image(image)
return texture

View File

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

View File

@@ -0,0 +1,64 @@
extends TextEdit
@onready var resize_handle = TextureRect.new()
var is_resizing = false
var resize_start_pos = Vector2()
var original_size = Vector2()
var min_size = Vector2(100, 50)
func _ready():
# Create resize handle as TextureRect child of TextEdit
resize_handle.texture = load("res://Assets/Icons/resize-handle.svg")
resize_handle.size = Vector2(32, 32)
resize_handle.mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE
resize_handle.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
add_child(resize_handle)
# Position handle in bottom-right corner
_update_handle_position()
# Connect signals
resize_handle.gui_input.connect(_on_resize_handle_input)
resized.connect(_update_handle_position)
func _gui_input(event):
if event is InputEventMouseButton and get_global_rect().has_point(get_viewport().get_mouse_position()):
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
set_v_scroll(get_v_scroll() - 2)
accept_event()
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
set_v_scroll(get_v_scroll() + 2)
accept_event()
func _update_handle_position():
if resize_handle:
resize_handle.position = Vector2(size.x - 32, size.y - 32)
func _on_resize_handle_input(event):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
is_resizing = true
resize_start_pos = event.global_position
original_size = size
else:
is_resizing = false
elif event is InputEventMouseMotion and is_resizing:
var delta = event.global_position - resize_start_pos
var new_size = original_size + delta
new_size.x = max(new_size.x, min_size.x)
new_size.y = max(new_size.y, min_size.y)
size = new_size
# Sync parent Control size
var parent_control = get_parent() as Control
if parent_control:
parent_control.size = new_size
parent_control.custom_minimum_size = new_size
if parent_control:
parent_control.size = new_size
parent_control.custom_minimum_size = new_size

View File

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

View File

@@ -0,0 +1,402 @@
class_name StyleManager
extends RefCounted
static var body_text_color: Color = Color.BLACK
static func parse_size(val):
if val == null: return null
if typeof(val) == TYPE_INT or typeof(val) == TYPE_FLOAT:
return float(val)
if val.ends_with("px"):
return float(val.replace("px", ""))
if val.ends_with("rem"):
return float(val.replace("rem", "")) * 16.0
if val.ends_with("%") or (val.ends_with("]") and "%" in val):
var clean_val = val.replace("[", "").replace("]", "")
return clean_val
if val == "full":
return null
return float(val)
static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, parser: HTMLParser) -> Control:
var styles = parser.get_element_styles_with_inheritance(element, "", [])
var label = null
var target = null
if not (node is FlexContainer):
target = node if node is RichTextLabel else node.get_node_or_null("RichTextLabel")
label = target
# Also check for Button nodes
if not target and node is HTMLButton:
var button_node = node.get_node_or_null("ButtonNode")
if button_node:
target = button_node
if element.tag_name == "input":
apply_input_border_styles(node, styles)
# Unified font applying for label and button
if target and styles.has("font-family") and styles["font-family"] not in ["sans-serif", "serif", "monospace"]:
var main_node = Engine.get_main_loop().current_scene
main_node.register_font_dependent_element(target, styles, element, parser)
var width = null
var height = null
if styles.has("width"):
width = parse_size(styles["width"])
if styles.has("height"):
height = parse_size(styles["height"])
# Skip width/height inheritance for buttons when inheriting from auto-sized containers
var skip_sizing = SizingUtils.should_skip_sizing(node, element, parser)
if (width != null or height != null) and not skip_sizing:
# FlexContainers handle percentage sizing differently than regular controls
if node is FlexContainer:
if width != null and typeof(width) != TYPE_STRING:
node.custom_minimum_size.x = width
var should_center_h = styles.has("mx-auto") or styles.has("justify-self-center") or (styles.has("text-align") and styles["text-align"] == "center")
node.size_flags_horizontal = Control.SIZE_SHRINK_CENTER if should_center_h else Control.SIZE_SHRINK_BEGIN
node.set_meta("size_flags_set_by_style_manager", true)
if height != null and typeof(height) != TYPE_STRING:
node.custom_minimum_size.y = height
var should_center_v = styles.has("my-auto") or styles.has("align-self-center")
node.size_flags_vertical = Control.SIZE_SHRINK_CENTER if should_center_v else Control.SIZE_SHRINK_BEGIN
if not node.has_meta("size_flags_set_by_style_manager"):
node.set_meta("size_flags_set_by_style_manager", true)
elif node is VBoxContainer or node is HBoxContainer or node is Container:
# Hcontainer nodes (like ul, ol)
SizingUtils.apply_container_dimension_sizing(node, width, height)
elif node is HTMLP:
# Only apply sizing if element has explicit size, otherwise preserve natural sizing
var element_styles = parser.get_element_styles_internal(element, "")
if element_styles.has("width") or element_styles.has("height"):
var orig_h_flag = node.size_flags_horizontal
var orig_v_flag = node.size_flags_vertical
SizingUtils.apply_regular_control_sizing(node, width, height)
if not element_styles.has("width"):
node.size_flags_horizontal = orig_h_flag
if not element_styles.has("height"):
node.size_flags_vertical = orig_v_flag
else:
# regular controls
SizingUtils.apply_regular_control_sizing(node, width, height)
if label and label != node:
label.anchors_preset = Control.PRESET_FULL_RECT
# Apply z-index
if styles.has("z-index"):
node.z_index = styles["z-index"]
# Apply opacity
if styles.has("opacity"):
node.modulate.a = styles["opacity"]
# Apply cursor
if styles.has("cursor"):
var cursor_shape = get_cursor_shape_from_type(styles["cursor"])
node.mouse_default_cursor_shape = cursor_shape
# Handle cursor inheritance for text elements (RichTextLabel)
# If current element has no cursor, traverse up parent chain to find one
if not styles.has("cursor") and label:
var current_parent = element.parent
while current_parent:
var parent_styles = parser.get_element_styles_with_inheritance(current_parent, "", [])
if parent_styles.has("cursor"):
var parent_cursor_shape = get_cursor_shape_from_type(parent_styles["cursor"])
label.mouse_default_cursor_shape = parent_cursor_shape
break # Found a cursor, stop traversing
current_parent = current_parent.parent
# Apply background color, border radius, borders
var needs_styling = styles.has("background-color") or styles.has("border-radius") or styles.has("border-width") or styles.has("border-top-width") or styles.has("border-right-width") or styles.has("border-bottom-width") or styles.has("border-left-width") or styles.has("border-color")
if needs_styling:
var target_node_for_bg = node if node is FlexContainer else label
if target_node_for_bg:
if styles.has("background-color"):
target_node_for_bg.set_meta("custom_css_background_color", styles["background-color"])
if styles.has("border-radius"):
target_node_for_bg.set_meta("custom_css_border_radius", styles["border-radius"])
# Border properties
if styles.has("border-width"):
target_node_for_bg.set_meta("custom_css_border_width", styles["border-width"])
if styles.has("border-color"):
target_node_for_bg.set_meta("custom_css_border_color", styles["border-color"])
# Individual border sides
var border_sides = ["top", "right", "bottom", "left"]
for side in border_sides:
var width_key = "border-" + side + "-width"
if styles.has(width_key):
target_node_for_bg.set_meta("custom_css_" + width_key.replace("-", "_"), styles[width_key])
if target_node_for_bg.has_method("add_background_rect"):
target_node_for_bg.call_deferred("add_background_rect")
if label:
apply_styles_to_label(label, styles, element, parser)
return node
static func apply_styles_to_label(label: Control, styles: Dictionary, element: HTMLParser.HTMLElement, parser, text_override: String = "") -> void:
if label is Button:
apply_font_to_button(label, styles)
return
if not label is RichTextLabel:
return
var text = text_override if text_override != "" else (element.get_preserved_text() if element.tag_name == "pre" else element.get_bbcode_formatted_text(parser))
var font_size = 24 # default
if styles.has("font-family"):
var font_family = styles["font-family"]
var font_resource = FontManager.get_font(font_family)
# set a sans-serif fallback first
if font_family not in ["sans-serif", "serif", "monospace"]:
if not FontManager.loaded_fonts.has(font_family):
# Font not loaded yet, use sans-serif as fallback
var fallback_font = FontManager.get_font("sans-serif")
apply_font_to_label(label, fallback_font)
if font_resource:
apply_font_to_label(label, font_resource)
# Apply font size
if styles.has("font-size"):
font_size = int(styles["font-size"])
var has_existing_bbcode = text.contains("[url=") or text.contains("[color=")
# Apply color
var color_tag = ""
if not has_existing_bbcode and styles.has("color"):
var color = styles["color"] as Color
if color == Color.BLACK and StyleManager.body_text_color != Color.BLACK:
color = StyleManager.body_text_color
color_tag = "[color=#%s]" % color.to_html(false)
elif not has_existing_bbcode and StyleManager.body_text_color != Color.BLACK:
color_tag = "[color=#%s]" % StyleManager.body_text_color.to_html(false)
# Apply text styling (but not for text with existing BBCode)
var bold_open = ""
var bold_close = ""
if not has_existing_bbcode and styles.has("font-bold") and styles["font-bold"]:
bold_open = "[b]"
bold_close = "[/b]"
var italic_open = ""
var italic_close = ""
if not has_existing_bbcode and styles.has("font-italic") and styles["font-italic"]:
italic_open = "[i]"
italic_close = "[/i]"
var underline_open = ""
var underline_close = ""
if not has_existing_bbcode and styles.has("underline") and styles["underline"]:
underline_open = "[u]"
underline_close = "[/u]"
# Apply monospace font
var mono_open = ""
var mono_close = ""
if styles.has("font-mono") and styles["font-mono"]:
# If font-family is already monospace, just use BBCode for styling
if not (styles.has("font-family") and styles["font-family"] == "monospace"):
mono_open = "[code]"
mono_close = "[/code]"
if styles.has("text-align"):
match styles["text-align"]:
"left":
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
"center":
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
"right":
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
"justify":
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_FILL
# Construct final text
var styled_text = "[font_size=%d]%s%s%s%s%s%s%s%s%s%s%s[/font_size]" % [
font_size,
color_tag,
bold_open,
italic_open,
underline_open,
mono_open,
text,
mono_close,
underline_close,
italic_close,
bold_close,
"[/color]" if color_tag.length() > 0 else "",
]
label.text = styled_text
static func apply_flex_container_properties(node: FlexContainer, styles: Dictionary) -> void:
FlexUtils.apply_flex_container_properties(node, styles)
static func apply_flex_item_properties(node: Control, styles: Dictionary) -> void:
FlexUtils.apply_flex_item_properties(node, styles)
static func parse_flex_value(val):
return FlexUtils.parse_flex_value(val)
static func apply_body_styles(body: HTMLParser.HTMLElement, parser: HTMLParser, website_container: Control, website_background: Control) -> void:
var styles = parser.get_element_styles_with_inheritance(body, "", [])
# Apply background color
if styles.has("background-color"):
var style_box = StyleBoxFlat.new()
style_box.bg_color = styles["background-color"] as Color
website_background.add_theme_stylebox_override("panel", style_box)
if styles.has("color"):
StyleManager.body_text_color = styles["color"]
# Apply padding
var has_padding = styles.has("padding") or styles.has("padding-top") or styles.has("padding-right") or styles.has("padding-bottom") or styles.has("padding-left")
if has_padding:
var margin_container = MarginContainer.new()
margin_container.name = "BodyMarginContainer"
margin_container.size_flags_horizontal = website_container.size_flags_horizontal
margin_container.size_flags_vertical = website_container.size_flags_vertical
# ScrollContainer
# |__ BodyMarginContainer
# |__ WebsiteContainer
var original_parent = website_container.get_parent()
var container_index = website_container.get_index()
original_parent.remove_child(website_container)
original_parent.add_child(margin_container)
original_parent.move_child(margin_container, container_index)
margin_container.add_child(website_container)
var margin_val = parse_size(styles["padding"])
margin_container.add_theme_constant_override("margin_left", margin_val)
margin_container.add_theme_constant_override("margin_right", margin_val)
margin_container.add_theme_constant_override("margin_top", margin_val)
margin_container.add_theme_constant_override("margin_bottom", margin_val)
# Apply individual padding values
var padding_sides = [
["padding-top", "margin_top"],
["padding-right", "margin_right"],
["padding-bottom", "margin_bottom"],
["padding-left", "margin_left"]
]
for side_pair in padding_sides:
var style_key = side_pair[0]
var margin_key = side_pair[1]
if styles.has(style_key):
var margin_val2 = parse_size(styles[style_key])
margin_container.add_theme_constant_override(margin_key, margin_val2)
static func parse_radius(radius_str: String) -> int:
return SizeUtils.parse_radius(radius_str)
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)
static func apply_font_to_button(button: Button, styles: Dictionary) -> void:
if styles.has("font-family"):
var font_family = styles["font-family"]
var font_resource = FontManager.get_font(font_family)
# Set fallback first for FOUT prevention
if font_family not in ["sans-serif", "serif", "monospace"]:
if not FontManager.loaded_fonts.has(font_family):
var fallback_font = FontManager.get_font("sans-serif")
button.add_theme_font_override("font", fallback_font)
if font_resource:
button.add_theme_font_override("font", font_resource)
static func get_cursor_shape_from_type(cursor_type: String) -> Control.CursorShape:
match cursor_type:
"pointer", "hand":
return Control.CURSOR_POINTING_HAND
"text":
return Control.CURSOR_IBEAM
"crosshair":
return Control.CURSOR_CROSS
"move":
return Control.CURSOR_MOVE
"not-allowed", "forbidden":
return Control.CURSOR_FORBIDDEN
"wait":
return Control.CURSOR_WAIT
"help":
return Control.CURSOR_HELP
"grab":
return Control.CURSOR_DRAG
"grabbing":
return Control.CURSOR_CAN_DROP
"e-resize", "ew-resize":
return Control.CURSOR_HSIZE
"n-resize", "ns-resize":
return Control.CURSOR_VSIZE
"ne-resize":
return Control.CURSOR_BDIAGSIZE
"nw-resize":
return Control.CURSOR_FDIAGSIZE
"se-resize":
return Control.CURSOR_FDIAGSIZE
"sw-resize":
return Control.CURSOR_BDIAGSIZE
"default", "auto", _:
return Control.CURSOR_ARROW
static func apply_input_border_styles(input_node: Control, styles: Dictionary) -> void:
if not BackgroundUtils.needs_background_wrapper(styles):
return
# Find the appropriate input control to style
var styleable_controls = []
# Get all potential input controls that support StyleBox
var line_edit = input_node.get_node_or_null("LineEdit")
var spinbox = input_node.get_node_or_null("SpinBox")
var file_container = input_node.get_node_or_null("FileContainer")
if line_edit: styleable_controls.append(line_edit)
if spinbox: styleable_controls.append(spinbox)
if file_container:
var file_button = file_container.get_node_or_null("FileButton")
if file_button: styleable_controls.append(file_button)
# Apply styles using BackgroundUtils
for control in styleable_controls:
var style_box = BackgroundUtils.create_stylebox_from_styles(styles)
# Set appropriate content margins for inputs if not specified
if not styles.has("padding") and not styles.has("padding-left"):
style_box.content_margin_left = 5.0
if not styles.has("padding") and not styles.has("padding-right"):
style_box.content_margin_right = 5.0
if not styles.has("padding") and not styles.has("padding-top"):
style_box.content_margin_top = 2.0
if not styles.has("padding") and not styles.has("padding-bottom"):
style_box.content_margin_bottom = 2.0
# Apply the style to the appropriate states
if control is LineEdit:
control.add_theme_stylebox_override("normal", style_box)
control.add_theme_stylebox_override("focus", style_box)
elif control is SpinBox:
control.add_theme_stylebox_override("normal", style_box)
control.add_theme_stylebox_override("focus", style_box)
elif control is Button:
control.add_theme_stylebox_override("normal", style_box)

View File

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

109
flumi/Scripts/Tab.gd Normal file
View File

@@ -0,0 +1,109 @@
class_name Tab
extends Control
signal tab_pressed
signal tab_closed
@onready var gradient_texture: TextureRect = %GradientTexture
@onready var button: Button = %Button
@onready var close_button: Button = %CloseButton
@onready var icon: TextureRect = %Icon
@onready var animation: AnimationPlayer = $AnimationPlayer
const TAB_GRADIENT: GradientTexture2D = preload("res://Scenes/Styles/TabGradient.tres")
const TAB_GRADIENT_DEFAULT: GradientTexture2D = preload("res://Scenes/Styles/TabGradientDefault.tres")
const TAB_GRADIENT_INACTIVE: GradientTexture2D = preload("res://Scenes/Styles/TabGradientInactive.tres")
const TAB_HOVER: StyleBoxFlat = preload("res://Scenes/Styles/TabHover.tres")
const TAB_DEFAULT: StyleBoxFlat = preload("res://Scenes/Styles/TabDefault.tres")
const CLOSE_BUTTON_HOVER: StyleBoxFlat = preload("res://Scenes/Styles/CloseButtonHover.tres")
const CLOSE_BUTTON_NORMAL: StyleBoxFlat = preload("res://Scenes/Styles/CloseButtonNormal.tres")
var is_active := false
var mouse_over_tab := false
var loading_tween: Tween
func _ready():
add_to_group("tabs")
gradient_texture.texture = gradient_texture.texture.duplicate()
gradient_texture.texture.gradient = gradient_texture.texture.gradient.duplicate()
func _process(_delta):
# NOTE: probably very inefficient
if mouse_over_tab:
var mouse_pos = get_global_mouse_position()
var close_button_rect = Rect2(close_button.global_position, close_button.size * close_button.scale)
if close_button_rect.has_point(mouse_pos):
close_button.add_theme_stylebox_override("normal", CLOSE_BUTTON_HOVER)
else:
close_button.add_theme_stylebox_override("normal", CLOSE_BUTTON_NORMAL)
func set_title(title: String) -> void:
button.text = title
func set_icon(new_icon: Texture) -> void:
icon.texture = new_icon
icon.rotation = 0
func update_icon_from_url(icon_url: String) -> void:
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
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)
var icon_resource = await Network.fetch_image(icon_url)
# Only update if tab still exists
if is_instance_valid(self):
set_icon(icon_resource)
if loading_tween:
loading_tween.kill()
loading_tween = null
func _on_button_mouse_entered() -> void:
mouse_over_tab = true
if is_active: return
gradient_texture.texture = TAB_GRADIENT_INACTIVE
func _on_button_mouse_exited() -> void:
mouse_over_tab = false
if is_active: return
gradient_texture.texture = TAB_GRADIENT_DEFAULT
func _exit_tree():
if loading_tween:
loading_tween.kill()
loading_tween = null
remove_from_group("tabs")
func _enter_tree() -> void:
$AnimationPlayer.play("appear")
func _on_button_pressed() -> void:
# Check if click was on close button area
var mouse_pos = get_global_mouse_position()
var close_button_rect = Rect2(close_button.global_position, close_button.size * close_button.scale)
if close_button_rect.has_point(mouse_pos):
_on_close_button_pressed()
else:
# Handle tab button click
tab_pressed.emit()
func _on_close_button_pressed() -> void:
tab_closed.emit()
animation.play("appear", -1, -1.0, true)
await animation.animation_finished
queue_free()

1
flumi/Scripts/Tab.gd.uid Normal file
View File

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

View File

@@ -0,0 +1,103 @@
class_name TabManager
extends HFlowContainer
var tabs: Array[Tab] = []
var active_tab := 0
@onready var main: Main = $"../.."
const TAB = preload("res://Scenes/Tab.tscn")
const TAB_NORMAL: StyleBoxFlat = preload("res://Scenes/Styles/TabNormal.tres")
const TAB_HOVER: StyleBoxFlat = preload("res://Scenes/Styles/TabHover.tres")
const TAB_DEFAULT: StyleBoxFlat = preload("res://Scenes/Styles/TabDefault.tres")
const TAB_HOVER_DEFAULT: StyleBoxFlat = preload("res://Scenes/Styles/TabHoverDefault.tres")
const TAB_GRADIENT: GradientTexture2D = preload("res://Scenes/Styles/TabGradient.tres")
const TAB_GRADIENT_DEFAULT: GradientTexture2D = preload("res://Scenes/Styles/TabGradientDefault.tres")
@onready var h_box_container: HBoxContainer = $HBoxContainer
func _ready() -> void:
tabs.assign(get_tree().get_nodes_in_group("tabs"))
set_active_tab(0)
for i in tabs.size():
tabs[i].tab_pressed.connect(_tab_pressed.bind(i))
tabs[i].tab_closed.connect(_tab_closed.bind(i))
func _tab_pressed(index: int) -> void:
set_active_tab(index)
func _tab_closed(index: int) -> void:
tabs.remove_at(index)
if tabs.is_empty():
get_tree().quit()
return
if index <= active_tab:
if index == active_tab:
# Closed tab was active, select right neighbor (or last tab if at end)
if index >= tabs.size():
active_tab = tabs.size() - 1
else:
active_tab = index
else:
# Closed tab was before active tab, shift active index down
active_tab -= 1
# Reconnect signals with updated indices
for i in tabs.size():
tabs[i].tab_pressed.disconnect(_tab_pressed)
tabs[i].tab_closed.disconnect(_tab_closed)
tabs[i].tab_pressed.connect(_tab_pressed.bind(i))
tabs[i].tab_closed.connect(_tab_closed.bind(i))
set_active_tab(active_tab)
func set_active_tab(index: int) -> void:
# old tab
tabs[active_tab].is_active = false
tabs[active_tab].button.add_theme_stylebox_override("normal", TAB_DEFAULT)
tabs[active_tab].button.add_theme_stylebox_override("pressed", TAB_DEFAULT)
tabs[active_tab].button.add_theme_stylebox_override("hover", TAB_HOVER_DEFAULT)
tabs[active_tab].gradient_texture.texture = TAB_GRADIENT_DEFAULT
# new tab
tabs[index].is_active = true
tabs[index].button.add_theme_stylebox_override("normal", TAB_NORMAL)
tabs[index].button.add_theme_stylebox_override("pressed", TAB_NORMAL)
tabs[index].button.add_theme_stylebox_override("hover", TAB_NORMAL)
tabs[index].gradient_texture.texture = TAB_GRADIENT
active_tab = index
func create_tab() -> void:
var index = tabs.size();
var tab = TAB.instantiate()
tabs.append(tab)
tab.tab_pressed.connect(_tab_pressed.bind(index))
tab.tab_closed.connect(_tab_closed.bind(index))
h_box_container.add_child(tab)
set_active_tab(index)
# WARNING: temporary
main.render()
func _input(_event: InputEvent) -> void:
if Input.is_action_just_pressed("NewTab"):
create_tab()
if Input.is_action_just_pressed("CloseTab"):
tabs[active_tab]._on_close_button_pressed()
if Input.is_action_just_pressed("NextTab"):
var next_tab = (active_tab + 1) % tabs.size()
set_active_tab(next_tab)
if Input.is_action_just_pressed("PreviousTab"):
var prev_tab = (active_tab - 1 + tabs.size()) % tabs.size()
set_active_tab(prev_tab - 1)
func _on_new_tab_button_pressed() -> void:
create_tab()

View File

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

4
flumi/Scripts/Tags/br.gd Normal file
View File

@@ -0,0 +1,4 @@
extends Control
func init(_element: HTMLParser.HTMLElement) -> void:
pass

View File

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

View File

@@ -0,0 +1,188 @@
class_name HTMLButton
extends Control
var current_element: HTMLParser.HTMLElement
var current_parser: HTMLParser
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
current_element = element
current_parser = parser
var button_node: Button = $ButtonNode
if element.has_attribute("disabled"):
button_node.disabled = true
var button_text = element.text_content.strip_edges()
if button_text.length() == 0:
button_text = element.get_bbcode_formatted_text(parser)
if button_text.length() > 0:
button_node.text = button_text
var natural_size = button_node.get_theme_default_font().get_string_size(
button_node.text,
HORIZONTAL_ALIGNMENT_LEFT,
-1,
button_node.get_theme_default_font_size()
) + Vector2(20, 10) # Add padding
# Force our container to use the natural size
custom_minimum_size = natural_size
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
size_flags_vertical = Control.SIZE_SHRINK_BEGIN
# Make button node fill the container
button_node.custom_minimum_size = Vector2.ZERO
button_node.size_flags_horizontal = Control.SIZE_FILL
button_node.size_flags_vertical = Control.SIZE_FILL
apply_button_styles(element, parser, natural_size)
func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser, natural_size: Vector2) -> void:
if not element or not parser:
return
var styles = parser.get_element_styles_with_inheritance(element, "", [])
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
var active_styles = parser.get_element_styles_with_inheritance(element, "active", [])
var button_node = $ButtonNode
if styles.has("cursor"):
var cursor_shape = StyleManager.get_cursor_shape_from_type(styles["cursor"])
mouse_default_cursor_shape = cursor_shape
button_node.mouse_default_cursor_shape = cursor_shape
# Apply text color with state-dependent colors
apply_button_text_color(button_node, styles, hover_styles, active_styles)
# Apply background color (hover: + active:)
if styles.has("background-color"):
var normal_color: Color = styles["background-color"]
var hover_color = normal_color # Default to normal color
var active_color = normal_color # Default to normal color
# Check if background color comes from inline styles
var inline_style = element.get_attribute("style")
var inline_normal_styles = parser.parse_inline_style_with_event(inline_style, "")
var has_inline_bg = inline_normal_styles.has("background-color")
if has_inline_bg:
# If user set inline bg, only use inline hover/active, ignore global ones
var inline_hover_styles = parser.parse_inline_style_with_event(inline_style, "hover")
var inline_active_styles = parser.parse_inline_style_with_event(inline_style, "active")
if inline_hover_styles.has("background-color"):
hover_color = inline_hover_styles["background-color"]
if inline_active_styles.has("background-color"):
active_color = inline_active_styles["background-color"]
elif inline_hover_styles.has("background-color"):
# Fallback: if hover is defined but active isn't, use hover for active
active_color = hover_color
else:
# No inline bg, use global CSS hover/active if available
if hover_styles.has("background-color"):
hover_color = hover_styles["background-color"]
if active_styles.has("background-color"):
active_color = active_styles["background-color"]
elif hover_styles.has("background-color"):
# Fallback: if hover is defined but active isn't, use hover for active
active_color = hover_color
apply_button_color_with_states(button_node, normal_color, hover_color, active_color)
# Apply corner radius
if styles.has("border-radius"):
var radius = StyleManager.parse_radius(styles["border-radius"])
apply_button_radius(button_node, radius)
var width = null
var height = null
if styles.has("width"):
width = SizingUtils.parse_size_value(styles["width"])
if styles.has("height"):
height = SizingUtils.parse_size_value(styles["height"])
# Only apply size flags if there's explicit sizing
if width != null or height != null:
apply_size_and_flags(self, width, height)
apply_size_and_flags(button_node, width, height, false)
else:
# Keep the natural sizing we set earlier
custom_minimum_size = natural_size
# Also ensure the ButtonNode doesn't override our size
button_node.custom_minimum_size = Vector2.ZERO
button_node.anchors_preset = Control.PRESET_FULL_RECT
func apply_button_text_color(button: Button, normal_styles: Dictionary, hover_styles: Dictionary, active_styles: Dictionary) -> void:
var normal_color = normal_styles.get("color", Color.WHITE)
var hover_color = hover_styles.get("color", normal_color)
var active_color = active_styles.get("color", hover_color)
button.add_theme_color_override("font_color", normal_color)
button.add_theme_color_override("font_hover_color", hover_color)
button.add_theme_color_override("font_pressed_color", active_color)
button.add_theme_color_override("font_focus_color", normal_color)
if button.disabled:
button.add_theme_color_override("font_disabled_color", normal_color)
func apply_button_color_with_states(button: Button, normal_color: Color, hover_color: Color, active_color: Color) -> void:
var style_normal = StyleBoxFlat.new()
var style_hover = StyleBoxFlat.new()
var style_pressed = StyleBoxFlat.new()
var radius: int = 0
style_normal.set_corner_radius_all(radius)
style_hover.set_corner_radius_all(radius)
style_pressed.set_corner_radius_all(radius)
# Set normal color
style_normal.bg_color = normal_color
# Set hover: color
# If hover isn't default, use it
if hover_color != Color():
style_hover.bg_color = hover_color
else:
# If no hover, fallback to normal color
style_hover.bg_color = normal_color
# Set active: color
if active_color != Color():
style_pressed.bg_color = active_color
elif hover_color != Color():
style_pressed.bg_color = hover_color # Fallback to hover if defined
else:
style_pressed.bg_color = normal_color # Final fallback to normal
button.add_theme_stylebox_override("normal", style_normal)
button.add_theme_stylebox_override("hover", style_hover)
button.add_theme_stylebox_override("pressed", style_pressed)
func apply_button_radius(button: Button, radius: int) -> void:
var style_normal = button.get_theme_stylebox("normal")
var style_hover = button.get_theme_stylebox("hover")
var style_pressed = button.get_theme_stylebox("pressed")
style_normal.set_corner_radius_all(radius)
style_hover.set_corner_radius_all(radius)
style_pressed.set_corner_radius_all(radius)
button.add_theme_stylebox_override("normal", style_normal)
button.add_theme_stylebox_override("hover", style_hover)
button.add_theme_stylebox_override("pressed", style_pressed)
func apply_size_and_flags(ctrl: Control, width: Variant, height: Variant, reset_layout := false) -> void:
if width != null or height != null:
ctrl.custom_minimum_size = Vector2(
width if width != null else ctrl.custom_minimum_size.x,
height if height != null else ctrl.custom_minimum_size.y
)
if width != null:
ctrl.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
if height != null:
ctrl.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
if reset_layout:
ctrl.position = Vector2.ZERO
ctrl.anchors_preset = Control.PRESET_FULL_RECT

View File

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

View File

@@ -0,0 +1,5 @@
class_name HTMLDiv
extends VBoxContainer
func init(_element: HTMLParser.HTMLElement):
pass

View File

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

View File

@@ -0,0 +1 @@
uid://4v2v83gyok8d

View File

@@ -0,0 +1,4 @@
extends VBoxContainer
func init(_element: HTMLParser.HTMLElement) -> void:
pass

View File

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

11
flumi/Scripts/Tags/img.gd Normal file
View File

@@ -0,0 +1,11 @@
extends TextureRect
func init(element: HTMLParser.HTMLElement) -> void:
var src = element.get_attribute("src")
if !src: return print("Ignoring <img/> tag without \"src\" attribute.")
texture = await Network.fetch_image(src)
var texture_size = texture.get_size()
custom_minimum_size = texture_size
size = texture_size

View File

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

367
flumi/Scripts/Tags/input.gd Normal file
View File

@@ -0,0 +1,367 @@
extends Control
static var button_groups: Dictionary = {}
const BROWSER_TEXT: Theme = preload("res://Scenes/Styles/BrowserText.tres")
var custom_hex_input: LineEdit
var _file_text_content: String = ""
var _file_binary_content: PackedByteArray = PackedByteArray()
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
var color_picker_button: ColorPickerButton = $ColorPickerButton
var picker: ColorPicker = color_picker_button.get_picker()
picker.sliders_visible = false
picker.presets_visible = false
picker.color_modes_visible = false
picker.hex_visible = false
# Create custom hex input
custom_hex_input = LineEdit.new()
custom_hex_input.placeholder_text = "#RRGGBB"
# Apply custom unique theme
var picker_theme = BROWSER_TEXT.duplicate()
picker_theme.set_color("font_color", "LineEdit", Color.WHITE)
picker_theme.set_color("font_placeholder_color", "LineEdit", Color(1, 1, 1, 0.6))
picker_theme.set_color("caret_color", "LineEdit", Color.WHITE)
var normal_style = picker_theme.get_stylebox("normal", "LineEdit").duplicate()
var focus_style = picker_theme.get_stylebox("focus", "LineEdit").duplicate()
normal_style.border_color = Color.WHITE
focus_style.border_color = Color.WHITE
picker_theme.set_stylebox("normal", "LineEdit", normal_style)
picker_theme.set_stylebox("focus", "LineEdit", focus_style)
custom_hex_input.theme = picker_theme
picker.add_child(custom_hex_input)
# Connect signals for two-way binding of Hex
picker.color_changed.connect(_on_picker_color_changed)
custom_hex_input.text_submitted.connect(_on_custom_hex_submitted)
# Set initial hex value
_on_picker_color_changed(picker.color)
picker.theme = picker_theme
color_picker_button.get_popup().theme = picker_theme
var input_type = element.get_attribute("type").to_lower()
var placeholder = element.get_attribute("placeholder")
var value = element.get_attribute("value")
var group = element.get_attribute("group")
var minlength = element.get_attribute("minlength")
var maxlength = element.get_attribute("maxlength")
var pattern = element.get_attribute("pattern")
var min_attr = element.get_attribute("min")
var max_attr = element.get_attribute("max")
var step_attr = element.get_attribute("step")
var accept = element.get_attribute("accept")
# Define which child should be active for each input type
var active_child_map = {
"checkbox": "CheckBox",
"radio": "RadioButton",
"color": "ColorPickerButton",
"password": "LineEdit",
"date": "DateButton",
"range": "HSlider",
"number": "SpinBox",
"file": "FileContainer"
}
var active_child_name = active_child_map.get(input_type, "LineEdit")
remove_unused_children(active_child_name)
var active_child = get_node(active_child_name)
active_child.visible = true
match input_type:
"checkbox":
var checkbox = active_child as CheckBox
if value and value == "true":
checkbox.button_pressed = true
"radio":
var radio = active_child as CheckBox
radio.toggle_mode = true
if value and value == "true":
radio.button_pressed = true
if group.length() > 0:
if not button_groups.has(group):
button_groups[group] = ButtonGroup.new()
radio.button_group = button_groups[group]
"color":
var color_button = active_child as ColorPickerButton
if value and value.length() > 0:
var color = Color.from_string(value, Color.WHITE)
color_button.color = color
"password":
var line_edit = active_child as LineEdit
line_edit.secret = true
setup_text_input(line_edit, placeholder, value, minlength, maxlength, pattern)
"date":
var date_button = active_child as DateButton
if value and value.length() > 0:
date_button.init_with_date(value)
else:
date_button.init()
"range":
var slider = active_child as HSlider
setup_range_input(slider, value, min_attr, max_attr, step_attr)
"number":
var spin_box = active_child as SpinBox
setup_number_input(spin_box, value, min_attr, max_attr, step_attr, placeholder)
"file":
var file_container = active_child as Control
setup_file_input(file_container, accept)
_: # Default case (text input)
var line_edit = active_child as LineEdit
line_edit.secret = false
setup_text_input(line_edit, placeholder, value, minlength, maxlength, pattern)
apply_input_styles(element, parser)
func remove_unused_children(keep_child_name: String) -> void:
for child in get_children():
if child.name != keep_child_name:
child.visible = false
child.queue_free()
else:
child.visible = true
func setup_text_input(line_edit: LineEdit, placeholder: String, value: String, minlength: String, maxlength: String, pattern: String) -> void:
if placeholder: line_edit.placeholder_text = placeholder
if value: line_edit.text = value
line_edit.max_length = maxlength.to_int()
if minlength.length() > 0 or pattern.length() > 0:
line_edit.text_changed.connect(_on_text_changed.bind(minlength, pattern))
func _on_text_changed(new_text: String, minlength: String, pattern: String) -> void:
var line_edit = get_node("LineEdit") as LineEdit
var is_valid = true
if minlength.length() > 0 and minlength.is_valid_int():
var min_len = minlength.to_int()
if new_text.length() < min_len and new_text.length() > 0:
is_valid = false
if pattern.length() > 0 and new_text.length() > 0:
var regex = RegEx.new()
if regex.compile(pattern) == OK:
if not regex.search(new_text):
is_valid = false
if is_valid:
line_edit.remove_theme_stylebox_override("normal")
line_edit.remove_theme_stylebox_override("focus")
line_edit.modulate = Color.WHITE
else:
var normal_style = create_red_border_style_from_theme(line_edit, "normal")
var focus_style = create_red_border_style_from_theme(line_edit, "focus")
line_edit.add_theme_stylebox_override("normal", normal_style)
line_edit.add_theme_stylebox_override("focus", focus_style)
line_edit.modulate = Color.WHITE
func create_red_border_style_from_theme(line_edit: LineEdit, style_name: String) -> StyleBoxFlat:
var original_style: StyleBoxFlat = line_edit.get_theme_stylebox(style_name)
var style: StyleBoxFlat = original_style.duplicate()
style.border_color = Color.RED
return style
func _on_picker_color_changed(new_color: Color) -> void:
var hex_string = "#" + new_color.to_html(false)
custom_hex_input.text = hex_string
func _on_custom_hex_submitted(new_text: String) -> void:
var cleaned_text = new_text.strip_edges()
if not cleaned_text.begins_with("#"):
cleaned_text = "#" + cleaned_text
var new_color = Color.from_string(cleaned_text, Color.WHITE)
var picker = ($ColorPickerButton as ColorPickerButton).get_picker()
picker.set_pick_color(new_color)
$ColorPickerButton.color = new_color
func setup_range_input(slider: HSlider, value: String, min_attr: String, max_attr: String, step_attr: String) -> void:
var min_val = min_attr.to_float() if min_attr.length() > 0 else 0.0
var max_val = max_attr.to_float() if max_attr.length() > 0 else 100.0
var step_val = step_attr.to_float() if step_attr.length() > 0 else 1.0
slider.min_value = min_val
slider.max_value = max_val
slider.step = step_val
slider.value = value.to_float() if value.length() > 0 else min_val
func setup_number_input(spin_box: SpinBox, value: String, min_attr: String, max_attr: String, step_attr: String, placeholder: String) -> void:
var min_val = min_attr.to_float() if min_attr.length() > 0 else -99999.0
var max_val = max_attr.to_float() if max_attr.length() > 0 else 99999.0
var step_val = step_attr.to_float() if step_attr.length() > 0 else 1.0
spin_box.min_value = min_val
spin_box.max_value = max_val
spin_box.step = step_val
spin_box.value = value.to_float() if value.length() > 0 else min_val
var line_edit = spin_box.get_line_edit()
line_edit.placeholder_text = placeholder
func setup_file_input(file_container: Control, accept: String = "") -> void:
var file_button = file_container.get_node("FileButton") as Button
var file_label = file_container.get_node("FileLabel") as Label
var file_dialog = file_container.get_node("FileDialog") as FileDialog
if accept.length() > 0:
setup_file_filters(file_dialog, accept)
file_button.pressed.connect(_on_file_button_pressed)
file_dialog.file_selected.connect(_on_file_selected)
file_label.text = "No file chosen"
func setup_file_filters(file_dialog: FileDialog, accept: String) -> void:
file_dialog.clear_filters()
var filters = accept.split(",")
var image_extensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"]
var audio_extensions = ["mp3", "wav", "ogg", "m4a", "flac"]
var video_extensions = ["mp4", "avi", "mov", "wmv", "flv", "webm"]
for filter in filters:
filter = filter.strip_edges()
if filter == "image/*":
for ext in image_extensions:
file_dialog.add_filter("*." + ext, "Image Files")
elif filter == "audio/*":
for ext in audio_extensions:
file_dialog.add_filter("*." + ext, "Audio Files")
elif filter == "video/*":
for ext in video_extensions:
file_dialog.add_filter("*." + ext, "Video Files")
elif filter.begins_with("."):
# Individual file extension
var ext = filter.substr(1)
file_dialog.add_filter("*" + filter, ext.to_upper() + " Files")
elif filter.contains("/"):
# MIME type - convert to common extensions
match filter:
"text/plain":
file_dialog.add_filter("*.txt", "Text Files")
"application/pdf":
file_dialog.add_filter("*.pdf", "PDF Files")
"application/json":
file_dialog.add_filter("*.json", "JSON Files")
"text/html":
file_dialog.add_filter("*.html", "HTML Files")
"text/css":
file_dialog.add_filter("*.css", "CSS Files")
"application/javascript":
file_dialog.add_filter("*.js", "JavaScript Files")
# If no valid filters were added, allow all files
if file_dialog.filters.size() == 0:
file_dialog.add_filter("*", "All Files")
func _on_file_button_pressed() -> void:
var file_dialog = get_node("FileContainer/FileDialog") as FileDialog
file_dialog.popup_centered(Vector2i(800, 600))
func _on_file_selected(path: String) -> void:
var file_label = get_node("FileContainer/FileLabel") as Label
var file_name = path.get_file()
file_label.text = file_name
var file = FileAccess.open(path, FileAccess.READ)
if file:
_file_text_content = file.get_as_text()
file.close()
file = FileAccess.open(path, FileAccess.READ)
_file_binary_content = file.get_buffer(file.get_length())
file.close()
# TODO: when adding Lua, make these actually usable
func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
if not element or not parser:
return
StyleManager.apply_element_styles(self, element, parser)
var styles = parser.get_element_styles_with_inheritance(element, "", [])
var width = null
var height = null
if styles.has("width"):
if styles["width"] == "full":
var parent_styles = parser.get_element_styles_with_inheritance(element.parent, "", []) if element.parent else {}
if parent_styles.has("width"):
var parent_width = SizingUtils.parse_size_value(parent_styles["width"])
if parent_width:
width = parent_width
else:
width = SizingUtils.parse_size_value(styles["width"])
if styles.has("height"):
height = SizingUtils.parse_size_value(styles["height"])
var active_child = null
for child in get_children():
if child.visible:
active_child = child
break
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)
)
active_child.custom_minimum_size = new_child_size
if width:
active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
if height:
active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
if active_child.size.x < new_child_size.x or (new_child_size.y > 0 and active_child.size.y < new_child_size.y):
active_child.size = new_child_size
custom_minimum_size = new_child_size
# Root Control adjusts size flags to match child
if width:
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
else:
size_flags_horizontal = Control.SIZE_EXPAND_FILL
if height:
size_flags_vertical = Control.SIZE_SHRINK_BEGIN
else:
size_flags_vertical = Control.SIZE_SHRINK_CENTER
else:
# No explicit CSS sizing - sync root Control with child's natural size
var child_natural_size = active_child.get_combined_minimum_size()
if child_natural_size == Vector2.ZERO:
child_natural_size = active_child.size
custom_minimum_size = child_natural_size
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
size_flags_vertical = Control.SIZE_SHRINK_CENTER
if active_child.name == "DateButton":
active_child.anchors_preset = Control.PRESET_TOP_LEFT
active_child.position = Vector2.ZERO

View File

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

8
flumi/Scripts/Tags/li.gd Normal file
View File

@@ -0,0 +1,8 @@
extends Control
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
# This is mainly for cases where <li> appears outside of <ul>/<ol>
var label: RichTextLabel = $RichTextLabel
var styles = parser.get_element_styles_with_inheritance(element, "", [])
StyleManager.apply_styles_to_label(label, styles, element, parser)

View File

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

117
flumi/Scripts/Tags/ol.gd Normal file
View File

@@ -0,0 +1,117 @@
extends VBoxContainer
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
var list_type = element.get_attribute("type").to_lower()
if list_type == "": list_type = "decimal" # Default
var item_count = 0
for child_element in element.children:
if child_element.tag_name == "li":
item_count += 1
var marker_min_width = await calculate_marker_width(list_type, item_count)
var index = 1
for child_element in element.children:
if child_element.tag_name == "li":
var li_node = create_li_node(child_element, list_type, index, marker_min_width, parser)
if li_node:
add_child(li_node)
index += 1
func calculate_marker_width(list_type: String, max_index: int) -> float:
var temp_label = RichTextLabel.new()
temp_label.bbcode_enabled = true
temp_label.fit_content = true
temp_label.scroll_active = false
temp_label.theme = BROWSER_TEXT
add_child(temp_label)
var marker_text = get_marker_for_type(list_type, max_index)
StyleManager.apply_styles_to_label(temp_label, {}, null, null, marker_text)
await get_tree().process_frame
var width = temp_label.get_content_width() + 5
remove_child(temp_label)
temp_label.queue_free()
return max(width, 30) # Minimum pixels
func create_li_node(element: HTMLParser.HTMLElement, list_type: String, index: int, marker_width: float = 30, parser: HTMLParser = null) -> Control:
var li_container = HBoxContainer.new()
# Create number/letter marker
var marker_label = RichTextLabel.new()
marker_label.custom_minimum_size = Vector2(marker_width, 0)
marker_label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
marker_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
marker_label.bbcode_enabled = true
marker_label.fit_content = true
marker_label.scroll_active = false
marker_label.theme = BROWSER_TEXT
var marker_text = get_marker_for_type(list_type, index)
var marker_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(marker_label, marker_styles, element, parser, marker_text)
# Create content
var content_label = RichTextLabel.new()
content_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content_label.bbcode_enabled = true
content_label.fit_content = true
content_label.scroll_active = false
content_label.theme = BROWSER_TEXT
var content_text = element.get_bbcode_formatted_text(parser)
var content_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(content_label, content_styles, element, parser, content_text)
li_container.add_theme_constant_override("separation", 0)
li_container.add_child(marker_label)
li_container.add_child(content_label)
var styles = parser.get_element_styles_with_inheritance(element, "", [])
if BackgroundUtils.needs_background_wrapper(styles):
var panel_container = BackgroundUtils.create_panel_container_with_background(styles)
panel_container.name = "Li"
# Get the VBoxContainer inside PanelContainer and replace it with our HBoxContainer
var vbox = panel_container.get_child(0)
panel_container.remove_child(vbox)
vbox.queue_free()
panel_container.add_child(li_container)
return panel_container
else:
return li_container
func get_marker_for_type(list_type: String, index: int) -> String:
match list_type:
"decimal":
return str(index) + "."
"zero-lead":
return "%02d." % index
"lower-alpha", "lower-roman":
return char(96 + index) + "." if list_type == "lower-alpha" else int_to_roman(index).to_lower() + "."
"upper-alpha", "upper-roman":
return char(64 + index) + "." if list_type == "upper-alpha" else int_to_roman(index) + "."
"none":
return ""
_:
return str(index) + "." # Default to decimal
func int_to_roman(num: int) -> String:
var values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
var symbols = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
var result = ""
for i in range(values.size()):
while num >= values[i]:
result += symbols[i]
num -= values[i]
return result

View File

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

View File

@@ -0,0 +1,10 @@
extends Control
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
# This is mainly for cases where <option> appears outside of <select>
var label = RichTextLabel.new()
label.bbcode_enabled = true
label.fit_content = true
label.scroll_active = false
label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
add_child(label)

View File

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

12
flumi/Scripts/Tags/p.gd Normal file
View File

@@ -0,0 +1,12 @@
class_name HTMLP
extends RichTextLabel
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
# 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(400, max(100, plain_text.length() * 12))
custom_minimum_size = Vector2(estimated_width, estimated_height)

View File

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

View File

@@ -0,0 +1,38 @@
extends Control
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
func init(element: HTMLParser.HTMLElement) -> void:
var option_button: OptionButton = $OptionButton
var selected_index = -1
var option_index = 0
# find <option>s
for child_element in element.children:
if child_element.tag_name == "option":
var option_text = child_element.text_content.strip_edges()
var option_value = child_element.get_attribute("value")
option_value = option_text
option_button.add_item(option_text, option_index)
option_button.set_item_metadata(option_index, option_value)
# Check if this option is selected
var is_selected = child_element.get_attribute("selected")
if is_selected.length() > 0 and selected_index == -1:
selected_index = option_index
# Check if this option is disabled
var is_disabled = child_element.get_attribute("disabled")
if is_disabled.length() > 0:
option_button.set_item_disabled(option_index, true)
option_index += 1
# Set the selected item
if selected_index >= 0:
option_button.selected = selected_index
custom_minimum_size = option_button.size

View File

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

View File

@@ -0,0 +1,26 @@
extends Control
var separator_node: Separator
func init(element: HTMLParser.HTMLElement) -> void:
var direction = element.get_attribute("direction")
if direction == "vertical":
separator_node = VSeparator.new()
separator_node.size_flags_vertical = Control.SIZE_EXPAND_FILL
separator_node.custom_minimum_size.x = 2
separator_node.layout_mode = 1
separator_node.anchors_preset = Control.PRESET_LEFT_WIDE
else:
separator_node = HSeparator.new()
separator_node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
separator_node.custom_minimum_size.y = 2
separator_node.layout_mode = 1
separator_node.anchors_preset = Control.PRESET_FULL_RECT
add_child(separator_node)
# Make the parent control also expand to fill available space
size_flags_horizontal = Control.SIZE_EXPAND_FILL
if direction == "vertical":
size_flags_vertical = Control.SIZE_EXPAND_FILL

View File

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

View File

@@ -0,0 +1,17 @@
class_name HTMLSpan
extends RichTextLabel
@onready var rich_text_label: RichTextLabel = self
@onready var background_rect: ColorRect = $BackgroundRect
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
func _ready():
if has_meta("custom_css_background_color"):
add_background_rect()
func add_background_rect():
var color = get_meta("custom_css_background_color")
background_rect.color = color
background_rect.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)

View File

@@ -0,0 +1 @@
uid://4pbphta3r67k

View File

@@ -0,0 +1,74 @@
extends Control
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
func init(element: HTMLParser.HTMLElement) -> void:
var text_edit: TextEdit = $TextEdit
var placeholder = element.get_attribute("placeholder")
var value = element.get_attribute("value")
var rows = element.get_attribute("rows")
var cols = element.get_attribute("cols")
var maxlength = element.get_attribute("maxlength")
var readonly = element.get_attribute("readonly")
var disabled = element.get_attribute("disabled")
# Set placeholder text
text_edit.placeholder_text = placeholder
# Set initial value
if value.length() > 0:
text_edit.text = value
elif element.text_content.length() > 0:
text_edit.text = element.text_content
# We assume to fit $rows amount of new lines
var line_height = text_edit.get_theme_default_font().get_height(text_edit.get_theme_default_font_size())
# We assume the biggest letter typed is "M" (77), and optimize to fit $cols amount of "M"
var char_width = text_edit.get_theme_default_font().get_char_size(77, text_edit.get_theme_default_font_size()).x
var min_height = line_height * (rows.to_int() if rows.length() > 0 else 4) + 26 # padding
var min_width = char_width * (cols.to_int() if cols.length() > 0 else 50) + 16 # padding
text_edit.custom_minimum_size = Vector2(min_width, min_height)
text_edit.size = Vector2(min_width, min_height)
text_edit.min_size = Vector2(min_width, min_height)
# Sync Control size with TextEdit
custom_minimum_size = text_edit.custom_minimum_size
# Set readonly state
if readonly.length() > 0:
text_edit.editable = false
# Set disabled state
if disabled.length() > 0:
text_edit.editable = false
var stylebox = StyleBoxFlat.new()
stylebox.bg_color = Color(0.8, 0.8, 0.8, 1.0)
stylebox.border_color = Color(0, 0, 0, 1.0)
stylebox.border_width_bottom = 1
stylebox.border_width_top = 1
stylebox.border_width_left = 1
stylebox.border_width_right = 1
stylebox.corner_radius_bottom_left = 3
stylebox.corner_radius_bottom_right = 3
stylebox.corner_radius_top_left = 3
stylebox.corner_radius_top_right = 3
text_edit.add_theme_stylebox_override("normal", stylebox)
text_edit.add_theme_stylebox_override("focus", stylebox)
text_edit.add_theme_stylebox_override("readonly", stylebox)
# Handle maxlength
if maxlength.length() > 0 and maxlength.is_valid_int():
var max_len = maxlength.to_int()
text_edit.text_changed.connect(_on_text_changed.bind(max_len))
func _on_text_changed(max_length: int) -> void:
var text_edit = $TextEdit as TextEdit
if text_edit.text.length() > max_length:
var cursor_pos = text_edit.get_caret_column()
var line_pos = text_edit.get_caret_line()
text_edit.text = text_edit.text.substr(0, max_length)
text_edit.set_caret_line(line_pos)
text_edit.set_caret_column(min(cursor_pos, text_edit.get_line(line_pos).length()))

View File

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

97
flumi/Scripts/Tags/ul.gd Normal file
View File

@@ -0,0 +1,97 @@
extends VBoxContainer
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
var list_type = element.get_attribute("type").to_lower()
if list_type == "": list_type = "disc" # Default
var marker_min_width = await calculate_marker_width(list_type)
for child_element in element.children:
if child_element.tag_name == "li":
var li_node = create_li_node(child_element, list_type, marker_min_width, parser)
if li_node:
add_child(li_node)
func calculate_marker_width(list_type: String) -> float:
var temp_label = RichTextLabel.new()
temp_label.bbcode_enabled = true
temp_label.fit_content = true
temp_label.scroll_active = false
temp_label.theme = BROWSER_TEXT
add_child(temp_label)
var bullet_text = get_bullet_for_type(list_type)
StyleManager.apply_styles_to_label(temp_label, {}, null, null, bullet_text)
await get_tree().process_frame
var width = temp_label.get_content_width() + 5 # padding
remove_child(temp_label)
temp_label.queue_free()
return max(width, 20) # Minimum pixels
func create_li_node(element: HTMLParser.HTMLElement, list_type: String, marker_width: float = 20, parser: HTMLParser = null) -> Control:
var li_container = HBoxContainer.new()
# Create bullet point
var bullet_label = RichTextLabel.new()
bullet_label.custom_minimum_size = Vector2(marker_width, 0)
bullet_label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
bullet_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
bullet_label.bbcode_enabled = true
bullet_label.fit_content = true
bullet_label.scroll_active = false
bullet_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
bullet_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
bullet_label.theme = BROWSER_TEXT
var bullet_text = get_bullet_for_type(list_type)
var bullet_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(bullet_label, bullet_styles, element, parser, bullet_text)
# Create content
var content_label = RichTextLabel.new()
content_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content_label.bbcode_enabled = true
content_label.fit_content = true
content_label.theme = BROWSER_TEXT
content_label.scroll_active = false
var content_text = element.get_bbcode_formatted_text(parser)
var content_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(content_label, content_styles, element, parser, content_text)
li_container.add_theme_constant_override("separation", 0)
li_container.add_child(bullet_label)
li_container.add_child(content_label)
var styles = parser.get_element_styles_with_inheritance(element, "", [])
if BackgroundUtils.needs_background_wrapper(styles):
var panel_container = BackgroundUtils.create_panel_container_with_background(styles)
panel_container.name = "Li"
# Get the VBoxContainer inside PanelContainer and replace it with our HBoxContainer
var vbox = panel_container.get_child(0)
panel_container.remove_child(vbox)
vbox.queue_free()
panel_container.add_child(li_container)
return panel_container
else:
return li_container
func get_bullet_for_type(list_type: String) -> String:
match list_type:
"circle":
return ""
"disc":
return ""
"square":
return ""
"none":
return " "
_:
return "" # Default to disc

View File

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

View File

@@ -0,0 +1,171 @@
class_name BackgroundUtils
extends RefCounted
static func create_stylebox_from_styles(styles: Dictionary = {}, container: Control = null) -> StyleBoxFlat:
var style_box = StyleBoxFlat.new()
# Background color
var bg_color = null
if styles.has("background-color"):
bg_color = styles["background-color"]
elif container and container.has_meta("custom_css_background_color"):
bg_color = container.get_meta("custom_css_background_color")
if bg_color:
style_box.bg_color = bg_color
else:
style_box.bg_color = Color.TRANSPARENT
# Border radius
var border_radius = null
if styles.has("border-radius"):
border_radius = styles["border-radius"]
elif container and container.has_meta("custom_css_border_radius"):
border_radius = container.get_meta("custom_css_border_radius")
if border_radius:
var radius = StyleManager.parse_radius(border_radius)
style_box.corner_radius_top_left = radius
style_box.corner_radius_top_right = radius
style_box.corner_radius_bottom_left = radius
style_box.corner_radius_bottom_right = radius
# Border properties
var has_border = false
style_box.border_width_top = 0
style_box.border_width_right = 0
style_box.border_width_bottom = 0
style_box.border_width_left = 0
var general_border_width = null
if styles.has("border-width"):
general_border_width = styles["border-width"]
elif container and container.has_meta("custom_css_border_width"):
general_border_width = container.get_meta("custom_css_border_width")
if general_border_width:
has_border = true
var parsed_width = StyleManager.parse_size(general_border_width)
style_box.border_width_top = parsed_width
style_box.border_width_right = parsed_width
style_box.border_width_bottom = parsed_width
style_box.border_width_left = parsed_width
var individual_border_keys = [
["border-top-width", "border_width_top"],
["border-right-width", "border_width_right"],
["border-bottom-width", "border_width_bottom"],
["border-left-width", "border_width_left"]
]
for pair in individual_border_keys:
var style_key = pair[0]
var property_name = pair[1]
var width = null
var meta_key = "custom_css_" + style_key.replace("-", "_")
if styles.has(style_key):
width = styles[style_key]
elif container and container.has_meta(meta_key):
width = container.get_meta(meta_key)
if width:
has_border = true
var parsed_width = StyleManager.parse_size(width)
style_box.set(property_name, parsed_width)
var border_color = Color.BLACK
if styles.has("border-color"):
border_color = styles["border-color"]
elif container and container.has_meta("custom_css_border_color"):
border_color = container.get_meta("custom_css_border_color")
if has_border:
style_box.border_color = border_color
# Padding as content margins
var has_padding = false
if styles.size() > 0:
has_padding = styles.has("padding") or styles.has("padding-top") or styles.has("padding-right") or styles.has("padding-bottom") or styles.has("padding-left")
elif container:
has_padding = container.has_meta("padding") or container.has_meta("padding-top") or container.has_meta("padding-right") or container.has_meta("padding-bottom") or container.has_meta("padding-left")
if has_padding:
# General padding
var padding_val = null
if styles.has("padding"):
padding_val = StyleManager.parse_size(styles["padding"])
elif container and container.has_meta("padding"):
padding_val = StyleManager.parse_size(container.get_meta("padding"))
if padding_val:
style_box.content_margin_left = padding_val
style_box.content_margin_right = padding_val
style_box.content_margin_top = padding_val
style_box.content_margin_bottom = padding_val
# Individual padding values override general padding
var padding_keys = [["padding-left", "content_margin_left"], ["padding-right", "content_margin_right"], ["padding-top", "content_margin_top"], ["padding-bottom", "content_margin_bottom"]]
for pair in padding_keys:
var key = pair[0]
var property = pair[1]
var val = null
if styles.has(key):
val = StyleManager.parse_size(styles[key])
elif container and container.has_meta(key):
val = StyleManager.parse_size(container.get_meta(key))
if val:
style_box.set(property, val)
return style_box
# for AutoSizingFlexContainer
static func update_background_panel(container: Control) -> void:
var needs_background = container.has_meta("custom_css_background_color") or container.has_meta("custom_css_border_radius")
var needs_padding = container.has_meta("padding") or container.has_meta("padding-top") or container.has_meta("padding-right") or container.has_meta("padding-bottom") or container.has_meta("padding-left")
var background_panel = get_background_panel(container)
if needs_background or needs_padding:
if not background_panel:
background_panel = Panel.new()
background_panel.name = "BackgroundPanel"
background_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
background_panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
container.add_child(background_panel)
container.move_child(background_panel, 0) # first child
var style_box = create_stylebox_from_styles({}, container)
background_panel.add_theme_stylebox_override("panel", style_box)
elif background_panel:
background_panel.queue_free()
# Helper methods for AutoSizingFlexContainer
static func get_background_panel(container: Control) -> Panel:
for child in container.get_children():
if child.name == "BackgroundPanel" and child is Panel:
return child
return null
static func is_background_panel(node: Node) -> bool:
return node.name == "BackgroundPanel" and node is Panel
# for any other tag
static func create_panel_container_with_background(styles: Dictionary) -> PanelContainer:
var panel_container = PanelContainer.new()
panel_container.name = "Div"
var vbox = VBoxContainer.new()
vbox.name = "VBoxContainer"
panel_container.add_child(vbox)
var style_box = create_stylebox_from_styles(styles)
panel_container.add_theme_stylebox_override("panel", style_box)
return panel_container
static func needs_background_wrapper(styles: Dictionary) -> bool:
return styles.has("background-color") or styles.has("border-radius") or styles.has("padding") or styles.has("padding-top") or styles.has("padding-right") or styles.has("padding-bottom") or styles.has("padding-left") or styles.has("border-width") or styles.has("border-top-width") or styles.has("border-right-width") or styles.has("border-bottom-width") or styles.has("border-left-width") or styles.has("border-color") or styles.has("border-style") or styles.has("border-top-color") or styles.has("border-right-color") or styles.has("border-bottom-color") or styles.has("border-left-color")

View File

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

View File

@@ -0,0 +1,108 @@
class_name ColorUtils
extends RefCounted
static func parse_color(color_string: String) -> Color:
color_string = color_string.strip_edges()
# Handle hex colors
if color_string.begins_with("#"):
return Color.from_string(color_string, Color.WHITE)
# Handle rgb/rgba
if color_string.begins_with("rgb"):
var regex = RegEx.new()
regex.compile("rgba?\\(([^)]+)\\)")
var result = regex.search(color_string)
if result:
var values = result.get_string(1).split(",")
if values.size() >= 3:
var r = values[0].strip_edges().to_float() / 255.0
var g = values[1].strip_edges().to_float() / 255.0
var b = values[2].strip_edges().to_float() / 255.0
var a = 1.0
if values.size() >= 4:
a = values[3].strip_edges().to_float()
return Color(r, g, b, a)
# Handle named colors - delegate to get_color function
return get_color(color_string)
static func get_color(color_name: String) -> Color:
# Common colors
match color_name:
"white": return Color.WHITE
"black": return Color.BLACK
"transparent": return Color.TRANSPARENT
# Gray scale
"slate-50": return Color.from_string("#f8fafc", Color.WHITE)
"slate-100": return Color.from_string("#f1f5f9", Color.WHITE)
"slate-200": return Color.from_string("#e2e8f0", Color.WHITE)
"slate-300": return Color.from_string("#cbd5e1", Color.WHITE)
"slate-400": return Color.from_string("#94a3b8", Color.WHITE)
"slate-500": return Color.from_string("#64748b", Color.WHITE)
"slate-600": return Color.from_string("#475569", Color.WHITE)
"slate-700": return Color.from_string("#334155", Color.WHITE)
"slate-800": return Color.from_string("#1e293b", Color.WHITE)
"slate-900": return Color.from_string("#0f172a", Color.WHITE)
"gray-50": return Color.from_string("#f9fafb", Color.WHITE)
"gray-100": return Color.from_string("#f3f4f6", Color.WHITE)
"gray-200": return Color.from_string("#e5e7eb", Color.WHITE)
"gray-300": return Color.from_string("#d1d5db", Color.WHITE)
"gray-400": return Color.from_string("#9ca3af", Color.WHITE)
"gray-500": return Color.from_string("#6b7280", Color.WHITE)
"gray-600": return Color.from_string("#4b5563", Color.WHITE)
"gray-700": return Color.from_string("#374151", Color.WHITE)
"gray-800": return Color.from_string("#1f2937", Color.WHITE)
"gray-900": return Color.from_string("#111827", Color.WHITE)
# Red
"red-50": return Color.from_string("#fef2f2", Color.WHITE)
"red-100": return Color.from_string("#fee2e2", Color.WHITE)
"red-200": return Color.from_string("#fecaca", Color.WHITE)
"red-300": return Color.from_string("#fca5a5", Color.WHITE)
"red-400": return Color.from_string("#f87171", Color.WHITE)
"red-500": return Color.from_string("#ef4444", Color.WHITE)
"red-600": return Color.from_string("#dc2626", Color.WHITE)
"red-700": return Color.from_string("#b91c1c", Color.WHITE)
"red-800": return Color.from_string("#991b1b", Color.WHITE)
"red-900": return Color.from_string("#7f1d1d", Color.WHITE)
# Green
"green-50": return Color.from_string("#f0fdf4", Color.WHITE)
"green-100": return Color.from_string("#dcfce7", Color.WHITE)
"green-200": return Color.from_string("#bbf7d0", Color.WHITE)
"green-300": return Color.from_string("#86efac", Color.WHITE)
"green-400": return Color.from_string("#4ade80", Color.WHITE)
"green-500": return Color.from_string("#22c55e", Color.WHITE)
"green-600": return Color.from_string("#16a34a", Color.WHITE)
"green-700": return Color.from_string("#15803d", Color.WHITE)
"green-800": return Color.from_string("#166534", Color.WHITE)
"green-900": return Color.from_string("#14532d", Color.WHITE)
# Blue
"blue-50": return Color.from_string("#eff6ff", Color.WHITE)
"blue-100": return Color.from_string("#dbeafe", Color.WHITE)
"blue-200": return Color.from_string("#bfdbfe", Color.WHITE)
"blue-300": return Color.from_string("#93c5fd", Color.WHITE)
"blue-400": return Color.from_string("#60a5fa", Color.WHITE)
"blue-500": return Color.from_string("#3b82f6", Color.WHITE)
"blue-600": return Color.from_string("#2563eb", Color.WHITE)
"blue-700": return Color.from_string("#1d4ed8", Color.WHITE)
"blue-800": return Color.from_string("#1e40af", Color.WHITE)
"blue-900": return Color.from_string("#1e3a8a", Color.WHITE)
# Yellow
"yellow-50": return Color.from_string("#fefce8", Color.WHITE)
"yellow-100": return Color.from_string("#fef9c3", Color.WHITE)
"yellow-200": return Color.from_string("#fef08a", Color.WHITE)
"yellow-300": return Color.from_string("#fde047", Color.WHITE)
"yellow-400": return Color.from_string("#facc15", Color.WHITE)
"yellow-500": return Color.from_string("#eab308", Color.WHITE)
"yellow-600": return Color.from_string("#ca8a04", Color.WHITE)
"yellow-700": return Color.from_string("#a16207", Color.WHITE)
"yellow-800": return Color.from_string("#854d0e", Color.WHITE)
"yellow-900": return Color.from_string("#713f12", Color.WHITE)
_: return Color.BLACK

View File

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

View File

@@ -0,0 +1,127 @@
class_name FlexUtils
extends RefCounted
static func apply_flex_container_properties(node, styles: Dictionary) -> void:
# Flex direction - default to row if not specified
if styles.has("flex-direction"):
match styles["flex-direction"]:
"row": node.flex_direction = FlexContainer.FlexDirection.Row
"row-reverse": node.flex_direction = FlexContainer.FlexDirection.RowReverse
"column": node.flex_direction = FlexContainer.FlexDirection.Column
"column-reverse": node.flex_direction = FlexContainer.FlexDirection.ColumnReverse
else:
node.flex_direction = FlexContainer.FlexDirection.Row
# Flex wrap
if styles.has("flex-wrap"):
match styles["flex-wrap"]:
"nowrap": node.flex_wrap = FlexContainer.FlexWrap.NoWrap
"wrap": node.flex_wrap = FlexContainer.FlexWrap.Wrap
"wrap-reverse": node.flex_wrap = FlexContainer.FlexWrap.WrapReverse
# Justify content
if styles.has("justify-content"):
match styles["justify-content"]:
"flex-start": node.justify_content = FlexContainer.JustifyContent.FlexStart
"flex-end": node.justify_content = FlexContainer.JustifyContent.FlexEnd
"center": node.justify_content = FlexContainer.JustifyContent.Center
"space-between": node.justify_content = FlexContainer.JustifyContent.SpaceBetween
"space-around": node.justify_content = FlexContainer.JustifyContent.SpaceAround
"space-evenly": node.justify_content = FlexContainer.JustifyContent.SpaceEvenly
# Align items
if styles.has("align-items"):
match styles["align-items"]:
"flex-start": node.align_items = FlexContainer.AlignItems.FlexStart
"flex-end": node.align_items = FlexContainer.AlignItems.FlexEnd
"center": node.align_items = FlexContainer.AlignItems.Center
"stretch": node.align_items = FlexContainer.AlignItems.Stretch
"baseline": node.align_items = FlexContainer.AlignItems.Baseline
# Align content
if styles.has("align-content"):
match styles["align-content"]:
"flex-start": node.align_content = FlexContainer.AlignContent.FlexStart
"flex-end": node.align_content = FlexContainer.AlignContent.FlexEnd
"center": node.align_content = FlexContainer.AlignContent.Center
"stretch": node.align_content = FlexContainer.AlignContent.Stretch
"space-between": node.align_content = FlexContainer.AlignContent.SpaceBetween
"space-around": node.align_content = FlexContainer.AlignContent.SpaceAround
# Gap
if styles.has("gap"):
# YGGutterAll = 2
node._root.set_gap(2, parse_flex_value(styles["gap"]))
if styles.has("row-gap"):
# YGGutterRow = 1
node._root.set_gap(1, parse_flex_value(styles["row-gap"]))
if styles.has("column-gap"):
# YGGutterColumn = 0
node._root.set_gap(0, parse_flex_value(styles["column-gap"]))
if styles.has("width"):
var width_val = styles["width"]
if width_val == "full":
# For flex containers, w-full should expand to fill parent
node.set_meta("should_fill_horizontal", true)
elif typeof(width_val) == TYPE_STRING and width_val.ends_with("%"):
node.set_meta("custom_css_width_percentage", width_val)
else:
node.set_meta("custom_css_width", SizingUtils.parse_size_value(width_val))
if styles.has("height"):
var height_val = styles["height"]
if height_val == "full":
# For flex containers, h-full should expand to fill parent
node.set_meta("should_fill_vertical", true)
elif typeof(height_val) == TYPE_STRING and height_val.ends_with("%"):
node.set_meta("custom_css_height_percentage", height_val)
else:
node.set_meta("custom_css_height", SizingUtils.parse_size_value(height_val))
if styles.has("background-color"):
node.set_meta("custom_css_background_color", styles["background-color"])
node.update_layout()
static func apply_flex_item_properties(node: Control, styles: Dictionary) -> void:
var properties: Dictionary = node.get_meta("flex_metas", {}).duplicate(true)
var changed = false
if styles.has("flex-grow"):
properties["grow"] = float(styles["flex-grow"])
changed = true
if styles.has("flex-shrink"):
properties["shrink"] = float(styles["flex-shrink"])
changed = true
if styles.has("flex-basis"):
properties["basis"] = parse_flex_value(styles["flex-basis"])
changed = true
if styles.has("align-self"):
var align_self_value = -1
match styles["align-self"]:
"auto": align_self_value = FlexContainer.AlignItems.Auto
"flex-start": align_self_value = FlexContainer.AlignItems.FlexStart
"flex-end": align_self_value = FlexContainer.AlignItems.FlexEnd
"center": align_self_value = FlexContainer.AlignItems.Center
"stretch": align_self_value = FlexContainer.AlignItems.Stretch
"baseline": align_self_value = FlexContainer.AlignItems.Baseline
if align_self_value != -1:
properties["align_self"] = align_self_value
changed = true
if changed:
node.set_meta("flex_metas", properties)
var parent = node.get_parent()
if parent is FlexContainer:
parent.update_layout()
static func parse_flex_value(val):
if val is float or val is int:
return float(val)
if val is String:
var s_val = val.strip_edges()
if s_val.is_valid_float():
return s_val.to_float()
if s_val.ends_with("%"):
return s_val.trim_suffix("%").to_float() / 100.0
if s_val.ends_with("px"):
return s_val.trim_suffix("px").to_float()
if s_val == "auto":
return "auto"
return null

View File

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

View File

@@ -0,0 +1,50 @@
class_name SizeUtils
extends RefCounted
# Utility functions for parsing CSS size values
static func parse_size(val: String) -> String:
if val == null or val.is_empty():
return "0px"
var named = {
"0": "0px", "1": "4px", "2": "8px", "3": "12px", "4": "16px", "5": "20px", "6": "24px", "8": "32px", "10": "40px",
"12": "48px", "16": "64px", "20": "80px", "24": "96px", "28": "112px", "32": "128px", "36": "144px", "40": "160px",
"44": "176px", "48": "192px", "52": "208px", "56": "224px", "60": "240px", "64": "256px", "72": "288px", "80": "320px", "96": "384px",
"3xs": "256px", "2xs": "288px", "xs": "320px", "sm": "384px", "md": "448px", "lg": "512px",
"xl": "576px", "2xl": "672px", "3xl": "768px", "4xl": "896px", "5xl": "1024px", "6xl": "1152px", "7xl": "1280px"
}
if named.has(val):
return named[val]
# Fractional (e.g. 1/2, 1/3)
if val.find("/") != -1:
var parts = val.split("/")
if parts.size() == 2 and \
parts[1].is_valid_int() and \
parts[0].is_valid_int() and \
int(parts[1]) != 0:
var frac = float(parts[0]) / float(parts[1])
return str(frac * 100.0) + "%"
if val.is_valid_int():
return str(int(val) * 16) + "px"
return val
static func extract_bracket_content(string: String, start_idx: int) -> String:
var open_idx = string.find("[", start_idx)
if open_idx == -1:
return ""
var close_idx = string.find("]", open_idx)
if close_idx == -1:
return ""
return string.substr(open_idx + 1, close_idx - open_idx - 1)
static func parse_radius(radius_str: String) -> int:
if radius_str.ends_with("px"):
return int(radius_str.replace("px", ""))
elif radius_str.ends_with("rem"):
return int(radius_str.replace("rem", "")) * 16
elif radius_str.is_valid_float():
return int(radius_str)
else:
return 0

View File

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

View File

@@ -0,0 +1,129 @@
class_name SizingUtils
extends RefCounted
# Utility functions for handling sizes in the UI
const DEFAULT_VIEWPORT_WIDTH = 800.0
const DEFAULT_VIEWPORT_HEIGHT = 600.0
static func parse_size_value(val):
if val == null: return null
if typeof(val) == TYPE_INT or typeof(val) == TYPE_FLOAT:
return float(val)
if val.ends_with("px"):
return float(val.replace("px", ""))
if val.ends_with("rem"):
return float(val.replace("rem", "")) * 16.0
if val.ends_with("%") or (val.ends_with("]") and "%" in val):
var clean_val = val.replace("[", "").replace("]", "")
return clean_val
if val == "full":
return null
return float(val)
static func should_skip_sizing(node: Control, element, parser) -> bool:
var element_styles = parser.get_element_styles_internal(element, "")
# Button sizing rules: Skip sizing only when button has no explicit size
# AND parent doesn't have explicit width (auto-inherited sizing)
if node is HTMLButton:
# If button has explicit size, don't skip sizing
if element_styles.has("width") or element_styles.has("height"):
return false
# Check if width is being inherited from parent with explicit size
var parent_element = element.parent
if parent_element:
var parent_styles = parser.get_element_styles_internal(parent_element, "")
var parent_has_explicit_width = parent_styles.has("width")
# Skip only if parent doesn't have explicit width (auto-inherited)
return not parent_has_explicit_width
return true
# Span sizing rules: Always skip sizing for spans since they're inline elements
# (flex containers use AutoSizingFlexContainer, not span.gd)
elif node is HTMLSpan:
return true
return false
static func apply_container_dimension_sizing(node: Control, width, height) -> void:
if width != null:
if is_percentage(width):
node.set_meta("container_percentage_width", width)
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
apply_container_percentage_sizing(node)
else:
node.custom_minimum_size.x = width
node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
if height != null:
if is_percentage(height):
node.set_meta("container_percentage_height", height)
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
apply_container_percentage_sizing(node)
else:
node.custom_minimum_size.y = height
node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
static func apply_regular_control_sizing(node: Control, width, height) -> void:
if width != null:
if is_percentage(width):
var estimated_width = calculate_percentage_size(width, DEFAULT_VIEWPORT_WIDTH)
node.custom_minimum_size.x = estimated_width
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
else:
node.custom_minimum_size.x = width
node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
if height != null:
if is_percentage(height):
var estimated_height = calculate_percentage_size(height, DEFAULT_VIEWPORT_HEIGHT)
node.custom_minimum_size.y = estimated_height
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
else:
node.custom_minimum_size.y = height
node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
static func is_percentage(value) -> bool:
return typeof(value) == TYPE_STRING and value.ends_with("%")
static func calculate_percentage_size(percentage_str: String, fallback_size: float) -> float:
var clean_percentage = percentage_str.replace("%", "")
var percentage = float(clean_percentage) / 100.0
return fallback_size * percentage
static func apply_container_percentage_sizing(node: Control) -> void:
var parent = node.get_parent()
if not parent:
return
var new_min_size = node.custom_minimum_size
if node.has_meta("container_percentage_width"):
var percentage_str = node.get_meta("container_percentage_width")
var parent_width = get_parent_dimension(parent, true, DEFAULT_VIEWPORT_WIDTH)
new_min_size.x = calculate_percentage_size(percentage_str, parent_width)
if node.has_meta("container_percentage_height"):
var percentage_str = node.get_meta("container_percentage_height")
var parent_height = get_parent_dimension(parent, false, DEFAULT_VIEWPORT_HEIGHT)
new_min_size.y = calculate_percentage_size(percentage_str, parent_height)
node.custom_minimum_size = new_min_size
static func get_parent_dimension(parent: Control, is_width: bool, fallback: float) -> float:
var size_value = parent.size.x if is_width else parent.size.y
if size_value > 0:
return size_value
var rect_size = parent.get_rect().size.x if is_width else parent.get_rect().size.y
if rect_size > 0:
return rect_size
var min_size = parent.custom_minimum_size.x if is_width else parent.custom_minimum_size.y
if min_size > 0:
return min_size
return fallback

View File

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

View File

@@ -0,0 +1,55 @@
class_name UtilityClassValidator
extends RefCounted
static var compiled_patterns: Array = []
# TODO: hardcoded colors gotta be swapped with Tailwind colors. stuff like "text-red-500" is considered a selector class
static func init_patterns():
if compiled_patterns.size() == 0:
var utility_patterns = [
"^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl)$", # font sizes
"^text-(left|center|right|justify)$", # text alignment
"^text-\\[.*\\]$", # custom text colors
"^text-(white|black|transparent|slate-\\d+|gray-\\d+|red-\\d+|green-\\d+|blue-\\d+|yellow-\\d+)$", # text colors
"^bg-\\[.*\\]$", # custom bg colors
"^bg-(white|black|transparent|slate-\\d+|gray-\\d+|red-\\d+|green-\\d+|blue-\\d+|yellow-\\d+)$", # bg colors
"^(w|h|min-w|min-h|max-w|max-h)-", # sizing
"^font-(bold|mono|italic|sans|serif)$", # font styles
"^font-\\[.*\\]$", # custom font families with brackets
"^font-[a-zA-Z][a-zA-Z0-9_-]*$", # custom font families without brackets
"^underline$",
"^flex", # flex utilities
"^items-", # align items
"^justify-", # justify content
"^content-", # align content
"^self-", # align self
"^order-", # order
"^gap-", # gap
"^(p|px|py|pt|pr|pb|pl)-", # padding
"^rounded", # border radius
"^basis-", # flex basis
"^(mx|my|m)-auto$", # margin auto for centering
"^border$", # general border
"^border-\\d+$", # border width (e.g., border-2)
"^border-\\[.*\\]$", # custom border width/color (e.g., border-[2px], border-[#ff0000])
"^border-none$", # border styles
"^border-(t|r|b|l)$", # individual border sides (e.g., border-t)
"^border-(t|r|b|l)-\\d+$", # individual border side widths (e.g., border-t-2)
"^border-(t|r|b|l)-\\[.*\\]$", # custom individual border sides (e.g., border-t-[2px])
"^border-(t|r|b|l)-(white|black|transparent|slate-\\d+|gray-\\d+|red-\\d+|green-\\d+|blue-\\d+|yellow-\\d+)$", # individual border side colors
"^border-(white|black|transparent|slate-\\d+|gray-\\d+|red-\\d+|green-\\d+|blue-\\d+|yellow-\\d+)$", # border colors
"^(hover|active):", # pseudo classes
]
for pattern in utility_patterns:
var regex = RegEx.new()
regex.compile(pattern)
compiled_patterns.append(regex)
static func is_utility_class(cls: String) -> bool:
# once
init_patterns()
for regex in compiled_patterns:
if regex.search(cls):
return true
return false

View File

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

362
flumi/Scripts/main.gd Normal file
View File

@@ -0,0 +1,362 @@
class_name Main
extends Control
@onready var website_container: Control = %WebsiteContainer
@onready var website_background: Control = %WebsiteBackground
@onready var tab_container: TabManager = $VBoxContainer/TabContainer
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
const AUTO_SIZING_FLEX_CONTAINER = preload("res://Scripts/AutoSizingFlexContainer.gd")
const P = preload("res://Scenes/Tags/p.tscn")
const IMG = preload("res://Scenes/Tags/img.tscn")
const SEPARATOR = preload("res://Scenes/Tags/separator.tscn")
const PRE = P
const BR = preload("res://Scenes/Tags/br.tscn")
const SPAN = preload("res://Scenes/Tags/span.tscn")
const H1 = P
const H2 = P
const H3 = P
const H4 = P
const H5 = P
const H6 = P
const FORM = preload("res://Scenes/Tags/form.tscn")
const INPUT = preload("res://Scenes/Tags/input.tscn")
const BUTTON = preload("res://Scenes/Tags/button.tscn")
const UL = preload("res://Scenes/Tags/ul.tscn")
const OL = preload("res://Scenes/Tags/ol.tscn")
const LI = preload("res://Scenes/Tags/li.tscn")
const SELECT = preload("res://Scenes/Tags/select.tscn")
const OPTION = preload("res://Scenes/Tags/option.tscn")
const TEXTAREA = preload("res://Scenes/Tags/textarea.tscn")
const DIV = preload("res://Scenes/Tags/div.tscn")
const MIN_SIZE = Vector2i(750, 200)
var font_dependent_elements: Array = []
func should_group_as_inline(element: HTMLParser.HTMLElement) -> bool:
# Don't group inputs unless they're inside a form
if element.tag_name == "input":
# Check if this element has a form ancestor
var parent = element.parent
while parent:
if parent.tag_name == "form":
return true
parent = parent.parent
return false
return element.is_inline_element()
func _ready():
ProjectSettings.set_setting("display/window/size/min_width", MIN_SIZE.x)
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
DisplayServer.window_set_min_size(MIN_SIZE)
func render() -> void:
# Clear existing content
for child in website_container.get_children():
child.queue_free()
font_dependent_elements.clear()
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()
parser.process_styles()
# Process and load all custom fonts defined in <font> tags
parser.process_fonts()
FontManager.load_all_fonts()
if parse_result.errors.size() > 0:
print("Parse errors: " + str(parse_result.errors))
var tab = tab_container.tabs[tab_container.active_tab]
var title = parser.get_title()
tab.set_title(title)
var icon = parser.get_icon()
tab.update_icon_from_url(icon)
var body = parser.find_first("body")
if body:
StyleManager.apply_body_styles(body, parser, website_container, website_background)
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:
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:
# ul/ol handle their own adding
if element.tag_name != "ul" and element.tag_name != "ol":
safe_add_child(website_container, element_node)
# 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
static func safe_add_child(parent: Node, child: Node) -> void:
if child.get_parent():
child.get_parent().remove_child(child)
parent.add_child(child)
func contains_hyperlink(element: HTMLParser.HTMLElement) -> bool:
if element.tag_name == "a":
return true
for child in element.children:
if contains_hyperlink(child):
return true
return false
func is_text_only_element(element: HTMLParser.HTMLElement) -> bool:
if element.children.size() == 0:
var text = element.get_collapsed_text()
return not text.is_empty()
return false
func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) -> Control:
var styles = parser.get_element_styles_with_inheritance(element, "", [])
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
var final_node: Control
var container_for_children: Node
# If this is an inline element AND not a flex container, do NOT recursively add child nodes for its children.
# Only create a node for the outermost inline group; nested inline tags are handled by BBCode.
if element.is_inline_element() and not is_flex_container:
final_node = await create_element_node_internal(element, parser)
if not final_node:
return null
final_node = StyleManager.apply_element_styles(final_node, element, parser)
# Flex item properties may still apply
StyleManager.apply_flex_item_properties(final_node, styles)
return final_node
if is_flex_container:
# The element's primary identity IS a flex container.
# We create it directly.
final_node = AUTO_SIZING_FLEX_CONTAINER.new()
final_node.name = "Flex_" + element.tag_name
container_for_children = final_node
# 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":
final_node.flex_direction = FlexContainer.FlexDirection.Column
website_container.add_child(final_node)
var temp_list = UL.instantiate() if element.tag_name == "ul" else OL.instantiate()
website_container.add_child(temp_list)
await temp_list.init(element, parser)
for child in temp_list.get_children():
temp_list.remove_child(child)
container_for_children.add_child(child)
website_container.remove_child(temp_list)
temp_list.queue_free()
# 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)
else:
final_node = await create_element_node_internal(element, parser)
if not final_node:
return null # Unsupported tag
# If final_node is a PanelContainer, children should go to the VBoxContainer inside
if final_node is PanelContainer and final_node.get_child_count() > 0:
container_for_children = final_node.get_child(0) # The VBoxContainer inside
else:
container_for_children = final_node
# 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:
StyleManager.apply_flex_container_properties(final_node, styles)
# Apply flex ITEM properties
StyleManager.apply_flex_item_properties(final_node, styles)
# Skip ul/ol and non-flex forms, they handle their own children
var skip_general_processing = false
if element.tag_name == "ul" or element.tag_name == "ol":
skip_general_processing = true
elif element.tag_name == "form":
skip_general_processing = not is_flex_container
if not skip_general_processing:
for child_element in element.children:
# Only add child nodes if the child is NOT an inline element
# UNLESS the parent is a flex container (inline elements become flex items)
if not child_element.is_inline_element() or is_flex_container:
var child_node = await create_element_node(child_element, parser)
if child_node and is_instance_valid(container_for_children):
safe_add_child(container_for_children, child_node)
return final_node
func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> Control:
var node: Control = null
match element.tag_name:
"p":
node = P.instantiate()
node.init(element)
"pre":
node = PRE.instantiate()
node.init(element)
"h1", "h2", "h3", "h4", "h5", "h6":
match element.tag_name:
"h1": node = H1.instantiate()
"h2": node = H2.instantiate()
"h3": node = H3.instantiate()
"h4": node = H4.instantiate()
"h5": node = H5.instantiate()
"h6": node = H6.instantiate()
node.init(element)
"br":
node = BR.instantiate()
node.init(element)
"img":
node = IMG.instantiate()
node.init(element)
"separator":
node = SEPARATOR.instantiate()
node.init(element)
"form":
var form_styles = parser.get_element_styles_with_inheritance(element, "", [])
var is_flex_form = form_styles.has("display") and ("flex" in form_styles["display"])
if is_flex_form:
# Don't create a form node here - return null so general processing takes over
return null
else:
node = FORM.instantiate()
node.init(element)
# Manually process children for non-flex forms
for child_element in element.children:
var child_node = await create_element_node(child_element, parser)
if child_node:
safe_add_child(node, child_node)
"input":
node = INPUT.instantiate()
node.init(element, parser)
"button":
node = BUTTON.instantiate()
node.init(element, parser)
"span", "b", "i", "u", "small", "mark", "code", "a":
node = SPAN.instantiate()
node.init(element, parser)
"ul":
node = UL.instantiate()
website_container.add_child(node)
await node.init(element, parser)
return node
"ol":
node = OL.instantiate()
website_container.add_child(node)
await node.init(element, parser)
return node
"li":
node = LI.instantiate()
node.init(element, parser)
"select":
node = SELECT.instantiate()
node.init(element)
"option":
node = OPTION.instantiate()
node.init(element, parser)
"textarea":
node = TEXTAREA.instantiate()
node.init(element)
"div":
var styles = parser.get_element_styles_with_inheritance(element, "", [])
# Create div container
if BackgroundUtils.needs_background_wrapper(styles):
node = BackgroundUtils.create_panel_container_with_background(styles)
else:
node = DIV.instantiate()
node.init(element)
var has_only_text = is_text_only_element(element)
if has_only_text:
var p_node = P.instantiate()
p_node.init(element)
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
safe_add_child(container_for_children, p_node)
_:
return null
return node
func register_font_dependent_element(label: Control, styles: Dictionary, element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
font_dependent_elements.append({
"label": label,
"styles": styles,
"element": element,
"parser": parser
})
func refresh_fonts(font_name: String) -> void:
# Find all elements that should use this font and refresh them
for element_info in font_dependent_elements:
var label = element_info["label"]
var styles = element_info["styles"]
var element = element_info["element"]
var parser = element_info["parser"]
if styles.has("font-family") and styles["font-family"] == font_name:
if is_instance_valid(label):
StyleManager.apply_styles_to_label(label, styles, element, parser)

View File

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