8const DEFAULT_COLORS := {
9 "surface": Color(
"#202a34"),
10 "surface_selected": Color(
"#18212a"),
11 "border": Color(
"#33404d"),
12 "text": Color(
"#eef3f8"),
13 "muted": Color(
"#aab6c4"),
14 "accent": Color(
"#f2c86b"),
15 "boost": Color(
"#55d878")
19var filter_row:HBoxContainer
20var header_row:HBoxContainer
21var scroll:ScrollContainer
23var content_margin:MarginContainer
27var colors := DEFAULT_COLORS.duplicate(true)
28var total_rows_created:int = 0
29var total_rows_reused:int = 0
30var pass_rows_created:int = 0
31var pass_rows_reused:int = 0
32var last_hidden:int = 0
35func create(parent:Node, insert_index:int, next_options:Dictionary = {}) -> VBoxContainer:
36 options = next_options.duplicate(true)
37 columns = options.get(
"columns", [])
38 filters = options.get(
"filters", [])
39 colors = DEFAULT_COLORS.duplicate(true)
40 for key
in options.get(
"colors", {}).keys():
41 colors[key] = options[
"colors"][key]
42 root = VBoxContainer.new()
43 root.name = str(options.get(
"name",
"InventoryList"))
44 root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
45 root.add_theme_constant_override(
"separation",
int(options.get(
"section_gap", 6)))
46 parent.add_child(root)
47 parent.move_child(root, insert_index)
54func render(records:Array, state:Dictionary = {}) -> void:
60 _style_filters(str(state.get(
"filter",
"all")))
61 _style_header(str(state.get(
"sort_key",
"")), bool(state.get(
"sort_ascending", true)))
62 var existing_rows:Array = rows.get_children()
63 for index
in range(records.size()):
65 if index < existing_rows.size()
and existing_rows[index]
is Button:
66 row = existing_rows[index]
68 total_rows_reused += 1
70 row = _create_row_shell()
72 pass_rows_created += 1
73 total_rows_created += 1
74 _configure_row(row, records[index])
76 for index
in range(records.size(), existing_rows.size()):
77 var row = existing_rows[index]
83func set_minimum_height(height:float) -> void:
85 scroll.custom_minimum_size.y = height
88func set_font_sizes(row_size:int, header_size:int = -1, filter_size:int = -1) -> void:
89 options[
"font_size"] = row_size
90 options[
"header_font_size"] = header_size
if header_size > 0
else row_size
91 options[
"filter_font_size"] = filter_size
if filter_size > 0
else row_size
94 _style_filters(str(options.get(
"active_filter",
"all")))
95 _style_header(str(options.get(
"active_sort_key",
"")), bool(options.get(
"active_sort_ascending", true)))
96 for row
in rows.get_children():
97 _apply_font_size_recursive(row, row_size)
100func set_enabled(enabled:bool) -> void:
103 root.mouse_filter = Control.MOUSE_FILTER_STOP
if enabled
else Control.MOUSE_FILTER_IGNORE
104 for row
in rows.get_children():
106 row.disabled =
not enabled
109func set_visible(visible:bool) -> void:
111 root.visible = visible
114func get_stats() -> Dictionary:
115 var row_count:int = 0
116 var visible_count:int = 0
118 row_count = rows.get_child_count()
119 for row
in rows.get_children():
120 if row
is CanvasItem
and row.visible:
124 "visible_rows": visible_count,
125 "hidden_last_pass": last_hidden,
126 "pass_rows_created": pass_rows_created,
127 "pass_rows_reused": pass_rows_reused,
128 "total_rows_created": total_rows_created,
129 "total_rows_reused": total_rows_reused
133func get_cache_stats() -> Dictionary:
137func _build_filters() -> void:
138 if filters.is_empty():
140 filter_row = HBoxContainer.new()
141 filter_row.name =
"Filters"
142 filter_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
143 filter_row.add_theme_constant_override(
"separation",
int(options.get(
"filter_gap", 6)))
144 root.add_child(filter_row)
145 for filter
in filters:
146 var button = Button.new()
147 button.name =
"%sFilter" % str(filter.get(
"id",
"all")).capitalize()
148 button.text = str(filter.get(
"label",
"Filter"))
149 button.custom_minimum_size = Vector2(0, float(options.get(
"filter_height", 40.0)))
150 button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
151 button.focus_mode = Control.FOCUS_NONE
152 var filter_id = str(filter.get(
"id",
"all"))
153 var callback = options.get(
"filter_callback", Callable())
154 if callback.is_valid():
155 button.pressed.connect(func(): callback.call(filter_id))
156 filter_row.add_child(button)
159func _build_header() -> void:
160 header_row = HBoxContainer.new()
161 header_row.name =
"Header"
162 header_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
163 header_row.add_theme_constant_override(
"separation", 0)
164 root.add_child(header_row)
165 for column
in columns:
166 var button = Button.new()
167 button.name =
"%sHeader" % str(column.get(
"key",
"column")).capitalize()
168 button.text = str(column.get(
"label",
"Column"))
169 button.custom_minimum_size = Vector2(0, float(options.get(
"header_height", 46.0)))
170 button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
171 button.size_flags_stretch_ratio = float(column.get(
"ratio", 1.0))
172 button.focus_mode = Control.FOCUS_NONE
173 var sort_key = str(column.get(
"sort_key", column.get(
"key",
"")))
174 var sortable = bool(column.get(
"sortable", true))
and sort_key !=
""
175 var callback = options.get(
"sort_callback", Callable())
176 if sortable
and callback.is_valid():
177 button.pressed.connect(func(): callback.call(sort_key))
179 button.disabled = true
180 header_row.add_child(button)
181 var spacer = Control.new()
182 spacer.name =
"ScrollbarPad"
183 spacer.custom_minimum_size = Vector2(float(options.get(
"scrollbar_padding", 38)), float(options.get(
"header_height", 46.0)))
184 header_row.add_child(spacer)
187func _build_scroll() -> void:
188 scroll = ScrollContainer.new()
189 scroll.name =
"Scroll"
190 scroll.custom_minimum_size = Vector2(0, float(options.get(
"height", 320.0)))
191 scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
192 scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
193 scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
194 scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_SHOW_ALWAYS
195 scroll.follow_focus = true
196 scroll.add_theme_constant_override(
"scrollbar_minimum_size",
int(options.get(
"scrollbar_width", 34)))
197 root.add_child(scroll)
199 content_margin = MarginContainer.new()
200 content_margin.name =
"ContentMargin"
201 content_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
202 content_margin.add_theme_constant_override(
"margin_right",
int(options.get(
"scrollbar_padding", 38)))
203 scroll.add_child(content_margin)
205 rows = VBoxContainer.new()
207 rows.size_flags_horizontal = Control.SIZE_EXPAND_FILL
208 rows.add_theme_constant_override(
"separation",
int(options.get(
"row_gap", 4)))
209 content_margin.add_child(rows)
210 var bar = scroll.get_v_scroll_bar()
212 bar.custom_minimum_size.x =
int(options.get(
"scrollbar_width", 34))
215func _style_filters(active_filter:String) -> void:
216 options[
"active_filter"] = active_filter
217 if filter_row == null:
219 for button
in filter_row.get_children():
220 if not button
is Button:
222 var active = str(button.name).to_lower().begins_with(active_filter)
223 _apply_button_style(button, active, true)
226func _style_header(sort_key:String, sort_ascending:bool) -> void:
227 options[
"active_sort_key"] = sort_key
228 options[
"active_sort_ascending"] = sort_ascending
229 if header_row == null:
231 for index
in range(columns.size()):
232 var button = header_row.get_child(index)
233 if not button
is Button:
235 var column = columns[index]
236 var key = str(column.get(
"sort_key", column.get(
"key",
"")))
237 var active = key == sort_key
238 var label = str(column.get(
"label",
"Column"))
240 label +=
" v" if sort_ascending
else " ^"
242 _apply_button_style(button, active, false)
245func _create_row_shell() -> Button:
246 var row = Button.new()
249 row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
250 row.focus_mode = Control.FOCUS_ALL
251 row.pressed.connect(func(): _emit_row_selected(row))
252 row.focus_entered.connect(func(): _emit_row_selected(row))
256func _configure_row(row:Button, record:Dictionary) -> void:
257 row.name =
"Row_%s" % str(record.get(
"id",
"item"))
258 row.set_meta(
"record_id", str(record.get(
"id",
"")))
259 row.custom_minimum_size = Vector2(0, float(options.get(
"row_height", 76.0)))
260 row.disabled = bool(record.get(
"disabled", false))
261 var selected = bool(record.get(
"selected", false))
262 var border = record.get(
"border_color", colors.get(
"border"))
264 border = colors.get(
"accent")
265 row.add_theme_stylebox_override(
"normal", _make_box(colors.get(
"surface_selected")
if selected
else colors.get(
"surface"), border, 0))
266 row.add_theme_stylebox_override(
"hover", _make_box(colors.get(
"surface_selected"), colors.get(
"accent"), 0))
267 row.add_theme_stylebox_override(
"pressed", _make_box(colors.get(
"surface_selected"), colors.get(
"accent"), 0))
268 row.add_theme_stylebox_override(
"disabled", _make_box(colors.get(
"surface"), border.darkened(0.25), 0))
269 for child
in row.get_children():
270 row.remove_child(child)
273 var margin = MarginContainer.new()
274 margin.set_anchors_preset(Control.PRESET_FULL_RECT)
275 margin.offset_left = 6
276 margin.offset_top = 5
277 margin.offset_right = -6
278 margin.offset_bottom = -5
279 margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
280 row.add_child(margin)
282 var cells = HBoxContainer.new()
283 cells.size_flags_horizontal = Control.SIZE_EXPAND_FILL
284 cells.add_theme_constant_override(
"separation", 0)
285 cells.mouse_filter = Control.MOUSE_FILTER_IGNORE
286 margin.add_child(cells)
287 for index
in range(columns.size()):
288 cells.add_child(_build_cell(record, columns[index], index))
289 _apply_font_size_recursive(row,
int(options.get(
"font_size", 16)))
292func _emit_row_selected(row:Button) -> void:
293 var callback = options.get(
"select_callback", Callable())
294 if callback.is_valid():
295 callback.call(str(row.get_meta(
"record_id",
"")))
298func _build_cell(record:Dictionary, column:Dictionary, index:int) -> Control:
299 var key = str(column.get(
"key",
""))
300 var values = record.get(
"values", {})
301 var cell = HBoxContainer.new()
302 cell.size_flags_horizontal = Control.SIZE_EXPAND_FILL
303 cell.size_flags_vertical = Control.SIZE_EXPAND_FILL
304 cell.size_flags_stretch_ratio = float(column.get(
"ratio", 1.0))
305 cell.add_theme_constant_override(
"separation", 5)
306 cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
307 if index == 0
and record.get(
"icon", null) != null:
308 var icon = TextureRect.new()
309 icon.texture = record.get(
"icon")
310 icon.custom_minimum_size = Vector2(34, 34)
311 icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
312 icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
313 icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
315 var value = values.get(key, {})
316 var label = Label.new()
317 label.text = str(value.get(
"text",
""))
318 label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
if bool(column.get(
"wrap", false))
else TextServer.AUTOWRAP_OFF
319 label.max_lines_visible =
int(column.get(
"lines", 1))
320 label.clip_text = true
321 label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
322 label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
323 label.horizontal_alignment =
int(column.get(
"align", value.get(
"align", HORIZONTAL_ALIGNMENT_LEFT)))
324 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
325 label.size_flags_vertical = Control.SIZE_EXPAND_FILL
326 label.add_theme_color_override(
"font_color", value.get(
"color", colors.get(
"text")))
327 label.mouse_filter = Control.MOUSE_FILTER_IGNORE
328 cell.add_child(label)
332func _apply_button_style(button:Button, active:bool, rounded:bool) -> void:
333 var border = colors.get(
"accent")
if active
else colors.get(
"border")
334 button.add_theme_stylebox_override(
"normal", _make_box(colors.get(
"surface"), border, 0
if not rounded
else 4))
335 button.add_theme_stylebox_override(
"hover", _make_box(colors.get(
"surface_selected"), colors.get(
"accent"), 0
if not rounded
else 4))
336 button.add_theme_stylebox_override(
"pressed", _make_box(colors.get(
"surface_selected"), colors.get(
"accent"), 0
if not rounded
else 4))
337 button.add_theme_color_override(
"font_color", colors.get(
"text"))
338 var size_key =
"filter_font_size" if rounded
else "header_font_size"
339 var font_size =
int(options.get(size_key, options.get(
"font_size", 16)))
340 button.add_theme_font_size_override(
"font_size", font_size)
341 button.add_theme_font_size_override(
"font_hover_size", font_size)
342 button.add_theme_font_size_override(
"font_pressed_size", font_size)
345func _make_box(bg:Color, border:Color, radius:int) -> StyleBoxFlat:
346 var style = StyleBoxFlat.new()
348 style.border_color = border
349 style.set_border_width_all(1)
350 style.set_corner_radius_all(radius)
351 style.content_margin_left = 6
352 style.content_margin_right = 6
353 style.content_margin_top = 6
354 style.content_margin_bottom = 6
358func _apply_font_size_recursive(node:Node, font_size:int) -> void:
360 node.add_theme_font_size_override(
"font_size", font_size)
362 node.add_theme_font_size_override(
"font_hover_size", font_size)
363 node.add_theme_font_size_override(
"font_pressed_size", font_size)
364 for child
in node.get_children():
365 _apply_font_size_recursive(child, font_size)