9func create(summary:Dictionary, actions:Array = [], options:Dictionary = {}) -> Control:
10 var root = VBoxContainer.new()
11 root.name = str(options.get(
"name",
"ResultsScreen"))
12 root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
13 root.size_flags_vertical = Control.SIZE_EXPAND_FILL
14 root.custom_minimum_size = options.get(
"minimum_size", Vector2.ZERO)
15 root.add_theme_constant_override(
"separation", int(options.get(
"separation", 10)))
16 var title = Label.new()
18 title.text = str(options.get(
"title", _title_for(summary)))
19 title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
20 title.add_theme_font_size_override(
"font_size", int(options.get(
"title_size", 22)))
22 var message = Label.new()
23 message.name =
"Message"
24 message.text = str(options.get(
"message", summary.get(
"result", {}).get(
"message",
"")))
25 message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
26 message.horizontal_alignment = int(options.get(
"message_alignment", HORIZONTAL_ALIGNMENT_LEFT))
27 message.visible = message.text !=
""
28 var details_scroll = ScrollContainer.new()
29 details_scroll.name =
"DetailsScroll"
30 details_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
31 details_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
32 details_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
33 root.add_child(details_scroll)
34 var details = VBoxContainer.new()
35 details.name =
"Details"
36 details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
37 details.add_theme_constant_override(
"separation", int(options.get(
"detail_separation", 8)))
38 details_scroll.add_child(details)
39 details.add_child(message)
40 if bool(options.get(
"show_sections", true)):
41 details.add_child(_section(
"Counters", _format_counts(summary.get(
"counters", {}),
"No counters recorded.")))
42 details.add_child(_section(
"Collected", _format_counts(summary.get(
"collected", {}),
"Nothing collected.")))
43 details.add_child(_section(
"Rewards", _format_counts(summary.get(
"rewards", {}),
"No rewards earned.")))
44 var objective_text =
"%d / %d required objectives" % [
45 int(summary.get(
"completed_required_objectives", 0)),
46 int(summary.get(
"required_objectives", 0))
48 details.add_child(_section(
"Objectives", objective_text))
49 var action_row = HBoxContainer.new()
50 action_row.name =
"Actions"
51 action_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
52 action_row.add_theme_constant_override(
"separation", 8)
53 var free_on_action :=
bool(options.get(
"free_on_action", false))
54 for action
in actions:
55 if not action
is Dictionary:
57 var button = Button.new()
58 button.name = str(action.get(
"name", _node_name(str(action.get(
"id",
"Action")))))
59 button.text = str(action.get(
"label", action.get(
"id",
"Action")))
60 button.disabled =
bool(action.get(
"disabled", false))
61 button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
62 button.custom_minimum_size =
Vector2(0, int(options.get(
"action_button_height", 44)))
63 var callback = action.get(
"callback", Callable())
64 button.pressed.connect(func():
65 _close_parent_popup(button, free_on_action)
66 if callback
is Callable
and callback.is_valid():
69 action_row.add_child(button)
70 root.add_child(action_row)
75func create_popup(owner:Node, summary:Dictionary, actions:Array = [], options:Dictionary = {}) -> PopupPanel:
76 var popup = PopupPanel.new()
77 popup.name = str(options.get(
"popup_name",
"ResultsPopup"))
79 var content_options = options.duplicate(true)
80 content_options[
"name"] = str(options.get(
"content_name",
"Content"))
81 content_options[
"minimum_size"] = options.get(
"minimum_size", Vector2.ZERO)
82 var content = create(summary, actions, content_options)
83 content.set_anchors_preset(Control.PRESET_FULL_RECT)
84 content.offset_left = int(options.get(
"margin", 18))
85 content.offset_top = int(options.get(
"margin", 18))
86 content.offset_right = -int(options.get(
"margin", 18))
87 content.offset_bottom = -int(options.get(
"margin", 18))
88 content.custom_minimum_size = Vector2.ZERO
89 popup.add_child(content)
91 owner.add_child(popup)
95func popup_centered(popup:PopupPanel, viewport_size:Vector2, options:Dictionary = {}) -> void:
96 if popup == null
or not is_instance_valid(popup):
98 var popup_size = fit_popup_to_viewport(popup, viewport_size, options)
99 popup.size = popup_size
100 _fit_content(popup, popup_size, int(options.get(
"content_margin", 18)))
101 if popup.is_inside_tree():
102 popup.popup_centered(popup_size)
106func show_popup(owner:Node, summary:Dictionary, actions:Array = [], options:Dictionary = {}) -> PopupPanel:
107 var popup:PopupPanel = create_popup(owner, summary, actions, options)
108 var viewport_size:Vector2 = _coerce_vector2(options.get(
"viewport_size", Vector2.ZERO), Vector2.ZERO)
109 if viewport_size == Vector2.ZERO
and owner != null
and owner.is_inside_tree():
110 viewport_size = owner.get_viewport_rect().size
111 if viewport_size == Vector2.ZERO:
112 var fallback_size:Vector2i = _coerce_size(options.get(
"size",
Vector2i(360, 320)),
Vector2i(360, 320))
113 viewport_size =
Vector2(float(fallback_size.x), float(fallback_size.y))
114 popup_centered(popup, viewport_size, options)
115 apply_theme_and_focus(
117 options.get(
"layout_manager", null),
118 options.get(
"settings_manager", null),
119 options.get(
"ui_flow_manager", null),
120 str(options.get(
"theme_id",
"dark")),
127func fit_popup_to_viewport(popup:PopupPanel, viewport_size:Vector2, options:Dictionary = {}) -> Vector2i:
128 var requested = _coerce_size(options.get(
"size", popup.size),
Vector2i(360, 320))
129 var margin := int(options.get(
"margin", 24))
130 var min_size = _coerce_size(options.get(
"minimum",
Vector2i(260, 180)),
Vector2i(260, 180))
131 var available =
Vector2i(max(1, int(viewport_size.x) - margin), max(1, int(viewport_size.y) - margin))
132 var effective_min =
Vector2i(min(min_size.x, available.x), min(min_size.y, available.y))
134 clamp(requested.x, effective_min.x, available.x),
135 clamp(requested.y, effective_min.y, available.y)
137 popup.min_size =
Vector2i(min(min_size.x, popup_size.x), min(min_size.y, popup_size.y))
141func _fit_content(popup:PopupPanel, popup_size:Vector2i, content_margin:int) -> void:
142 if popup == null
or not is_instance_valid(popup):
144 var content = popup.get_node_or_null(
"Content")
145 if not content
is Control:
147 content.set_anchors_preset(Control.PRESET_FULL_RECT)
148 content.offset_left = content_margin
149 content.offset_top = content_margin
150 content.offset_right = -content_margin
151 content.offset_bottom = -content_margin
152 content.custom_minimum_size = Vector2.ZERO
153 content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
154 content.size_flags_vertical = Control.SIZE_EXPAND_FILL
155 var details_scroll = content.get_node_or_null(
"DetailsScroll")
156 if details_scroll
is Control:
157 details_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
160func apply_theme_and_focus(popup:PopupPanel, layout_manager, settings_manager = null, ui_flow_manager = null, theme_id:String =
"dark", options:Dictionary = {}) -> void:
161 if popup == null
or not is_instance_valid(popup):
163 if layout_manager
is Object
and layout_manager.has_method(
"apply_theme"):
164 layout_manager.apply_theme(popup, theme_id)
165 if settings_manager
is Object
and settings_manager.has_method(
"apply_text_size"):
166 settings_manager.apply_text_size(popup)
167 if not (ui_flow_manager
is Object):
169 if options.has(
"focus_node")
and ui_flow_manager.has_method(
"focus_named"):
170 ui_flow_manager.focus_named(popup, str(options.get(
"focus_node",
"")))
171 elif options.has(
"focus_labels")
and ui_flow_manager.has_method(
"focus_button_by_text"):
172 ui_flow_manager.focus_button_by_text(popup, options.get(
"focus_labels", []))
173 elif ui_flow_manager.has_method(
"focus_first"):
174 ui_flow_manager.focus_first(popup)
177func _title_for(summary:Dictionary) -> String:
178 var mode = str(summary.get(
"mode",
"Run")).replace(
"_",
" ").capitalize()
179 var status = str(summary.get(
"status",
"complete")).replace(
"_",
" ").capitalize()
180 return "%s %s" % [mode, status]
183func _section(title_text:String, body_text:String) -> Control:
184 var panel = VBoxContainer.new()
185 panel.name = _node_name(title_text)
186 panel.add_theme_constant_override(
"separation", 2)
187 var title = Label.new()
189 title.text = title_text
190 title.add_theme_font_size_override(
"font_size", 16)
191 panel.add_child(title)
192 var body = Label.new()
194 body.text = body_text
195 body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
196 panel.add_child(body)
200func _format_counts(counts, empty_label:String) -> String:
201 if not counts
is Dictionary:
204 var keys = counts.keys()
207 var amount = int(counts[key])
210 parts.append(
"%dx %s" % [amount, str(key).replace(
"_",
" ").capitalize()])
211 return empty_label
if parts.is_empty()
else ", ".join(parts)
214func _node_name(value:String) -> String:
215 var parts = value.replace(
"-",
"_").replace(
" ",
"_").split(
"_")
218 text += str(part).capitalize()
219 return text
if text !=
"" else "Node"
222func _coerce_size(value, fallback:Vector2i) -> Vector2i:
223 if value
is Vector2i:
226 return Vector2i(int(value.x), int(value.y))
227 if value
is Dictionary:
228 return Vector2i(int(value.get(
"x", fallback.x)), int(value.get(
"y", fallback.y)))
232func _coerce_vector2(value, fallback:Vector2) -> Vector2:
235 if value
is Vector2i:
236 return Vector2(float(value.x), float(value.y))
237 if value
is Dictionary:
238 return Vector2(float(value.get(
"x", fallback.x)), float(value.get(
"y", fallback.y)))
242func _close_parent_popup(node:Node, should_free:bool = false) -> void:
243 if node == null
or not is_instance_valid(node):
246 while current != null:
247 if current
is PopupPanel:
252 current = current.get_parent()