diff --git a/Assets/Icons/globe.svg b/Assets/Icons/globe.svg new file mode 100644 index 0000000..9ac14fb --- /dev/null +++ b/Assets/Icons/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Assets/Icons/globe.svg.import b/Assets/Icons/globe.svg.import new file mode 100644 index 0000000..f2e0445 --- /dev/null +++ b/Assets/Icons/globe.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqpx2lgo0yecb" +path="res://.godot/imported/globe.svg-2e20bad5dc0399b0e6d7dae13a9b962d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/globe.svg" +dest_files=["res://.godot/imported/globe.svg-2e20bad5dc0399b0e6d7dae13a9b962d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/Assets/gurted.svg b/Assets/gurted.svg new file mode 100644 index 0000000..f7bd59c --- /dev/null +++ b/Assets/gurted.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Assets/gurted.svg.import b/Assets/gurted.svg.import new file mode 100644 index 0000000..5b2fa94 --- /dev/null +++ b/Assets/gurted.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ctpe0lbehepen" +path="res://.godot/imported/gurted.svg-77b32dc1cb120dd6b0c05e9883f93cc4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/gurted.svg" +dest_files=["res://.godot/imported/gurted.svg-77b32dc1cb120dd6b0c05e9883f93cc4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/Scenes/Tab.tscn b/Scenes/Tab.tscn index 7aa7bd5..e58fc08 100644 --- a/Scenes/Tab.tscn +++ b/Scenes/Tab.tscn @@ -6,7 +6,7 @@ [ext_resource type="Texture2D" uid="uid://c7u7a1u1v04bx" path="res://Scenes/Styles/TabGradientDefault.tres" id="3_q3baj"] [ext_resource type="StyleBox" uid="uid://bx3sgro1ageff" path="res://Scenes/Styles/TabDefault.tres" id="4_ib6pj"] [ext_resource type="Texture2D" uid="uid://dglkjumm1q4lo" path="res://Assets/Icons/23x23-empty.svg" id="5_ib6pj"] -[ext_resource type="Texture2D" uid="uid://bslojb4cmwnvn" path="res://icon.svg" id="6_ib6pj"] +[ext_resource type="Texture2D" uid="uid://bqpx2lgo0yecb" path="res://Assets/Icons/globe.svg" id="6_ib6pj"] [ext_resource type="StyleBox" uid="uid://dn8exdnk8tjce" path="res://Scenes/Styles/CloseButtonHover.tres" id="6_pisds"] [ext_resource type="StyleBox" uid="uid://dn6r16retee3l" path="res://Scenes/Styles/CloseButtonNormal.tres" id="7_1ohlo"] diff --git a/Scripts/B9/CSSParser.gd b/Scripts/B9/CSSParser.gd index f3bb459..91adb51 100644 --- a/Scripts/B9/CSSParser.gd +++ b/Scripts/B9/CSSParser.gd @@ -162,6 +162,32 @@ func parse_utility_class(rule: CSSRule, utility_name: String) -> void: rule.properties["background-color"] = color return + # e.g. max-w-[123px], w-[50%], h-[2rem] + if utility_name.match("^max-w-\\[.*\\]$"): + var val = extract_bracket_content(utility_name, 6) + rule.properties["max-width"] = val + return + if utility_name.match("^max-h-\\[.*\\]$"): + var val = extract_bracket_content(utility_name, 6) + rule.properties["max-height"] = val + return + if utility_name.match("^min-w-\\[.*\\]$"): + var val = extract_bracket_content(utility_name, 6) + rule.properties["min-width"] = val + return + if utility_name.match("^min-h-\\[.*\\]$"): + var val = extract_bracket_content(utility_name, 6) + rule.properties["min-height"] = val + return + if utility_name.match("^w-\\[.*\\]$"): + var val = extract_bracket_content(utility_name, 2) + rule.properties["width"] = val + return + if utility_name.match("^h-\\[.*\\]$"): + var val = extract_bracket_content(utility_name, 2) + rule.properties["height"] = val + return + # Handle font weight if utility_name == "font-bold": rule.properties["font-bold"] = true @@ -194,19 +220,76 @@ func parse_utility_class(rule: CSSRule, utility_name: String) -> void: "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) + rule.properties["width"] = parse_size(val) + return + # Height + if utility_name.begins_with("h-"): + var val = utility_name.substr(2) + rule.properties["height"] = parse_size(val) + return + # Min width + if utility_name.begins_with("min-w-"): + var val = utility_name.substr(6) + rule.properties["min-width"] = parse_size(val) + return + # Min height + if utility_name.begins_with("min-h-"): + var val = utility_name.substr(6) + rule.properties["min-height"] = parse_size(val) + return + # Max width + if utility_name.begins_with("max-w-"): + var val = utility_name.substr(6) + rule.properties["max-width"] = parse_size(val) + return + # Max height + if utility_name.begins_with("max-h-"): + var val = utility_name.substr(6) + rule.properties["max-height"] = parse_size(val) + return + # Handle more utility classes as needed # Add more cases here for other utilities +func parse_size(val: String) -> String: + 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(): + 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 + # Helper to extract content inside first matching brackets after a given index -func extract_bracket_content(str: String, start_idx: int) -> String: - var open_idx = str.find("[", start_idx) +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 = str.find("]", open_idx) + var close_idx = string.find("]", open_idx) if close_idx == -1: return "" - return str.substr(open_idx + 1, close_idx - open_idx - 1) + return string.substr(open_idx + 1, close_idx - open_idx - 1) func parse_color(color_string: String) -> Color: print("DEBUG: parsing color: ", color_string) diff --git a/Scripts/MaxSizeControl.gd b/Scripts/MaxSizeControl.gd new file mode 100644 index 0000000..a12d352 --- /dev/null +++ b/Scripts/MaxSizeControl.gd @@ -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 diff --git a/Scripts/MaxSizeControl.gd.uid b/Scripts/MaxSizeControl.gd.uid new file mode 100644 index 0000000..563d1e3 --- /dev/null +++ b/Scripts/MaxSizeControl.gd.uid @@ -0,0 +1 @@ +uid://cmxmcn3ghw8t2 diff --git a/Scripts/main.gd b/Scripts/main.gd index 4f19610..c8ec4ef 100644 --- a/Scripts/main.gd +++ b/Scripts/main.gd @@ -82,7 +82,11 @@ both spaces and line breaks - @@ -122,7 +126,7 @@ line breaks

Range Slider

- +

Number Input

@@ -203,7 +207,7 @@ line breaks
  • is
  • a test
  • - + ".to_utf8_buffer() @@ -244,19 +248,20 @@ line breaks for inline_element in inline_elements: var inline_node = await create_element_node(inline_element, parser) if inline_node: - hbox.add_child(inline_node) + safe_add_child(hbox, inline_node) # Handle hyperlinks for all inline elements if contains_hyperlink(inline_element) and inline_node.rich_text_label: inline_node.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta))) - website_container.add_child(hbox) + + 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": - website_container.add_child(element_node) - + safe_add_child(website_container, element_node) + # Handle hyperlinks for all elements if contains_hyperlink(element) and 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))) @@ -265,12 +270,87 @@ line breaks i += 1 -func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, parser: HTMLParser) -> void: +func safe_add_child(parent: Node, child: Node) -> void: + if child.get_parent(): + child.get_parent().remove_child(child) + parent.add_child(child) + +func parse_size(val): + 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("%"): + # Not supported directly, skip + return null + return float(val) + +func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, parser: HTMLParser) -> Control: var styles = parser.get_element_styles(element) var label = node if node is RichTextLabel else node.get_node_or_null("RichTextLabel") + var max_width = null + var max_height = null + var min_width = null + var min_height = null + var width = null + var height = null + + # Handle width/height/min/max + if styles.has("width"): + width = parse_size(styles["width"]) + if styles.has("height"): + height = parse_size(styles["height"]) + if styles.has("min-width"): + min_width = parse_size(styles["min-width"]) + if styles.has("min-height"): + min_height = parse_size(styles["min-height"]) + if styles.has("max-width"): + max_width = parse_size(styles["max-width"]) + if styles.has("max-height"): + max_height = parse_size(styles["max-height"]) + + # Apply min size + if min_width != null or min_height != null: + node.custom_minimum_size = Vector2( + min_width if min_width != null else node.custom_minimum_size.x, + min_height if min_height != null else node.custom_minimum_size.y + ) + # Apply w/h size + if width != null or height != null: + node.custom_minimum_size = Vector2( + width if width != null else node.custom_minimum_size.x, + height if height != null else node.custom_minimum_size.y + ) + + # Set size flags to shrink (without center) so it doesn't expand beyond minimum + if width != null: + node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + if height != null: + node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + + if label and label != node: # If label is a child of node + label.anchors_preset = Control.PRESET_FULL_RECT + label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + label.size_flags_vertical = Control.SIZE_EXPAND_FILL + + # Apply max constraints via MaxSizeContainer + var result_node = node + if max_width != null or max_height != null: + var max_container = MaxSizeControl.new() + max_container.max_size = Vector2( + max_width if max_width != null else -1, + max_height if max_height != null else -1 + ) + + safe_add_child(website_container, max_container) + result_node = max_container + if label: apply_styles_to_label(label, styles, element, parser) + return result_node func apply_styles_to_label(label: RichTextLabel, styles: Dictionary, element: HTMLParser.HTMLElement, parser) -> void: var text = element.get_bbcode_formatted_text(parser) # pass parser @@ -325,6 +405,16 @@ func apply_styles_to_label(label: RichTextLabel, styles: Dictionary, element: HT 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%s%s[/font_size]" % [ font_size, @@ -363,27 +453,27 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser = n node = P.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "h1": node = H1.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "h2": node = H2.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "h3": node = H3.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "h4": node = H4.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "h5": node = H5.instantiate() node.init(element) @@ -399,6 +489,8 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser = n "img": node = IMG.instantiate() node.init(element) + if parser: + node = apply_element_styles(node, element, parser) "separator": node = SEPARATOR.instantiate() node.init(element) @@ -413,49 +505,53 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser = n "input": node = INPUT.instantiate() node.init(element) + if parser: + node = apply_element_styles(node, element, parser) "button": node = BUTTON.instantiate() node.init(element) + if parser: + node = apply_element_styles(node, element, parser) "span": node = SPAN.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "b": node = SPAN.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "i": node = SPAN.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "u": node = SPAN.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "small": node = SPAN.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "mark": node = SPAN.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "code": node = SPAN.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "a": node = SPAN.instantiate() node.init(element) if parser: - apply_element_styles(node, element, parser) + node = apply_element_styles(node, element, parser) "ul": node = UL.instantiate() website_container.add_child(node) # Add to scene tree first @@ -469,15 +565,23 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser = n "li": node = LI.instantiate() node.init(element) + if parser: + node = apply_element_styles(node, element, parser) "select": node = SELECT.instantiate() node.init(element) + if parser: + node = apply_element_styles(node, element, parser) "option": node = OPTION.instantiate() node.init(element) + if parser: + node = apply_element_styles(node, element, parser) "textarea": node = TEXTAREA.instantiate() node.init(element) + if parser: + node = apply_element_styles(node, element, parser) _: return null diff --git a/project.godot b/project.godot index c145239..6bf84ed 100644 --- a/project.godot +++ b/project.godot @@ -10,10 +10,10 @@ config_version=5 [application] -config/name="gurted" +config/name="Gurted" run/main_scene="uid://bytm7bt2s4ak8" config/features=PackedStringArray("4.4", "Forward Plus") -config/icon="res://icon.svg" +config/icon="uid://ctpe0lbehepen" [autoload] @@ -28,7 +28,7 @@ window/stretch/aspect="ignore" [editor_plugins] -enabled=PackedStringArray("res://addons/DatePicker/plugin.cfg", "res://addons/SmoothScroll/plugin.cfg") +enabled=PackedStringArray("res://addons/DatePicker/plugin.cfg", "res://addons/MaxSizeContainer/plugin.cfg", "res://addons/SmoothScroll/plugin.cfg") [file_customization]