object-fit, multithreaded image loading
This commit is contained in:
@@ -890,6 +890,13 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) ->
|
|||||||
rule.properties["opacity"] = val.to_int() / 100.0
|
rule.properties["opacity"] = val.to_int() / 100.0
|
||||||
return
|
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
|
# Handle more utility classes as needed
|
||||||
# Add more cases here for other utilities
|
# Add more cases here for other utilities
|
||||||
|
|
||||||
|
|||||||
@@ -2014,7 +2014,7 @@ var HTML_CONTENTy = """<head>
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
""".to_utf8_buffer()
|
""".to_utf8_buffer()
|
||||||
var HTML_CONTENT = """<head>
|
var HTML_CONTENTyea = """<head>
|
||||||
<title>setInterval & Network Image Demo</title>
|
<title>setInterval & Network Image Demo</title>
|
||||||
<icon src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Lua-Logo.svg/256px-Lua-Logo.svg.png">
|
<icon src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Lua-Logo.svg/256px-Lua-Logo.svg.png">
|
||||||
<meta name="theme-color" content="#7c3aed">
|
<meta name="theme-color" content="#7c3aed">
|
||||||
@@ -2182,3 +2182,77 @@ var HTML_CONTENT = """<head>
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
""".to_utf8_buffer()
|
""".to_utf8_buffer()
|
||||||
|
|
||||||
|
var HTML_CONTENT = """<head>
|
||||||
|
<title>Object-Fit CSS Demo</title>
|
||||||
|
<icon src="https://picsum.photos/32/32?random=1">
|
||||||
|
<meta name="theme-color" content="#0ea5e9">
|
||||||
|
<meta name="description" content="Demonstrating object-fit CSS properties for images">
|
||||||
|
<style>
|
||||||
|
body { bg-[#f1f5f9] p-8 }
|
||||||
|
h1 { text-[#0ea5e9] text-4xl font-bold text-center mb-8 }
|
||||||
|
h2 { text-[#0369a1] text-2xl font-semibold mb-4 }
|
||||||
|
.demo-grid { flex flex-wrap gap-6 justify-center }
|
||||||
|
.demo-item { bg-white p-6 rounded-xl shadow-lg }
|
||||||
|
.image-container { w-[300px] h-[200px] border-2 border-[#cbd5e1] rounded-lg overflow-hidden mb-4 }
|
||||||
|
.code-block { bg-[#1e293b] text-[#e2e8f0] p-3 rounded font-mono text-sm }
|
||||||
|
.description { text-[#475569] text-sm mb-2 }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>🖼️ Object-Fit CSS Demonstration</h1>
|
||||||
|
|
||||||
|
<div style="demo-grid">
|
||||||
|
<!-- Object-fit: none (STRETCH_KEEP) -->
|
||||||
|
<div style="demo-item">
|
||||||
|
<h2>object-none</h2>
|
||||||
|
<div style="description">Image keeps original size, may overflow container</div>
|
||||||
|
<div style="image-container">
|
||||||
|
<img src="https://picsum.photos/400/700?random=1" style="w-full h-full object-none" />
|
||||||
|
</div>
|
||||||
|
<div style="code-block">object-none</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Object-fit: fill (STRETCH_SCALE) -->
|
||||||
|
<div style="demo-item">
|
||||||
|
<h2>object-fill</h2>
|
||||||
|
<div style="description">Image fills container, may distort aspect ratio</div>
|
||||||
|
<div style="image-container">
|
||||||
|
<img src="https://picsum.photos/500/600?random=2" style="w-full h-full object-fill" />
|
||||||
|
</div>
|
||||||
|
<div style="code-block">object-fill</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Object-fit: contain (STRETCH_KEEP_ASPECT) -->
|
||||||
|
<div style="demo-item">
|
||||||
|
<h2>object-contain</h2>
|
||||||
|
<div style="description">Image fits inside container while preserving aspect ratio</div>
|
||||||
|
<div style="image-container">
|
||||||
|
<img src="https://picsum.photos/700/600?random=3" style="w-full h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<div style="code-block">object-contain</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Object-fit: cover (STRETCH_KEEP_ASPECT_COVERED) -->
|
||||||
|
<div style="demo-item">
|
||||||
|
<h2>object-cover</h2>
|
||||||
|
<div style="description">Image covers entire container while preserving aspect ratio</div>
|
||||||
|
<div style="image-container">
|
||||||
|
<img src="https://picsum.photos/200/300?random=4" style="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div style="code-block">object-cover</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="bg-white p-6 rounded-xl shadow-lg mt-8 max-w-4xl mx-auto">
|
||||||
|
<h2>📝 Object-fit Property Mapping</h2>
|
||||||
|
<div style="text-[#475569]">
|
||||||
|
<p><strong>object-none:</strong> Godot's STRETCH_KEEP - Image keeps original dimensions</p>
|
||||||
|
<p><strong>object-fill:</strong> Godot's STRETCH_SCALE - Image stretches to fill container</p>
|
||||||
|
<p><strong>object-contain:</strong> Godot's STRETCH_KEEP_ASPECT - Image fits inside with preserved aspect ratio</p>
|
||||||
|
<p><strong>object-cover:</strong> Godot's STRETCH_KEEP_ASPECT_COVERED - Image covers container with preserved aspect ratio</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
""".to_utf8_buffer()
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ func fetch_image(url: String) -> ImageTexture:
|
|||||||
var load_error
|
var load_error
|
||||||
|
|
||||||
# Load image based on content type
|
# Load image based on content type
|
||||||
if content_type.contains("png") or url.to_lower().ends_with(".png"):
|
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_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)
|
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"):
|
elif content_type.contains("webp") or url.to_lower().ends_with(".webp"):
|
||||||
load_error = image.load_webp_from_buffer(body)
|
load_error = image.load_webp_from_buffer(body)
|
||||||
elif content_type.contains("bmp"):
|
elif content_type.contains("bmp"):
|
||||||
@@ -48,10 +48,10 @@ func fetch_image(url: String) -> ImageTexture:
|
|||||||
elif content_type.contains("tga"):
|
elif content_type.contains("tga"):
|
||||||
load_error = image.load_tga_from_buffer(body)
|
load_error = image.load_tga_from_buffer(body)
|
||||||
else:
|
else:
|
||||||
print("Unknown or missing content-type. Attempting bruteforce converting across PNG, JPG and WebP...")
|
print("Unknown or missing content-type. Attempting bruteforce converting across JPEG, PNG and WebP...")
|
||||||
load_error = image.load_png_from_buffer(body)
|
|
||||||
if load_error != OK:
|
|
||||||
load_error = image.load_jpg_from_buffer(body)
|
load_error = image.load_jpg_from_buffer(body)
|
||||||
|
if load_error != OK:
|
||||||
|
load_error = image.load_png_from_buffer(body)
|
||||||
if load_error != OK:
|
if load_error != OK:
|
||||||
load_error = image.load_webp_from_buffer(body)
|
load_error = image.load_webp_from_buffer(body)
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ static func parse_size(val):
|
|||||||
if val.ends_with("%") or (val.ends_with("]") and "%" in val):
|
if val.ends_with("%") or (val.ends_with("]") and "%" in val):
|
||||||
var clean_val = val.replace("[", "").replace("]", "")
|
var clean_val = val.replace("[", "").replace("]", "")
|
||||||
return clean_val
|
return clean_val
|
||||||
if val == "full":
|
|
||||||
return null
|
|
||||||
return float(val)
|
return float(val)
|
||||||
|
|
||||||
static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, parser: HTMLParser) -> Control:
|
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":
|
if element.tag_name == "input":
|
||||||
apply_input_border_styles(node, styles)
|
apply_input_border_styles(node, styles)
|
||||||
|
elif element.tag_name == "img":
|
||||||
|
apply_image_styles(node, styles)
|
||||||
|
|
||||||
# Unified font applying for label and button
|
# Unified font applying for label and button
|
||||||
if target and styles.has("font-family") and styles["font-family"] not in ["sans-serif", "serif", "monospace"]:
|
if target and styles.has("font-family") and styles["font-family"] not in ["sans-serif", "serif", "monospace"]:
|
||||||
@@ -88,6 +88,12 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
|
|||||||
node.size_flags_horizontal = orig_h_flag
|
node.size_flags_horizontal = orig_h_flag
|
||||||
if not element_styles.has("height"):
|
if not element_styles.has("height"):
|
||||||
node.size_flags_vertical = orig_v_flag
|
node.size_flags_vertical = orig_v_flag
|
||||||
|
else:
|
||||||
|
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:
|
else:
|
||||||
# regular controls
|
# regular controls
|
||||||
SizingUtils.apply_regular_control_sizing(node, width, height, styles)
|
SizingUtils.apply_regular_control_sizing(node, width, height, styles)
|
||||||
@@ -627,3 +633,21 @@ static func apply_input_border_styles(input_node: Control, styles: Dictionary) -
|
|||||||
control.add_theme_stylebox_override("focus", style_box)
|
control.add_theme_stylebox_override("focus", style_box)
|
||||||
elif control is Button:
|
elif control is Button:
|
||||||
control.add_theme_stylebox_override("normal", style_box)
|
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
|
||||||
|
|||||||
@@ -1,11 +1,38 @@
|
|||||||
extends TextureRect
|
extends TextureRect
|
||||||
|
|
||||||
func init(element: HTMLParser.HTMLElement, _parser: HTMLParser) -> void:
|
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
||||||
var src = element.get_attribute("src")
|
var src = element.get_attribute("src")
|
||||||
if !src: return print("Ignoring <img/> tag without \"src\" attribute.")
|
if !src: return print("Ignoring <img/> 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)
|
texture = await Network.fetch_image(src)
|
||||||
|
|
||||||
|
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()
|
var texture_size = texture.get_size()
|
||||||
custom_minimum_size = texture_size
|
custom_minimum_size = texture_size
|
||||||
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
|
||||||
@@ -158,7 +158,7 @@ static func create_panel_container_with_background(styles: Dictionary, hover_sty
|
|||||||
vbox.name = "VBoxContainer"
|
vbox.name = "VBoxContainer"
|
||||||
# Allow mouse events to pass through to the parent PanelContainer
|
# Allow mouse events to pass through to the parent PanelContainer
|
||||||
vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
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)
|
panel_container.add_child(vbox)
|
||||||
|
|
||||||
var style_box = create_stylebox_from_styles(styles)
|
var style_box = create_stylebox_from_styles(styles)
|
||||||
|
|||||||
@@ -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",
|
"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",
|
"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",
|
"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):
|
if named.has(val):
|
||||||
return named[val]
|
return named[val]
|
||||||
|
|||||||
Reference in New Issue
Block a user