From 3a722fdf1e74958f43a3a0d1a794d8a215455e65 Mon Sep 17 00:00:00 2001
From: Face <69168154+face-hh@users.noreply.github.com>
Date: Sat, 9 Aug 2025 15:54:59 +0300
Subject: [PATCH] object-fit, multithreaded image loading
---
flumi/Scripts/B9/CSSParser.gd | 7 +++
flumi/Scripts/Constants.gd | 76 +++++++++++++++++++++++++-
flumi/Scripts/Network.gd | 12 ++--
flumi/Scripts/StyleManager.gd | 32 +++++++++--
flumi/Scripts/Tags/img.gd | 35 ++++++++++--
flumi/Scripts/Utils/BackgroundUtils.gd | 2 +-
flumi/Scripts/Utils/SizeUtils.gd | 3 +-
7 files changed, 150 insertions(+), 17 deletions(-)
diff --git a/flumi/Scripts/B9/CSSParser.gd b/flumi/Scripts/B9/CSSParser.gd
index 2662d33..a5b45a5 100644
--- a/flumi/Scripts/B9/CSSParser.gd
+++ b/flumi/Scripts/B9/CSSParser.gd
@@ -890,6 +890,13 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) ->
rule.properties["opacity"] = val.to_int() / 100.0
return
+ # Handle object-fit classes for images
+ match utility_name:
+ "object-none": rule.properties["object-fit"] = "none"; return
+ "object-fill": rule.properties["object-fit"] = "fill"; return
+ "object-contain": rule.properties["object-fit"] = "contain"; return
+ "object-cover": rule.properties["object-fit"] = "cover"; return
+
# Handle more utility classes as needed
# Add more cases here for other utilities
diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd
index e0c5dfb..7634fee 100644
--- a/flumi/Scripts/Constants.gd
+++ b/flumi/Scripts/Constants.gd
@@ -2014,7 +2014,7 @@ var HTML_CONTENTy = """
""".to_utf8_buffer()
-var HTML_CONTENT = """
+var HTML_CONTENTyea = """
setInterval & Network Image Demo
@@ -2182,3 +2182,77 @@ var HTML_CONTENT = """
""".to_utf8_buffer()
+
+var HTML_CONTENT = """
+ Object-Fit CSS Demo
+
+
+
+
+
+
+
+ 🖼️ Object-Fit CSS Demonstration
+
+
+
+
+
object-none
+
Image keeps original size, may overflow container
+
+

+
+
object-none
+
+
+
+
+
object-fill
+
Image fills container, may distort aspect ratio
+
+

+
+
object-fill
+
+
+
+
+
object-contain
+
Image fits inside container while preserving aspect ratio
+
+

+
+
object-contain
+
+
+
+
+
object-cover
+
Image covers entire container while preserving aspect ratio
+
+

+
+
object-cover
+
+
+
+
+
📝 Object-fit Property Mapping
+
+
object-none: Godot's STRETCH_KEEP - Image keeps original dimensions
+
object-fill: Godot's STRETCH_SCALE - Image stretches to fill container
+
object-contain: Godot's STRETCH_KEEP_ASPECT - Image fits inside with preserved aspect ratio
+
object-cover: Godot's STRETCH_KEEP_ASPECT_COVERED - Image covers container with preserved aspect ratio
+
+
+
+""".to_utf8_buffer()
diff --git a/flumi/Scripts/Network.gd b/flumi/Scripts/Network.gd
index 9678f9f..1f04040 100644
--- a/flumi/Scripts/Network.gd
+++ b/flumi/Scripts/Network.gd
@@ -37,10 +37,10 @@ func fetch_image(url: String) -> ImageTexture:
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"):
+ if 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("png") or url.to_lower().ends_with(".png"):
+ load_error = image.load_png_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"):
@@ -48,10 +48,10 @@ func fetch_image(url: String) -> ImageTexture:
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)
+ print("Unknown or missing content-type. Attempting bruteforce converting across JPEG, PNG and WebP...")
+ load_error = image.load_jpg_from_buffer(body)
if load_error != OK:
- load_error = image.load_jpg_from_buffer(body)
+ load_error = image.load_png_from_buffer(body)
if load_error != OK:
load_error = image.load_webp_from_buffer(body)
diff --git a/flumi/Scripts/StyleManager.gd b/flumi/Scripts/StyleManager.gd
index 9b6f752..bac6d76 100644
--- a/flumi/Scripts/StyleManager.gd
+++ b/flumi/Scripts/StyleManager.gd
@@ -14,8 +14,6 @@ static func parse_size(val):
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:
@@ -34,6 +32,8 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
if element.tag_name == "input":
apply_input_border_styles(node, styles)
+ elif element.tag_name == "img":
+ apply_image_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"]:
@@ -89,8 +89,14 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
if not element_styles.has("height"):
node.size_flags_vertical = orig_v_flag
else:
- # regular controls
- SizingUtils.apply_regular_control_sizing(node, width, height, styles)
+ if element.tag_name == "img" and SizingUtils.is_percentage(width) and SizingUtils.is_percentage(height):
+ node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ node.size_flags_vertical = Control.SIZE_EXPAND_FILL
+ # Clear any hardcoded sizing
+ node.custom_minimum_size = Vector2.ZERO
+ else:
+ # regular controls
+ SizingUtils.apply_regular_control_sizing(node, width, height, styles)
if label and label != node:
label.anchors_preset = Control.PRESET_FULL_RECT
@@ -627,3 +633,21 @@ static func apply_input_border_styles(input_node: Control, styles: Dictionary) -
control.add_theme_stylebox_override("focus", style_box)
elif control is Button:
control.add_theme_stylebox_override("normal", style_box)
+
+static func apply_image_styles(image_node: Control, styles: Dictionary) -> void:
+ if not image_node is TextureRect:
+ return
+
+ var texture_rect = image_node as TextureRect
+
+ if styles.has("object-fit"):
+ var object_fit = styles["object-fit"]
+ match object_fit:
+ "none":
+ texture_rect.stretch_mode = TextureRect.STRETCH_KEEP
+ "fill":
+ texture_rect.stretch_mode = TextureRect.STRETCH_SCALE
+ "contain":
+ texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT
+ "cover":
+ texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
diff --git a/flumi/Scripts/Tags/img.gd b/flumi/Scripts/Tags/img.gd
index 0e82b45..87226ca 100644
--- a/flumi/Scripts/Tags/img.gd
+++ b/flumi/Scripts/Tags/img.gd
@@ -1,11 +1,38 @@
extends TextureRect
-func init(element: HTMLParser.HTMLElement, _parser: HTMLParser) -> void:
+func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
var src = element.get_attribute("src")
if !src: return print("Ignoring
tag without \"src\" attribute.")
+ load_image_async(src, element, parser)
+
+func load_image_async(src: String, element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
+ # Wait until this node is in the scene tree
+ if not is_inside_tree():
+ await tree_entered
+
texture = await Network.fetch_image(src)
- var texture_size = texture.get_size()
- custom_minimum_size = texture_size
- size = texture_size
+ if !is_instance_valid(texture):
+ print("Failed to load image: ", src)
+ return
+
+ var element_styles = parser.get_element_styles_internal(element, "")
+ var has_width = element_styles.has("width")
+ var has_height = element_styles.has("height")
+
+ if not has_width and not has_height:
+ var texture_size = texture.get_size()
+ custom_minimum_size = texture_size
+ size = texture_size
+ else:
+ var width_val = element_styles.get("width", "")
+ var height_val = element_styles.get("height", "")
+
+ if width_val == "100%" and height_val == "100%" or width_val == "full" and height_val == "full":
+ size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ size_flags_vertical = Control.SIZE_EXPAND_FILL
+ custom_minimum_size = Vector2.ZERO
+ else:
+ custom_minimum_size = Vector2(1, 1)
+ size = Vector2(100, 100) # StyleManager will handle this
\ No newline at end of file
diff --git a/flumi/Scripts/Utils/BackgroundUtils.gd b/flumi/Scripts/Utils/BackgroundUtils.gd
index 5c0bd68..de9d2d2 100644
--- a/flumi/Scripts/Utils/BackgroundUtils.gd
+++ b/flumi/Scripts/Utils/BackgroundUtils.gd
@@ -158,7 +158,7 @@ static func create_panel_container_with_background(styles: Dictionary, hover_sty
vbox.name = "VBoxContainer"
# Allow mouse events to pass through to the parent PanelContainer
vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
- vbox.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
+ vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
panel_container.add_child(vbox)
var style_box = create_stylebox_from_styles(styles)
diff --git a/flumi/Scripts/Utils/SizeUtils.gd b/flumi/Scripts/Utils/SizeUtils.gd
index a64dbac..5001a43 100644
--- a/flumi/Scripts/Utils/SizeUtils.gd
+++ b/flumi/Scripts/Utils/SizeUtils.gd
@@ -12,7 +12,8 @@ static func parse_size(val: String) -> String:
"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"
+ "xl": "576px", "2xl": "672px", "3xl": "768px", "4xl": "896px", "5xl": "1024px", "6xl": "1152px", "7xl": "1280px",
+ "full": "100%"
}
if named.has(val):
return named[val]