diff --git a/quiz/MusicListPanel.tscn b/quiz/MusicListPanel.tscn index 09edac4..23009b1 100644 --- a/quiz/MusicListPanel.tscn +++ b/quiz/MusicListPanel.tscn @@ -13,6 +13,7 @@ script = ExtResource("part_script") [node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=707750459] layout_mode = 2 follow_focus = true +horizontal_scroll_mode = 0 script = ExtResource("7_dj026") [node name="MusicList" type="VBoxContainer" parent="ScrollContainer" unique_id=373398568] @@ -20,4 +21,4 @@ unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 -theme_override_constants/separation = 10 +theme_override_constants/separation = 4 diff --git a/quiz/SongRow.tscn b/quiz/SongRow.tscn index 51e1355..4fc9150 100644 --- a/quiz/SongRow.tscn +++ b/quiz/SongRow.tscn @@ -1,63 +1,29 @@ [gd_scene format=3 uid="uid://1jbtmhbqolqe"] -[ext_resource type="Texture2D" uid="uid://c8r7ykphss4c2" path="res://characters/Link.png" id="1_ll28s"] [ext_resource type="Script" uid="uid://cjo13wixip8mq" path="res://quiz/song_row.gd" id="1_lspki"] [node name="SongRow" type="PanelContainer"] -custom_minimum_size = Vector2(1000, 0) -offset_right = 1088.0 -offset_bottom = 59.0 theme_type_variation = &"SongRow" script = ExtResource("1_lspki") [node name="HBoxContainer" type="HBoxContainer" parent="."] layout_mode = 2 -size_flags_vertical = 0 +mouse_filter = 2 +theme_override_constants/separation = 12 [node name="SongListLabel" type="Label" parent="HBoxContainer"] unique_name_in_owner = true -texture_filter = 2 -custom_minimum_size = Vector2(865, 28) layout_mode = 2 size_flags_horizontal = 3 mouse_filter = 1 theme_type_variation = &"SongRowLabel" -text = "Final Fantasy Dissidia 012 Duodecim (Sista Fantasin Meningsskiljaktigheter 012 Tolv) - 'Crystal Cave -arrange-'' from FINAL FANTASY III" -autowrap_mode = 2 +vertical_alignment = 1 +autowrap_mode = 3 -[node name="Panel" type="Panel" parent="HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 0 - -[node name="SongListPlayers" type="HBoxContainer" parent="HBoxContainer"] +[node name="SongListPlayers" type="VBoxContainer" parent="HBoxContainer"] unique_name_in_owner = true +visible = false layout_mode = 2 -size_flags_vertical = 0 - -[node name="TextureRect" type="TextureRect" parent="HBoxContainer/SongListPlayers"] -custom_minimum_size = Vector2(50, 25) -layout_mode = 2 -texture = ExtResource("1_ll28s") -expand_mode = 1 -stretch_mode = 5 - -[node name="TextureRect2" type="TextureRect" parent="HBoxContainer/SongListPlayers"] -custom_minimum_size = Vector2(50, 25) -layout_mode = 2 -texture = ExtResource("1_ll28s") -expand_mode = 1 -stretch_mode = 6 - -[node name="TextureRect3" type="TextureRect" parent="HBoxContainer/SongListPlayers"] -custom_minimum_size = Vector2(50, 25) -layout_mode = 2 -texture = ExtResource("1_ll28s") -expand_mode = 1 - -[node name="TextureRect4" type="TextureRect" parent="HBoxContainer/SongListPlayers"] -custom_minimum_size = Vector2(50, 25) -layout_mode = 2 -texture = ExtResource("1_ll28s") -expand_mode = 1 -stretch_mode = 6 +size_flags_vertical = 4 +mouse_filter = 2 +theme_type_variation = &"SongRowPictureLines" diff --git a/quiz/music_list_panel.gd b/quiz/music_list_panel.gd index ec8b90b..6ebe252 100644 --- a/quiz/music_list_panel.gd +++ b/quiz/music_list_panel.gd @@ -14,9 +14,12 @@ var _song_list_object: PackedScene = preload("res://quiz/SongRow.tscn") ## Shows the songs in the playlist again, so it shows their latest state. func refresh() -> void: Settings.delete_children(_rows) - for s: SongObject in Playlist.get_playlist(): + var songs: Array[SongObject] = Playlist.get_playlist() + for i: int in songs.size(): + var s: SongObject = songs[i] var new_song_list_object: SongRow = _song_list_object.instantiate() new_song_list_object.song_object = s + new_song_list_object.index = i new_song_list_object.gui_input.connect(_on_row_input.bind(s.song_number)) _rows.add_child(new_song_list_object) diff --git a/quiz/song_row.gd b/quiz/song_row.gd index b0b433b..d948c2a 100644 --- a/quiz/song_row.gd +++ b/quiz/song_row.gd @@ -1,37 +1,105 @@ class_name SongRow extends PanelContainer -## One song in the music list: its number, soundtrack and title, and a marker for +## One song in the music list: its number, soundtrack and title, and the character of ## each player who got a point for it. +## +## The pictures are on the right and the text takes the room they leave, wrapping only +## when it does not fit on one line. With many pictures they go on more lines, as even as +## possible and at most [constant MAX_PICTURES_PER_LINE] on each, so the text keeps most +## of the row. Every line ends at the row's right edge. The row is as tall as the taller +## of the text and the pictures. + +## The most pictures on one line before they go on another. +const MAX_PICTURES_PER_LINE: int = 4 +## The size of each picture. Characters are twice as wide as they are tall. +const PICTURE_SIZE: Vector2 = Vector2(56, 28) var song_object: SongObject +## The row's place in the list, for the alternating background. +var index: int = 0 + +var _hovered: bool = false @onready var song_list_label: Label = %SongListLabel -@onready var song_list_players: HBoxContainer = %SongListPlayers +## The lines of pictures, top to bottom, each an HBoxContainer. +@onready var song_list_players: VBoxContainer = %SongListPlayers func _ready() -> void: + mouse_entered.connect(_set_hovered.bind(true)) + mouse_exited.connect(_set_hovered.bind(false)) set_label() update_players() +## The pictures shown, line by line. +func pictures() -> Array[TextureRect]: + var shown: Array[TextureRect] = [] + for line: Node in song_list_players.get_children(): + shown.append_array(line.get_children()) + return shown + + +## The number of pictures on a full line for [param pictures] pictures: as few lines as +## [constant MAX_PICTURES_PER_LINE] allows, with the pictures spread evenly over them, +## e.g. 3 and 2 for five pictures instead of 4 and 1. +static func picture_columns(pictures: int) -> int: + if pictures <= 0: + return 1 + var lines: int = ceili(float(pictures) / MAX_PICTURES_PER_LINE) + return ceili(float(pictures) / lines) + + func set_label() -> void: if song_object != null: - var size_to_change: int = song_object.players_given_point.size() * 50 - song_list_label.custom_minimum_size = Vector2(1000 - size_to_change, 28) - if song_object.is_playing: - song_list_label.theme_type_variation = &"SongRowLabelPlaying" + song_list_label.theme_type_variation = &"SongRowLabelPlaying" \ + if song_object.is_playing else &"SongRowLabel" song_list_label.text = song_object.get_song_info() + _update_style() func update_players() -> void: Settings.delete_children(song_list_players) + var scorers: Array[PlayerObject] = [] if song_object != null: # Show players in the same order as the player list for player: PlayerObject in Settings.player_array: if song_object.players_given_point.has(player.id): - var texture: TextureRect = TextureRect.new() - texture.texture = player.character - texture.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL - texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - texture.custom_minimum_size = Vector2(50, 25) - song_list_players.add_child(texture) + scorers.append(player) + var per_line: int = picture_columns(scorers.size()) + var line: HBoxContainer = null + for i: int in scorers.size(): + if i % per_line == 0: + line = HBoxContainer.new() + line.theme_type_variation = &"SongRowPictureLine" + line.alignment = BoxContainer.ALIGNMENT_END + line.mouse_filter = Control.MOUSE_FILTER_IGNORE + song_list_players.add_child(line) + line.add_child(_make_picture(scorers[i])) + song_list_players.visible = !scorers.is_empty() + + +func _make_picture(player: PlayerObject) -> TextureRect: + var picture: TextureRect = TextureRect.new() + picture.texture = player.character + picture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + picture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + picture.custom_minimum_size = PICTURE_SIZE + picture.tooltip_text = player.player_name + # Clicks on a picture still click the row + picture.mouse_filter = Control.MOUSE_FILTER_PASS + return picture + + +func _set_hovered(hovered: bool) -> void: + _hovered = hovered + _update_style() + + +func _update_style() -> void: + if _hovered: + theme_type_variation = &"SongRowHover" + elif song_object != null && song_object.is_playing: + theme_type_variation = &"SongRowPlaying" + else: + theme_type_variation = &"SongRowOdd" if index % 2 != 0 else &"SongRow" diff --git a/tests/data/layout_baseline.json b/tests/data/layout_baseline.json index f319609..7d5f613 100644 --- a/tests/data/layout_baseline.json +++ b/tests/data/layout_baseline.json @@ -1001,66 +1001,57 @@ }, "Quiz/MusicListPanel/ScrollContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow": { "rect": [ - 64.0, - 384.0, - 1088.0, - 28.0 + 70.0, + 390.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/Panel": { - "rect": [ - 1068.0, - 384.0, - 80.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 384.0, - 1000.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers": { "rect": [ - 1152.0, - 384.0, + 78.0, + 394.0, 0.0, 0.0 ], - "visible": true + "visible": false }, "Quiz/MusicPlayer": { "rect": [ @@ -4603,111 +4594,93 @@ }, "Quiz/MusicListPanel/ScrollContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1": { "rect": [ - 64.0, - 422.0, - 1088.0, - 28.0 + 70.0, + 430.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer": { "rect": [ - 64.0, - 422.0, - 1088.0, + 78.0, + 434.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer/Panel": { - "rect": [ - 1068.0, - 422.0, - 80.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 422.0, - 1000.0, + 78.0, + 434.0, + 1060.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer/SongListPlayers": { "rect": [ - 1152.0, - 422.0, + 78.0, + 434.0, 0.0, 0.0 ], - "visible": true + "visible": false }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow": { "rect": [ - 64.0, - 384.0, - 1088.0, - 28.0 + 70.0, + 390.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/Panel": { - "rect": [ - 1068.0, - 384.0, - 80.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 384.0, - 1000.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers": { "rect": [ - 1152.0, - 384.0, + 78.0, + 394.0, 0.0, 0.0 ], - "visible": true + "visible": false }, "Quiz/MusicPlayer": { "rect": [ @@ -11454,66 +11427,57 @@ }, "Quiz/MusicListPanel/ScrollContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow": { "rect": [ - 64.0, - 384.0, - 1088.0, - 28.0 + 70.0, + 390.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/Panel": { - "rect": [ - 1068.0, - 384.0, - 80.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 384.0, - 1000.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers": { "rect": [ - 1152.0, - 384.0, + 78.0, + 394.0, 0.0, 0.0 ], - "visible": true + "visible": false }, "Quiz/MusicPlayer": { "rect": [ @@ -15398,127 +15362,118 @@ }, "Quiz/MusicListPanel/ScrollContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1": { "rect": [ - 64.0, - 422.0, - 1088.0, - 28.0 + 70.0, + 430.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer": { "rect": [ - 64.0, - 422.0, - 1088.0, + 78.0, + 434.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer/Panel": { - "rect": [ - 1068.0, - 422.0, - 80.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 422.0, - 1000.0, + 78.0, + 434.0, + 1060.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer/SongListPlayers": { "rect": [ - 1152.0, - 422.0, + 78.0, + 434.0, 0.0, 0.0 ], - "visible": true + "visible": false }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow": { "rect": [ - 64.0, - 384.0, - 1088.0, - 28.0 + 70.0, + 390.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/Panel": { - "rect": [ - 968.0, - 384.0, - 75.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 384.0, - 900.0, + 78.0, + 394.0, + 932.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers": { "rect": [ - 1047.0, - 384.0, - 105.0, - 25.0 + 1022.0, + 394.0, + 116.0, + 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers/#0": { "rect": [ - 1047.0, - 384.0, - 51.0, - 25.0 + 1022.0, + 394.0, + 116.0, + 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers/#1": { + "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers/#0/#0": { "rect": [ - 1102.0, - 384.0, - 50.0, - 25.0 + 1022.0, + 394.0, + 56.0, + 28.0 + ], + "visible": true + }, + "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers/#0/#1": { + "rect": [ + 1082.0, + 394.0, + 56.0, + 28.0 ], "visible": true }, @@ -23530,66 +23485,57 @@ }, "Quiz/MusicListPanel/ScrollContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow": { "rect": [ - 64.0, - 384.0, - 1088.0, - 28.0 + 70.0, + 390.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/Panel": { - "rect": [ - 1068.0, - 384.0, - 80.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 384.0, - 1000.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers": { "rect": [ - 1152.0, - 384.0, + 78.0, + 394.0, 0.0, 0.0 ], - "visible": true + "visible": false }, "Quiz/MusicPlayer": { "rect": [ @@ -36583,127 +36529,118 @@ }, "Quiz/MusicListPanel/ScrollContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1": { "rect": [ - 64.0, - 422.0, - 1088.0, - 28.0 + 70.0, + 430.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer": { "rect": [ - 64.0, - 422.0, - 1088.0, + 78.0, + 434.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer/Panel": { - "rect": [ - 1068.0, - 422.0, - 80.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 422.0, - 1000.0, + 78.0, + 434.0, + 1060.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/#1/HBoxContainer/SongListPlayers": { "rect": [ - 1152.0, - 422.0, + 78.0, + 434.0, 0.0, 0.0 ], - "visible": true + "visible": false }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow": { "rect": [ - 64.0, - 384.0, - 1088.0, - 28.0 + 70.0, + 390.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/Panel": { - "rect": [ - 968.0, - 384.0, - 75.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 384.0, - 900.0, + 78.0, + 394.0, + 932.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers": { "rect": [ - 1047.0, - 384.0, - 105.0, - 25.0 + 1022.0, + 394.0, + 116.0, + 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers/#0": { "rect": [ - 1047.0, - 384.0, - 51.0, - 25.0 + 1022.0, + 394.0, + 116.0, + 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers/#1": { + "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers/#0/#0": { "rect": [ - 1102.0, - 384.0, - 50.0, - 25.0 + 1022.0, + 394.0, + 56.0, + 28.0 + ], + "visible": true + }, + "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers/#0/#1": { + "rect": [ + 1082.0, + 394.0, + 56.0, + 28.0 ], "visible": true }, @@ -41251,66 +41188,57 @@ }, "Quiz/MusicListPanel/ScrollContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList": { "rect": [ - 64.0, - 384.0, - 1088.0, - 512.0 + 70.0, + 390.0, + 1076.0, + 500.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow": { "rect": [ - 64.0, - 384.0, - 1088.0, - 28.0 + 70.0, + 390.0, + 1076.0, + 36.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer": { "rect": [ - 64.0, - 384.0, - 1088.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, - "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/Panel": { - "rect": [ - 1068.0, - 384.0, - 80.0, - 0.0 - ], - "visible": true - }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListLabel": { "rect": [ - 64.0, - 384.0, - 1000.0, + 78.0, + 394.0, + 1060.0, 28.0 ], "visible": true }, "Quiz/MusicListPanel/ScrollContainer/MusicList/SongRow/HBoxContainer/SongListPlayers": { "rect": [ - 1152.0, - 384.0, + 78.0, + 394.0, 0.0, 0.0 ], - "visible": true + "visible": false }, "Quiz/MusicPlayer": { "rect": [ diff --git a/tests/integration/test_song_row.gd b/tests/integration/test_song_row.gd new file mode 100644 index 0000000..cbe853e --- /dev/null +++ b/tests/integration/test_song_row.gd @@ -0,0 +1,365 @@ +extends GdUnitTestSuite +## How a song in the music list is laid out: the players' pictures on the right in even +## lines of at most four, each ending at the row's right edge, the text in the room they leave, wrapping only when it does +## not fit, and the row as tall as the taller of the two. Checked for every number of +## players, with short, debug mode and very long song texts. + +const SHORT: Array[String] = ["Chrono Trigger", "Corridors of Time"] +const LONG: Array[String] = [ + "Final Fantasy Dissidia 012 Duodecim (Sista Fantasin Meningsskiljaktigheter 012 Tolv)", + "'Crystal Cave -arrange-' from FINAL FANTASY III", +] +## The row's padding, from the SongRow styles in the theme. +const PADDING: float = 8.0 + +var _snapshot: AutoloadSnapshot +var _runner: GdUnitSceneRunner +var _panel: MusicListPanel +# The panel's width in the quiz, which songs must never make wider +var _panel_width: float + + +func before_test() -> void: + _snapshot = AutoloadSnapshot.take([Settings, Playlist]) + Settings.is_debug = false + Playlist.reset_playlist() + Settings.player_array.clear() + for i: int in Settings.MAX_NUMBER_OF_PLAYERS: + Settings.player_array.append(PlayerObject.new("Player %d" % (i + 1))) + _runner = scene_runner("res://quiz/MusicListPanel.tscn") + _panel = _runner.scene() + _panel_width = _panel.size.x + + +func after_test() -> void: + await await_idle_frame() + _snapshot.restore() + + +# Adds a played and answered song with points for the first [param players] players +func _add_song(title: Array[String], players: int) -> SongObject: + var song: SongObject = SongObject.new() + song.song_number = Playlist.song_object_array.size() + song.soundtrack_title = title[0] + song.song_title = title[1] + song.has_played = true + song.is_answered = true + for i: int in players: + song.add_point(Settings.player_array[i].id) + Playlist.add_to_playlist(song) + return song + + +func _show() -> void: + _panel.refresh() + await _runner.simulate_frames(3) + + +func _rows() -> Array[SongRow]: + var rows: Array[SongRow] = [] + rows.assign(_panel.get_node("%MusicList").get_children()) + return rows + + +func _pictures(row: SongRow) -> Array[TextureRect]: + return row.pictures() + + +# How wide the row's text would be on one line +func _text_width(row: SongRow) -> float: + var label: Label = row.song_list_label + return label.get_theme_font(&"font").get_string_size(label.text, HORIZONTAL_ALIGNMENT_LEFT, + -1, label.get_theme_font_size(&"font_size")).x + + +func _one_line_height() -> float: + _add_song(SHORT, 0) + await _show() + var height: float = _rows()[0].size.y + Playlist.reset_playlist() + return height + + +# Every number of players with every kind of text, in normal and debug mode +func _each_layout(check: Callable) -> void: + for debug: bool in [false, true]: + Settings.is_debug = debug + for title: Array[String] in [SHORT, LONG]: + Playlist.reset_playlist() + for players: int in Settings.MAX_NUMBER_OF_PLAYERS + 1: + _add_song(title, players) + await _show() + assert_float(_panel.size.x).override_failure_message( + "The songs made the list wider than its place in the quiz") \ + .is_equal(_panel_width) + for row: SongRow in _rows(): + check.call(row, "%d players, %s text%s" % [_pictures(row).size(), + "long" if title == LONG else "short", " (debug)" if debug else ""]) + + +# Columns + +func test_pictures_are_spread_evenly_over_as_few_lines_as_possible() -> void: + var columns: Array[int] = [] + for pictures: int in range(0, 9): + columns.append(SongRow.picture_columns(pictures)) + + # 5 pictures are 3 + 2, not 4 + 1; 7 are 4 + 3 + assert_array(columns).contains_exactly([1, 1, 2, 3, 4, 3, 3, 4, 4]) + + +# Placement + +func test_pictures_are_at_the_right_end_of_the_row() -> void: + await _each_layout(func(row: SongRow, layout: String) -> void: + if _pictures(row).is_empty(): + assert_bool(row.song_list_players.visible).override_failure_message(layout).is_false() + return + var pictures: Rect2 = row.song_list_players.get_global_rect() + assert_float(pictures.end.x).override_failure_message(layout) \ + .is_equal_approx(row.get_global_rect().end.x - PADDING, 1.0)) + + +func test_every_line_of_pictures_ends_at_the_right_edge() -> void: + await _each_layout(func(row: SongRow, layout: String) -> void: + var right_edge: float = row.get_global_rect().end.x - PADDING + for line: HBoxContainer in row.song_list_players.get_children(): + var last: TextureRect = line.get_child(line.get_child_count() - 1) + assert_float(last.get_global_rect().end.x).override_failure_message(layout) \ + .is_equal_approx(right_edge, 1.0)) + + +func test_text_and_pictures_never_overlap_and_stay_inside_the_row() -> void: + await _each_layout(func(row: SongRow, layout: String) -> void: + var row_rect: Rect2 = row.get_global_rect().grow(0.5) + var text: Rect2 = row.song_list_label.get_global_rect() + assert_bool(row_rect.encloses(text)).override_failure_message( + "Text outside the row: " + layout).is_true() + for picture: TextureRect in _pictures(row): + var rect: Rect2 = picture.get_global_rect() + assert_bool(row_rect.encloses(rect)).override_failure_message( + "Picture outside the row: " + layout).is_true() + assert_float(rect.position.x).override_failure_message( + "Picture over the text: " + layout).is_greater_equal(text.end.x)) + + +func test_text_gets_all_the_room_the_pictures_leave() -> void: + await _each_layout(func(row: SongRow, layout: String) -> void: + var text: Rect2 = row.song_list_label.get_global_rect() + assert_float(text.position.x).override_failure_message(layout) \ + .is_equal_approx(row.get_global_rect().position.x + PADDING, 1.0) + var right_end: float = row.get_global_rect().end.x - PADDING + if row.song_list_players.visible: + right_end = row.song_list_players.get_global_rect().position.x \ + - row.get_node("HBoxContainer").get_theme_constant(&"separation") + assert_float(text.end.x).override_failure_message(layout) \ + .is_equal_approx(right_end, 1.0)) + + +func test_text_wraps_only_when_it_does_not_fit_on_one_line() -> void: + await _each_layout(func(row: SongRow, layout: String) -> void: + var lines: int = row.song_list_label.get_line_count() + var room: float = row.song_list_label.size.x + if _text_width(row) <= room - 1: + assert_int(lines).override_failure_message("Wrapped though it fits: " + layout) \ + .is_equal(1) + elif _text_width(row) > room + 1: + assert_int(lines).override_failure_message("Not wrapped: " + layout) \ + .is_greater(1)) + + +func test_every_layout_holds_in_a_narrower_list() -> void: + _panel.size.x = 640 + _panel_width = 640 + await _runner.simulate_frames(1) + + await _each_layout(func(row: SongRow, layout: String) -> void: + var text: Rect2 = row.song_list_label.get_global_rect() + assert_bool(row.get_global_rect().grow(0.5).encloses(text)).override_failure_message( + "Text outside the row: " + layout).is_true() + if row.song_list_players.visible: + assert_float(row.song_list_players.get_global_rect().position.x) \ + .override_failure_message("Pictures over the text: " + layout) \ + .is_greater_equal(text.end.x)) + + +func test_short_text_is_one_line_even_with_every_player() -> void: + _add_song(SHORT, Settings.MAX_NUMBER_OF_PLAYERS) + await _show() + + assert_int(_rows()[0].song_list_label.get_line_count()).is_equal(1) + + +func test_long_text_wraps_and_keeps_every_word() -> void: + _add_song(LONG, 2) + await _show() + var label: Label = _rows()[0].song_list_label + + assert_int(label.get_line_count()).is_greater(1) + assert_str(label.text).ends_with("FINAL FANTASY III") + assert_int(label.get_visible_line_count()).is_equal(label.get_line_count()) + + +# Height + +func test_row_is_one_line_high_with_up_to_four_pictures() -> void: + var one_line: float = await _one_line_height() + for players: int in range(0, SongRow.MAX_PICTURES_PER_LINE + 1): + _add_song(SHORT, players) + await _show() + + for row: SongRow in _rows(): + assert_float(row.size.y).override_failure_message( + "%d pictures" % _pictures(row).size()).is_equal_approx(one_line, 0.5) + + +func test_five_pictures_go_on_two_lines_of_three_and_two() -> void: + _add_song(SHORT, 5) + await _show() + + assert_array(_lines_of_pictures(_rows()[0])).contains_exactly([3, 2]) + + +func test_seven_pictures_go_on_two_lines_of_four_and_three() -> void: + _add_song(SHORT, 7) + await _show() + + assert_array(_lines_of_pictures(_rows()[0])).contains_exactly([4, 3]) + + +# How many pictures are on each line, top to bottom +func _lines_of_pictures(row: SongRow) -> Array[int]: + var by_top: Dictionary[float, int] = {} + for picture: TextureRect in _pictures(row): + var top: float = roundf(picture.global_position.y) + by_top[top] = by_top.get(top, 0) + 1 + var tops: Array[float] = [] + tops.assign(by_top.keys()) + tops.sort() + var lines: Array[int] = [] + for top: float in tops: + lines.append(by_top[top]) + return lines + + +func test_row_grows_to_fit_two_lines_of_pictures_and_centres_the_text() -> void: + var one_line: float = await _one_line_height() + _add_song(SHORT, 5) + await _show() + var row: SongRow = _rows()[0] + var pictures: Rect2 = row.song_list_players.get_global_rect() + + assert_float(row.size.y).is_greater(one_line) + assert_float(pictures.size.y).is_equal_approx( + SongRow.PICTURE_SIZE.y * 2 + row.song_list_players.get_theme_constant( + &"separation"), 0.5) + assert_bool(row.get_global_rect().encloses(pictures)).is_true() + # The single line of text is in the middle, beside the pictures + assert_int(row.song_list_label.vertical_alignment).is_equal(VERTICAL_ALIGNMENT_CENTER) + assert_float(row.song_list_label.get_global_rect().get_center().y) \ + .is_equal_approx(pictures.get_center().y, 1.0) + + +func test_pictures_are_centred_beside_text_on_many_lines() -> void: + _add_song(LONG, 1) + await _show() + var row: SongRow = _rows()[0] + var text: Rect2 = row.song_list_label.get_global_rect() + var pictures: Rect2 = row.song_list_players.get_global_rect() + + assert_int(row.song_list_label.get_line_count()).is_greater(1) + assert_float(pictures.size.y).is_less(text.size.y) + assert_float(pictures.get_center().y).is_equal_approx(text.get_center().y, 1.0) + + +func test_every_picture_has_the_same_size() -> void: + await _each_layout(func(row: SongRow, layout: String) -> void: + for picture: TextureRect in _pictures(row): + assert_vector(picture.size).override_failure_message(layout) \ + .is_equal(SongRow.PICTURE_SIZE)) + + +func test_rows_never_need_sideways_scrolling() -> void: + Settings.is_debug = true + for players: int in Settings.MAX_NUMBER_OF_PLAYERS + 1: + _add_song(LONG, players) + await _show() + var scroll: ScrollContainer = _panel.get_node("ScrollContainer") + + assert_bool(scroll.get_h_scroll_bar().visible).is_false() + assert_float(_panel.size.x).is_equal(_panel_width) + for row: SongRow in _rows(): + assert_float(row.get_global_rect().end.x) \ + .is_less_equal(scroll.get_global_rect().end.x + 0.5) + + +# Pictures + +func test_pictures_show_who_got_a_point_in_player_list_order() -> void: + var song: SongObject = _add_song(SHORT, 0) + # Points given in another order than the player list + song.add_point(Settings.player_array[2].id) + song.add_point(Settings.player_array[0].id) + await _show() + + var names: Array[String] = [] + for picture: TextureRect in _pictures(_rows()[0]): + names.append(picture.tooltip_text) + assert_array(names).contains_exactly(["Player 1", "Player 3"]) + assert_object(_pictures(_rows()[0])[0].texture).is_same(Settings.player_array[0].character) + + +func test_pictures_are_redone_when_points_change() -> void: + var song: SongObject = _add_song(SHORT, 4) + await _show() + song.add_point(Settings.player_array[4].id) + + _rows()[0].update_players() + await _runner.simulate_frames(2) + + assert_array(_lines_of_pictures(_rows()[0])).contains_exactly([3, 2]) + + +func test_clicking_a_picture_clicks_the_song() -> void: + _add_song(SHORT, 0) + _add_song(SHORT, 3) + await _show() + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel)) + var picture: TextureRect = _pictures(_rows()[1])[1] + + _runner.set_mouse_position( + _panel.get_viewport().get_final_transform() * picture.get_global_rect().get_center()) + await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await _runner.await_input_processed() + + await signals.is_emitted("song_clicked", [1]) + + +# Styles + +func test_rows_alternate_and_the_playing_song_stands_out() -> void: + for i: int in 4: + _add_song(SHORT, 0) + Playlist.song_object_array[2].is_playing = true + await _show() + + var styles: Array[StringName] = [] + for row: SongRow in _rows(): + styles.append(row.theme_type_variation) + assert_array(styles).contains_exactly( + [&"SongRow", &"SongRowOdd", &"SongRowPlaying", &"SongRowOdd"]) + assert_str(_rows()[2].song_list_label.theme_type_variation).is_equal("SongRowLabelPlaying") + assert_str(_rows()[1].song_list_label.theme_type_variation).is_equal("SongRowLabel") + + +func test_hovering_a_row_highlights_it_until_the_mouse_leaves() -> void: + _add_song(SHORT, 0) + _add_song(SHORT, 0) + await _show() + var row: SongRow = _rows()[1] + + row.mouse_entered.emit() + assert_str(row.theme_type_variation).is_equal("SongRowHover") + + row.mouse_exited.emit() + assert_str(row.theme_type_variation).is_equal("SongRowOdd") diff --git a/tests/integration/test_song_row.gd.uid b/tests/integration/test_song_row.gd.uid new file mode 100644 index 0000000..b954eff --- /dev/null +++ b/tests/integration/test_song_row.gd.uid @@ -0,0 +1 @@ +uid://dnggvrk5thb2v diff --git a/tests/integration/test_theme.gd b/tests/integration/test_theme.gd index 7151e4f..ba720e6 100644 --- a/tests/integration/test_theme.gd +++ b/tests/integration/test_theme.gd @@ -32,6 +32,11 @@ const STYLES: Dictionary = { "HighlightMedium": "Label", "SongRowLabel": "Label", "SongRowLabelPlaying": "Label", + "SongRowOdd": "PanelContainer", + "SongRowHover": "PanelContainer", + "SongRowPlaying": "PanelContainer", + "SongRowPictureLine": "HBoxContainer", + "SongRowPictureLines": "VBoxContainer", "StatisticsCard": "PanelContainer", "StatisticsCardHover": "PanelContainer", "StatisticsGrid": "GridContainer", diff --git a/themes/README.md b/themes/README.md index 05b2195..2f849ce 100644 --- a/themes/README.md +++ b/themes/README.md @@ -35,7 +35,9 @@ inspector or `theme_type_variation` in code: | `DarkSpacer` | Panel | The dark gaps in the player list and the add player row | | `CharacterCard` | Panel | A character in the character select popup | | `MusicList` | PanelContainer | The list of songs in the quiz | -| `SongRow` | PanelContainer | One song in that list | +| `SongRow`, `SongRowOdd` | PanelContainer | Alternating songs in that list | +| `SongRowHover`, `SongRowPlaying` | PanelContainer | The song under the mouse, and the one that is playing | +| `SongRowPictureLines`, `SongRowPictureLine` | VBoxContainer, HBoxContainer | The space between the lines of player pictures on a song, and between the pictures | | `ShortcutRowDark` | PanelContainer | Every other row in the shortcuts popup | | `ListRowEven`, `ListRowOdd` | PanelContainer | Alternating rows in the inspiration, sync and statistics song lists | | `ListRowHover` | PanelContainer | A row in the statistics song list under the mouse | diff --git a/themes/main_theme.tres b/themes/main_theme.tres index 6f4f409..05d701c 100644 --- a/themes/main_theme.tres +++ b/themes/main_theme.tres @@ -311,7 +311,15 @@ corner_radius_bottom_right = 4 corner_radius_bottom_left = 4 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_33f1l"] +content_margin_left = 6.0 +content_margin_top = 6.0 +content_margin_right = 6.0 +content_margin_bottom = 6.0 bg_color = Color(0.257813, 0.257813, 0.257813, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wbnll"] content_margin_left = 8.0 @@ -581,13 +589,50 @@ corner_radius_bottom_left = 3 corner_detail = 5 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oxgwg"] -bg_color = Color(0.180392, 0.180392, 0.180392, 1) +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(1, 1, 1, 0.05) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_r1nnu"] -bg_color = Color(0.180392, 0.180392, 0.180392, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_songrowodd"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(1, 1, 1, 0.11) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lkqs8"] -bg_color = Color(0.180392, 0.180392, 0.180392, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_songrowhover"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(1, 1, 1, 0.2) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_songrowplaying"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(1, 0.5, 0, 0.12) +border_width_left = 4 +border_color = Color(1, 0.5, 0, 1) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1tv07"] content_margin_left = 4.0 @@ -955,13 +1000,21 @@ ShortcutRowDark/base_type = &"PanelContainer" ShortcutRowDark/styles/panel = SubResource("StyleBoxFlat_anjdx") SongRow/base_type = &"PanelContainer" SongRow/styles/panel = SubResource("StyleBoxFlat_oxgwg") +SongRowHover/base_type = &"PanelContainer" +SongRowHover/styles/panel = SubResource("StyleBoxFlat_songrowhover") SongRowLabel/base_type = &"Label" SongRowLabel/font_sizes/font_size = 20 -SongRowLabel/styles/normal = SubResource("StyleBoxFlat_r1nnu") SongRowLabelPlaying/base_type = &"Label" SongRowLabelPlaying/colors/font_color = Color(1, 0.5, 0, 1) SongRowLabelPlaying/font_sizes/font_size = 20 -SongRowLabelPlaying/styles/normal = SubResource("StyleBoxFlat_lkqs8") +SongRowOdd/base_type = &"PanelContainer" +SongRowOdd/styles/panel = SubResource("StyleBoxFlat_songrowodd") +SongRowPictureLine/base_type = &"HBoxContainer" +SongRowPictureLine/constants/separation = 4 +SongRowPictureLines/base_type = &"VBoxContainer" +SongRowPictureLines/constants/separation = 4 +SongRowPlaying/base_type = &"PanelContainer" +SongRowPlaying/styles/panel = SubResource("StyleBoxFlat_songrowplaying") SongStatsHeader/base_type = &"PanelContainer" SongStatsHeader/styles/panel = SubResource("StyleBoxEmpty_songstatsheader") SongStatsSecondary/base_type = &"RichTextLabel"