diff --git a/common/button_bounce.gd b/common/button_bounce.gd
new file mode 100644
index 0000000..f1d48c2
--- /dev/null
+++ b/common/button_bounce.gd
@@ -0,0 +1,69 @@
+class_name ButtonBounce
+extends RefCounted
+## Small animations that make buttons feel alive: a button grows a little under the mouse,
+## shrinks while it is held down, and can pop when its icon changes.
+##
+## The button is scaled around its middle, so its place in a container does not change.
+
+## How big a button is under the mouse.
+const HOVER_SCALE: Vector2 = Vector2(1.08, 1.08)
+## How big a button is while it is held down.
+const PRESSED_SCALE: Vector2 = Vector2(0.92, 0.92)
+## How small a button starts when it pops.
+const POP_SCALE: Vector2 = Vector2(0.8, 0.8)
+## How long growing or shrinking takes, in seconds.
+const DURATION: float = 0.08
+## How long a pop takes, in seconds.
+const POP_DURATION: float = 0.18
+
+const _TWEEN_META: StringName = &"button_bounce_tween"
+const _PIVOT_META: StringName = &"button_bounce_pivot"
+
+
+## Makes [param button] grow under the mouse and shrink while held down.
+static func add_to(button: BaseButton) -> void:
+ _keep_pivot_centred(button)
+ button.mouse_entered.connect(func() -> void:
+ if !button.disabled:
+ _scale_to(button, HOVER_SCALE))
+ button.mouse_exited.connect(func() -> void: _scale_to(button, Vector2.ONE))
+ button.button_down.connect(func() -> void: _scale_to(button, PRESSED_SCALE))
+ button.button_up.connect(func() -> void: _scale_to(button, resting_scale(button)))
+
+
+## The size [param button] goes back to when it is not held down: bigger under the mouse.
+static func resting_scale(button: BaseButton) -> Vector2:
+ return HOVER_SCALE if button.is_hovered() && !button.disabled else Vector2.ONE
+
+
+## Shrinks [param button] and lets it spring back, e.g. when its icon changes.
+static func pop(button: BaseButton) -> void:
+ _keep_pivot_centred(button)
+ button.scale = POP_SCALE
+ var tween: Tween = _new_tween(button)
+ tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
+ tween.tween_property(button, ^"scale", resting_scale(button), POP_DURATION)
+
+
+static func _scale_to(button: BaseButton, target: Vector2) -> void:
+ var tween: Tween = _new_tween(button)
+ tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
+ tween.tween_property(button, ^"scale", target, DURATION)
+
+
+# A new tween for [param button], stopping the one before so they never fight
+static func _new_tween(button: BaseButton) -> Tween:
+ if button.has_meta(_TWEEN_META):
+ var running: Tween = button.get_meta(_TWEEN_META)
+ if running.is_valid():
+ running.kill()
+ var tween: Tween = button.create_tween()
+ button.set_meta(_TWEEN_META, tween)
+ return tween
+
+
+static func _keep_pivot_centred(button: BaseButton) -> void:
+ button.pivot_offset = button.size / 2
+ if !button.has_meta(_PIVOT_META):
+ button.set_meta(_PIVOT_META, true)
+ button.resized.connect(func() -> void: button.pivot_offset = button.size / 2)
diff --git a/common/button_bounce.gd.uid b/common/button_bounce.gd.uid
new file mode 100644
index 0000000..8c5d343
--- /dev/null
+++ b/common/button_bounce.gd.uid
@@ -0,0 +1 @@
+uid://c4vekt3m8q1p7
diff --git a/icons/pause_button_icon.svg b/icons/pause_button_icon.svg
new file mode 100644
index 0000000..d3e8d87
--- /dev/null
+++ b/icons/pause_button_icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/pause_button_icon.svg.import b/icons/pause_button_icon.svg.import
new file mode 100644
index 0000000..1a23ea9
--- /dev/null
+++ b/icons/pause_button_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://djdsgbhw8gpj"
+path="res://.godot/imported/pause_button_icon.svg-cf86876de39506a769c5610b329fdb75.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/pause_button_icon.svg"
+dest_files=["res://.godot/imported/pause_button_icon.svg-cf86876de39506a769c5610b329fdb75.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/play_button_icon.svg b/icons/play_button_icon.svg
new file mode 100644
index 0000000..d556145
--- /dev/null
+++ b/icons/play_button_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/play_button_icon.svg.import b/icons/play_button_icon.svg.import
new file mode 100644
index 0000000..a2506c7
--- /dev/null
+++ b/icons/play_button_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dkweun1f426gl"
+path="res://.godot/imported/play_button_icon.svg-76b19c7150b176f215ad401b4dd9488d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/play_button_icon.svg"
+dest_files=["res://.godot/imported/play_button_icon.svg-76b19c7150b176f215ad401b4dd9488d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/restart_light_icon.svg b/icons/restart_light_icon.svg
new file mode 100644
index 0000000..63a2e63
--- /dev/null
+++ b/icons/restart_light_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/restart_light_icon.svg.import b/icons/restart_light_icon.svg.import
new file mode 100644
index 0000000..9da2d5e
--- /dev/null
+++ b/icons/restart_light_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://4t7vk634k0v4"
+path="res://.godot/imported/restart_light_icon.svg-3822a30546bff23ba1adb74c1723ba80.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/restart_light_icon.svg"
+dest_files=["res://.godot/imported/restart_light_icon.svg-3822a30546bff23ba1adb74c1723ba80.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/volume_light_icon.svg b/icons/volume_light_icon.svg
new file mode 100644
index 0000000..104ea8c
--- /dev/null
+++ b/icons/volume_light_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/volume_light_icon.svg.import b/icons/volume_light_icon.svg.import
new file mode 100644
index 0000000..8e97865
--- /dev/null
+++ b/icons/volume_light_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d0uyxivsv58uh"
+path="res://.godot/imported/volume_light_icon.svg-a1b08f489924a01123c9c4f7fa5e16a0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/volume_light_icon.svg"
+dest_files=["res://.godot/imported/volume_light_icon.svg-a1b08f489924a01123c9c4f7fa5e16a0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/player/MusicPlayer.tscn b/player/MusicPlayer.tscn
index 10a1357..908de27 100644
--- a/player/MusicPlayer.tscn
+++ b/player/MusicPlayer.tscn
@@ -1,9 +1,10 @@
-[gd_scene load_steps=10 format=3 uid="uid://ds15cgsf8vpvc"]
+[gd_scene format=3 uid="uid://ds15cgsf8vpvc"]
[ext_resource type="Script" uid="uid://d1v0rquma6nt8" path="res://player/music_player.gd" id="1_t24ra"]
[ext_resource type="AudioStream" uid="uid://n2g8jddr85h2" path="res://sounds/01. Opening.mp3" id="2_xti80"]
-[ext_resource type="Texture2D" uid="uid://comxqfiykp54f" path="res://icons/play_icon_light.svg" id="3_6g308"]
-[ext_resource type="Texture2D" uid="uid://ccb6rvbldlgdg" path="res://icons/reload_light_icon.svg" id="4_jleuo"]
+[ext_resource type="Texture2D" path="res://icons/play_button_icon.svg" id="3_6g308"]
+[ext_resource type="Texture2D" path="res://icons/restart_light_icon.svg" id="4_jleuo"]
+[ext_resource type="Texture2D" path="res://icons/volume_light_icon.svg" id="6_volume"]
[ext_resource type="PackedScene" uid="uid://w400rnew7453" path="res://player/VolumeSlider.tscn" id="5_iifuj"]
[sub_resource type="InputEventKey" id="InputEventKey_r4qo3"]
@@ -29,7 +30,7 @@ events = [SubResource("InputEventKey_2nvce")]
[node name="MusicPlayer" type="Control"]
layout_mode = 3
offset_right = 1792.0
-offset_bottom = 71.0
+offset_bottom = 42.0
mouse_filter = 2
script = ExtResource("1_t24ra")
@@ -46,52 +47,81 @@ stream = ExtResource("2_xti80")
bus = &"music"
[node name="PlayerBar" type="PanelContainer" parent="."]
-layout_mode = 0
-offset_left = 1.0
-offset_right = 1792.0
-offset_bottom = 32.0
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+theme_type_variation = &"PlayerBar"
[node name="MusicPlayerContainer" type="HBoxContainer" parent="PlayerBar"]
layout_mode = 2
-size_flags_vertical = 4
+mouse_filter = 2
+theme_override_constants/separation = 10
-[node name="PlayTextureButton" type="TextureButton" parent="PlayerBar/MusicPlayerContainer"]
+[node name="PlayButton" type="Button" parent="PlayerBar/MusicPlayerContainer"]
unique_name_in_owner = true
-custom_minimum_size = Vector2(32, 32)
+custom_minimum_size = Vector2(34, 34)
layout_mode = 2
+size_flags_vertical = 4
+tooltip_text = "Play or pause (Alt+X)"
focus_mode = 0
+theme_type_variation = &"PlayerButtonPrimary"
action_mode = 0
shortcut = SubResource("Shortcut_ipcfh")
-texture_normal = ExtResource("3_6g308")
-ignore_texture_size = true
-stretch_mode = 0
+shortcut_feedback = false
+shortcut_in_tooltip = false
+icon = ExtResource("3_6g308")
+icon_alignment = 1
+expand_icon = true
-[node name="RestartTextureButton" type="TextureButton" parent="PlayerBar/MusicPlayerContainer"]
+[node name="RestartButton" type="Button" parent="PlayerBar/MusicPlayerContainer"]
unique_name_in_owner = true
-custom_minimum_size = Vector2(32, 32)
+custom_minimum_size = Vector2(34, 34)
layout_mode = 2
+size_flags_vertical = 4
+tooltip_text = "Restart the song (Alt+Z)"
focus_mode = 0
+theme_type_variation = &"PlayerButton"
action_mode = 0
shortcut = SubResource("Shortcut_i7swq")
-texture_normal = ExtResource("4_jleuo")
-ignore_texture_size = true
-stretch_mode = 0
-flip_h = true
+shortcut_feedback = false
+shortcut_in_tooltip = false
+icon = ExtResource("4_jleuo")
+icon_alignment = 1
+expand_icon = true
[node name="MusicPlayerSlider" type="HSlider" parent="PlayerBar/MusicPlayerContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
+theme_type_variation = &"ProgressSlider"
scrollable = false
[node name="MusicTimeLabel" type="Label" parent="PlayerBar/MusicPlayerContainer"]
unique_name_in_owner = true
+custom_minimum_size = Vector2(120, 0)
layout_mode = 2
-text = "0:00 / 0:00"
+theme_type_variation = &"PlayerTime"
+text = "00:00 / 00:00"
+horizontal_alignment = 1
+
+[node name="VolumeIcon" type="TextureRect" parent="PlayerBar/MusicPlayerContainer"]
+custom_minimum_size = Vector2(22, 22)
+layout_mode = 2
+size_flags_vertical = 4
+mouse_filter = 2
+texture = ExtResource("6_volume")
+expand_mode = 1
+stretch_mode = 5
[node name="VolumeSlider" parent="PlayerBar/MusicPlayerContainer" instance=ExtResource("5_iifuj")]
unique_name_in_owner = true
-custom_minimum_size = Vector2(100, 0)
+custom_minimum_size = Vector2(110, 0)
layout_mode = 2
size_flags_vertical = 4
+tooltip_text = "Volume"
+theme_type_variation = &"VolumeBar"
diff --git a/player/music_player.gd b/player/music_player.gd
index 6fffd72..f619e32 100644
--- a/player/music_player.gd
+++ b/player/music_player.gd
@@ -2,19 +2,22 @@ class_name MusicPlayer
extends Control
## The bar that plays the songs, with play, restart, progress and volume. Whether a song
## repeats is a quiz setting ([code]Settings.auto_repeat_song[/code]).
+##
+## The buttons grow a little under the mouse and the play button pops when it switches
+## between play and pause, see [ButtonBounce].
signal update_song_list
signal play_next_song
-var play_icon: Texture = preload("res://icons/play_icon_light.svg")
-var pause_icon: Texture = preload("res://icons/pause_icon_light.svg")
+var play_icon: Texture = preload("res://icons/play_button_icon.svg")
+var pause_icon: Texture = preload("res://icons/pause_button_icon.svg")
var is_changing: bool = false
var playback_position: float
var stream: AudioStream
var song_finished: bool = false
-@onready var play_button: TextureButton = %PlayTextureButton
-@onready var restart_button: TextureButton = %RestartTextureButton
+@onready var play_button: Button = %PlayButton
+@onready var restart_button: Button = %RestartButton
@onready var audio_player: AudioStreamPlayer = $AudioStreamPlayer
@onready var sound_player: AudioStreamPlayer = $AudioSoundPlayer
@onready var sound_effect_player: AudioStreamPlayer = $AudioSoundEffectPlayer
@@ -26,6 +29,8 @@ var song_finished: bool = false
func _ready() -> void:
play_button.pressed.connect(play_or_pause)
restart_button.pressed.connect(restart)
+ ButtonBounce.add_to(play_button)
+ ButtonBounce.add_to(restart_button)
progress_slider.drag_started.connect(_on_drag_started)
progress_slider.drag_ended.connect(_on_drag_ended)
@@ -43,6 +48,19 @@ func _process(_delta: float) -> void:
music_time_label.text = format_text(progress_slider.value, 0.0)
+## Whether the play button shows pause, because a song is playing.
+func shows_playing() -> bool:
+ return play_button.icon == pause_icon
+
+
+# Shows pause on the play button while a song plays, and play otherwise
+func _show_playing(playing: bool) -> void:
+ var icon: Texture = pause_icon if playing else play_icon
+ if play_button.icon != icon:
+ play_button.icon = icon
+ ButtonBounce.pop(play_button)
+
+
func format_time(time: float) -> String:
var total_seconds: int = roundi(time)
@warning_ignore("integer_division")
@@ -64,7 +82,7 @@ func play_or_pause() -> void:
if song_finished:
restart()
elif audio_player.stream_paused:
- play_button.texture_normal = pause_icon
+ _show_playing(true)
audio_player.stream_paused = false
song_finished = false
audio_player.seek(playback_position)
@@ -81,7 +99,7 @@ func play_or_pause() -> void:
func pause() -> void:
audio_player.stream_paused = true
playback_position = audio_player.get_playback_position()
- play_button.texture_normal = play_icon
+ _show_playing(false)
func restart() -> void:
@@ -91,12 +109,12 @@ func restart() -> void:
progress_slider.value = 0
playback_position = audio_player.get_playback_position()
audio_player.seek(playback_position)
- play_button.texture_normal = pause_icon
+ _show_playing(true)
audio_player.play()
func _on_finished() -> void:
- play_button.texture_normal = play_icon
+ _show_playing(false)
song_finished = true
if !Settings.stop_after_current:
play_next_song.emit()
@@ -150,7 +168,7 @@ func play_sound(sound_name: AudioStream) -> void:
audio_player.stop()
sound_player.play()
song_finished = true
- play_button.texture_normal = play_icon
+ _show_playing(false)
progress_slider.value = 0
@@ -159,7 +177,7 @@ func play_song(song: AudioStream) -> void:
audio_player.play()
sound_player.stop()
song_finished = false
- play_button.texture_normal = pause_icon
+ _show_playing(true)
stream = audio_player.stream
progress_slider.value = 0
progress_slider.max_value = round(stream.get_length())
@@ -179,7 +197,7 @@ func play_song_object(song_object_no: int) -> void:
sound_player.stop()
audio_player.play()
song_finished = false
- play_button.texture_normal = pause_icon
+ _show_playing(true)
stream = audio_player.stream
progress_slider.value = 0
progress_slider.max_value = round(stream.get_length())
diff --git a/quiz/Quiz.tscn b/quiz/Quiz.tscn
index 3d79b56..b34c7bc 100644
--- a/quiz/Quiz.tscn
+++ b/quiz/Quiz.tscn
@@ -48,9 +48,9 @@ offset_bottom = 384.0
[node name="MusicPlayer" parent="." unique_id=1990459832 instance=ExtResource("5_emn36")]
layout_mode = 0
offset_left = 64.0
-offset_top = 992.0
+offset_top = 984.0
offset_right = 1856.0
-offset_bottom = 1063.0
+offset_bottom = 1026.0
[node name="PlayersPanel" parent="." unique_id=1708375945 instance=ExtResource("scene_PlayersPanel")]
layout_mode = 0
diff --git a/tests/data/layout_baseline.json b/tests/data/layout_baseline.json
index 7d5f613..7d88e0d 100644
--- a/tests/data/layout_baseline.json
+++ b/tests/data/layout_baseline.json
@@ -1056,71 +1056,80 @@
"Quiz/MusicPlayer": {
"rect": [
64.0,
- 992.0,
+ 984.0,
1792.0,
- 71.0
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 64.0,
+ 984.0,
+ 1792.0,
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 70.0,
+ 988.0,
+ 1772.0,
+ 34.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicPlayerSlider": {
"rect": [
- 137.0,
- 1000.0,
- 1532.0,
+ 158.0,
+ 997.0,
+ 1402.0,
16.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicTimeLabel": {
"rect": [
- 1673.0,
- 996.0,
- 79.0,
- 23.0
+ 1570.0,
+ 992.0,
+ 120.0,
+ 26.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayButton": {
"rect": [
- 65.0,
- 992.0,
- 32.0,
- 32.0
+ 70.0,
+ 988.0,
+ 34.0,
+ 34.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartButton": {
"rect": [
- 101.0,
- 992.0,
- 32.0,
- 32.0
+ 114.0,
+ 988.0,
+ 34.0,
+ 34.0
+ ],
+ "visible": true
+ },
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeIcon": {
+ "rect": [
+ 1700.0,
+ 994.0,
+ 22.0,
+ 22.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeSlider": {
"rect": [
- 1756.0,
- 1000.0,
- 100.0,
+ 1732.0,
+ 997.0,
+ 110.0,
16.0
],
"visible": true
@@ -4685,71 +4694,80 @@
"Quiz/MusicPlayer": {
"rect": [
64.0,
- 992.0,
+ 984.0,
1792.0,
- 71.0
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 64.0,
+ 984.0,
+ 1792.0,
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 70.0,
+ 988.0,
+ 1772.0,
+ 34.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicPlayerSlider": {
"rect": [
- 137.0,
- 1000.0,
- 1514.0,
+ 158.0,
+ 997.0,
+ 1402.0,
16.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicTimeLabel": {
"rect": [
- 1655.0,
- 996.0,
- 97.0,
- 23.0
+ 1570.0,
+ 992.0,
+ 120.0,
+ 26.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayButton": {
"rect": [
- 65.0,
- 992.0,
- 32.0,
- 32.0
+ 70.0,
+ 988.0,
+ 34.0,
+ 34.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartButton": {
"rect": [
- 101.0,
- 992.0,
- 32.0,
- 32.0
+ 114.0,
+ 988.0,
+ 34.0,
+ 34.0
+ ],
+ "visible": true
+ },
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeIcon": {
+ "rect": [
+ 1700.0,
+ 994.0,
+ 22.0,
+ 22.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeSlider": {
"rect": [
- 1756.0,
- 1000.0,
- 100.0,
+ 1732.0,
+ 997.0,
+ 110.0,
16.0
],
"visible": true
@@ -11482,71 +11500,80 @@
"Quiz/MusicPlayer": {
"rect": [
64.0,
- 992.0,
+ 984.0,
1792.0,
- 71.0
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 64.0,
+ 984.0,
+ 1792.0,
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 70.0,
+ 988.0,
+ 1772.0,
+ 34.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicPlayerSlider": {
"rect": [
- 137.0,
- 1000.0,
- 1532.0,
+ 158.0,
+ 997.0,
+ 1402.0,
16.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicTimeLabel": {
"rect": [
- 1673.0,
- 996.0,
- 79.0,
- 23.0
+ 1570.0,
+ 992.0,
+ 120.0,
+ 26.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayButton": {
"rect": [
- 65.0,
- 992.0,
- 32.0,
- 32.0
+ 70.0,
+ 988.0,
+ 34.0,
+ 34.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartButton": {
"rect": [
- 101.0,
- 992.0,
- 32.0,
- 32.0
+ 114.0,
+ 988.0,
+ 34.0,
+ 34.0
+ ],
+ "visible": true
+ },
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeIcon": {
+ "rect": [
+ 1700.0,
+ 994.0,
+ 22.0,
+ 22.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeSlider": {
"rect": [
- 1756.0,
- 1000.0,
- 100.0,
+ 1732.0,
+ 997.0,
+ 110.0,
16.0
],
"visible": true
@@ -15480,71 +15507,80 @@
"Quiz/MusicPlayer": {
"rect": [
64.0,
- 992.0,
+ 984.0,
1792.0,
- 71.0
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 64.0,
+ 984.0,
+ 1792.0,
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 70.0,
+ 988.0,
+ 1772.0,
+ 34.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicPlayerSlider": {
"rect": [
- 137.0,
- 1000.0,
- 1514.0,
+ 158.0,
+ 997.0,
+ 1402.0,
16.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicTimeLabel": {
"rect": [
- 1655.0,
- 996.0,
- 97.0,
- 23.0
+ 1570.0,
+ 992.0,
+ 120.0,
+ 26.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayButton": {
"rect": [
- 65.0,
- 992.0,
- 32.0,
- 32.0
+ 70.0,
+ 988.0,
+ 34.0,
+ 34.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartButton": {
"rect": [
- 101.0,
- 992.0,
- 32.0,
- 32.0
+ 114.0,
+ 988.0,
+ 34.0,
+ 34.0
+ ],
+ "visible": true
+ },
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeIcon": {
+ "rect": [
+ 1700.0,
+ 994.0,
+ 22.0,
+ 22.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeSlider": {
"rect": [
- 1756.0,
- 1000.0,
- 100.0,
+ 1732.0,
+ 997.0,
+ 110.0,
16.0
],
"visible": true
@@ -23540,71 +23576,80 @@
"Quiz/MusicPlayer": {
"rect": [
64.0,
- 992.0,
+ 984.0,
1792.0,
- 71.0
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 64.0,
+ 984.0,
+ 1792.0,
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 70.0,
+ 988.0,
+ 1772.0,
+ 34.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicPlayerSlider": {
"rect": [
- 137.0,
- 1000.0,
- 1532.0,
+ 158.0,
+ 997.0,
+ 1402.0,
16.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicTimeLabel": {
"rect": [
- 1673.0,
- 996.0,
- 79.0,
- 23.0
+ 1570.0,
+ 992.0,
+ 120.0,
+ 26.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayButton": {
"rect": [
- 65.0,
- 992.0,
- 32.0,
- 32.0
+ 70.0,
+ 988.0,
+ 34.0,
+ 34.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartButton": {
"rect": [
- 101.0,
- 992.0,
- 32.0,
- 32.0
+ 114.0,
+ 988.0,
+ 34.0,
+ 34.0
+ ],
+ "visible": true
+ },
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeIcon": {
+ "rect": [
+ 1700.0,
+ 994.0,
+ 22.0,
+ 22.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeSlider": {
"rect": [
- 1756.0,
- 1000.0,
- 100.0,
+ 1732.0,
+ 997.0,
+ 110.0,
16.0
],
"visible": true
@@ -36647,71 +36692,80 @@
"Quiz/MusicPlayer": {
"rect": [
64.0,
- 992.0,
+ 984.0,
1792.0,
- 71.0
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 64.0,
+ 984.0,
+ 1792.0,
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 70.0,
+ 988.0,
+ 1772.0,
+ 34.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicPlayerSlider": {
"rect": [
- 137.0,
- 1000.0,
- 1514.0,
+ 158.0,
+ 997.0,
+ 1402.0,
16.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicTimeLabel": {
"rect": [
- 1655.0,
- 996.0,
- 97.0,
- 23.0
+ 1570.0,
+ 992.0,
+ 120.0,
+ 26.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayButton": {
"rect": [
- 65.0,
- 992.0,
- 32.0,
- 32.0
+ 70.0,
+ 988.0,
+ 34.0,
+ 34.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartButton": {
"rect": [
- 101.0,
- 992.0,
- 32.0,
- 32.0
+ 114.0,
+ 988.0,
+ 34.0,
+ 34.0
+ ],
+ "visible": true
+ },
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeIcon": {
+ "rect": [
+ 1700.0,
+ 994.0,
+ 22.0,
+ 22.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeSlider": {
"rect": [
- 1756.0,
- 1000.0,
- 100.0,
+ 1732.0,
+ 997.0,
+ 110.0,
16.0
],
"visible": true
@@ -41243,71 +41297,80 @@
"Quiz/MusicPlayer": {
"rect": [
64.0,
- 992.0,
+ 984.0,
1792.0,
- 71.0
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 64.0,
+ 984.0,
+ 1792.0,
+ 42.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer": {
"rect": [
- 65.0,
- 992.0,
- 1791.0,
- 32.0
+ 70.0,
+ 988.0,
+ 1772.0,
+ 34.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicPlayerSlider": {
"rect": [
- 137.0,
- 1000.0,
- 1514.0,
+ 158.0,
+ 997.0,
+ 1402.0,
16.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/MusicTimeLabel": {
"rect": [
- 1655.0,
- 996.0,
- 97.0,
- 23.0
+ 1570.0,
+ 992.0,
+ 120.0,
+ 26.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/PlayButton": {
"rect": [
- 65.0,
- 992.0,
- 32.0,
- 32.0
+ 70.0,
+ 988.0,
+ 34.0,
+ 34.0
],
"visible": true
},
- "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartTextureButton": {
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/RestartButton": {
"rect": [
- 101.0,
- 992.0,
- 32.0,
- 32.0
+ 114.0,
+ 988.0,
+ 34.0,
+ 34.0
+ ],
+ "visible": true
+ },
+ "Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeIcon": {
+ "rect": [
+ 1700.0,
+ 994.0,
+ 22.0,
+ 22.0
],
"visible": true
},
"Quiz/MusicPlayer/PlayerBar/MusicPlayerContainer/VolumeSlider": {
"rect": [
- 1756.0,
- 1000.0,
- 100.0,
+ 1732.0,
+ 997.0,
+ 110.0,
16.0
],
"visible": true
diff --git a/tests/fakes/ui_tour.gd b/tests/fakes/ui_tour.gd
index e63c8db..93414b1 100644
--- a/tests/fakes/ui_tour.gd
+++ b/tests/fakes/ui_tour.gd
@@ -446,6 +446,11 @@ func _freeze_music_player() -> void:
music_player.sound_player.stop()
music_player.progress_slider.value = 0
music_player.music_time_label.text = "00:00 / 00:01"
+ # The buttons' animations would otherwise be caught halfway, see ButtonBounce
+ for button: Button in [music_player.play_button, music_player.restart_button]:
+ if button.has_meta(&"button_bounce_tween"):
+ (button.get_meta(&"button_bounce_tween") as Tween).kill()
+ button.scale = Vector2.ONE
## The log starts each row with the time of day; use a fixed time instead.
diff --git a/tests/integration/test_music_player_scene.gd b/tests/integration/test_music_player_scene.gd
index dc08d1a..2351ec0 100644
--- a/tests/integration/test_music_player_scene.gd
+++ b/tests/integration/test_music_player_scene.gd
@@ -26,3 +26,129 @@ func _click(control: Control) -> void:
func test_empty_space_lets_clicks_through() -> void:
assert_int(_player.mouse_filter).is_equal(Control.MOUSE_FILTER_IGNORE)
+
+
+func _song() -> AudioStreamMP3:
+ var song: AudioStreamMP3 = AudioStreamMP3.new()
+ song.data = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
+ return song
+
+
+func _buttons() -> Array[Button]:
+ return [_player.play_button, _player.restart_button]
+
+
+# Waits until the buttons' animations have finished
+func _settle() -> void:
+ await await_millis(int(maxf(ButtonBounce.DURATION, ButtonBounce.POP_DURATION) * 1000) + 80)
+
+
+# Icons
+
+func test_icons_sit_inside_their_buttons_with_room_around_them() -> void:
+ await _runner.simulate_frames(1)
+ for button: Button in _buttons():
+ var style: StyleBox = button.get_theme_stylebox(&"normal")
+ for side: Side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]:
+ assert_float(style.get_content_margin(side)).override_failure_message(button.name) \
+ .is_greater_equal(8.0)
+ assert_bool(button.expand_icon).override_failure_message(button.name).is_true()
+ assert_int(button.icon_alignment).is_equal(HORIZONTAL_ALIGNMENT_CENTER)
+ # A square button, so the round background is a circle around the icon
+ assert_float(button.size.x).override_failure_message(button.name) \
+ .is_equal(button.size.y)
+ var icon_room: Vector2 = button.size - style.get_minimum_size()
+ assert_float(icon_room.x).override_failure_message(button.name).is_greater(12.0)
+
+
+func test_buttons_are_centred_in_the_bar() -> void:
+ await _runner.simulate_frames(1)
+ var bar: Rect2 = _player.get_node("PlayerBar").get_global_rect()
+ for button: Button in _buttons():
+ var rect: Rect2 = button.get_global_rect()
+ assert_bool(bar.encloses(rect)).override_failure_message(button.name).is_true()
+ assert_float(rect.get_center().y).override_failure_message(button.name) \
+ .is_equal_approx(bar.get_center().y, 1.0)
+
+
+func test_play_button_stands_out_from_restart() -> void:
+ assert_str(_player.play_button.theme_type_variation).is_equal("PlayerButtonPrimary")
+ assert_str(_player.restart_button.theme_type_variation).is_equal("PlayerButton")
+ assert_str(_player.progress_slider.theme_type_variation).is_equal("ProgressSlider")
+ assert_str(_player.volume_slider.theme_type_variation).is_equal("VolumeBar")
+ assert_str(_player.music_time_label.theme_type_variation).is_equal("PlayerTime")
+
+
+func test_buttons_have_tooltips_with_their_keys() -> void:
+ assert_str(_player.play_button.tooltip_text).contains("Alt+X")
+ assert_str(_player.restart_button.tooltip_text).contains("Alt+Z")
+
+
+# Play and pause
+
+func test_play_button_shows_play_until_a_song_plays() -> void:
+ assert_bool(_player.shows_playing()).is_false()
+
+ _player.play_song(_song())
+
+ assert_bool(_player.shows_playing()).is_true()
+ assert_object(_player.play_button.icon).is_same(_player.pause_icon)
+
+
+func test_pausing_shows_play_again() -> void:
+ await _runner.simulate_frames(1)
+ _player.play_song(_song())
+
+ await _click(_player.play_button)
+
+ assert_bool(_player.audio_player.stream_paused).is_true()
+ assert_bool(_player.shows_playing()).is_false()
+
+
+func test_play_button_pops_when_it_changes() -> void:
+ await _runner.simulate_frames(1)
+
+ _player.play_song(_song())
+
+ assert_vector(_player.play_button.scale).is_equal(ButtonBounce.POP_SCALE)
+ await _settle()
+ assert_vector(_player.play_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
+
+
+func test_play_button_does_not_pop_when_it_stays_the_same() -> void:
+ _player.play_song(_song())
+ await _settle()
+
+ _player.play_song(_song())
+
+ assert_vector(_player.play_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
+
+
+# Hover
+
+func test_buttons_grow_under_the_mouse_and_shrink_back() -> void:
+ await _runner.simulate_frames(1)
+ for button: Button in _buttons():
+ _runner.set_mouse_position(_player.get_viewport().get_final_transform()
+ * button.get_global_rect().get_center())
+ await _runner.simulate_frames(2)
+ await _settle()
+ assert_vector(button.scale).override_failure_message(button.name) \
+ .is_equal_approx(ButtonBounce.HOVER_SCALE, Vector2(0.01, 0.01))
+
+ _runner.set_mouse_position(Vector2.ZERO)
+ await _runner.simulate_frames(2)
+ await _settle()
+ assert_vector(button.scale).override_failure_message(button.name) \
+ .is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
+
+
+func test_growing_keeps_the_button_in_place() -> void:
+ await _runner.simulate_frames(1)
+ var before: Vector2 = _player.restart_button.get_global_rect().get_center()
+
+ _player.restart_button.mouse_entered.emit()
+ await _settle()
+
+ assert_vector(_player.restart_button.get_global_rect().get_center()) \
+ .is_equal_approx(before, Vector2(0.5, 0.5))
diff --git a/tests/integration/test_scene_split.gd b/tests/integration/test_scene_split.gd
index 6a05846..7da3745 100644
--- a/tests/integration/test_scene_split.gd
+++ b/tests/integration/test_scene_split.gd
@@ -313,3 +313,15 @@ func test_alt_s_opens_search() -> void:
await _press_keys([KEY_ALT, KEY_S])
assert_bool((_window.get_node("SearchPopup") as Control).visible).is_true()
+
+
+func test_music_player_fits_between_the_song_buttons_and_quiz_settings() -> void:
+ var bar: Rect2 = _quiz.get_node("MusicPlayer/PlayerBar").get_global_rect()
+ var above: Rect2 = _quiz.get_node("SongControls/NextButton").get_global_rect()
+ var below: Rect2 = _quiz.get_node("QuizSettingsButton").get_global_rect()
+
+ assert_float(bar.position.y).is_greater_equal(above.end.y + 4)
+ assert_float(bar.end.y).is_less_equal(below.position.y - 2)
+ # As wide as the music list above it
+ var list: Rect2 = _quiz.get_node("MusicListPanel").get_global_rect()
+ assert_float(bar.position.x).is_equal(list.position.x)
diff --git a/tests/integration/test_shortcuts_popup.gd b/tests/integration/test_shortcuts_popup.gd
index 366cd7c..0db2155 100644
--- a/tests/integration/test_shortcuts_popup.gd
+++ b/tests/integration/test_shortcuts_popup.gd
@@ -58,8 +58,8 @@ func test_buttons_do_what_the_list_says() -> void:
var expected: Dictionary = {
"SongControls/NextButton": "Next song",
"SongControls/ShowAnswerButton": "Show answer",
- "MusicPlayer/PlayerBar/MusicPlayerContainer/PlayTextureButton": "Play or pause",
- "MusicPlayer/PlayerBar/MusicPlayerContainer/RestartTextureButton": "Restart song",
+ "MusicPlayer/PlayerBar/MusicPlayerContainer/PlayButton": "Play or pause",
+ "MusicPlayer/PlayerBar/MusicPlayerContainer/RestartButton": "Restart song",
}
for path: String in expected:
var button: BaseButton = _quiz.get_node(path)
diff --git a/tests/integration/test_theme.gd b/tests/integration/test_theme.gd
index ba720e6..4069b6d 100644
--- a/tests/integration/test_theme.gd
+++ b/tests/integration/test_theme.gd
@@ -37,6 +37,12 @@ const STYLES: Dictionary = {
"SongRowPlaying": "PanelContainer",
"SongRowPictureLine": "HBoxContainer",
"SongRowPictureLines": "VBoxContainer",
+ "PlayerBar": "PanelContainer",
+ "PlayerButton": "Button",
+ "PlayerButtonPrimary": "Button",
+ "PlayerTime": "Label",
+ "ProgressSlider": "HSlider",
+ "VolumeBar": "HSlider",
"StatisticsCard": "PanelContainer",
"StatisticsCardHover": "PanelContainer",
"StatisticsGrid": "GridContainer",
diff --git a/tests/unit/test_button_bounce.gd b/tests/unit/test_button_bounce.gd
new file mode 100644
index 0000000..169632d
--- /dev/null
+++ b/tests/unit/test_button_bounce.gd
@@ -0,0 +1,80 @@
+extends GdUnitTestSuite
+## ButtonBounce: buttons grow under the mouse, shrink while held down, pop when asked to,
+## and always around their middle.
+
+var _button: Button
+
+
+func before_test() -> void:
+ _button = auto_free(Button.new())
+ _button.size = Vector2(40, 40)
+ add_child(_button)
+ ButtonBounce.add_to(_button)
+
+
+func _settle() -> void:
+ await await_millis(int(maxf(ButtonBounce.DURATION, ButtonBounce.POP_DURATION) * 1000) + 80)
+
+
+func test_starts_at_its_normal_size_scaled_around_its_middle() -> void:
+ assert_vector(_button.scale).is_equal(Vector2.ONE)
+ assert_vector(_button.pivot_offset).is_equal(Vector2(20, 20))
+
+
+func test_pivot_follows_the_size() -> void:
+ _button.size = Vector2(60, 30)
+
+ assert_vector(_button.pivot_offset).is_equal(Vector2(30, 15))
+
+
+func test_grows_when_the_mouse_enters_and_shrinks_back_when_it_leaves() -> void:
+ _button.mouse_entered.emit()
+ await _settle()
+ assert_vector(_button.scale).is_equal_approx(ButtonBounce.HOVER_SCALE, Vector2(0.01, 0.01))
+
+ _button.mouse_exited.emit()
+ await _settle()
+ assert_vector(_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
+
+
+func test_shrinks_while_held_down() -> void:
+ _button.button_down.emit()
+ await _settle()
+
+ assert_vector(_button.scale).is_equal_approx(ButtonBounce.PRESSED_SCALE, Vector2(0.01, 0.01))
+
+
+func test_goes_back_to_normal_when_let_go_away_from_the_mouse() -> void:
+ _button.button_down.emit()
+ await _settle()
+
+ _button.button_up.emit()
+ await _settle()
+
+ assert_vector(_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
+
+
+func test_disabled_button_does_not_grow() -> void:
+ _button.disabled = true
+
+ _button.mouse_entered.emit()
+ await _settle()
+
+ assert_vector(_button.scale).is_equal(Vector2.ONE)
+
+
+func test_pop_starts_small_and_springs_back() -> void:
+ ButtonBounce.pop(_button)
+
+ assert_vector(_button.scale).is_equal(ButtonBounce.POP_SCALE)
+ await _settle()
+ assert_vector(_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
+
+
+func test_a_new_animation_stops_the_one_before() -> void:
+ _button.mouse_entered.emit()
+ _button.button_down.emit()
+ await _settle()
+
+ # Only the last one ran to the end
+ assert_vector(_button.scale).is_equal_approx(ButtonBounce.PRESSED_SCALE, Vector2(0.01, 0.01))
diff --git a/tests/unit/test_button_bounce.gd.uid b/tests/unit/test_button_bounce.gd.uid
new file mode 100644
index 0000000..393a019
--- /dev/null
+++ b/tests/unit/test_button_bounce.gd.uid
@@ -0,0 +1 @@
+uid://b8tyllgau53y6
diff --git a/themes/README.md b/themes/README.md
index 2f849ce..533bbda 100644
--- a/themes/README.md
+++ b/themes/README.md
@@ -51,6 +51,10 @@ inspector or `theme_type_variation` in code:
| `TabButton` | Button | The Quiz and Statistics tabs; the chosen one is orange |
| `StatisticsCard`, `StatisticsCardHover` | PanelContainer | A soundtrack card in the statistics, and the one under the mouse |
| `StatisticsGrid` | GridContainer | The grid of soundtrack cards, with the space between them |
+| `PlayerBar` | PanelContainer | The music player's rounded bar |
+| `PlayerButtonPrimary`, `PlayerButton` | Button | The round play button, and the restart button; the icon's colour on hover comes from here |
+| `ProgressSlider`, `VolumeBar` | HSlider | How far the song has played (orange), and the volume |
+| `PlayerTime` | Label | The time played and the song's length |
| `SongStatsHeader` | PanelContainer | The column titles above the statistics song list, spaced like its rows |
| `SongStatsText`, `SongStatsSecondary` | RichTextLabel | A song's name and its soundtrack (dimmer) in the statistics song list |
diff --git a/themes/main_theme.tres b/themes/main_theme.tres
index 05d701c..2cd531b 100644
--- a/themes/main_theme.tres
+++ b/themes/main_theme.tres
@@ -819,6 +819,158 @@ corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerbar"]
+content_margin_left = 6.0
+content_margin_top = 4.0
+content_margin_right = 14.0
+content_margin_bottom = 4.0
+bg_color = Color(0.16, 0.16, 0.16, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerbutton"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(1, 1, 1, 0)
+corner_radius_top_left = 17
+corner_radius_top_right = 17
+corner_radius_bottom_right = 17
+corner_radius_bottom_left = 17
+corner_detail = 12
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerbuttonhover"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(1, 1, 1, 0.12)
+corner_radius_top_left = 17
+corner_radius_top_right = 17
+corner_radius_bottom_right = 17
+corner_radius_bottom_left = 17
+corner_detail = 12
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerbuttonpressed"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(1, 1, 1, 0.2)
+corner_radius_top_left = 17
+corner_radius_top_right = 17
+corner_radius_bottom_right = 17
+corner_radius_bottom_left = 17
+corner_detail = 12
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerprimary"]
+content_margin_left = 9.0
+content_margin_top = 9.0
+content_margin_right = 9.0
+content_margin_bottom = 9.0
+bg_color = Color(1, 0.5, 0, 1)
+corner_radius_top_left = 17
+corner_radius_top_right = 17
+corner_radius_bottom_right = 17
+corner_radius_bottom_left = 17
+corner_detail = 12
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerprimaryhover"]
+content_margin_left = 9.0
+content_margin_top = 9.0
+content_margin_right = 9.0
+content_margin_bottom = 9.0
+bg_color = Color(1, 0.6, 0.18, 1)
+corner_radius_top_left = 17
+corner_radius_top_right = 17
+corner_radius_bottom_right = 17
+corner_radius_bottom_left = 17
+corner_detail = 12
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerprimarypressed"]
+content_margin_left = 9.0
+content_margin_top = 9.0
+content_margin_right = 9.0
+content_margin_bottom = 9.0
+bg_color = Color(0.82, 0.4, 0, 1)
+corner_radius_top_left = 17
+corner_radius_top_right = 17
+corner_radius_bottom_right = 17
+corner_radius_bottom_left = 17
+corner_detail = 12
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerprimarydisabled"]
+content_margin_left = 9.0
+content_margin_top = 9.0
+content_margin_right = 9.0
+content_margin_bottom = 9.0
+bg_color = Color(0.4, 0.4, 0.4, 1)
+corner_radius_top_left = 17
+corner_radius_top_right = 17
+corner_radius_bottom_right = 17
+corner_radius_bottom_left = 17
+corner_detail = 12
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playertrack"]
+content_margin_left = 0.0
+content_margin_top = 3.0
+content_margin_right = 0.0
+content_margin_bottom = 3.0
+bg_color = Color(0, 0, 0, 0.35)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progressfill"]
+content_margin_left = 0.0
+content_margin_top = 3.0
+content_margin_right = 0.0
+content_margin_bottom = 3.0
+bg_color = Color(1, 0.5, 0, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progressfillhover"]
+content_margin_left = 0.0
+content_margin_top = 3.0
+content_margin_right = 0.0
+content_margin_bottom = 3.0
+bg_color = Color(1, 0.62, 0.22, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_volumefill"]
+content_margin_left = 0.0
+content_margin_top = 3.0
+content_margin_right = 0.0
+content_margin_bottom = 3.0
+bg_color = Color(0.8, 0.8, 0.8, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_volumefillhover"]
+content_margin_left = 0.0
+content_margin_top = 3.0
+content_margin_right = 0.0
+content_margin_bottom = 3.0
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_playerfocus"]
+
[resource]
default_font = ExtResource("1_default_font")
Button/colors/font_color = Color(0.875, 0.875, 0.875, 1)
@@ -940,6 +1092,32 @@ OptionButton/styles/pressed_mirrored = SubResource("StyleBoxFlat_a0ksy")
Palette/colors/backdrop = Color(0, 0, 0, 0.55)
Panel/styles/panel = SubResource("StyleBoxFlat_hwwyi")
PanelContainer/styles/panel = SubResource("StyleBoxFlat_wl3gy")
+PlayerBar/base_type = &"PanelContainer"
+PlayerBar/styles/panel = SubResource("StyleBoxFlat_playerbar")
+PlayerButton/base_type = &"Button"
+PlayerButton/colors/icon_hover_color = Color(1, 0.5, 0, 1)
+PlayerButton/colors/icon_hover_pressed_color = Color(1, 0.5, 0, 1)
+PlayerButton/colors/icon_normal_color = Color(0.88, 0.88, 0.88, 1)
+PlayerButton/colors/icon_pressed_color = Color(1, 0.6, 0.2, 1)
+PlayerButton/styles/focus = SubResource("StyleBoxEmpty_playerfocus")
+PlayerButton/styles/hover = SubResource("StyleBoxFlat_playerbuttonhover")
+PlayerButton/styles/hover_pressed = SubResource("StyleBoxFlat_playerbuttonpressed")
+PlayerButton/styles/normal = SubResource("StyleBoxFlat_playerbutton")
+PlayerButton/styles/pressed = SubResource("StyleBoxFlat_playerbuttonpressed")
+PlayerButtonPrimary/base_type = &"Button"
+PlayerButtonPrimary/colors/icon_hover_color = Color(1, 1, 1, 1)
+PlayerButtonPrimary/colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
+PlayerButtonPrimary/colors/icon_normal_color = Color(1, 1, 1, 1)
+PlayerButtonPrimary/colors/icon_pressed_color = Color(1, 1, 1, 1)
+PlayerButtonPrimary/styles/disabled = SubResource("StyleBoxFlat_playerprimarydisabled")
+PlayerButtonPrimary/styles/focus = SubResource("StyleBoxEmpty_playerfocus")
+PlayerButtonPrimary/styles/hover = SubResource("StyleBoxFlat_playerprimaryhover")
+PlayerButtonPrimary/styles/hover_pressed = SubResource("StyleBoxFlat_playerprimarypressed")
+PlayerButtonPrimary/styles/normal = SubResource("StyleBoxFlat_playerprimary")
+PlayerButtonPrimary/styles/pressed = SubResource("StyleBoxFlat_playerprimarypressed")
+PlayerTime/base_type = &"Label"
+PlayerTime/colors/font_color = Color(0.82, 0.82, 0.82, 1)
+PlayerTime/font_sizes/font_size = 18
PopupBackground/base_type = &"Panel"
PopupBackground/styles/panel = SubResource("StyleBoxFlat_vputg")
PopupMenu/colors/font_accelerator_color = Color(0.7, 0.7, 0.7, 0.8)
@@ -990,6 +1168,10 @@ ScrollContainer/colors/scroll_hint_horizontal_color = Color(0, 0, 0, 1)
ScrollContainer/colors/scroll_hint_vertical_color = Color(0, 0, 0, 1)
ScrollContainer/styles/focus = SubResource("StyleBoxFlat_35dwi")
ScrollContainer/styles/panel = SubResource("StyleBoxEmpty_r3gy8")
+ProgressSlider/base_type = &"HSlider"
+ProgressSlider/styles/grabber_area = SubResource("StyleBoxFlat_progressfill")
+ProgressSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_progressfillhover")
+ProgressSlider/styles/slider = SubResource("StyleBoxFlat_playertrack")
SearchField/base_type = &"LineEdit"
SearchField/font_sizes/font_size = 28
SearchResult/base_type = &"RichTextLabel"
@@ -1079,5 +1261,9 @@ VScrollBar/styles/scroll = SubResource("StyleBoxFlat_6fedo")
VScrollBar/styles/scroll_focus = SubResource("StyleBoxFlat_8sdcv")
VSeparator/constants/separation = 4
VSeparator/styles/separator = SubResource("StyleBoxLine_r3erx")
+VolumeBar/base_type = &"HSlider"
+VolumeBar/styles/grabber_area = SubResource("StyleBoxFlat_volumefill")
+VolumeBar/styles/grabber_area_highlight = SubResource("StyleBoxFlat_volumefillhover")
+VolumeBar/styles/slider = SubResource("StyleBoxFlat_playertrack")
WindowBackground/base_type = &"Panel"
WindowBackground/styles/panel = SubResource("StyleBoxFlat_2mdtn")