Reach Settings/Profile from the pause menu, with a way back

The pause menu (Escape during a question/CCS/flashcards run) only had
Resume/UI Sounds/Exit Run/Log Out. Settings already has an equivalent
"Interface sounds" toggle, so drop the duplicate from the pause menu.
More importantly, "player" and "ccs_player" hide the entire app shell
(header + sidebar), so there was no way to reach Settings or Profile
at all mid-session - add Profile/Settings buttons to the pause menu to
close that gap.

Navigating there follows the existing return_screen payload
convention (already used by simulation_history/results/review) rather
than inventing new navigation infrastructure: _navigate_from_pause()
remembers current_screen_name and passes it through show_screen's
payload. Both screens show a "Back to session" button when that's
set, and hide it otherwise (including on ordinary non-paused visits).

show_screen() unconditionally resumes the question timer via
_close_app_menu() as a side effect of any navigation, so
_navigate_from_pause() re-pauses it immediately when leaving "player" -
the timer then resumes for real, for free, when the user actually
returns via show_screen(return_screen).

progress_profile.gd rebuilds its whole child tree on every theme
toggle (_rebuild -> _build), which would otherwise wipe the button's
visibility - added _refresh_back_to_session_visibility(), called both
after _build() and from on_show(), so it survives a theme change while
paused.

Flipped tests/web_responsive_test.gd's assertion, which previously
required the pause menu NOT to have a Profile button.

Verified end-to-end headlessly: opening the pause menu from a live
"player" session, navigating to Settings and Profile, confirming
current_question_pause_started_at is correctly paused while away and
resumed on return, confirming the back-to-session button appears only
when reached via pause (not on ordinary nav), and confirming it
survives a dark/light theme toggle on the Profile screen.
This commit is contained in:
hz
2026-07-25 22:30:03 -05:00
parent aa5497c516
commit 46953ee71a
4 changed files with 73 additions and 12 deletions
+23 -9
View File
@@ -11,7 +11,6 @@ var header_title: Label
var environment_badge: Label
var app_menu: PanelContainer
var menu_exit_button: Button
var sound_toggle: CheckButton
var theme_button: Button
var feedback_button: Button
var feedback_dialog: Control
@@ -855,7 +854,7 @@ func _build_app_menu() -> void:
app_menu.visible = false
app_menu.top_level = true
app_menu.mouse_filter = Control.MOUSE_FILTER_STOP
app_menu.custom_minimum_size = Vector2(340, 230)
app_menu.custom_minimum_size = Vector2(340, 290)
app_menu.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.CYAN))
add_child(app_menu)
@@ -877,13 +876,21 @@ func _build_app_menu() -> void:
resume.pressed.connect(_close_app_menu)
content.add_child(resume)
sound_toggle = CheckButton.new()
sound_toggle.text = "UI Sounds"
sound_toggle.button_pressed = UISoundService.are_sounds_enabled()
sound_toggle.custom_minimum_size = Vector2(0, 44)
UISoundService.set_button_sound(sound_toggle, "none")
sound_toggle.toggled.connect(UISoundService.set_sounds_enabled)
content.add_child(sound_toggle)
var profile_button := Button.new()
profile_button.text = "Profile"
profile_button.custom_minimum_size = Vector2(0, 48)
Style.style_chip(profile_button, Style.CYAN)
UISoundService.set_button_sound(profile_button, "open")
profile_button.pressed.connect(_navigate_from_pause.bind("profile"))
content.add_child(profile_button)
var settings_button := Button.new()
settings_button.text = "Settings"
settings_button.custom_minimum_size = Vector2(0, 48)
Style.style_chip(settings_button, Style.CYAN)
UISoundService.set_button_sound(settings_button, "open")
settings_button.pressed.connect(_navigate_from_pause.bind("settings"))
content.add_child(settings_button)
menu_exit_button = Button.new()
menu_exit_button.text = "Exit Run"
@@ -929,6 +936,13 @@ func _close_app_menu() -> void:
BlockSessionService.resume_current_question_timer()
func _navigate_from_pause(target_screen: String) -> void:
var return_screen := current_screen_name
show_screen(target_screen, {"return_screen": return_screen})
if return_screen == "player":
BlockSessionService.pause_current_question_timer()
func _exit_run() -> void:
_close_app_menu()
if current_screen_name == "ccs_player":
+23 -1
View File
@@ -19,6 +19,8 @@ var outcome_consent: CheckBox
var outcome_status: Label
var outcome_delete: Button
var connected_accounts_list: VBoxContainer
var return_screen := ""
var back_to_session_button: Button
func _ready() -> void:
@@ -38,7 +40,10 @@ func _ready() -> void:
_build()
func on_show(_payload: Variant = null) -> void:
func on_show(payload: Variant = null) -> void:
var wrapper: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else {}
return_screen = str(wrapper.get("return_screen", ""))
_refresh_back_to_session_visibility()
_render()
AccountProfileService.refresh()
DashboardService.refresh()
@@ -63,6 +68,11 @@ func _build() -> void:
status_label = _label("", 13, Brand.muted_color(_dark()))
status_label.visible = false
page.add_child(status_label)
back_to_session_button = _button("Back to session", "", true)
back_to_session_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
UISoundService.set_button_sound(back_to_session_button, "back")
back_to_session_button.pressed.connect(_return_to_session)
page.add_child(back_to_session_button)
columns = GridContainer.new()
columns.columns = 2
columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -87,6 +97,7 @@ func _build() -> void:
right.add_child(_outcome_card())
right.add_child(_connected_accounts_card())
right.add_child(_account_actions_card())
_refresh_back_to_session_visibility()
_update_responsive()
_render()
@@ -510,6 +521,17 @@ func _button(text: String, icon_name: String = "", primary: bool = false) -> But
return button
func _refresh_back_to_session_visibility() -> void:
if back_to_session_button != null:
back_to_session_button.visible = not return_screen.is_empty()
func _return_to_session() -> void:
if return_screen.is_empty():
return
main.show_screen(return_screen)
func _action_button(text: String, icon_name: String, color: Color = Color.TRANSPARENT) -> Button:
var button := _button(text, icon_name, false)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
+23 -1
View File
@@ -13,6 +13,8 @@ var followups_toggle: CheckButton
var question_scale_value_label: Label
var ccs_scale_value_label: Label
var core_block_size_value_label: Label
var return_screen := ""
var back_to_session_button: Button
func _ready() -> void:
@@ -39,6 +41,17 @@ func _ready() -> void:
subtitle.add_theme_color_override("font_color", Brand.SLATE)
add_child(subtitle)
back_to_session_button = Button.new()
back_to_session_button.text = "Back to session"
back_to_session_button.custom_minimum_size = Vector2(0, 44)
back_to_session_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
back_to_session_button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
back_to_session_button.add_theme_color_override("font_color", Color.WHITE)
back_to_session_button.visible = false
UISoundService.set_button_sound(back_to_session_button, "back")
back_to_session_button.pressed.connect(_return_to_session)
add_child(back_to_session_button)
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(520, 0)
panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
@@ -98,7 +111,10 @@ func _ready() -> void:
_refresh_text_scale_labels()
func on_show(_payload: Variant = null) -> void:
func on_show(payload: Variant = null) -> void:
var wrapper: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else {}
return_screen = str(wrapper.get("return_screen", ""))
back_to_session_button.visible = not return_screen.is_empty()
theme_toggle.set_pressed_no_signal(UIThemeService.is_dark_mode())
sound_toggle.set_pressed_no_signal(UISoundService.are_sounds_enabled())
followups_toggle.set_pressed_no_signal(BlockSessionService.show_review_followups)
@@ -209,6 +225,12 @@ func _open_legal_document(kind: String) -> void:
main.open_legal_document(kind)
func _return_to_session() -> void:
if return_screen.is_empty():
return
main.show_screen(return_screen)
func _on_theme_changed(is_dark: bool) -> void:
theme_toggle.set_pressed_no_signal(is_dark)
var panel := theme_toggle.get_parent().get_parent() as PanelContainer
+4 -1
View File
@@ -50,10 +50,13 @@ func _run() -> void:
_assert(main_screen.account_chevron_button.get_index() == main_screen.account_button.get_index() + 1, "the account avatar and chevron should remain adjacent")
_assert(main_screen.header_panel.find_child("HeaderStats", true, false) == null, "the application header should not display rating or role labels")
var pause_menu_has_profile := false
var pause_menu_has_settings := false
for menu_button in main_screen.app_menu.find_children("*", "Button", true, false):
if menu_button.text == "Profile":
pause_menu_has_profile = true
_assert(not pause_menu_has_profile, "the paused-run menu should not duplicate profile navigation")
if menu_button.text == "Settings":
pause_menu_has_settings = true
_assert(pause_menu_has_profile and pause_menu_has_settings, "the paused-run menu should offer Profile and Settings navigation")
_assert(main_screen.session_expiry_dialog != null, "the application shell should construct a global session-expiry prompt")
main_screen.session_expiry_dialog._show_warning(120)
_assert(main_screen.session_expiry_dialog.visible, "the session-expiry prompt should cover every authenticated screen")