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")
17const DENSITY_PRESETS := {
20 "header_height": 38.0,
21 "filter_height": 36.0,
27 "header_height": 46.0,
28 "filter_height": 40.0,
34 "header_height": 52.0,
35 "filter_height": 46.0,
42var filter_row:HBoxContainer
43var header_row:HBoxContainer
44var scroll_frame:PanelContainer
45var scroll:ScrollContainer
47var content_margin:MarginContainer
51var colors := DEFAULT_COLORS.duplicate(true)
52var total_rows_created:int = 0
53var total_rows_reused:int = 0
54var pass_rows_created:int = 0
55var pass_rows_reused:int = 0
56var last_hidden:int = 0
59func create(parent:Node, insert_index:int, next_options:Dictionary = {}) -> VBoxContainer:
60 options = _options_with_density(next_options)
61 columns = options.get(
"columns", [])
62 filters = options.get(
"filters", [])
63 colors = DEFAULT_COLORS.duplicate(true)
64 for key
in options.get(
"colors", {}).keys():
65 colors[key] = options[
"colors"][key]
66 root = VBoxContainer.new()
67 root.name = str(options.get(
"name",
"InventoryList"))
68 root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
69 root.add_theme_constant_override(
"separation",
int(options.get(
"section_gap", 6)))
70 parent.add_child(root)
71 parent.move_child(root, insert_index)
78func render(records:Array, state:Dictionary = {}) -> void:
84 _style_filters(str(state.get(
"filter",
"all")))
85 _style_header(str(state.get(
"sort_key",
"")), bool(state.get(
"sort_ascending", true)))
86 var existing_rows:Array = rows.get_children()
87 for index
in range(records.size()):
89 if index < existing_rows.size()
and existing_rows[index]
is Button:
90 row = existing_rows[index]
92 total_rows_reused += 1
94 row = _create_row_shell()
96 pass_rows_created += 1
97 total_rows_created += 1
98 _configure_row(row, records[index])
100 for index
in range(records.size(), existing_rows.size()):
101 var row = existing_rows[index]
102 if row
is CanvasItem:
107func set_minimum_height(height:float) -> void:
109 scroll.custom_minimum_size.y = height
112func set_available_height(height:float) -> void:
115 var target_height:float = maxf(0.0, height)
116 root.custom_minimum_size.y = target_height
117 var chrome_height:float = 0.0
118 var visible_sections:int = 0
119 for section
in [filter_row, header_row]:
120 if section
is Control
and section.visible:
121 chrome_height += _section_minimum_height(section)
122 visible_sections += 1
123 if scroll_frame != null
and scroll_frame.visible:
124 visible_sections += 1
125 var gap:float =
float(root.get_theme_constant(
"separation")) *
float(max(0, visible_sections - 1))
126 var scroll_height:float = maxf(0.0, target_height - chrome_height - gap)
127 if scroll_frame != null:
128 scroll_frame.custom_minimum_size.y = scroll_height
130 scroll.custom_minimum_size.y = scroll_height
133func _section_minimum_height(section:Control) -> float:
136 var height:float = section.custom_minimum_size.y
137 for child
in section.get_children():
138 if child
is Control
and child.visible:
139 height = maxf(height, child.custom_minimum_size.y)
143func set_density(density_id:String) -> void:
144 var density_options:Dictionary = _density_options(density_id)
145 if density_options.is_empty():
147 options[
"density"] = density_id
148 for key
in density_options.keys():
149 options[key] = density_options[key]
150 if filter_row != null:
151 filter_row.add_theme_constant_override(
"separation",
int(options.get(
"filter_gap", 6)))
152 for button
in filter_row.get_children():
154 button.custom_minimum_size.y =
float(options.get(
"filter_height", 40.0))
155 if header_row != null:
156 for child
in header_row.get_children():
158 child.custom_minimum_size.y =
float(options.get(
"header_height", 46.0))
160 rows.add_theme_constant_override(
"separation",
int(options.get(
"row_gap", 4)))
161 for row
in rows.get_children():
163 row.custom_minimum_size.y =
float(options.get(
"row_height", 76.0))
166func set_font_sizes(row_size:int, header_size:int = -1, filter_size:int = -1) -> void:
167 options[
"font_size"] = row_size
168 options[
"header_font_size"] = header_size
if header_size > 0
else row_size
169 options[
"filter_font_size"] = filter_size
if filter_size > 0
else row_size
172 _style_filters(str(options.get(
"active_filter",
"all")))
173 _style_header(str(options.get(
"active_sort_key",
"")), bool(options.get(
"active_sort_ascending", true)))
174 for row
in rows.get_children():
175 _apply_font_size_recursive(row, row_size)
178func set_enabled(enabled:bool) -> void:
181 root.mouse_filter = Control.MOUSE_FILTER_STOP
if enabled
else Control.MOUSE_FILTER_IGNORE
182 for row
in rows.get_children():
184 row.disabled =
not enabled
187func set_visible(visible:bool) -> void:
189 root.visible = visible
192func get_stats() -> Dictionary:
193 var row_count:int = 0
194 var visible_count:int = 0
196 row_count = rows.get_child_count()
197 for row
in rows.get_children():
198 if row
is CanvasItem
and row.visible:
202 "visible_rows": visible_count,
203 "hidden_last_pass": last_hidden,
204 "pass_rows_created": pass_rows_created,
205 "pass_rows_reused": pass_rows_reused,
206 "total_rows_created": total_rows_created,
207 "total_rows_reused": total_rows_reused
211func get_cache_stats() -> Dictionary:
215func _build_filters() -> void:
216 if filters.is_empty():
218 filter_row = HBoxContainer.new()
219 filter_row.name =
"Filters"
220 filter_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
221 filter_row.add_theme_constant_override(
"separation",
int(options.get(
"filter_gap", 6)))
222 root.add_child(filter_row)
223 for filter
in filters:
224 var button = Button.new()
225 button.name =
"%sFilter" % str(filter.get(
"id",
"all")).capitalize()
226 button.text = str(filter.get(
"label",
"Filter"))
227 button.custom_minimum_size = Vector2(0,
float(options.get(
"filter_height", 40.0)))
228 button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
229 button.focus_mode = Control.FOCUS_NONE
230 var filter_id = str(filter.get(
"id",
"all"))
231 var callback = options.get(
"filter_callback", Callable())
232 if callback.is_valid():
233 button.pressed.connect(func(): callback.call(filter_id))
234 filter_row.add_child(button)
237func _options_with_density(next_options:Dictionary) -> Dictionary:
238 var merged:Dictionary = {}
239 var density_options:Dictionary = _density_options(str(next_options.get(
"density",
"comfortable")))
240 for key
in density_options.keys():
241 merged[key] = density_options[key]
242 for key
in next_options.keys():
243 merged[key] = next_options[key]
247func _density_options(density_id:String) -> Dictionary:
248 if not DENSITY_PRESETS.has(density_id):
250 return DENSITY_PRESETS[density_id].duplicate(true)
253func _build_header() -> void:
254 header_row = HBoxContainer.new()
255 header_row.name =
"Header"
256 header_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
257 header_row.add_theme_constant_override(
"separation", 0)
258 root.add_child(header_row)
259 for column
in columns:
260 var button = Button.new()
261 button.name =
"%sHeader" % str(column.get(
"key",
"column")).capitalize()
262 button.text = str(column.get(
"label",
"Column"))
263 button.custom_minimum_size = Vector2(0,
float(options.get(
"header_height", 46.0)))
264 button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
265 button.size_flags_stretch_ratio =
float(column.get(
"ratio", 1.0))
266 button.focus_mode = Control.FOCUS_NONE
267 var sort_key = str(column.get(
"sort_key", column.get(
"key",
"")))
268 var sortable = bool(column.get(
"sortable", true))
and sort_key !=
""
269 var callback = options.get(
"sort_callback", Callable())
270 if sortable
and callback.is_valid():
271 button.pressed.connect(func(): callback.call(sort_key))
273 button.disabled = true
274 header_row.add_child(button)
275 var spacer = Control.new()
276 spacer.name =
"ScrollbarPad"
277 spacer.custom_minimum_size = Vector2(
float(options.get(
"scrollbar_padding", 38)),
float(options.get(
"header_height", 46.0)))
278 header_row.add_child(spacer)
281func _build_scroll() -> void:
282 scroll_frame = PanelContainer.new()
283 scroll_frame.name =
"ScrollFrame"
284 scroll_frame.size_flags_horizontal = Control.SIZE_EXPAND_FILL
285 scroll_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL
286 scroll_frame.add_theme_stylebox_override(
"panel", _make_box(colors.get(
"surface"), colors.get(
"border"),
int(options.get(
"frame_radius", 0))))
287 root.add_child(scroll_frame)
289 scroll = ScrollContainer.new()
290 scroll.name =
"Scroll"
291 scroll.custom_minimum_size = Vector2(0,
float(options.get(
"height", 320.0)))
292 scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
293 scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
294 scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
295 scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_SHOW_ALWAYS
296 scroll.follow_focus = true
297 scroll.add_theme_constant_override(
"scrollbar_minimum_size",
int(options.get(
"scrollbar_width", 34)))
298 scroll_frame.add_child(scroll)
300 content_margin = MarginContainer.new()
301 content_margin.name =
"ContentMargin"
302 content_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
303 content_margin.add_theme_constant_override(
"margin_left",
int(options.get(
"scrollbar_content_padding", 4)))
304 content_margin.add_theme_constant_override(
"margin_top",
int(options.get(
"scrollbar_content_padding", 4)))
305 content_margin.add_theme_constant_override(
"margin_bottom",
int(options.get(
"scrollbar_content_padding", 4)))
306 content_margin.add_theme_constant_override(
"margin_right",
int(options.get(
"scrollbar_padding", 38)))
307 scroll.add_child(content_margin)
309 rows = VBoxContainer.new()
311 rows.size_flags_horizontal = Control.SIZE_EXPAND_FILL
312 rows.add_theme_constant_override(
"separation",
int(options.get(
"row_gap", 4)))
313 content_margin.add_child(rows)
314 var bar = scroll.get_v_scroll_bar()
316 bar.custom_minimum_size.x =
int(options.get(
"scrollbar_width", 34))
319func _style_filters(active_filter:String) -> void:
320 options[
"active_filter"] = active_filter
321 if filter_row == null:
323 for button
in filter_row.get_children():
324 if not button
is Button:
326 var active = str(button.name).to_lower().begins_with(active_filter)
327 _apply_button_style(button, active, true)
330func _style_header(sort_key:String, sort_ascending:bool) -> void:
331 options[
"active_sort_key"] = sort_key
332 options[
"active_sort_ascending"] = sort_ascending
333 if header_row == null:
335 for index
in range(columns.size()):
336 var button = header_row.get_child(index)
337 if not button
is Button:
339 var column = columns[index]
340 var key = str(column.get(
"sort_key", column.get(
"key",
"")))
341 var active = key == sort_key
342 var label = str(column.get(
"label",
"Column"))
344 label +=
" v" if sort_ascending
else " ^"
346 _apply_button_style(button, active, false)
349func _create_row_shell() -> Button:
350 var row = Button.new()
353 row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
354 row.focus_mode = Control.FOCUS_ALL
355 row.pressed.connect(func(): _emit_row_selected(row))
356 row.focus_entered.connect(func(): _emit_row_selected(row))
360func _configure_row(row:Button, record:Dictionary) -> void:
361 row.name =
"Row_%s" % str(record.get(
"id",
"item"))
362 row.set_meta(
"record_id", str(record.get(
"id",
"")))
363 row.tooltip_text = str(record.get(
"tooltip",
""))
364 row.custom_minimum_size = Vector2(0,
float(options.get(
"row_height", 76.0)))
365 row.disabled = bool(record.get(
"disabled", false))
366 var selected = bool(record.get(
"selected", false))
367 var border = record.get(
"border_color", colors.get(
"border"))
369 border = colors.get(
"accent")
370 row.add_theme_stylebox_override(
"normal", _make_box(colors.get(
"surface_selected")
if selected
else colors.get(
"surface"), border, 0))
371 row.add_theme_stylebox_override(
"hover", _make_box(colors.get(
"surface_selected"), colors.get(
"accent"), 0))
372 row.add_theme_stylebox_override(
"pressed", _make_box(colors.get(
"surface_selected"), colors.get(
"accent"), 0))
373 row.add_theme_stylebox_override(
"disabled", _make_box(colors.get(
"surface"), border.darkened(0.25), 0))
374 for child
in row.get_children():
375 row.remove_child(child)
378 var margin = MarginContainer.new()
379 margin.set_anchors_preset(Control.PRESET_FULL_RECT)
380 margin.offset_left = 6
381 margin.offset_top = 5
382 margin.offset_right = -6
383 margin.offset_bottom = -5
384 margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
385 row.add_child(margin)
387 var cells = HBoxContainer.new()
388 cells.size_flags_horizontal = Control.SIZE_EXPAND_FILL
389 cells.add_theme_constant_override(
"separation", 0)
390 cells.mouse_filter = Control.MOUSE_FILTER_IGNORE
391 margin.add_child(cells)
392 for index
in range(columns.size()):
393 cells.add_child(_build_cell(record, columns[index], index))
394 _apply_font_size_recursive(row,
int(options.get(
"font_size", 16)))
397func _emit_row_selected(row:Button) -> void:
398 var callback = options.get(
"select_callback", Callable())
399 if callback.is_valid():
400 callback.call(str(row.get_meta(
"record_id",
"")))
403func _build_cell(record:Dictionary, column:Dictionary, index:int) -> Control:
404 var key = str(column.get(
"key",
""))
405 var values = record.get(
"values", {})
406 var cell = HBoxContainer.new()
407 cell.size_flags_horizontal = Control.SIZE_EXPAND_FILL
408 cell.size_flags_vertical = Control.SIZE_EXPAND_FILL
409 cell.size_flags_stretch_ratio =
float(column.get(
"ratio", 1.0))
410 cell.add_theme_constant_override(
"separation", 5)
411 cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
412 if index == 0
and record.get(
"icon", null) != null:
413 var icon = TextureRect.new()
414 icon.texture = record.get(
"icon")
415 icon.custom_minimum_size = Vector2(34, 34)
416 icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
417 icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
418 icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
420 var value = values.get(key, {})
421 var label = Label.new()
422 label.text = str(value.get(
"text",
""))
423 label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
if bool(column.get(
"wrap", false))
else TextServer.AUTOWRAP_OFF
424 label.max_lines_visible =
int(column.get(
"lines", 1))
425 label.clip_text = true
426 label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
427 label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
428 label.horizontal_alignment =
int(column.get(
"align", value.get(
"align", HORIZONTAL_ALIGNMENT_LEFT)))
429 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
430 label.size_flags_vertical = Control.SIZE_EXPAND_FILL
431 label.add_theme_color_override(
"font_color", value.get(
"color", colors.get(
"text")))
432 label.mouse_filter = Control.MOUSE_FILTER_IGNORE
433 cell.add_child(label)
437func _apply_button_style(button:Button, active:bool, rounded:bool) -> void:
438 var border = colors.get(
"accent")
if active
else colors.get(
"border")
439 button.add_theme_stylebox_override(
"normal", _make_box(colors.get(
"surface"), border, 0
if not rounded
else 4))
440 button.add_theme_stylebox_override(
"hover", _make_box(colors.get(
"surface_selected"), colors.get(
"accent"), 0
if not rounded
else 4))
441 button.add_theme_stylebox_override(
"pressed", _make_box(colors.get(
"surface_selected"), colors.get(
"accent"), 0
if not rounded
else 4))
442 button.add_theme_color_override(
"font_color", colors.get(
"text"))
443 var size_key =
"filter_font_size" if rounded
else "header_font_size"
444 var font_size =
int(options.get(size_key, options.get(
"font_size", 16)))
445 button.add_theme_font_size_override(
"font_size", font_size)
446 button.add_theme_font_size_override(
"font_hover_size", font_size)
447 button.add_theme_font_size_override(
"font_pressed_size", font_size)
450func _make_box(bg:Color, border:Color, radius:int) -> StyleBoxFlat:
451 var style = StyleBoxFlat.new()
453 style.border_color = border
454 style.set_border_width_all(1)
455 style.set_corner_radius_all(radius)
456 style.content_margin_left = 6
457 style.content_margin_right = 6
458 style.content_margin_top = 6
459 style.content_margin_bottom = 6
463func _apply_font_size_recursive(node:Node, font_size:int) -> void:
465 node.add_theme_font_size_override(
"font_size", font_size)
467 node.add_theme_font_size_override(
"font_hover_size", font_size)
468 node.add_theme_font_size_override(
"font_pressed_size", font_size)
469 for child
in node.get_children():
470 _apply_font_size_recursive(child, font_size)