feat: Add StageDialogStyler for enhanced dialog management

- Introduced StageDialogStyler class to render child-facing dialogs as shared in-stage cards.
- Preserved native AcceptDialog/ConfirmationDialog signals for stage workflows.
- Implemented methods for applying styles, managing visibility, and handling button actions.
- Added support for dynamic theming and responsive design based on viewport size.
- Created utility functions for styling buttons and managing dialog content.
This commit is contained in:
2026-10-23 01:16:22 -04:00
parent c6f3930dcb
commit 0a5ef0a61e
10 changed files with 1724 additions and 238 deletions
@@ -0,0 +1,644 @@
class_name StageDialogStyler
extends RefCounted
## Renders child-facing dialogs as a shared in-stage card while preserving the
## native AcceptDialog/ConfirmationDialog signals used by the stage workflows.
const THEME_DEFAULTS := preload("res://scripts/stage/sandbox_theme.gd")
const OVERLAY_META := &"stage_dialog_overlay"
const BODY_META := &"stage_dialog_body"
const BODY_SCROLL_META := &"stage_dialog_body_scroll"
const NOTE_META := &"stage_dialog_note"
const DETAIL_META := &"stage_dialog_detail"
const ACTIONS_META := &"stage_dialog_actions"
const EXTRAS_META := &"stage_dialog_extra_controls"
const CARD_META := &"stage_dialog_card"
const TITLE_META := &"stage_dialog_title"
const ICON_META := &"stage_dialog_icon"
const ICON_LABEL_META := &"stage_dialog_icon_label"
const ICON_BADGE_META := &"stage_dialog_icon_badge"
const PRIMARY_META := &"stage_dialog_primary_button"
const DESTRUCTIVE_META := &"stage_dialog_destructive_buttons"
const OPEN_META := &"stage_dialog_open"
static func apply(
dialog: Window,
stage_theme: SandboxTheme,
ui_font: Font,
primary_button: Button = null,
destructive_buttons: Array = []
) -> void:
if dialog == null:
return
var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new()
if not dialog.has_meta(OVERLAY_META):
_install_dialog_surface(dialog, active_theme, ui_font)
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
var primary: Button = primary_button if primary_button != null else ok_button
dialog.set_meta(PRIMARY_META, primary)
dialog.set_meta(DESTRUCTIVE_META, destructive_buttons.duplicate())
_refresh_dialog_surface(dialog, active_theme, ui_font, primary, destructive_buttons)
static func popup(dialog: Window) -> void:
if dialog == null:
return
var active_theme: SandboxTheme = _theme_for(dialog)
var font: Font = dialog.get_meta(&"stage_dialog_font") as Font if dialog.has_meta(&"stage_dialog_font") else null
var primary: Button = dialog.get_meta(PRIMARY_META, null) as Button
var destructive_buttons: Array = dialog.get_meta(DESTRUCTIVE_META, []) as Array
_refresh_dialog_surface(dialog, active_theme, font, primary, destructive_buttons)
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
if overlay == null:
return
dialog.hide()
dialog.set_meta(OPEN_META, true)
overlay.show()
var host_viewport: Viewport = overlay.get_viewport()
if host_viewport != null:
var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size)
var card: PanelContainer = dialog.get_meta(CARD_META) as PanelContainer
if card != null:
var card_width: int = active_theme.integer("popups.confirmation.card_width", 560, 320, 720)
card.custom_minimum_size.x = minf(float(card_width), maxf(280.0, float(viewport_size.x - 36)))
_fit_body_scroll(dialog, viewport_size, active_theme)
var tree: SceneTree = dialog.get_tree()
if tree != null:
var fit_after_final_layout := func() -> void:
if not is_instance_valid(dialog) or not dialog.is_inside_tree() or not is_open(dialog):
return
var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size) if host_viewport != null else Vector2i.ZERO
_fit_body_scroll(dialog, viewport_size, active_theme)
var fit_after_layout := func() -> void:
if is_instance_valid(tree):
tree.process_frame.connect(fit_after_final_layout, Object.CONNECT_ONE_SHOT)
var defer_fit_one_frame := func() -> void:
if is_instance_valid(tree):
tree.process_frame.connect(fit_after_layout, Object.CONNECT_ONE_SHOT)
tree.process_frame.connect(defer_fit_one_frame, Object.CONNECT_ONE_SHOT)
var focus_primary: Button = dialog.get_meta(PRIMARY_META, null) as Button
var focus_target: Button = visual_button(focus_primary)
if focus_target != null and focus_target.visible:
focus_target.call_deferred("grab_focus")
static func is_open(dialog: Window) -> bool:
if dialog == null or not is_instance_valid(dialog):
return false
if dialog.has_meta(OVERLAY_META):
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
return overlay != null and is_instance_valid(overlay) and overlay.visible
return dialog.visible
static func is_styled(dialog: Window) -> bool:
return dialog != null and is_instance_valid(dialog) and dialog.has_meta(OVERLAY_META)
static func hide(dialog: Window) -> void:
if dialog == null or not is_instance_valid(dialog):
return
dialog.set_meta(OPEN_META, false)
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control if dialog.has_meta(OVERLAY_META) else null
if overlay != null and is_instance_valid(overlay):
overlay.hide()
if dialog.visible:
dialog.hide()
static func dismiss_on_escape(dialog: Window) -> void:
if dialog == null or not is_instance_valid(dialog):
return
if dialog is ConfirmationDialog:
var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button()
var visible_cancel: Button = visual_button(cancel_button)
if visible_cancel != null and visible_cancel.visible:
visible_cancel.pressed.emit()
return
if dialog.has_signal("close_requested"):
dialog.emit_signal("close_requested")
hide(dialog)
static func visual_button(original_button: Button) -> Button:
if original_button == null:
return null
var visual: Variant = original_button.get_meta(&"stage_dialog_visual_button", null)
return visual as Button if visual is Button else original_button
static func panel_style(stage_theme: SandboxTheme, include_content_padding: bool = true) -> StyleBoxFlat:
var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new()
var style := StyleBoxFlat.new()
style.bg_color = active_theme.color("popups.confirmation.surface_color", Color("#202633FA"))
style.border_color = active_theme.color("popups.confirmation.border_color", Color("#667386"))
style.set_border_width_all(active_theme.integer("popups.confirmation.border_width", 2, 1, 4))
style.set_corner_radius_all(active_theme.integer("popups.confirmation.corner_radius", 18, 10, 28))
if include_content_padding:
var padding: int = active_theme.integer("popups.confirmation.padding", 22, 12, 36)
style.content_margin_left = padding
style.content_margin_right = padding
style.content_margin_top = padding
style.content_margin_bottom = padding
return style
static func style_button(button: Button, role: StringName, stage_theme: SandboxTheme, ui_font: Font = null) -> void:
if button == null:
return
var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new()
var action: Color = active_theme.color("popups.confirmation.action_color", Color("#087DBA"))
var accent: Color = active_theme.color("popups.confirmation.accent_color", Color("#8BD9FF"))
var danger: Color = active_theme.color("popups.confirmation.danger_color", Color("#D94A56"))
var secondary: Color = active_theme.color("popups.confirmation.secondary_surface", Color("#293242"))
var secondary_border: Color = active_theme.color("popups.confirmation.secondary_border", Color("#718096"))
var normal: Color = action
var hover: Color = action.lightened(0.12)
var pressed: Color = action.darkened(0.12)
var border: Color = accent
var label_color: Color = Color("#F5F7FA")
match role:
&"primary":
normal = action
hover = active_theme.color("stage_panels.action.primary_hover_color", action.lightened(0.12))
pressed = active_theme.color("stage_panels.action.primary_pressed_color", action.darkened(0.12))
border = accent
&"danger":
normal = active_theme.color("popups.confirmation.danger_surface", Color("#44262D"))
hover = active_theme.color("popups.confirmation.danger_hover_surface", Color("#3B252C"))
pressed = active_theme.color("popups.confirmation.danger_pressed_surface", Color("#4A252D"))
border = danger
label_color = Color("#FFE6E8")
_:
normal = secondary
hover = active_theme.color("stage_panels.action.hover_surface", Color("#303A4B"))
pressed = active_theme.color("stage_panels.action.selected_surface", Color("#12394A"))
border = secondary_border
var border_width: int = active_theme.integer("popups.confirmation.button_border_width", 2, 1, 5)
var radius: int = active_theme.integer("popups.confirmation.button_radius", 12, 8, 24)
button.add_theme_stylebox_override("normal", _button_box(normal, border, border_width, radius))
button.add_theme_stylebox_override("hover", _button_box(hover, border.lightened(0.12), border_width + 1, radius))
button.add_theme_stylebox_override("pressed", _button_box(pressed, border, border_width + 1, radius))
button.add_theme_stylebox_override("hover_pressed", _button_box(pressed.lightened(0.05), border, border_width + 1, radius))
button.add_theme_stylebox_override("disabled", _button_box(secondary.darkened(0.18), secondary_border.darkened(0.35), 1, radius))
button.add_theme_stylebox_override("focus", _button_box(normal, active_theme.color("popups.confirmation.focus_color", Color("#8BD9FF")), border_width + 1, radius))
button.add_theme_color_override("font_color", label_color)
button.add_theme_color_override("font_hover_color", label_color)
button.add_theme_color_override("font_pressed_color", label_color)
button.add_theme_color_override("font_disabled_color", Color("#AAB2C0"))
button.add_theme_font_size_override("font_size", active_theme.integer("popups.confirmation.button_font_size", 20, 16, 32))
var min_height: int = active_theme.integer("popups.confirmation.button_height", 52, 44, 80)
var min_width: int = active_theme.integer("popups.confirmation.button_min_width", 112, 88, 240)
button.custom_minimum_size = Vector2(maxf(button.custom_minimum_size.x, float(min_width)), maxf(button.custom_minimum_size.y, float(min_height)))
if ui_font != null:
button.add_theme_font_override("font", _strong_font(ui_font) if role == &"primary" else ui_font)
button.focus_mode = Control.FOCUS_ALL
static func _install_dialog_surface(dialog: Window, stage_theme: SandboxTheme, ui_font: Font) -> void:
dialog.set_meta(&"stage_dialog_theme", stage_theme)
if ui_font != null:
dialog.set_meta(&"stage_dialog_font", ui_font)
dialog.theme = stage_theme.control_theme()
dialog.borderless = true
dialog.transparent = true
dialog.transparent_bg = true
dialog.unresizable = true
dialog.wrap_controls = false
dialog.add_theme_color_override("title_color", Color.TRANSPARENT)
dialog.add_theme_font_size_override("title_font_size", 1)
if ui_font != null:
dialog.add_theme_font_override("font", ui_font)
var overlay_host: Node = _find_overlay_host(dialog)
if overlay_host == null:
return
var overlay := Control.new()
overlay.name = "StageDialogOverlay"
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
overlay.z_index = 100
overlay.z_as_relative = false
overlay.visible = false
overlay.gui_input.connect(_on_overlay_input.bind(dialog))
overlay_host.add_child(overlay)
dialog.set_meta(OVERLAY_META, overlay)
dialog.set_meta(OPEN_META, false)
dialog.set_meta(&"stage_dialog_overlay_host", overlay_host)
dialog.tree_exiting.connect(_on_dialog_tree_exiting.bind(dialog, overlay), CONNECT_ONE_SHOT)
if dialog.has_signal("confirmed"):
dialog.connect("confirmed", _on_native_dialog_closed.bind(dialog))
if dialog.has_signal("canceled"):
dialog.connect("canceled", _on_native_dialog_closed.bind(dialog))
if dialog.has_signal("custom_action"):
dialog.connect("custom_action", _on_native_dialog_custom_action.bind(dialog))
if dialog.has_signal("close_requested"):
dialog.connect("close_requested", _on_native_dialog_closed.bind(dialog))
var backdrop := ColorRect.new()
backdrop.name = "Backdrop"
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
backdrop.color = stage_theme.color("popups.confirmation.backdrop_color", Color("#080D15B8"))
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
overlay.add_child(backdrop)
var center := CenterContainer.new()
center.name = "CardCenter"
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
overlay.add_child(center)
var card := PanelContainer.new()
card.name = "DialogCard"
card.custom_minimum_size = Vector2(560.0, 0.0)
card.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
card.size_flags_vertical = Control.SIZE_SHRINK_CENTER
card.add_theme_stylebox_override("panel", panel_style(stage_theme))
center.add_child(card)
dialog.set_meta(CARD_META, card)
var content := VBoxContainer.new()
content.name = "DialogContent"
content.add_theme_constant_override("separation", stage_theme.integer("popups.confirmation.body_gap", 12, 4, 24))
card.add_child(content)
var header := HBoxContainer.new()
header.name = "DialogHeader"
header.add_theme_constant_override("separation", 12)
content.add_child(header)
var icon_badge := PanelContainer.new()
icon_badge.name = "IconBadge"
icon_badge.custom_minimum_size = Vector2(40.0, 40.0)
icon_badge.add_theme_stylebox_override("panel", _icon_box(Color("#173A4B"), Color("#4DB9FF")))
header.add_child(icon_badge)
dialog.set_meta(ICON_BADGE_META, icon_badge)
var icon_label := Label.new()
icon_label.name = "DialogIcon"
icon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
icon_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
icon_label.add_theme_font_size_override("font_size", 22)
icon_badge.add_child(icon_label)
dialog.set_meta(ICON_LABEL_META, icon_label)
var title_label := Label.new()
title_label.name = "DialogTitle"
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
title_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.title_font_size", 28, 20, 40))
if ui_font != null:
title_label.add_theme_font_override("font", _strong_font(ui_font))
header.add_child(title_label)
dialog.set_meta(TITLE_META, title_label)
var body := VBoxContainer.new()
body.name = "DialogBody"
body.add_theme_constant_override("separation", 10)
body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var body_scroll := ScrollContainer.new()
body_scroll.name = "DialogBodyScroll"
body_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
body_scroll.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
body_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
content.add_child(body_scroll)
body_scroll.add_child(body)
dialog.set_meta(BODY_META, body)
dialog.set_meta(BODY_SCROLL_META, body_scroll)
var body_label := Label.new()
body_label.name = "DialogMessage"
body_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
body_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
body_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
body_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
body_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32))
body.add_child(body_label)
dialog.set_meta(&"stage_dialog_message", body_label)
var detail_box := PanelContainer.new()
detail_box.name = "DialogDetails"
detail_box.add_theme_stylebox_override("panel", _detail_box(stage_theme))
var detail_label := Label.new()
detail_label.name = "DetailsText"
detail_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32))
detail_box.add_child(detail_label)
body.add_child(detail_box)
dialog.set_meta(DETAIL_META, detail_box)
var note := Label.new()
note.name = "DialogNote"
note.size_flags_horizontal = Control.SIZE_EXPAND_FILL
note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
note.add_theme_color_override("font_color", Color("#B8C7D8"))
note.add_theme_font_size_override("font_size", 17)
body.add_child(note)
dialog.set_meta(NOTE_META, note)
var action_row := HBoxContainer.new()
action_row.name = "DialogActions"
action_row.alignment = BoxContainer.ALIGNMENT_END
action_row.add_theme_constant_override("separation", stage_theme.integer("popups.confirmation.footer_gap", 12, 8, 24))
content.add_child(action_row)
dialog.set_meta(ACTIONS_META, action_row)
var extras: Array[Control] = []
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button() if dialog is ConfirmationDialog else null
var default_label: Label = (dialog as AcceptDialog).get_label() if dialog is AcceptDialog else null
if default_label != null:
default_label.visible = false
if ok_button != null:
ok_button.visible = false
if cancel_button != null:
cancel_button.visible = false
for child: Node in dialog.get_children():
if child is Control and not child is Button and child != default_label and child != ok_button and child != cancel_button:
var control: Control = child as Control
control.reparent(body)
extras.append(control)
dialog.set_meta(EXTRAS_META, extras)
dialog.hide()
static func _refresh_dialog_surface(
dialog: Window,
stage_theme: SandboxTheme,
ui_font: Font,
primary_button: Button = null,
destructive_buttons: Array = []
) -> void:
if not dialog.has_meta(OVERLAY_META):
return
dialog.set_meta(&"stage_dialog_theme", stage_theme)
if ui_font != null:
dialog.set_meta(&"stage_dialog_font", ui_font)
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button() if dialog is ConfirmationDialog else null
var original_message: String = String((dialog as AcceptDialog).dialog_text) if dialog is AcceptDialog else ""
var body_label: Label = dialog.get_meta(&"stage_dialog_message") as Label
var detail_box: PanelContainer = dialog.get_meta(DETAIL_META) as PanelContainer
var detail_label: Label = detail_box.get_child(0) as Label if detail_box != null and detail_box.get_child_count() > 0 else null
var note_label: Label = dialog.get_meta(NOTE_META) as Label
var body_text: String = original_message
var note_text: String = ""
var note_marker: int = body_text.find("\n\nℹ️")
if note_marker >= 0:
note_text = body_text.substr(note_marker + 2)
body_text = body_text.substr(0, note_marker)
var detail_marker: int = body_text.find("\n\n•")
var detail_text: String = ""
if detail_marker >= 0:
detail_text = body_text.substr(detail_marker + 2)
body_text = body_text.substr(0, detail_marker)
if body_label != null:
body_label.text = body_text
body_label.visible = not body_text.is_empty()
if detail_label != null and detail_box != null:
detail_label.text = detail_text
detail_box.visible = not detail_text.is_empty()
if note_label != null:
note_label.text = note_text
note_label.visible = not note_text.is_empty()
var heading: Dictionary = _split_heading(dialog.title, String(dialog.get_meta(ICON_META, _icon_for_dialog(dialog))))
var title_label: Label = dialog.get_meta(TITLE_META) as Label
var icon_label: Label = dialog.get_meta(ICON_LABEL_META) as Label
if title_label != null:
title_label.text = String(heading.get("title", dialog.title))
var font_size: int = stage_theme.integer("popups.confirmation.title_font_size", 28, 20, 40)
title_label.add_theme_font_size_override("font_size", font_size)
title_label.add_theme_color_override("font_color", stage_theme.color("global.text_color", Color("#F5F7FA")))
if ui_font != null:
title_label.add_theme_font_override("font", _strong_font(ui_font))
if icon_label != null:
icon_label.text = String(heading.get("icon", "❔"))
var icon_badge: PanelContainer = dialog.get_meta(ICON_BADGE_META) as PanelContainer
if icon_badge != null:
var heading_title: String = String(heading.get("title", "")).to_lower()
var badge_background: Color = Color("#173A4B")
var badge_border: Color = Color("#4DB9FF")
if String(heading.get("icon", "")).contains("⚠"):
badge_background = Color("#493A13")
badge_border = Color("#D7AE28")
elif String(heading.get("icon", "")).contains("🗑") or "erase" in heading_title or "discard" in heading_title:
badge_background = Color("#48262D")
badge_border = Color("#E66B75")
icon_badge.add_theme_stylebox_override("panel", _icon_box(badge_background, badge_border))
var text_color: Color = stage_theme.color("global.text_color", Color("#F5F7FA"))
if body_label != null:
body_label.add_theme_color_override("font_color", text_color)
if detail_label != null:
detail_label.add_theme_color_override("font_color", text_color)
var extras: Array = dialog.get_meta(EXTRAS_META, []) as Array
for extra: Variant in extras:
if extra is Control:
_apply_control_font(extra as Control, ui_font, stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32))
var action_row: HBoxContainer = dialog.get_meta(ACTIONS_META) as HBoxContainer
if action_row != null:
for old_button: Node in action_row.get_children():
action_row.remove_child(old_button)
old_button.queue_free()
var primary: Button = primary_button if primary_button != null else ok_button
if ok_button != null:
ok_button.visible = false
if cancel_button != null:
cancel_button.visible = false
for raw_button: Variant in destructive_buttons:
if raw_button is Button:
(raw_button as Button).visible = false
var ordered: Array[Button] = []
_append_unique_button(ordered, primary)
for raw_destructive: Variant in destructive_buttons:
if raw_destructive is Button:
_append_unique_button(ordered, raw_destructive as Button)
_append_unique_button(ordered, cancel_button)
_append_unique_button(ordered, ok_button)
for original: Button in ordered:
var visual: Button = Button.new()
visual.text = original.text
visual.name = "Action_%s" % String(original.name)
visual.size_flags_horizontal = Control.SIZE_SHRINK_END
var role: StringName = &"secondary"
if original == primary:
role = &"primary"
elif _contains_button(destructive_buttons, original):
role = &"danger"
style_button(visual, role, stage_theme, ui_font)
visual.pressed.connect(func() -> void: original.pressed.emit())
visual.set_meta(&"stage_dialog_original_button", original)
original.set_meta(&"stage_dialog_visual_button", visual)
if action_row != null:
action_row.add_child(visual)
static func _on_overlay_input(event: InputEvent, dialog: Window) -> void:
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
dismiss_on_escape(dialog)
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
if overlay != null and overlay.get_viewport() != null:
overlay.get_viewport().set_input_as_handled()
static func _fit_body_scroll(dialog: Window, viewport_size: Vector2i, stage_theme: SandboxTheme) -> void:
var body_scroll: ScrollContainer = dialog.get_meta(BODY_SCROLL_META) as ScrollContainer
var body: Control = dialog.get_meta(BODY_META) as Control
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
var header: Control = overlay.get_node_or_null("CardCenter/DialogCard/DialogContent/DialogHeader") as Control if overlay != null else null
var actions: Control = dialog.get_meta(ACTIONS_META) as Control
var card: Control = dialog.get_meta(CARD_META) as Control
if body_scroll == null or body == null or header == null or actions == null or card == null:
return
var card_width: float = card.custom_minimum_size.x
var card_padding: float = float(stage_theme.integer("popups.confirmation.padding", 22, 12, 36))
var content_gap: float = float(stage_theme.integer("popups.confirmation.body_gap", 12, 4, 24))
var body_width: float = maxf(0.0, card_width - card_padding * 2.0)
var preferred_body_height: float = body.get_combined_minimum_size().y
var fixed_height: float = header.get_combined_minimum_size().y + actions.get_combined_minimum_size().y + card_padding * 2.0 + content_gap * 2.0
var max_body_height: float = maxf(48.0, float(viewport_size.y) - 36.0 - fixed_height)
if preferred_body_height > max_body_height:
var vertical_bar: VScrollBar = body_scroll.get_v_scroll_bar()
if vertical_bar != null:
body_width = maxf(0.0, body_width - vertical_bar.get_combined_minimum_size().x)
body.custom_minimum_size.x = body_width
body.size.x = body_width
preferred_body_height = body.get_combined_minimum_size().y
body_scroll.custom_minimum_size = Vector2(0.0, minf(preferred_body_height, max_body_height))
static func _find_overlay_host(dialog: Window) -> Node:
var ancestor: Node = dialog.get_parent()
while ancestor != null:
if ancestor is CanvasLayer:
return ancestor
ancestor = ancestor.get_parent()
var tree: SceneTree = dialog.get_tree()
var scene: Node = tree.current_scene if tree != null else null
if scene != null:
var ui_layer: Node = scene.get_node_or_null("UI")
if ui_layer is CanvasLayer:
return ui_layer
var existing: Node = scene.get_node_or_null("StageDialogLayer")
if existing is CanvasLayer:
return existing
var layer := CanvasLayer.new()
layer.name = "StageDialogLayer"
layer.layer = 100
scene.add_child(layer)
return layer
return null
static func _on_native_dialog_closed(dialog: Window) -> void:
hide(dialog)
static func _on_native_dialog_custom_action(_action: String, dialog: Window) -> void:
hide(dialog)
static func _on_dialog_tree_exiting(dialog: Window, overlay: Control) -> void:
if dialog != null and is_instance_valid(dialog):
dialog.set_meta(OPEN_META, false)
if overlay != null and is_instance_valid(overlay):
overlay.queue_free()
static func _append_unique_button(buttons: Array[Button], button: Button) -> void:
if button != null and not buttons.has(button):
buttons.append(button)
static func _contains_button(buttons: Array, button: Button) -> bool:
for item: Variant in buttons:
if item is Button and item == button:
return true
return false
static func _split_heading(raw_title: String, fallback_icon: String) -> Dictionary:
var icon: String = fallback_icon
var title: String = raw_title
var space: int = raw_title.find(" ")
if space > 0 and space < 5:
var possible_icon: String = raw_title.substr(0, space)
if possible_icon.length() <= 4 and not possible_icon.is_empty():
icon = possible_icon
title = raw_title.substr(space + 1).strip_edges()
if icon.is_empty():
icon = "❔"
return {"icon": icon, "title": title}
static func _icon_for_dialog(dialog: Window) -> String:
var dialog_name: String = dialog.name.to_lower()
if "artwork" in dialog_name or "scenery" in dialog_name:
return "🖼️"
if "asset" in dialog_name or "error" in dialog_name or "warning" in dialog_name:
return "⚠️"
if "speech" in dialog_name or "speak" in dialog_name:
return "🗣️"
if "rename" in dialog_name:
return "📝"
return "❔"
static func _theme_for(dialog: Window) -> SandboxTheme:
var raw: Variant = dialog.get_meta(&"stage_dialog_theme", null)
return raw as SandboxTheme if raw is SandboxTheme else THEME_DEFAULTS.new()
static func _apply_control_font(control: Control, ui_font: Font, size: int) -> void:
control.add_theme_font_size_override("font_size", size)
if ui_font != null:
control.add_theme_font_override("font", ui_font)
for child: Node in control.get_children():
if child is Control:
_apply_control_font(child as Control, ui_font, size)
static func _icon_box(background: Color, border: Color) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(1)
style.set_corner_radius_all(9)
return style
static func _detail_box(stage_theme: SandboxTheme) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color("#151C28")
style.border_color = Color("#39465B")
style.set_border_width_all(1)
style.set_corner_radius_all(9)
style.content_margin_left = 12.0
style.content_margin_right = 12.0
style.content_margin_top = 8.0
style.content_margin_bottom = 8.0
return style
static func _button_box(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(width)
style.set_corner_radius_all(radius)
style.content_margin_left = 12.0
style.content_margin_right = 12.0
style.content_margin_top = 8.0
style.content_margin_bottom = 8.0
return style
static func _strong_font(base_font: Font) -> Font:
if base_font == null:
return ThemeDB.fallback_font
var strong_font := FontVariation.new()
strong_font.base_font = base_font
strong_font.variation_embolden = 0.55
return strong_font
@@ -0,0 +1 @@
uid://6iflwn2v1lkg