custom font (<font>, font-[name])

This commit is contained in:
Face
2025-07-31 22:02:03 +03:00
parent a0c72bd94b
commit e0f0a545e4
10 changed files with 198 additions and 11 deletions

View File

@@ -25,7 +25,7 @@ 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`.
- Fonts use **Flash of Unstyled Text (FOUT)** as opposed to **Flash of Invisible Text (FOIT)**, meaning the text with custom fonts will render with a generic font (sans-serif) while the custom ones downloads.
Supported styles:

View File

@@ -238,10 +238,24 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) ->
rule.properties["font-bold"] = true
return
# Handle font mono
# 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":

View File

@@ -12,8 +12,8 @@ class HTMLElement:
func _init(tag: String = ""):
tag_name = tag
func get_attribute(name_: String) -> String:
return attributes.get(name_, "")
func get_attribute(name_: String, default: String = "") -> String:
return attributes.get(name_, default)
func has_attribute(name_: String) -> bool:
return attributes.has(name_)
@@ -293,6 +293,17 @@ func get_icon() -> String:
var icon_element = find_first("icon")
return icon_element.get_attribute("src")
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:

View File

@@ -24,12 +24,14 @@ pre { text-xl font-mono }
button { bg-[#1b1b1b] rounded-md text-white hover:bg-[#2a2a2a] active:bg-[#101010] }
"""
var HTML_CONTENT = """<head>
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 }
@@ -41,7 +43,7 @@ var HTML_CONTENT = """<head>
<body style="bg-[#0f172a] p-8 text-white">
<h1 style="text-center mb-4">📊 My Dashboard</h1>
<h1 style="text-center mb-4 font-roboto">📊 My Dashboard</h1>
<!-- Top Summary Cards -->
<div style="flex flex-row gap-4 justify-center flex-wrap">
@@ -98,13 +100,15 @@ var HTML_CONTENT = """<head>
</body>
""".to_utf8_buffer()
var HTML_CONTENT2 = "<head>
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 }
@@ -122,6 +126,13 @@ var HTML_CONTENT2 = "<head>
<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>
@@ -303,7 +314,7 @@ So
<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()
</body>""".to_utf8_buffer()
var HTML_CONTENT3 = """<head>
<title>Task Manager</title>

91
Scripts/FontManager.gd Normal file
View 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

View File

@@ -0,0 +1 @@
uid://c0kg201cqluo8

View File

@@ -25,6 +25,10 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
if not (node is FlexContainer):
label = node if node is RichTextLabel else node.get_node_or_null("RichTextLabel")
if label 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(label, styles, element, parser)
var width = null
var height = null
@@ -84,6 +88,21 @@ static func apply_styles_to_label(label: RichTextLabel, styles: Dictionary, elem
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"])
@@ -120,8 +139,10 @@ static func apply_styles_to_label(label: RichTextLabel, styles: Dictionary, elem
var mono_open = ""
var mono_close = ""
if styles.has("font-mono") and styles["font-mono"]:
mono_open = "[code]"
mono_close = "[/code]"
# 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":
@@ -214,3 +235,9 @@ static func apply_body_styles(body: HTMLParser.HTMLElement, parser: HTMLParser,
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)

1
Scripts/Tags/font.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://4v2v83gyok8d

View File

@@ -13,7 +13,9 @@ static func init_patterns():
"^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)$", # font styles
"^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

View File

@@ -32,6 +32,8 @@ const DIV = preload("res://Scenes/Tags/div.tscn")
const MIN_SIZE = Vector2i(750, 200)
var font_dependent_elements: Array = []
func _ready():
ProjectSettings.set_setting("display/window/size/min_width", MIN_SIZE.x)
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
@@ -42,6 +44,10 @@ func render() -> void:
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)
@@ -49,6 +55,9 @@ func render() -> void:
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))
@@ -295,3 +304,23 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
return null
return node
func register_font_dependent_element(label: RichTextLabel, 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)