custom font (<font>, font-[name])
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
91
Scripts/FontManager.gd
Normal 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
|
||||
1
Scripts/FontManager.gd.uid
Normal file
1
Scripts/FontManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c0kg201cqluo8
|
||||
@@ -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
1
Scripts/Tags/font.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://4v2v83gyok8d
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user