From 46953ee71a09446890f583b15b7f6ded95916eaa Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 22:30:03 -0500 Subject: [PATCH] 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. --- scripts/ui/main.gd | 32 +++++++++++++++++++++++--------- scripts/ui/progress_profile.gd | 24 +++++++++++++++++++++++- scripts/ui/settings_screen.gd | 24 +++++++++++++++++++++++- tests/web_responsive_test.gd | 5 ++++- 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/scripts/ui/main.gd b/scripts/ui/main.gd index b5c6669..dbbdc5f 100644 --- a/scripts/ui/main.gd +++ b/scripts/ui/main.gd @@ -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": diff --git a/scripts/ui/progress_profile.gd b/scripts/ui/progress_profile.gd index 971fb05..43c285e 100644 --- a/scripts/ui/progress_profile.gd +++ b/scripts/ui/progress_profile.gd @@ -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 diff --git a/scripts/ui/settings_screen.gd b/scripts/ui/settings_screen.gd index 970ed1a..335d634 100644 --- a/scripts/ui/settings_screen.gd +++ b/scripts/ui/settings_screen.gd @@ -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 diff --git a/tests/web_responsive_test.gd b/tests/web_responsive_test.gd index 22e4065..2d3906f 100644 --- a/tests/web_responsive_test.gd +++ b/tests/web_responsive_test.gd @@ -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")