RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
inventory_list.gd
Go to the documentation of this file.
1extends RefCounted
2
3
7
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")
16}
17
18var root:VBoxContainer
19var filter_row:HBoxContainer
20var header_row:HBoxContainer
21var scroll:ScrollContainer
22var rows:VBoxContainer
23var content_margin:MarginContainer
24var columns := []
25var filters := []
26var options := {}
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
33
34
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)
48 _build_filters()
49 _build_header()
50 _build_scroll()
51 return root
52
53
54func render(records:Array, state:Dictionary = {}) -> void:
55 if root == null:
56 return
57 pass_rows_created = 0
58 pass_rows_reused = 0
59 last_hidden = 0
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()):
64 var row:Button = null
65 if index < existing_rows.size() and existing_rows[index] is Button:
66 row = existing_rows[index]
67 pass_rows_reused += 1
68 total_rows_reused += 1
69 else:
70 row = _create_row_shell()
71 rows.add_child(row)
72 pass_rows_created += 1
73 total_rows_created += 1
74 _configure_row(row, records[index])
75 row.visible = true
76 for index in range(records.size(), existing_rows.size()):
77 var row = existing_rows[index]
78 if row is CanvasItem:
79 row.visible = false
80 last_hidden += 1
81
82
83func set_minimum_height(height:float) -> void:
84 if scroll != null:
85 scroll.custom_minimum_size.y = height
86
87
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
92 if rows == null:
93 return
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)
98
99
100func set_enabled(enabled:bool) -> void:
101 if root == null:
102 return
103 root.mouse_filter = Control.MOUSE_FILTER_STOP if enabled else Control.MOUSE_FILTER_IGNORE
104 for row in rows.get_children():
105 if row is Button:
106 row.disabled = not enabled
107
108
109func set_visible(visible:bool) -> void:
110 if root != null:
111 root.visible = visible
112
113
114func get_stats() -> Dictionary:
115 var row_count:int = 0
116 var visible_count:int = 0
117 if rows != null:
118 row_count = rows.get_child_count()
119 for row in rows.get_children():
120 if row is CanvasItem and row.visible:
121 visible_count += 1
122 return {
123 "rows": row_count,
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
130 }
131
132
133func get_cache_stats() -> Dictionary:
134 return get_stats()
135
136
137func _build_filters() -> void:
138 if filters.is_empty():
139 return
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)
157
158
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))
178 else:
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)
185
186
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)
198
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)
204
205 rows = VBoxContainer.new()
206 rows.name = "Rows"
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()
211 if bar != null:
212 bar.custom_minimum_size.x = int(options.get("scrollbar_width", 34))
213
214
215func _style_filters(active_filter:String) -> void:
216 options["active_filter"] = active_filter
217 if filter_row == null:
218 return
219 for button in filter_row.get_children():
220 if not button is Button:
221 continue
222 var active = str(button.name).to_lower().begins_with(active_filter)
223 _apply_button_style(button, active, true)
224
225
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:
230 return
231 for index in range(columns.size()):
232 var button = header_row.get_child(index)
233 if not button is Button:
234 continue
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"))
239 if active:
240 label += " v" if sort_ascending else " ^"
241 button.text = label
242 _apply_button_style(button, active, false)
243
244
245func _create_row_shell() -> Button:
246 var row = Button.new()
247 row.text = ""
248 row.clip_text = true
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))
253 return row
254
255
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"))
263 if selected:
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)
271 child.queue_free()
272
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)
281
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)))
290
291
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", "")))
296
297
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
314 cell.add_child(icon)
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)
329 return cell
330
331
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)
343
344
345func _make_box(bg:Color, border:Color, radius:int) -> StyleBoxFlat:
346 var style = StyleBoxFlat.new()
347 style.bg_color = bg
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
355 return style
356
357
358func _apply_font_size_recursive(node:Node, font_size:int) -> void:
359 if node is Control:
360 node.add_theme_font_size_override("font_size", font_size)
361 if node is Button:
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)