Make the buttons consistent and rework the player list
Build / test (push) Successful in 1m30s
Build / build (push) Successful in 1m17s

Buttons get 12 px of side padding. The top bar buttons, the quiz toolbar and
Show answer/Next are laid out in rows with 12 px gaps and are all 36 px tall,
and the ±1 buttons use a compact StepButton style. "Randomize new track" is now
"Next song".

Player rows show the name and the score apart, alternate like the other lists
and highlight the character under the mouse. The name field is a LineEdit with
a 10-letter limit; blank and duplicate names are refused, and a full list says
"Up to 7 players". Remove buttons update when editing is switched instead of
every frame, and the spacer panels are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SansanandClaude Opus 5.5 committed 2026-10-11 21:35:24 +02:00
1 parent e4352a9f34
commit 70e970b24c
39 files changed
+2054 -2177

No files matched your search

File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -48,6 +48,8 @@ func before_test() -> void:
_runner = scene_runner("res://main/MainWindow.tscn")
_window = _runner.scene()
_quiz = _window.get_node("Quiz")
# Rows of buttons are laid out by their containers at the end of the first frame
await _runner.simulate_frames(1)
## Override to change the fake server before the window starts and sends its first
@@ -82,7 +84,7 @@ func _play_first_song() -> void:
## Adds a player through the name field, the same way the user does.
func _add_player(player_name: String) -> PlayerObject:
var name_field: TextEdit = _quiz.get_node(
var name_field: LineEdit = _quiz.get_node(
"PlayersPanel/VBoxContainer/AddPlayerContainer/PlayerNameField")
name_field.text = player_name
_quiz.players_panel.add_player()
@@ -101,7 +103,7 @@ func _player_row(player_object: PlayerObject) -> PlayerRow:
## Removes a player with the remove button in its row.
func _remove_player(player_object: PlayerObject) -> void:
(_player_row(player_object).get_node("HBoxContainer/RemovePlayer") as BaseButton) \
(_player_row(player_object).get_node("%RemovePlayer") as BaseButton) \
.pressed.emit()
await _runner.simulate_frames(1)
+1 -1
View File
@@ -435,7 +435,7 @@ func _settle() -> void:
func _add_player(player_name: String) -> void:
(_node("PlayersPanel").get_node("VBoxContainer/AddPlayerContainer/PlayerNameField") \
as TextEdit).text = player_name
as LineEdit).text = player_name
_call("add_player")
+2 -2
View File
@@ -14,12 +14,12 @@ func _before_window_starts() -> void:
func before_test() -> void:
super()
await super()
_popup = _window.get_node("AboutPopup")
func _open_with_top_bar_button() -> void:
await _click_at((_window.get_node("TopBar/AboutButton") as Control).get_global_rect()
await _click_at((_window.get_node("TopBar/%AboutButton") as Control).get_global_rect()
.get_center())
+1 -1
View File
@@ -15,7 +15,7 @@ func _before_window_starts() -> void:
func before_test() -> void:
super()
await super()
_settings = _window.settings_popup
await _runner.simulate_frames(2)
@@ -21,7 +21,7 @@ func _before_window_starts() -> void:
func before_test() -> void:
super()
await super()
_picked.clear()
_popup = _quiz.get_node("CharacterSelectPopup")
_popup.character_selected.connect(
+2 -1
View File
@@ -167,7 +167,8 @@ func test_players_rows_show_the_saved_scores() -> void:
await _restart_game([SONG_INFO[0], SECOND_SONG])
var row: PlayerRow = _player_row(Settings.player_array[0])
assert_str(row.player_name_field.text).ends_with("Crono: 1")
assert_str(row.player_name_field.text).is_equal("Crono")
assert_str(row.score_label.text).is_equal("1")
func test_song_downloaded_ahead_is_kept_and_not_downloaded_again() -> void:
+1 -1
View File
@@ -6,7 +6,7 @@ var _qr: QRPopup
func before_test() -> void:
super()
await super()
_log = _window.get_node("LogPopup")
_qr = _window.get_node("QRPopup")
+1 -1
View File
@@ -161,7 +161,7 @@ func test_cache_keeps_filling_after_a_failed_download() -> void:
func _open_settings() -> void:
(_window.get_node("TopBar/SettingsButton") as Button).pressed.emit()
(_window.get_node("TopBar/%SettingsButton") as Button).pressed.emit()
func _close_settings() -> void:
+2 -2
View File
@@ -9,7 +9,7 @@ var _top_bar: TopBar
func before_test() -> void:
_root = create_temp_dir("offline_%d" % randi())
super()
await super()
_settings = _window.get_node("SettingsPopup")
_top_bar = _window.get_node("TopBar")
@@ -314,7 +314,7 @@ func test_new_game_turns_show_winner_back_into_next() -> void:
await _runner.simulate_frames(2)
var next_button: Button = _quiz.get_node("SongControls/NextButton")
assert_str(next_button.text).is_equal("Randomize new track")
assert_str(next_button.text).is_equal("Next song")
await _play_first_song()
assert_str(Playlist.song_object_array[Playlist.currently_playing_song].song_title) \
.is_equal("01. Opening")
+1 -1
View File
@@ -10,7 +10,7 @@ var _lucca: PlayerObject
func before_test() -> void:
super()
await super()
_crono = _add_player("Crono")
_marle = _add_player("Marle")
_lucca = _add_player("Lucca")
+94 -4
View File
@@ -118,7 +118,7 @@ func test_remove_button_removes_the_player_and_row() -> void:
var crono: PlayerObject = Settings.player_array[0]
var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel))
(_rows()[0].get_node("HBoxContainer/RemovePlayer") as BaseButton).pressed.emit()
(_rows()[0].get_node("%RemovePlayer") as BaseButton).pressed.emit()
await signals.is_emitted("player_removed", [crono])
assert_array(Settings.player_array.map(
@@ -132,7 +132,7 @@ func test_character_button_asks_for_a_new_character() -> void:
var row: PlayerRow = _rows()[0]
var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel))
(row.get_node("HBoxContainer/Character") as BaseButton).pressed.emit()
(row.get_node("%Character") as BaseButton).pressed.emit()
await signals.is_emitted("change_character_requested", [row, "Crono"])
@@ -143,5 +143,95 @@ func test_reload_shows_new_scores() -> void:
_panel.reload()
assert_str(((_rows()[0] as PlayerRow).get_node("HBoxContainer/Name") as Label).text) \
.ends_with("Crono: 3")
assert_str((_rows()[0] as PlayerRow).score_label.text).is_equal("3")
func test_row_shows_the_name_and_the_score_apart() -> void:
_add("Crono")
var row: PlayerRow = _rows()[0]
row.add_point()
row.add_point()
assert_str(row.player_name_field.text).is_equal("Crono")
assert_str(row.score_label.text).is_equal("2")
func test_names_are_trimmed_and_blank_ones_refused() -> void:
_add(" Crono ")
_add(" ")
assert_array(Settings.player_array.map(
func(p: PlayerObject) -> String: return p.player_name)).contains_exactly(["Crono"])
func test_a_name_can_only_be_used_once() -> void:
_add("Crono")
_add("crono")
assert_int(Settings.player_array.size()).is_equal(1)
assert_str(_panel.new_player_name_field.text).is_equal("crono")
func test_name_field_takes_at_most_ten_letters() -> void:
_panel.new_player_name_field.insert_text_at_caret("Abcdefghijklmnop")
assert_str(_panel.new_player_name_field.text).is_equal("Abcdefghij")
func test_full_list_says_so_instead_of_offering_the_name_field() -> void:
await _click(_panel.add_players_button)
for i: int in Settings.MAX_NUMBER_OF_PLAYERS:
_add("Player %d" % i)
assert_bool(_panel.add_player_hint.visible).is_true()
assert_str(_panel.add_player_hint.text).is_equal("Up to 7 players")
assert_bool(_panel.new_player_name_field.visible).is_false()
(_rows()[0].get_node("%RemovePlayer") as BaseButton).pressed.emit()
assert_bool(_panel.add_player_hint.visible).is_false()
assert_bool(_panel.new_player_name_field.visible).is_true()
func test_remove_buttons_show_only_while_editing() -> void:
_add("Crono")
var remove: Control = _rows()[0].get_node("%RemovePlayer")
assert_bool(remove.visible).is_false()
await _click(_panel.add_players_button)
assert_bool((_rows()[0].get_node("%RemovePlayer") as Control).visible).is_true()
await _click(_panel.add_players_button)
assert_bool((_rows()[0].get_node("%RemovePlayer") as Control).visible).is_false()
func test_rows_alternate_like_the_other_lists() -> void:
_add("Crono")
_add("Marle")
_add("Lucca")
assert_array(_rows().map(func(row: PlayerRow) -> String: return row.theme_type_variation)) \
.contains_exactly(["ListRowEven", "ListRowOdd", "ListRowEven"])
func test_character_is_highlighted_under_the_mouse() -> void:
_add("Crono")
var row: PlayerRow = _rows()[0]
row.character.mouse_entered.emit()
assert_str(row.character_frame.theme_type_variation).is_equal("PlayerCharacterHover")
row.character.mouse_exited.emit()
assert_str(row.character_frame.theme_type_variation).is_equal("PlayerCharacter")
assert_str(row.character.tooltip_text).is_equal("Change character")
func test_seven_players_fit_without_scrolling_while_adding() -> void:
await _click(_panel.add_players_button)
for i: int in Settings.MAX_NUMBER_OF_PLAYERS:
_add("Player %d" % i)
await _runner.simulate_frames(2)
var panel: Rect2 = _panel.get_global_rect()
var last: Rect2 = (_rows().back() as Control).get_global_rect()
assert_bool(panel.encloses(last)).is_true()
+1 -1
View File
@@ -7,7 +7,7 @@ var _button: Button
func before_test() -> void:
super()
await super()
_card = _quiz.get_node("QuizSettingsCard")
_button = _quiz.get_node("QuizSettingsButton")
+1
View File
@@ -16,6 +16,7 @@ var _toolbar: QuizToolbar
func before_test() -> void:
_runner = scene_runner("res://quiz/QuizToolbar.tscn")
_toolbar = _runner.scene()
await _runner.simulate_frames(1)
func _button(button_name: String) -> Button:
+10 -6
View File
@@ -28,9 +28,12 @@ const LOCKED_BY_SYNC: Array[String] = [
]
const TOP_BAR_CHILDREN: Array[String] = [
"TopPanelContainer", "Title", "SyncButton", "SettingsButton", "ShortcutsButton",
"DebugLabel", "UpdateLabel", "OfflineLabel",
"AboutButton", "QRButton", "QuizTab", "StatisticsTab",
"TopPanelContainer", "Title", "DebugLabel", "UpdateLabel", "OfflineLabel", "Buttons",
"QuizTab", "StatisticsTab",
]
## The buttons in the top bar's row on the right, in order.
const TOP_BAR_BUTTONS: Array[String] = [
"SyncButton", "SettingsButton", "ShortcutsButton", "AboutButton", "QRButton",
]
## Each top bar button and the popup it opens in the main window.
@@ -44,7 +47,7 @@ const TOP_BAR_POPUPS: Dictionary = {
func _top_bar_button(button_name: String) -> Button:
return _window.get_node("TopBar/" + button_name)
return _window.get_node("TopBar/%" + button_name)
func _children(node: Node) -> Array[String]:
@@ -90,6 +93,7 @@ func test_quiz_settings_button_sits_under_the_player_bar() -> void:
func test_top_bar_holds_only_title_and_buttons() -> void:
assert_array(_children(_window.get_node("TopBar"))) \
.contains_exactly_in_any_order(TOP_BAR_CHILDREN)
assert_array(_children(_window.get_node("TopBar/Buttons"))).contains_exactly(TOP_BAR_BUTTONS)
func test_quiz_and_top_bar_let_clicks_through_where_they_are_empty() -> void:
@@ -148,7 +152,7 @@ func test_points_from_the_player_list_reach_the_song_and_log() -> void:
await _play_first_song()
var crono: PlayerObject = _add_player("Crono")
(_player_row(crono).get_node("HBoxContainer/AddPoint") as BaseButton).pressed.emit()
(_player_row(crono).get_node("%AddPoint") as BaseButton).pressed.emit()
assert_array(Playlist.song_object_array[0].players_given_point).contains_exactly([crono.id])
var log_rows: Node = _window.get_node("%LogPopup").get_node("%LogVBoxContainer")
@@ -160,7 +164,7 @@ func test_character_button_in_the_player_list_picks_a_new_character() -> void:
var crono: PlayerObject = _add_player("Crono")
var character_select: BasePopup = _quiz.get_node("CharacterSelectPopup")
(_player_row(crono).get_node("HBoxContainer/Character") as BaseButton).pressed.emit()
(_player_row(crono).get_node("%Character") as BaseButton).pressed.emit()
assert_bool(character_select.visible).is_true()
var texture: ImageTexture = ImageTexture.create_from_image(
+1 -1
View File
@@ -15,7 +15,7 @@ func _before_window_starts() -> void:
func before_test() -> void:
super()
await super()
_popup = _window.get_node("SearchPopup")
+1 -1
View File
@@ -5,7 +5,7 @@ var _popup: SettingsPopup
func before_test() -> void:
super()
await super()
_popup = _window.get_node("SettingsPopup")
+1 -1
View File
@@ -6,7 +6,7 @@ var _popup: ShortcutsPopup
func before_test() -> void:
super()
await super()
_popup = _window.get_node("ShortcutsPopup")
+1
View File
@@ -9,6 +9,7 @@ var _controls: SongControls
func before_test() -> void:
_runner = scene_runner("res://quiz/SongControls.tscn")
_controls = _runner.scene()
await _runner.simulate_frames(1)
func _click(button: Button) -> void:
+8 -8
View File
@@ -33,12 +33,12 @@ func _before_window_starts() -> void:
func before_test() -> void:
super()
await super()
_view = _window.get_node("StatisticsView")
func _open_statistics() -> void:
await _click_at(_center(_window.get_node("TopBar/StatisticsTab")))
await _click_at(_center(_window.get_node("TopBar/%StatisticsTab")))
await _runner.simulate_frames(2)
@@ -73,7 +73,7 @@ func _visible_texts(list: VBoxContainer) -> Array[String]:
func test_quiz_tab_is_chosen_at_startup() -> void:
assert_bool(_view.visible).is_false()
assert_bool((_window.get_node("TopBar/QuizTab") as Button).button_pressed).is_true()
assert_bool((_window.get_node("TopBar/%QuizTab") as Button).button_pressed).is_true()
assert_int(_server.count("/statistics/soundtracks")).is_equal(0)
@@ -82,8 +82,8 @@ func test_clicking_statistics_tab_shows_statistics_instead_of_the_quiz() -> void
assert_bool(_view.visible).is_true()
assert_bool(_quiz.visible).is_false()
assert_bool((_window.get_node("TopBar/StatisticsTab") as Button).button_pressed).is_true()
assert_bool((_window.get_node("TopBar/QuizTab") as Button).button_pressed).is_false()
assert_bool((_window.get_node("TopBar/%StatisticsTab") as Button).button_pressed).is_true()
assert_bool((_window.get_node("TopBar/%QuizTab") as Button).button_pressed).is_false()
assert_vector(_view.get_global_rect().position).is_equal(Vector2(0, 56))
assert_vector(_view.get_global_rect().size).is_equal(Vector2(1920, 1024))
@@ -91,11 +91,11 @@ func test_clicking_statistics_tab_shows_statistics_instead_of_the_quiz() -> void
func test_clicking_quiz_tab_goes_back_to_the_quiz() -> void:
await _open_statistics()
await _click_at(_center(_window.get_node("TopBar/QuizTab")))
await _click_at(_center(_window.get_node("TopBar/%QuizTab")))
assert_bool(_view.visible).is_false()
assert_bool(_quiz.visible).is_true()
assert_bool((_window.get_node("TopBar/QuizTab") as Button).button_pressed).is_true()
assert_bool((_window.get_node("TopBar/%QuizTab") as Button).button_pressed).is_true()
func test_clicks_on_statistics_do_not_reach_the_quiz_behind() -> void:
@@ -119,7 +119,7 @@ func test_song_keeps_playing_behind_statistics() -> void:
func test_statistics_are_loaded_again_each_time_the_tab_opens() -> void:
await _open_statistics()
await _click_at(_center(_window.get_node("TopBar/QuizTab")))
await _click_at(_center(_window.get_node("TopBar/%QuizTab")))
await _open_statistics()
assert_int(_server.count("/statistics/soundtracks")).is_equal(2)
+1 -1
View File
@@ -5,7 +5,7 @@ var _crono: PlayerObject
func before_test() -> void:
super()
await super()
_crono = _add_player("Crono")
await _play_first_song()
Settings.winning_score = 1
+5 -1
View File
@@ -14,7 +14,11 @@ const GAME_FOLDERS: Array[String] = [
const STYLES: Dictionary = {
"WindowBackground": "Panel",
"PopupBackground": "Panel",
"DarkSpacer": "Panel",
"StepButton": "Button",
"PlayerList": "PanelContainer",
"PlayerCharacter": "PanelContainer",
"PlayerCharacterHover": "PanelContainer",
"PlayerScore": "Label",
"CharacterCard": "Panel",
"CharacterCardHover": "Panel",
"SettingsRowLabel": "Label",
+3 -3
View File
@@ -5,7 +5,7 @@ var _top_bar: TopBar
func before_test() -> void:
super()
await super()
_top_bar = _window.get_node("TopBar")
@@ -33,9 +33,9 @@ func test_message_fits_between_the_tabs_and_the_buttons() -> void:
var text_width: float = _top_bar.update_label.get_minimum_size().x
assert_float(text_width).is_less_equal(label.size.x)
assert_float(label.position.x).is_greater(
(_top_bar.get_node("StatisticsTab") as Control).get_global_rect().end.x)
(_top_bar.get_node("%StatisticsTab") as Control).get_global_rect().end.x)
assert_float(label.end.x).is_less(
(_top_bar.get_node("SyncButton") as Control).get_global_rect().position.x)
(_top_bar.get_node("%SyncButton") as Control).get_global_rect().position.x)
func test_message_stays_on_the_statistics_tab() -> void:
+1 -1
View File
@@ -8,7 +8,7 @@ var _popup: WinnerPopup
func before_test() -> void:
super()
await super()
_crono = _add_player("Crono")
_marle = _add_player("Marle")
_popup = _quiz.get_node("WinnerPopup")