move wayfinder files to Flumi folder
This commit is contained in:
204
flumi/Scripts/AutoSizingFlexContainer.gd
Normal file
204
flumi/Scripts/AutoSizingFlexContainer.gd
Normal 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
|
||||
1
flumi/Scripts/AutoSizingFlexContainer.gd.uid
Normal file
1
flumi/Scripts/AutoSizingFlexContainer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://feiw2baeu5ye
|
||||
828
flumi/Scripts/B9/CSSParser.gd
Normal file
828
flumi/Scripts/B9/CSSParser.gd
Normal 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
|
||||
1
flumi/Scripts/B9/CSSParser.gd.uid
Normal file
1
flumi/Scripts/B9/CSSParser.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cffcjsiwgyln
|
||||
405
flumi/Scripts/B9/HTMLParser.gd
Normal file
405
flumi/Scripts/B9/HTMLParser.gd
Normal 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
|
||||
1
flumi/Scripts/B9/HTMLParser.gd.uid
Normal file
1
flumi/Scripts/B9/HTMLParser.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bw7u24wgio8ij
|
||||
592
flumi/Scripts/Constants.gd
Normal file
592
flumi/Scripts/Constants.gd
Normal 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()
|
||||
1
flumi/Scripts/Constants.gd.uid
Normal file
1
flumi/Scripts/Constants.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b4qt7eno8g41p
|
||||
91
flumi/Scripts/FontManager.gd
Normal file
91
flumi/Scripts/FontManager.gd
Normal 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
|
||||
1
flumi/Scripts/FontManager.gd.uid
Normal file
1
flumi/Scripts/FontManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c0kg201cqluo8
|
||||
68
flumi/Scripts/MaxSizeControl.gd
Normal file
68
flumi/Scripts/MaxSizeControl.gd
Normal 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
|
||||
1
flumi/Scripts/MaxSizeControl.gd.uid
Normal file
1
flumi/Scripts/MaxSizeControl.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cmxmcn3ghw8t2
|
||||
61
flumi/Scripts/Network.gd
Normal file
61
flumi/Scripts/Network.gd
Normal 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
|
||||
1
flumi/Scripts/Network.gd.uid
Normal file
1
flumi/Scripts/Network.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bbfpng3opsnyp
|
||||
64
flumi/Scripts/ResizableTextEdit.gd
Normal file
64
flumi/Scripts/ResizableTextEdit.gd
Normal 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
|
||||
|
||||
1
flumi/Scripts/ResizableTextEdit.gd.uid
Normal file
1
flumi/Scripts/ResizableTextEdit.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c5xpoyqcg1p8k
|
||||
402
flumi/Scripts/StyleManager.gd
Normal file
402
flumi/Scripts/StyleManager.gd
Normal 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)
|
||||
1
flumi/Scripts/StyleManager.gd.uid
Normal file
1
flumi/Scripts/StyleManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dg2rhkcabusn1
|
||||
109
flumi/Scripts/Tab.gd
Normal file
109
flumi/Scripts/Tab.gd
Normal 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
1
flumi/Scripts/Tab.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://crpnnfqm3k5xv
|
||||
103
flumi/Scripts/TabContainer.gd
Normal file
103
flumi/Scripts/TabContainer.gd
Normal 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()
|
||||
1
flumi/Scripts/TabContainer.gd.uid
Normal file
1
flumi/Scripts/TabContainer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cy0c74thgjwok
|
||||
4
flumi/Scripts/Tags/br.gd
Normal file
4
flumi/Scripts/Tags/br.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
extends Control
|
||||
|
||||
func init(_element: HTMLParser.HTMLElement) -> void:
|
||||
pass
|
||||
1
flumi/Scripts/Tags/br.gd.uid
Normal file
1
flumi/Scripts/Tags/br.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://csd2kcqixac65
|
||||
188
flumi/Scripts/Tags/button.gd
Normal file
188
flumi/Scripts/Tags/button.gd
Normal 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
|
||||
1
flumi/Scripts/Tags/button.gd.uid
Normal file
1
flumi/Scripts/Tags/button.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cks35eudcm1wj
|
||||
5
flumi/Scripts/Tags/div.gd
Normal file
5
flumi/Scripts/Tags/div.gd
Normal file
@@ -0,0 +1,5 @@
|
||||
class_name HTMLDiv
|
||||
extends VBoxContainer
|
||||
|
||||
func init(_element: HTMLParser.HTMLElement):
|
||||
pass
|
||||
1
flumi/Scripts/Tags/div.gd.uid
Normal file
1
flumi/Scripts/Tags/div.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ckks1ccehq6al
|
||||
1
flumi/Scripts/Tags/font.gd.uid
Normal file
1
flumi/Scripts/Tags/font.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://4v2v83gyok8d
|
||||
4
flumi/Scripts/Tags/form.gd
Normal file
4
flumi/Scripts/Tags/form.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
extends VBoxContainer
|
||||
|
||||
func init(_element: HTMLParser.HTMLElement) -> void:
|
||||
pass
|
||||
1
flumi/Scripts/Tags/form.gd.uid
Normal file
1
flumi/Scripts/Tags/form.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cn2iolk6biupv
|
||||
11
flumi/Scripts/Tags/img.gd
Normal file
11
flumi/Scripts/Tags/img.gd
Normal 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
|
||||
1
flumi/Scripts/Tags/img.gd.uid
Normal file
1
flumi/Scripts/Tags/img.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dgakysfyq773t
|
||||
367
flumi/Scripts/Tags/input.gd
Normal file
367
flumi/Scripts/Tags/input.gd
Normal 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
|
||||
1
flumi/Scripts/Tags/input.gd.uid
Normal file
1
flumi/Scripts/Tags/input.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://kv6ebscarj2e
|
||||
8
flumi/Scripts/Tags/li.gd
Normal file
8
flumi/Scripts/Tags/li.gd
Normal 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)
|
||||
1
flumi/Scripts/Tags/li.gd.uid
Normal file
1
flumi/Scripts/Tags/li.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ps8duq0aw3tu
|
||||
117
flumi/Scripts/Tags/ol.gd
Normal file
117
flumi/Scripts/Tags/ol.gd
Normal 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
|
||||
1
flumi/Scripts/Tags/ol.gd.uid
Normal file
1
flumi/Scripts/Tags/ol.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bbkebg4aihve3
|
||||
10
flumi/Scripts/Tags/option.gd
Normal file
10
flumi/Scripts/Tags/option.gd
Normal 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)
|
||||
1
flumi/Scripts/Tags/option.gd.uid
Normal file
1
flumi/Scripts/Tags/option.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c66r24cncb1dp
|
||||
12
flumi/Scripts/Tags/p.gd
Normal file
12
flumi/Scripts/Tags/p.gd
Normal 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)
|
||||
1
flumi/Scripts/Tags/p.gd.uid
Normal file
1
flumi/Scripts/Tags/p.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cg6kjvlx3an1j
|
||||
38
flumi/Scripts/Tags/select.gd
Normal file
38
flumi/Scripts/Tags/select.gd
Normal 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
|
||||
1
flumi/Scripts/Tags/select.gd.uid
Normal file
1
flumi/Scripts/Tags/select.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bmu8q4rm1wopd
|
||||
26
flumi/Scripts/Tags/separator.gd
Normal file
26
flumi/Scripts/Tags/separator.gd
Normal 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
|
||||
1
flumi/Scripts/Tags/separator.gd.uid
Normal file
1
flumi/Scripts/Tags/separator.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://rol353cupbbf
|
||||
17
flumi/Scripts/Tags/span.gd
Normal file
17
flumi/Scripts/Tags/span.gd
Normal 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)
|
||||
1
flumi/Scripts/Tags/span.gd.uid
Normal file
1
flumi/Scripts/Tags/span.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://4pbphta3r67k
|
||||
74
flumi/Scripts/Tags/textarea.gd
Normal file
74
flumi/Scripts/Tags/textarea.gd
Normal 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()))
|
||||
1
flumi/Scripts/Tags/textarea.gd.uid
Normal file
1
flumi/Scripts/Tags/textarea.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bmcx7mr4nye6a
|
||||
97
flumi/Scripts/Tags/ul.gd
Normal file
97
flumi/Scripts/Tags/ul.gd
Normal 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
|
||||
1
flumi/Scripts/Tags/ul.gd.uid
Normal file
1
flumi/Scripts/Tags/ul.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cu1g4a1tv6ngw
|
||||
171
flumi/Scripts/Utils/BackgroundUtils.gd
Normal file
171
flumi/Scripts/Utils/BackgroundUtils.gd
Normal 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")
|
||||
1
flumi/Scripts/Utils/BackgroundUtils.gd.uid
Normal file
1
flumi/Scripts/Utils/BackgroundUtils.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c2itd75f1j1n0
|
||||
108
flumi/Scripts/Utils/ColorUtils.gd
Normal file
108
flumi/Scripts/Utils/ColorUtils.gd
Normal 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
|
||||
1
flumi/Scripts/Utils/ColorUtils.gd.uid
Normal file
1
flumi/Scripts/Utils/ColorUtils.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cuwui6hmwuiip
|
||||
127
flumi/Scripts/Utils/FlexUtils.gd
Normal file
127
flumi/Scripts/Utils/FlexUtils.gd
Normal 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
|
||||
1
flumi/Scripts/Utils/FlexUtils.gd.uid
Normal file
1
flumi/Scripts/Utils/FlexUtils.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dibofhomw401r
|
||||
50
flumi/Scripts/Utils/SizeUtils.gd
Normal file
50
flumi/Scripts/Utils/SizeUtils.gd
Normal 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
|
||||
1
flumi/Scripts/Utils/SizeUtils.gd.uid
Normal file
1
flumi/Scripts/Utils/SizeUtils.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ciqaxogsxvs24
|
||||
129
flumi/Scripts/Utils/SizingUtils.gd
Normal file
129
flumi/Scripts/Utils/SizingUtils.gd
Normal 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
|
||||
1
flumi/Scripts/Utils/SizingUtils.gd.uid
Normal file
1
flumi/Scripts/Utils/SizingUtils.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bsjm8qf4ry06e
|
||||
55
flumi/Scripts/Utils/UtilityClassValidator.gd
Normal file
55
flumi/Scripts/Utils/UtilityClassValidator.gd
Normal 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
|
||||
1
flumi/Scripts/Utils/UtilityClassValidator.gd.uid
Normal file
1
flumi/Scripts/Utils/UtilityClassValidator.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://criqj88gjrh4m
|
||||
362
flumi/Scripts/main.gd
Normal file
362
flumi/Scripts/main.gd
Normal 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)
|
||||
1
flumi/Scripts/main.gd.uid
Normal file
1
flumi/Scripts/main.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bg5iqnwic1rio
|
||||
Reference in New Issue
Block a user