diff --git a/README.md b/README.md index 3772243..41554a9 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,41 @@ Issues: Notes: - **< input />** is sort-of inline in normal web. We render it as a block element (new-line). - A single `RichTextLabel` for inline text tags should stop, we should use invididual ones so it's easier to style and achieve separation through a `vboxcontainer`. + + +Supported styles: + +- **Font style:** + - `font-bold` + - `font-italic` + - `underline` +- **Font size:** + - `text-xs` → 12 + - `text-sm` → 14 + - `text-base` → 16 + - `text-lg` → 18 + - `text-xl` → 20 + - `text-2xl` → 24 + - `text-3xl` → 30 + - `text-4xl` → 36 + - `text-5xl` → 48 + - `text-6xl` → 60 +- **Font family:** + - `font-mono` +- **Text color:** + - `text-[color]` +- **Background color:** + - `bg-[color]` +- **Flexbox** +- `flex` / `inline-flex` (display: flex/inline-flex) +- `flex-row`, `flex-row-reverse`, `flex-col`, `flex-col-reverse` (flex-direction) +- `flex-nowrap`, `flex-wrap`, `flex-wrap-reverse` (flex-wrap) +- `justify-start`, `justify-end`, `justify-center`, `justify-between`, `justify-around`, `justify-evenly` (justify-content) +- `items-start`, `items-end`, `items-center`, `items-baseline`, `items-stretch` (align-items) +- `content-start`, `content-end`, `content-center`, `content-between`, `content-around`, `content-evenly`, `content-stretch` (align-content) +- `gap-{size}`, `row-gap-{size}`, `col-gap-{size}` (gap, row-gap, column-gap) +- `flex-grow-{n}` (flex-grow) +- `flex-shrink-{n}` (flex-shrink) +- `basis-{size}` (flex-basis) +- `self-auto`, `self-start`, `self-end`, `self-center`, `self-stretch`, `self-baseline` (align-self) +- `order-{n}` (order) \ No newline at end of file diff --git a/Ref/methods.gd b/Ref/methods.gd deleted file mode 100644 index 701b587..0000000 --- a/Ref/methods.gd +++ /dev/null @@ -1,72 +0,0 @@ -func _test_find_methods(parser: HTMLParser): - _log_result("\n--- Testing Find Methods ---") - - # Test find_all - var all_scripts = parser.find_all("script") - _log_result("All script tags: " + str(all_scripts.size())) - - var scripts_with_src = parser.find_all("script", "src") - _log_result("Script tags with src: " + str(scripts_with_src.size())) - - # Test find_first - var first_meta = parser.find_first("meta") - if first_meta: - _log_result("First meta tag found with attributes: " + str(first_meta.attributes)) - else: - _log_result("No meta tags found") - - # Test find_by_id (won't find anything in our example) - var by_id = parser.find_by_id("main") - _log_result("Element with id 'main': " + str(by_id != null)) - -func _test_attribute_extraction(parser: HTMLParser): - _log_result("\n--- Testing Attribute Extraction ---") - - var script_sources = parser.get_attribute_values("script", "src") - _log_result("Script sources: " + str(script_sources)) - - var meta_names = parser.get_attribute_values("meta", "name") - _log_result("Meta names: " + str(meta_names)) - - var hrefs = parser.get_attribute_values("icon", "href") - _log_result("Icon hrefs: " + str(hrefs)) - -func _test_convenience_methods(parser: HTMLParser): - _log_result("\n--- Testing Convenience Methods ---") - - # Test convenience methods - var title = parser.get_title() - _log_result("Page title: '" + title + "'") - - var theme_color = parser.get_meta_content("theme-color") - _log_result("Theme color: '" + theme_color + "'") - - var description = parser.get_meta_content("description") - _log_result("Description: '" + description + "'") - - var all_scripts = parser.get_all_scripts() - _log_result("All script sources: " + str(all_scripts)) - - var all_images = parser.get_all_images() - _log_result("All image sources: " + str(all_images)) - - var all_stylesheets = parser.get_all_stylesheets() - _log_result("All stylesheets: " + str(all_stylesheets)) - -func _log_element_tree(element: HTMLParser.HTMLElement, depth: int): - if depth == 0: - _log_result("\n--- Element Tree ---") - - var indent = " ".repeat(depth) - var element_info = indent + element.tag_name - - if element.attributes.size() > 0: - element_info += " " + str(element.attributes) - - if element.text_content.length() > 0: - element_info += " [text: '" + element.text_content.substr(0, 30) + "']" - - _log_result(element_info) - - for child in element.children: - _log_element_tree(child, depth + 1) diff --git a/Ref/methods.gd.uid b/Ref/methods.gd.uid deleted file mode 100644 index ab7af83..0000000 --- a/Ref/methods.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dyeeapgo7qlng diff --git a/Scenes/Tags/div.tscn b/Scenes/Tags/div.tscn new file mode 100644 index 0000000..0d63e3b --- /dev/null +++ b/Scenes/Tags/div.tscn @@ -0,0 +1,9 @@ +[gd_scene load_steps=2 format=3 uid="uid://lyg7t5edfneu"] + +[ext_resource type="Script" uid="uid://ckks1ccehq6al" path="res://Scripts/Tags/div.gd" id="1_div"] + +[node name="Div" type="VBoxContainer"] +anchors_preset = 10 +anchor_right = 1.0 +grow_horizontal = 2 +script = ExtResource("1_div") diff --git a/Scenes/Tags/span.tscn b/Scenes/Tags/span.tscn index 0e1ace1..c8106cf 100644 --- a/Scenes/Tags/span.tscn +++ b/Scenes/Tags/span.tscn @@ -3,10 +3,13 @@ [ext_resource type="Script" uid="uid://4pbphta3r67k" path="res://Scripts/Tags/span.gd" id="1_span"] [ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_theme"] -[node name="RichTextLabel" type="RichTextLabel"] +[node name="Span" type="RichTextLabel"] +z_index = 1 anchors_preset = 10 anchor_right = 1.0 grow_horizontal = 2 +size_flags_horizontal = 0 +size_flags_vertical = 0 focus_mode = 2 mouse_default_cursor_shape = 1 theme = ExtResource("2_theme") @@ -18,3 +21,14 @@ autowrap_mode = 0 vertical_alignment = 2 selection_enabled = true script = ExtResource("1_span") + +[node name="BackgroundRect" type="ColorRect" parent="."] +z_index = -1 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +color = Color(1, 1, 1, 0) diff --git a/Scenes/main.tscn b/Scenes/main.tscn index 0eec7bc..689f383 100644 --- a/Scenes/main.tscn +++ b/Scenes/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=27 format=3 uid="uid://bytm7bt2s4ak8"] +[gd_scene load_steps=29 format=3 uid="uid://bytm7bt2s4ak8"] [ext_resource type="Script" uid="uid://bg5iqnwic1rio" path="res://Scripts/main.gd" id="1_8q3xr"] [ext_resource type="Texture2D" uid="uid://df1m4j7uxi63v" path="res://Assets/Icons/chevron-down.svg" id="2_6bp64"] @@ -11,6 +11,8 @@ [ext_resource type="Texture2D" uid="uid://cehbtwq6gq0cn" path="res://Assets/Icons/plus.svg" id="5_ynf5e"] [ext_resource type="Script" uid="uid://bgqglerkcylxx" path="res://addons/SmoothScroll/SmoothScrollContainer.gd" id="10_d1ilt"] [ext_resource type="Script" uid="uid://b7h0k2h2qwlqv" path="res://addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd" id="11_6iyac"] +[ext_resource type="Script" uid="uid://2dcp833juv7b" path="res://addons/godot-flexbox/flex_container.gd" id="12_6iyac"] +[ext_resource type="PackedScene" uid="uid://bkj3x5y2m8qrl" path="res://Scenes/Tags/span.tscn" id="13_fdnlq"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_344ge"] @@ -216,11 +218,53 @@ metadata/_custom_type_script = "uid://bgqglerkcylxx" [node name="WebsiteContainer" type="VBoxContainer" parent="VBoxContainer/SmoothScrollContainer"] unique_name_in_owner = true +custom_minimum_size = Vector2(200, 200) layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 theme_override_constants/separation = 22 +[node name="Control" type="Control" parent="VBoxContainer/SmoothScrollContainer/WebsiteContainer"] +layout_mode = 2 +mouse_filter = 1 + +[node name="FlexContainer" type="Container" parent="VBoxContainer/SmoothScrollContainer/WebsiteContainer/Control"] +custom_minimum_size = Vector2(40, 60) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("12_6iyac") +flex_direction = 0 +justify_content = 1 +align_items = 2 +align_content = 2 +metadata/_custom_type_script = "uid://2dcp833juv7b" + +[node name="FlexContainer" type="Container" parent="VBoxContainer/SmoothScrollContainer/WebsiteContainer/Control/FlexContainer"] +custom_minimum_size = Vector2(40, 40) +layout_mode = 2 +script = ExtResource("12_6iyac") +metadata/_custom_type_script = "uid://2dcp833juv7b" +metadata/flex_metas = { +&"align_self": 0, +&"grow": 0.0 +} + +[node name="RichTextLabel" parent="VBoxContainer/SmoothScrollContainer/WebsiteContainer/Control/FlexContainer/FlexContainer" instance=ExtResource("13_fdnlq")] +layout_mode = 2 +mouse_filter = 1 + +[node name="RichTextLabel2" parent="VBoxContainer/SmoothScrollContainer/WebsiteContainer/Control/FlexContainer/FlexContainer" instance=ExtResource("13_fdnlq")] +layout_mode = 2 +mouse_filter = 1 + +[node name="RichTextLabel3" parent="VBoxContainer/SmoothScrollContainer/WebsiteContainer/Control/FlexContainer/FlexContainer" instance=ExtResource("13_fdnlq")] +layout_mode = 2 +mouse_filter = 1 + [node name="WebsiteBackground" type="Panel" parent="."] z_index = -1 layout_mode = 1 diff --git a/Scripts/AutoSizingFlexContainer.gd b/Scripts/AutoSizingFlexContainer.gd new file mode 100644 index 0000000..8ed5394 --- /dev/null +++ b/Scripts/AutoSizingFlexContainer.gd @@ -0,0 +1,131 @@ +@tool +class_name AutoSizingFlexContainer +extends FlexContainer + +signal flex_resized + +var bgcolor: Color = Color(0,0,0,0) + +func set_background_color(color: Color) -> void: + bgcolor = color + queue_redraw() + +func _notification(what: int) -> void: + super._notification(what) + + if what == NOTIFICATION_DRAW and bgcolor.a > 0: + draw_rect(Rect2(Vector2.ZERO, size), bgcolor) + +# This is the overridden layout logic for the auto-sizing container +func _resort() -> void: + #size_flags_horizontal = 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 + + 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") + var auto_size_height = not has_meta("custom_css_height") + + var available_width = NAN if auto_size_width else size.x + var available_height = NAN if auto_size_height else size.y + + _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 = 0.0 + if has_meta("custom_css_width"): + custom_w = float(get_meta("custom_css_width")) + + var custom_h = 0.0 + if has_meta("custom_css_height"): + custom_h = float(get_meta("custom_css_height")) + + var needed_size = Vector2( + max(custom_w, computed_size.x), + max(custom_h, computed_size.y) + ) + + # 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 + if auto_size_height: + new_min_size.y = needed_size.y + + if not custom_minimum_size.is_equal_approx(new_min_size): + custom_minimum_size = new_min_size + + # 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)) + + + if has_meta("custom_css_background_color"): + set_background_color(get_meta("custom_css_background_color")) + + queue_redraw() + emit_signal("flex_resized") diff --git a/Scripts/AutoSizingFlexContainer.gd.uid b/Scripts/AutoSizingFlexContainer.gd.uid new file mode 100644 index 0000000..4a7fbc4 --- /dev/null +++ b/Scripts/AutoSizingFlexContainer.gd.uid @@ -0,0 +1 @@ +uid://feiw2baeu5ye diff --git a/Scripts/B9/CSSParser.gd b/Scripts/B9/CSSParser.gd index 91adb51..0d5515c 100644 --- a/Scripts/B9/CSSParser.gd +++ b/Scripts/B9/CSSParser.gd @@ -258,6 +258,96 @@ func parse_utility_class(rule: CSSRule, utility_name: String) -> void: rule.properties["max-height"] = 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"] = parse_size(val) + return + if utility_name.begins_with("row-gap-"): + var val = utility_name.substr(8) + rule.properties["row-gap"] = parse_size(val) + return + if utility_name.begins_with("col-gap-"): + var val = utility_name.substr(8) + rule.properties["column-gap"] = 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"] = 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 + # Handle more utility classes as needed # Add more cases here for other utilities @@ -292,7 +382,6 @@ func extract_bracket_content(string: String, start_idx: int) -> String: return string.substr(open_idx + 1, close_idx - open_idx - 1) func parse_color(color_string: String) -> Color: - print("DEBUG: parsing color: ", color_string) color_string = color_string.strip_edges() # Handle hex colors diff --git a/Scripts/B9/HTMLParser.gd b/Scripts/B9/HTMLParser.gd index 9976687..c6a0889 100644 --- a/Scripts/B9/HTMLParser.gd +++ b/Scripts/B9/HTMLParser.gd @@ -55,21 +55,6 @@ var xml_parser: XMLParser var bitcode: PackedByteArray var parse_result: ParseResult -var DEFAULT_CSS := """ -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] } -""" - func _init(data: PackedByteArray): bitcode = data xml_parser = XMLParser.new() @@ -125,7 +110,7 @@ func process_styles() -> void: return # Collect all style element content - var css_content = DEFAULT_CSS + 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(): diff --git a/Scripts/Constants.gd b/Scripts/Constants.gd index de099c8..73bc4eb 100644 --- a/Scripts/Constants.gd +++ b/Scripts/Constants.gd @@ -4,3 +4,229 @@ 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 = """ +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 } +""" + +var HTML_CONTENT = "
+