class_name SongStatsList extends Control ## A scrollable list of songs with their soundtrack and how often they were played, under ## a header naming the columns. Shown in the statistics view instead of the soundtrack ## cards when Songs is chosen. ## ## The server has tens of thousands of songs, so only the rows on screen exist. They are ## reused for other songs as the list scrolls. ## Emitted when a song is clicked. signal song_pressed(song: SongInfoForStats) const ROW_SCENE: PackedScene = preload("res://statistics/SongStatsRow.tscn") ## How many rows one turn of the mouse wheel scrolls. const ROWS_PER_WHEEL_STEP: int = 3 ## The songs in the list, in the order shown. var songs: Array[SongInfoForStats] = [] # The rows that fit on screen, top to bottom var _rows: Array[SongStatsRow] = [] var _row_height: float = 0.0 var _search_text: String = "" @onready var rows_area: Control = %RowsArea @onready var scroll_bar: VScrollBar = %ScrollBar func _ready() -> void: var measured: SongStatsRow = ROW_SCENE.instantiate() add_child(measured) _row_height = measured.get_combined_minimum_size().y measured.free() rows_area.resized.connect(_fit_rows) scroll_bar.value_changed.connect(func(_value: float) -> void: _show_rows()) rows_area.gui_input.connect(_on_gui_input) _fit_rows() ## Shows [param new_songs] from the top, with the letters [param search_text] matched ## highlighted. func show_songs(new_songs: Array[SongInfoForStats], search_text: String = "") -> void: songs = new_songs _search_text = search_text _update_scroll_bar() scroll_bar.value = 0 _show_rows() ## The rows showing a song now, top to bottom. func shown_rows() -> Array[SongStatsRow]: var shown: Array[SongStatsRow] = [] for row: SongStatsRow in _rows: if row.visible: shown.append(row) return shown ## Scrolls so song number [param index] is at the top, as far as the list goes. func scroll_to(index: int) -> void: scroll_bar.value = index * _row_height # Makes as many rows as fit on screen, plus one for the row partly scrolled out func _fit_rows() -> void: var needed: int = ceili(rows_area.size.y / _row_height) + 1 while _rows.size() < needed: var row: SongStatsRow = ROW_SCENE.instantiate() rows_area.add_child(row) row.pressed.connect(song_pressed.emit) _rows.append(row) while _rows.size() > needed: _rows.pop_back().free() for row: SongStatsRow in _rows: row.size = Vector2(rows_area.size.x, _row_height) _update_scroll_bar() _show_rows() func _update_scroll_bar() -> void: scroll_bar.max_value = songs.size() * _row_height scroll_bar.page = rows_area.size.y scroll_bar.step = 0 scroll_bar.visible = scroll_bar.max_value > scroll_bar.page func _show_rows() -> void: var first: int = floori(scroll_bar.value / _row_height) var offset: float = scroll_bar.value - first * _row_height for i: int in _rows.size(): var row: SongStatsRow = _rows[i] var index: int = first + i row.visible = index < songs.size() if row.visible: row.position = Vector2(0, i * _row_height - offset) row.show_song(songs[index], index, _search_text) func _on_gui_input(event: InputEvent) -> void: if !(event is InputEventMouseButton) || !event.pressed: return var step: float = _row_height * ROWS_PER_WHEEL_STEP match event.button_index: MOUSE_BUTTON_WHEEL_UP: scroll_bar.value -= step * event.factor if event.factor > 0 else step MOUSE_BUTTON_WHEEL_DOWN: scroll_bar.value += step * event.factor if event.factor > 0 else step _: return rows_area.accept_event()