use hbox instead of richtextlabel for nested text tags, complete invite creation
This commit is contained in:
@@ -31,5 +31,4 @@ Issues:
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- **< input />** is sort-of inline in normal web. We render it as a block element (new-line).
|
- **< 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.
|
- 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.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<input id="password" type="password" placeholder="Password" required="true" />
|
<input id="password" type="password" placeholder="Password" required="true" />
|
||||||
<button type="submit" id="submit">Log In</button>
|
<button type="submit" id="submit">Log In</button>
|
||||||
</form>
|
</form>
|
||||||
<p style="text-center mt-4 text-[#999999] text-base">Don't have an account? <a href="/signup.html">Register here</a></p>
|
<p style="text-center mt-4 text-[#999999] text-sm">Don't have an account? <a href="/signup.html">Register here</a></p>
|
||||||
|
|
||||||
<p id="log-output" style="min-h-24"></p>
|
<p id="log-output" style="min-h-24"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white active:border-red-500
|
w-64 p-3 border border-gray-600 rounded-md bg-[#374151] text-white active:border-red-500
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ local tldSelector = gurt.select('#tld-selector')
|
|||||||
local loadingElement = gurt.select('#tld-loading')
|
local loadingElement = gurt.select('#tld-loading')
|
||||||
local displayElement = gurt.select('#invite-code-display')
|
local displayElement = gurt.select('#invite-code-display')
|
||||||
local remainingElement = gurt.select('#remaining')
|
local remainingElement = gurt.select('#remaining')
|
||||||
|
local redeemBtn = gurt.select('#redeem-invite-btn')
|
||||||
|
|
||||||
local options
|
local options
|
||||||
|
|
||||||
@@ -169,6 +170,10 @@ local function createInvite()
|
|||||||
displayElement.text = 'Invite code: ' .. inviteCode .. ' (copied to clipboard)'
|
displayElement.text = 'Invite code: ' .. inviteCode .. ' (copied to clipboard)'
|
||||||
displayElement:show()
|
displayElement:show()
|
||||||
Clipboard.write(inviteCode)
|
Clipboard.write(inviteCode)
|
||||||
|
|
||||||
|
user.registrations_remaining = user.registrations_remaining - 1
|
||||||
|
updateUserInfo()
|
||||||
|
|
||||||
print('Invite code created and copied to clipboard: ' .. inviteCode)
|
print('Invite code created and copied to clipboard: ' .. inviteCode)
|
||||||
else
|
else
|
||||||
print('Failed to create invite: ' .. response:text())
|
print('Failed to create invite: ' .. response:text())
|
||||||
@@ -197,7 +202,11 @@ local function redeemInvite(code)
|
|||||||
updateUserInfo()
|
updateUserInfo()
|
||||||
|
|
||||||
-- Clear form
|
-- Clear form
|
||||||
gurt.select('#invite-code-input').text = ''
|
gurt.select('#invite-code-input').value = ''
|
||||||
|
redeemBtn.text = 'Success!'
|
||||||
|
gurt.setTimeout(function()
|
||||||
|
redeemBtn.text = 'Redeem'
|
||||||
|
end, 1000)
|
||||||
else
|
else
|
||||||
local error = response:text()
|
local error = response:text()
|
||||||
showError('redeem-error', 'Failed to redeem invite: ' .. error)
|
showError('redeem-error', 'Failed to redeem invite: ' .. error)
|
||||||
@@ -236,8 +245,8 @@ end)
|
|||||||
|
|
||||||
gurt.select('#create-invite-btn'):on('click', createInvite)
|
gurt.select('#create-invite-btn'):on('click', createInvite)
|
||||||
|
|
||||||
gurt.select('#redeem-invite-btn'):on('click', function()
|
redeemBtn:on('click', function()
|
||||||
local code = gurt.select('#invite-code-input').text
|
local code = gurt.select('#invite-code-input').value
|
||||||
if code and code ~= '' then
|
if code and code ~= '' then
|
||||||
redeemInvite(code)
|
redeemInvite(code)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<input id="confirm-password" type="password" placeholder="Confirm Password" required="true" />
|
<input id="confirm-password" type="password" placeholder="Confirm Password" required="true" />
|
||||||
<button type="submit" id="submit">Create Account</button>
|
<button type="submit" id="submit">Create Account</button>
|
||||||
</form>
|
</form>
|
||||||
<p style="text-center mt-4 text-[#999999] text-base">Already have an account? <a href="index.html">Login here</a></p>
|
<p style="text-center mt-4 text-[#999999] text-sm">Already have an account? <a href="index.html">Login here</a></p>
|
||||||
|
|
||||||
<p id="log-output" style="min-h-24"></p>
|
<p id="log-output" style="min-h-24"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,17 +197,32 @@ pub(crate) async fn create_invite(_ctx: &ServerContext, app_state: AppState, cla
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert invite code into database
|
let mut tx = app_state.db.begin().await
|
||||||
let insert_result = sqlx::query(
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
"INSERT INTO invite_codes (code, created_by, created_at) VALUES ($1, $2, $3)"
|
|
||||||
)
|
|
||||||
.bind(&invite_code)
|
|
||||||
.bind(claims.user_id)
|
|
||||||
.bind(Utc::now())
|
|
||||||
.execute(&app_state.db)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match insert_result {
|
let affected_rows = sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1 AND registrations_remaining > 0")
|
||||||
|
.bind(claims.user_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database error"))?
|
||||||
|
.rows_affected();
|
||||||
|
|
||||||
|
if affected_rows == 0 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_json_body(&Error {
|
||||||
|
msg: "No registrations remaining to create invite",
|
||||||
|
error: "INSUFFICIENT_REGISTRATIONS".into(),
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query("INSERT INTO invite_codes (code, created_by, created_at) VALUES ($1, $2, $3)")
|
||||||
|
.bind(&invite_code)
|
||||||
|
.bind(claims.user_id)
|
||||||
|
.bind(Utc::now())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
|
match tx.commit().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let response = serde_json::json!({
|
let response = serde_json::json!({
|
||||||
"invite_code": invite_code
|
"invite_code": invite_code
|
||||||
@@ -252,7 +267,7 @@ pub(crate) async fn redeem_invite(ctx: &ServerContext, app_state: AppState, clai
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining + 3 WHERE id = $1")
|
sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining + 1 WHERE id = $1")
|
||||||
.bind(claims.user_id)
|
.bind(claims.user_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
@@ -262,7 +277,7 @@ pub(crate) async fn redeem_invite(ctx: &ServerContext, app_state: AppState, clai
|
|||||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
let response = serde_json::json!({
|
let response = serde_json::json!({
|
||||||
"registrations_added": 3
|
"registrations_added": 1
|
||||||
});
|
});
|
||||||
Ok(GurtResponse::ok().with_json_body(&response)?)
|
Ok(GurtResponse::ok().with_json_body(&response)?)
|
||||||
}
|
}
|
||||||
@@ -282,20 +297,6 @@ pub(crate) async fn redeem_invite(ctx: &ServerContext, app_state: AppState, clai
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn create_domain_invite(_ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
pub(crate) async fn create_domain_invite(_ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||||
// Check if user has domain invite codes remaining
|
|
||||||
let user: (i32,) = sqlx::query_as("SELECT domain_invite_codes FROM users WHERE id = $1")
|
|
||||||
.bind(claims.user_id)
|
|
||||||
.fetch_one(&app_state.db)
|
|
||||||
.await
|
|
||||||
.map_err(|_| GurtError::invalid_message("User not found"))?;
|
|
||||||
|
|
||||||
if user.0 <= 0 {
|
|
||||||
return Ok(GurtResponse::bad_request().with_json_body(&Error {
|
|
||||||
msg: "No domain invite codes remaining",
|
|
||||||
error: "NO_INVITES_REMAINING".into(),
|
|
||||||
})?);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate random domain invite code
|
// Generate random domain invite code
|
||||||
let invite_code: String = {
|
let invite_code: String = {
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
@@ -312,6 +313,20 @@ pub(crate) async fn create_domain_invite(_ctx: &ServerContext, app_state: AppSta
|
|||||||
let mut tx = app_state.db.begin().await
|
let mut tx = app_state.db.begin().await
|
||||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
|
let affected_rows = sqlx::query("UPDATE users SET domain_invite_codes = domain_invite_codes - 1 WHERE id = $1 AND domain_invite_codes > 0")
|
||||||
|
.bind(claims.user_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database error"))?
|
||||||
|
.rows_affected();
|
||||||
|
|
||||||
|
if affected_rows == 0 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_json_body(&Error {
|
||||||
|
msg: "No domain invite codes remaining",
|
||||||
|
error: "NO_INVITES_REMAINING".into(),
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO domain_invite_codes (code, created_by, created_at) VALUES ($1, $2, $3)"
|
"INSERT INTO domain_invite_codes (code, created_by, created_at) VALUES ($1, $2, $3)"
|
||||||
)
|
)
|
||||||
@@ -322,12 +337,6 @@ pub(crate) async fn create_domain_invite(_ctx: &ServerContext, app_state: AppSta
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
sqlx::query("UPDATE users SET domain_invite_codes = domain_invite_codes - 1 WHERE id = $1")
|
|
||||||
.bind(claims.user_id)
|
|
||||||
.execute(&mut *tx)
|
|
||||||
.await
|
|
||||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
|
||||||
|
|
||||||
tx.commit().await
|
tx.commit().await
|
||||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
|
|||||||
@@ -85,14 +85,23 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
|
|||||||
|
|
||||||
let domain_id = domain_row.0;
|
let domain_id = domain_row.0;
|
||||||
|
|
||||||
// Decrease user's registrations remaining
|
let affected_rows = sqlx::query(
|
||||||
sqlx::query(
|
"UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1 AND registrations_remaining > 0",
|
||||||
"UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1",
|
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.execute(&app.db)
|
.execute(&app.db)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| GurtError::invalid_message("Failed to update user registrations"))?;
|
.map_err(|_| GurtError::invalid_message("Failed to update user registrations"))?
|
||||||
|
.rows_affected();
|
||||||
|
|
||||||
|
if affected_rows == 0 {
|
||||||
|
sqlx::query("DELETE FROM domains WHERE id = $1")
|
||||||
|
.bind(domain_id)
|
||||||
|
.execute(&app.db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database cleanup error"))?;
|
||||||
|
return Err(GurtError::invalid_message("No registrations remaining"));
|
||||||
|
}
|
||||||
|
|
||||||
if !app.config.discord.bot_token.is_empty() && app.config.discord.channel_id != 0 {
|
if !app.config.discord.bot_token.is_empty() && app.config.discord.channel_id != 0 {
|
||||||
let domain_registration = DomainRegistration {
|
let domain_registration = DomainRegistration {
|
||||||
@@ -123,20 +132,6 @@ pub(crate) async fn create_domain(
|
|||||||
app_state: AppState,
|
app_state: AppState,
|
||||||
claims: Claims,
|
claims: Claims,
|
||||||
) -> Result<GurtResponse> {
|
) -> Result<GurtResponse> {
|
||||||
// Check if user has registrations remaining
|
|
||||||
let user: (i32,) = sqlx::query_as("SELECT registrations_remaining FROM users WHERE id = $1")
|
|
||||||
.bind(claims.user_id)
|
|
||||||
.fetch_one(&app_state.db)
|
|
||||||
.await
|
|
||||||
.map_err(|_| GurtError::invalid_message("User not found"))?;
|
|
||||||
|
|
||||||
if user.0 <= 0 {
|
|
||||||
return Ok(GurtResponse::bad_request().with_json_body(&Error {
|
|
||||||
msg: "Failed to create domain",
|
|
||||||
error: "No registrations remaining".into(),
|
|
||||||
})?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let domain: Domain = serde_json::from_slice(ctx.body())
|
let domain: Domain = serde_json::from_slice(ctx.body())
|
||||||
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
|
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
[gd_scene load_steps=3 format=3 uid="uid://fr6lovy4ikok"]
|
[gd_scene load_steps=2 format=3 uid="uid://fr6lovy4ikok"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://cg6kjvlx3an1j" path="res://Scripts/Tags/p.gd" id="1_pnbfg"]
|
[ext_resource type="Script" uid="uid://cg6kjvlx3an1j" path="res://Scripts/Tags/p.gd" id="1_pnbfg"]
|
||||||
[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_1glvj"]
|
|
||||||
|
|
||||||
[node name="p" type="RichTextLabel"]
|
[node name="p" type="HBoxContainer"]
|
||||||
anchors_preset = 10
|
anchors_preset = 10
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
offset_bottom = 19.0
|
offset_bottom = 19.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
focus_mode = 2
|
size_flags_vertical = 8
|
||||||
mouse_default_cursor_shape = 1
|
|
||||||
theme = ExtResource("2_1glvj")
|
|
||||||
theme_override_colors/default_color = Color(0, 0, 0, 1)
|
|
||||||
bbcode_enabled = true
|
|
||||||
text = "Placeholder"
|
|
||||||
fit_content = true
|
|
||||||
vertical_alignment = 1
|
|
||||||
selection_enabled = true
|
|
||||||
script = ExtResource("1_pnbfg")
|
script = ExtResource("1_pnbfg")
|
||||||
|
|||||||
@@ -436,7 +436,9 @@ static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictio
|
|||||||
color = "#" + color.to_html(false)
|
color = "#" + color.to_html(false)
|
||||||
else:
|
else:
|
||||||
color = str(color)
|
color = str(color)
|
||||||
formatted_content = "[color=%s]%s[/color]" % [color, formatted_content]
|
var color_pattern = "[color=%s]" % color
|
||||||
|
if not formatted_content.contains(color_pattern):
|
||||||
|
formatted_content = "[color=%s]%s[/color]" % [color, formatted_content]
|
||||||
|
|
||||||
# Apply tag-specific formatting
|
# Apply tag-specific formatting
|
||||||
match element.tag_name:
|
match element.tag_name:
|
||||||
@@ -471,8 +473,8 @@ static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictio
|
|||||||
var href = element.get_attribute("href")
|
var href = element.get_attribute("href")
|
||||||
|
|
||||||
if href.length() > 0:
|
if href.length() > 0:
|
||||||
# Pass raw href - URL resolution happens in handle_link_click
|
if not formatted_content.contains("[url="):
|
||||||
formatted_content = "[url=%s]%s[/url]" % [href, formatted_content]
|
formatted_content = "[url=%s]%s[/url]" % [href, formatted_content]
|
||||||
|
|
||||||
return formatted_content
|
return formatted_content
|
||||||
|
|
||||||
|
|||||||
@@ -489,6 +489,10 @@ func _on_input_focus_lost(subscription: EventSubscription) -> void:
|
|||||||
current_text = dom_node.get_text()
|
current_text = dom_node.get_text()
|
||||||
elif "text" in dom_node:
|
elif "text" in dom_node:
|
||||||
current_text = dom_node.text
|
current_text = dom_node.text
|
||||||
|
else:
|
||||||
|
var element = dom_parser.find_by_id(subscription.element_id)
|
||||||
|
if element:
|
||||||
|
current_text = element.text_content
|
||||||
|
|
||||||
var event_info = {"value": current_text}
|
var event_info = {"value": current_text}
|
||||||
_execute_lua_callback(subscription, [event_info])
|
_execute_lua_callback(subscription, [event_info])
|
||||||
@@ -723,10 +727,7 @@ func _handle_text_setting(operation: Dictionary):
|
|||||||
|
|
||||||
var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements)
|
var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements)
|
||||||
if element:
|
if element:
|
||||||
# Always update the HTML element's text content first
|
# If the element has a DOM node, update it directly without updating text_content
|
||||||
element.text_content = text
|
|
||||||
|
|
||||||
# If the element has a DOM node, update it too
|
|
||||||
var element_id = get_or_assign_element_id(element)
|
var element_id = get_or_assign_element_id(element)
|
||||||
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
|
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
if dom_node:
|
if dom_node:
|
||||||
@@ -734,24 +735,37 @@ func _handle_text_setting(operation: Dictionary):
|
|||||||
var button_node = dom_node.get_node_or_null("ButtonNode")
|
var button_node = dom_node.get_node_or_null("ButtonNode")
|
||||||
if button_node and button_node is Button:
|
if button_node and button_node is Button:
|
||||||
button_node.text = text
|
button_node.text = text
|
||||||
|
element.text_content = text
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if element.tag_name == "p" and dom_node.has_method("set_text"):
|
||||||
|
dom_node.set_text(text)
|
||||||
|
element.text_content = text
|
||||||
|
return
|
||||||
|
|
||||||
|
element.text_content = text
|
||||||
|
|
||||||
|
if dom_node:
|
||||||
var text_node = get_dom_node(dom_node, "text")
|
var text_node = get_dom_node(dom_node, "text")
|
||||||
if text_node:
|
if text_node:
|
||||||
if text_node is RichTextLabel:
|
if text_node is RichTextLabel:
|
||||||
StyleManager.apply_styles_to_label(text_node, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text)
|
StyleManager.apply_styles_to_label(text_node, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text)
|
||||||
text_node.call_deferred("_auto_resize_to_content")
|
try_apply_auto_resize(text_node)
|
||||||
elif text_node.has_method("set_text"):
|
elif text_node.has_method("set_text"):
|
||||||
text_node.set_text(text)
|
text_node.set_text(text)
|
||||||
elif "text" in text_node:
|
elif "text" in text_node:
|
||||||
text_node.text = text
|
text_node.text = text
|
||||||
if text_node.has_method("_auto_resize_to_content"):
|
try_apply_auto_resize(text_node)
|
||||||
text_node.call_deferred("_auto_resize_to_content")
|
|
||||||
else:
|
else:
|
||||||
var rich_text_label = _find_rich_text_label_recursive(dom_node)
|
var rich_text_label = _find_rich_text_label_recursive(dom_node)
|
||||||
if rich_text_label:
|
if rich_text_label:
|
||||||
StyleManager.apply_styles_to_label(rich_text_label, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text)
|
StyleManager.apply_styles_to_label(rich_text_label, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text)
|
||||||
rich_text_label.call_deferred("_auto_resize_to_content")
|
try_apply_auto_resize(rich_text_label)
|
||||||
|
|
||||||
|
func try_apply_auto_resize(text_node: Node) -> void:
|
||||||
|
var parent = text_node.get_parent()
|
||||||
|
if parent and parent.has_method("_apply_auto_resize_to_label"):
|
||||||
|
parent.call_deferred("_apply_auto_resize_to_label", text_node)
|
||||||
|
|
||||||
func _find_rich_text_label_recursive(node: Node) -> RichTextLabel:
|
func _find_rich_text_label_recursive(node: Node) -> RichTextLabel:
|
||||||
if node is RichTextLabel:
|
if node is RichTextLabel:
|
||||||
|
|||||||
@@ -464,17 +464,17 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
var new_height = max(active_child.custom_minimum_size.y, active_child.size.y)
|
var new_height = max(active_child.custom_minimum_size.y, active_child.size.y)
|
||||||
|
|
||||||
if width:
|
if width:
|
||||||
if width == "100%":
|
if typeof(width) == TYPE_STRING and width == "100%":
|
||||||
active_child.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
active_child.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
new_width = 0
|
new_width = 0
|
||||||
elif SizingUtils.is_percentage(width):
|
elif typeof(width) == TYPE_STRING and SizingUtils.is_percentage(width):
|
||||||
new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
|
new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
|
||||||
else:
|
else:
|
||||||
new_width = width
|
new_width = width
|
||||||
|
|
||||||
if height:
|
if height:
|
||||||
if SizingUtils.is_percentage(height):
|
if typeof(height) == TYPE_STRING and SizingUtils.is_percentage(height):
|
||||||
new_height = SizingUtils.calculate_percentage_size(height, SizingUtils.DEFAULT_VIEWPORT_HEIGHT)
|
new_height = SizingUtils.calculate_percentage_size(height, SizingUtils.DEFAULT_VIEWPORT_HEIGHT)
|
||||||
else:
|
else:
|
||||||
new_height = height
|
new_height = height
|
||||||
@@ -483,7 +483,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
|
|
||||||
active_child.custom_minimum_size = new_child_size
|
active_child.custom_minimum_size = new_child_size
|
||||||
|
|
||||||
if width and width != "100%":
|
if width and not (typeof(width) == TYPE_STRING and width == "100%"):
|
||||||
active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
if height:
|
if height:
|
||||||
active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
@@ -494,7 +494,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
custom_minimum_size = new_child_size
|
custom_minimum_size = new_child_size
|
||||||
|
|
||||||
# Root Control adjusts size flags to match child
|
# Root Control adjusts size flags to match child
|
||||||
if width and width != "100%":
|
if width and not (typeof(width) == TYPE_STRING and width == "100%"):
|
||||||
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
else:
|
else:
|
||||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|||||||
@@ -1,75 +1,127 @@
|
|||||||
class_name HTMLP
|
class_name HTMLP
|
||||||
extends RichTextLabel
|
extends HBoxContainer
|
||||||
|
|
||||||
var element_styles: Dictionary = {}
|
var _element: HTMLParser.HTMLElement
|
||||||
|
var _parser: HTMLParser
|
||||||
|
|
||||||
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
||||||
element_styles = parser.get_element_styles_with_inheritance(element, "", [])
|
_element = element
|
||||||
|
_parser = parser
|
||||||
text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
|
|
||||||
|
|
||||||
# Allow mouse events to pass through to parent containers for hover effects while keeping text selection
|
|
||||||
mouse_filter = Control.MOUSE_FILTER_PASS
|
|
||||||
|
|
||||||
autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
||||||
|
|
||||||
call_deferred("_auto_resize_to_content")
|
|
||||||
|
|
||||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
|
|
||||||
func _auto_resize_to_content():
|
if get_child_count() > 0:
|
||||||
if not is_inside_tree():
|
return
|
||||||
await tree_entered
|
|
||||||
|
var content_parts = []
|
||||||
|
var current_text = ""
|
||||||
|
|
||||||
|
var element_text = element.text_content
|
||||||
|
var child_texts = []
|
||||||
|
|
||||||
|
for child in element.children:
|
||||||
|
child_texts.append(child.text_content)
|
||||||
|
|
||||||
|
var parent_only_text = element_text
|
||||||
|
for child_text in child_texts:
|
||||||
|
parent_only_text = parent_only_text.replace(child_text, "")
|
||||||
|
|
||||||
|
if not parent_only_text.strip_edges().is_empty():
|
||||||
|
var parent_label = create_styled_label(parent_only_text.strip_edges(), element, parser)
|
||||||
|
|
||||||
|
for child in element.children:
|
||||||
|
var child_label = create_styled_label(child.get_bbcode_formatted_text(parser), element, parser)
|
||||||
|
|
||||||
|
if contains_hyperlink(child):
|
||||||
|
child_label.meta_clicked.connect(_on_meta_clicked)
|
||||||
|
|
||||||
|
func create_styled_label(text: String, element: HTMLParser.HTMLElement, parser: HTMLParser) -> RichTextLabel:
|
||||||
|
var label = RichTextLabel.new()
|
||||||
|
label.fit_content = true
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
label.bbcode_enabled = true
|
||||||
|
add_child(label)
|
||||||
|
|
||||||
|
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||||
|
StyleManager.apply_styles_to_label(label, styles, element, parser, text)
|
||||||
|
|
||||||
|
call_deferred("_apply_auto_resize_to_label", label)
|
||||||
|
return label
|
||||||
|
|
||||||
|
func _apply_auto_resize_to_label(label: RichTextLabel):
|
||||||
|
if not label.is_inside_tree():
|
||||||
|
await label.tree_entered
|
||||||
|
|
||||||
var min_width = 20
|
var min_width = 20
|
||||||
var max_width = 800
|
var max_width = 800
|
||||||
var min_height = 30
|
var min_height = 30
|
||||||
|
|
||||||
fit_content = true
|
label.fit_content = true
|
||||||
|
|
||||||
var original_autowrap = autowrap_mode
|
var original_autowrap = label.autowrap_mode
|
||||||
autowrap_mode = TextServer.AUTOWRAP_OFF
|
label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||||
|
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
|
|
||||||
var natural_width = size.x
|
var natural_width = label.size.x
|
||||||
|
natural_width *= 1.0 # font weight multiplier simplified
|
||||||
var font_weight_multiplier = _get_font_weight_multiplier()
|
|
||||||
natural_width *= font_weight_multiplier
|
|
||||||
|
|
||||||
var desired_width = clampf(natural_width, min_width, max_width)
|
var desired_width = clampf(natural_width, min_width, max_width)
|
||||||
|
|
||||||
autowrap_mode = original_autowrap
|
label.autowrap_mode = original_autowrap
|
||||||
|
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
|
|
||||||
var content_height = get_content_height()
|
label.custom_minimum_size = Vector2(desired_width, 0)
|
||||||
var explicit_height = custom_minimum_size.y if custom_minimum_size.y > 0 else 0.0
|
|
||||||
var final_height = explicit_height if explicit_height > 0 else max(content_height, min_height)
|
|
||||||
custom_minimum_size = Vector2(desired_width, final_height)
|
|
||||||
|
|
||||||
queue_redraw()
|
label.queue_redraw()
|
||||||
|
|
||||||
func _get_font_weight_multiplier() -> float:
|
func contains_hyperlink(element: HTMLParser.HTMLElement) -> bool:
|
||||||
#if element_styles.has("font-black"):
|
if element.tag_name == "a":
|
||||||
# return 1.12
|
return true
|
||||||
#elif element_styles.has("font-extrabold"):
|
|
||||||
# return 1.10
|
|
||||||
#elif element_styles.has("font-bold"):
|
|
||||||
# return 1.04
|
|
||||||
#elif element_styles.has("font-semibold"):
|
|
||||||
# return 1.06
|
|
||||||
#elif element_styles.has("font-medium"):
|
|
||||||
# return 1.03
|
|
||||||
#elif element_styles.has("font-light"):
|
|
||||||
# return 0.98
|
|
||||||
#elif element_styles.has("font-extralight") or element_styles.has("font-thin"):
|
|
||||||
# return 0.95
|
|
||||||
|
|
||||||
#var text_content = get_parsed_text()
|
for child in element.children:
|
||||||
|
if contains_hyperlink(child):
|
||||||
|
return true
|
||||||
|
|
||||||
#if text_content.contains("[b]"):
|
return false
|
||||||
# return 1.08
|
|
||||||
|
|
||||||
return 1.0
|
func _on_meta_clicked(meta: Variant) -> void:
|
||||||
|
var current = get_parent()
|
||||||
|
while current:
|
||||||
|
if current.has_method("handle_link_click"):
|
||||||
|
current.handle_link_click(meta)
|
||||||
|
break
|
||||||
|
current = current.get_parent()
|
||||||
|
|
||||||
|
func get_text() -> String:
|
||||||
|
var text_parts = []
|
||||||
|
for child in get_children():
|
||||||
|
if child is RichTextLabel:
|
||||||
|
text_parts.append(child.get_parsed_text())
|
||||||
|
return " ".join(text_parts)
|
||||||
|
|
||||||
|
func set_text(new_text: String) -> void:
|
||||||
|
# Clear existing children immediately
|
||||||
|
for child in get_children():
|
||||||
|
remove_child(child)
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
if _element and _parser:
|
||||||
|
var label = create_styled_label(new_text, _element, _parser)
|
||||||
|
else:
|
||||||
|
var label = create_label(new_text)
|
||||||
|
|
||||||
|
func create_label(text: String) -> RichTextLabel:
|
||||||
|
var label = RichTextLabel.new()
|
||||||
|
label.text = text
|
||||||
|
label.fit_content = true
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
label.bbcode_enabled = true
|
||||||
|
add_child(label)
|
||||||
|
call_deferred("_apply_auto_resize_to_label", label)
|
||||||
|
return label
|
||||||
|
|||||||
Reference in New Issue
Block a user