14var node_prefix :=
"StatusFeed"
17func push(message:String, now_ms:int, options:Dictionary = {}) -> void:
18 message = _normalize(message)
21 if bool(options.get(
"dedupe_last", true))
and message == last_message:
23 last_message = message
24 var until_ms = now_ms + int(options.get(
"duration_ms", 3600))
25 for entry
in messages:
26 if entry
is Dictionary
and str(entry.get(
"text",
"")) == message:
27 entry[
"until"] = until_ms
33 var limit = int(options.get(
"history_limit", 4))
34 while messages.size() > limit:
38func render(parent:Control, now_ms:int, rect:Rect2, options:Dictionary = {}) -> void:
41 node_prefix = str(options.get(
"node_prefix", node_prefix))
43 if messages.is_empty():
46 var max_entries = int(options.get(
"max_entries", 4))
47 var gap = float(options.get(
"gap", 6.0))
48 var padding = float(options.get(
"padding", 8.0))
49 var line_height = float(options.get(
"line_height", 18.0))
50 var max_lines = int(options.get(
"max_lines", 4))
51 var font_size = int(options.get(
"font_size", 13))
52 var y = rect.position.y
53 var rendered_count = min(max_entries, messages.size())
54 for index
in range(rendered_count):
55 var entry = messages[index]
56 if not entry
is Dictionary:
58 var wrapped = wrap_text(str(entry.get(
"text",
"")), rect.size.x - padding * 2.0, options)
59 var height = padding * 2.0 + float(wrapped.get(
"line_count", 1)) * line_height
60 var alpha = max(float(options.get(
"min_alpha", 0.42)), float(options.get(
"alpha", 0.82)) - float(index) * float(options.get(
"alpha_step", 0.12)))
61 var bg = _panel_node(parent, _indexed_name(
"%sPanel" % node_prefix, index))
62 bg.position = Vector2(rect.position.x, y)
63 bg.size = Vector2(rect.size.x, height)
64 bg.color = Color(str(options.get(
"background",
"#0a0d10")))
66 bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
68 var label = _label_node(parent, _indexed_name(
"%sText" % node_prefix, index))
69 label.text = str(wrapped.get(
"text",
""))
70 label.position = bg.position + Vector2(padding, padding * 0.6)
71 label.size = Vector2(rect.size.x - padding * 2.0, height - padding)
72 label.clip_contents = true
73 label.autowrap_mode = TextServer.AUTOWRAP_OFF
74 label.max_lines_visible = max_lines
75 label.add_theme_font_size_override(
"font_size", font_size)
76 label.mouse_filter = Control.MOUSE_FILTER_IGNORE
79 _hide_unused_nodes(parent, rendered_count)
82func expire(now_ms:int) -> void:
83 for index
in range(messages.size() - 1, -1, -1):
84 var entry = messages[index]
85 if not entry
is Dictionary
or now_ms > int(entry.get(
"until", 0)):
86 messages.remove_at(index)
87 if messages.is_empty():
91func clear_nodes(parent:Node) -> void:
94 _hide_unused_nodes(parent, 0)
97func get_messages() -> Array:
98 return messages.duplicate(true)
101func set_messages(value:Array) -> void:
102 messages = value.duplicate(true)
105func wrap_text(message:String, width:float, options:Dictionary = {}) -> Dictionary:
106 var chars_per_line = max(12, int(floor(width / float(options.get(
"char_width", 8.0)))))
107 var max_lines = int(options.get(
"max_lines", 4))
110 for raw_word
in _normalize(message).split(
" ", false):
111 var word = str(raw_word)
114 elif current.length() + 1 + word.length() <= chars_per_line:
115 current +=
" " + word
117 lines.append(current)
120 lines.append(current)
121 if lines.size() > max_lines:
122 lines = lines.slice(0, max_lines)
123 lines[max_lines - 1] = str(lines[max_lines - 1]).trim_suffix(
".") +
"..."
125 "text":
"\n".join(lines),
126 "line_count": max(1, lines.size())
130func _normalize(message:String) -> String:
131 return " ".join(message.replace(
"\n",
" ").split(
" ", false)).strip_edges()
134func _indexed_name(base_name:String, index:int) -> String:
135 return base_name
if index == 0
else "%s%d" % [base_name, index]
138func _panel_node(parent:Control, node_name:String) -> ColorRect:
139 var node = parent.get_node_or_null(node_name)
140 if node
is ColorRect:
142 var created = ColorRect.new()
143 created.name = node_name
144 created.mouse_filter = Control.MOUSE_FILTER_IGNORE
145 parent.add_child(created)
149func _label_node(parent:Control, node_name:String) -> Label:
150 var node = parent.get_node_or_null(node_name)
153 var created = Label.new()
154 created.name = node_name
155 created.mouse_filter = Control.MOUSE_FILTER_IGNORE
156 parent.add_child(created)
160func _hide_unused_nodes(parent:Node, used_count:int) -> void:
161 for child
in parent.get_children():
162 var child_name = str(child.name)
163 if not child_name.begins_with(node_prefix):
165 var index = _node_index(child_name)
166 if index >= used_count
and child
is CanvasItem:
167 child.visible = false
170func _node_index(child_name:String) -> int:
171 var suffix = child_name.trim_prefix(
"%sPanel" % node_prefix)
172 if child_name.begins_with(
"%sText" % node_prefix):
173 suffix = child_name.trim_prefix(
"%sText" % node_prefix)
174 return int(suffix)
if suffix.is_valid_int()
else 0