extends Control const Style := preload("res://scripts/ui/ui_style.gd") const Brand := preload("res://scripts/ui/medmind_style.gd") const NavigationRoutes := preload("res://scripts/ui/navigation_routes.gd") var screens := {} var header_panel: PanelContainer 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 var notes_dialog: Control var session_expiry_dialog: Control var legal_document_dialog: Control var theme_service var current_screen_name := "" var screens_margin: MarginContainer var screens_host: MarginContainer var shell_root: HBoxContainer var sidebar: PanelContainer var sidebar_content: VBoxContainer var drawer_backdrop: ColorRect var drawer_open := false var menu_button: Button var account_button: Button var account_chevron_button: Button var account_menu: PanelContainer var nav_buttons: Dictionary = {} var current_nav_key := "home" var compact_shell := false var app_background: ColorRect func _ready() -> void: theme_service = get_node("/root/UIThemeService") Style.apply_app_theme(self) _build_ui() theme_service.theme_changed.connect(_on_theme_changed) resized.connect(_update_responsive_layout) _update_responsive_layout() show_screen(("legal_consent" if AuthState.requires_legal_acceptance else "dashboard") if AuthState.is_logged_in else "login") func _update_responsive_layout() -> void: compact_shell = size.x < 900.0 if sidebar != null: sidebar.visible = AuthState.is_logged_in and not compact_shell and _screen_uses_shell(current_screen_name) if menu_button != null: menu_button.visible = AuthState.is_logged_in and compact_shell and _screen_uses_shell(current_screen_name) _apply_screen_margins(current_screen_name) if feedback_button != null: feedback_button.visible = AuthState.is_logged_in and size.x >= 560.0 if account_chevron_button != null: account_chevron_button.visible = size.x >= 480.0 if drawer_open and not compact_shell: _close_drawer() if app_menu != null: app_menu.custom_minimum_size.x = minf(340.0, maxf(280.0, size.x - 32.0)) if app_menu.visible: app_menu.size = app_menu.custom_minimum_size app_menu.position = (get_viewport_rect().size - app_menu.size) * 0.5 func show_screen(screen_name: String, payload: Variant = null) -> void: _close_app_menu() _close_account_menu() _close_drawer() for child in screens_host.get_children(): if child.visible and child.has_method("on_hide"): child.on_hide() child.visible = false var screen = screens.get(screen_name) if screen == null: return current_screen_name = screen_name _update_header_title(screen_name) _apply_screen_margins(screen_name) screen.visible = true if header_panel != null: header_panel.visible = _screen_uses_shell(screen_name) _update_shell_visibility(screen_name) _update_active_navigation(screen_name, payload) if screen.has_method("on_show"): screen.on_show(payload) func _apply_screen_margins(screen_name: String) -> void: if screens_margin == null or screen_name.is_empty(): return var is_fullscreen_study := screen_name == "player" var margin := 0 if screen_name == "login" else (12 if is_fullscreen_study else (20 if compact_shell else 44)) screens_margin.add_theme_constant_override("margin_left", margin) screens_margin.add_theme_constant_override("margin_right", margin) screens_margin.add_theme_constant_override("margin_top", 0 if screen_name == "login" else (12 if is_fullscreen_study else (20 if compact_shell else 34))) screens_margin.add_theme_constant_override("margin_bottom", 0 if screen_name == "login" else (12 if is_fullscreen_study else 28)) func _build_ui() -> void: app_background = ColorRect.new() app_background.color = Brand.background_color(UIThemeService.is_dark_mode()) app_background.material = Brand.background_material(UIThemeService.is_dark_mode()) app_background.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(app_background) shell_root = HBoxContainer.new() shell_root.name = "Root" shell_root.set_anchors_preset(Control.PRESET_FULL_RECT) shell_root.add_theme_constant_override("separation", 0) add_child(shell_root) sidebar = _build_sidebar() shell_root.add_child(sidebar) var main_column := VBoxContainer.new() main_column.name = "MainColumn" main_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL main_column.add_theme_constant_override("separation", 0) shell_root.add_child(main_column) header_panel = PanelContainer.new() header_panel.name = "Header" header_panel.add_theme_stylebox_override("panel", _top_bar_box()) main_column.add_child(header_panel) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 80) header.add_theme_constant_override("separation", 14) header.alignment = BoxContainer.ALIGNMENT_CENTER header_panel.add_child(header) menu_button = Button.new() menu_button.icon = preload("res://assets/icons/lucide/menu.svg") menu_button.add_theme_constant_override("icon_max_width", 24) menu_button.tooltip_text = "Open navigation" menu_button.custom_minimum_size = Vector2(48, 48) menu_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER menu_button.visible = false menu_button.pressed.connect(_open_drawer) header.add_child(menu_button) header_title = Label.new() header_title.add_theme_font_override("font", Brand.font) header_title.add_theme_font_size_override("font_size", 22) header_title.add_theme_color_override("font_color", Brand.text_color(UIThemeService.is_dark_mode())) header.add_child(header_title) environment_badge = Label.new() environment_badge.text = "UAT" environment_badge.tooltip_text = "User acceptance testing environment" environment_badge.visible = str(ProjectSettings.get_setting("step3/deployment_environment", "development")) == "uat" environment_badge.add_theme_font_override("font", Brand.font) environment_badge.add_theme_font_size_override("font_size", 13) environment_badge.add_theme_color_override("font_color", Color("052e16")) environment_badge.add_theme_stylebox_override("normal", _environment_badge_box()) header.add_child(environment_badge) var header_spacer := Control.new() header_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(header_spacer) feedback_button = Button.new() feedback_button.text = "Feedback" feedback_button.tooltip_text = "Send general feedback or report a bug" feedback_button.custom_minimum_size = Vector2(124, 42) feedback_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER _style_header_feedback_button(feedback_button) feedback_button.pressed.connect(open_feedback_dialog) header.add_child(feedback_button) theme_button = Button.new() theme_button.tooltip_text = "Switch color theme" theme_button.custom_minimum_size = Vector2(44, 44) theme_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER theme_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER theme_button.alignment = HORIZONTAL_ALIGNMENT_CENTER theme_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER theme_button.focus_mode = Control.FOCUS_NONE theme_button.pressed.connect(theme_service.toggle_theme) _style_shell_theme_button(theme_button) header.add_child(theme_button) _refresh_theme_button() account_button = Button.new() account_button.tooltip_text = "Open account menu" account_button.custom_minimum_size = Vector2(44, 44) account_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER account_button.pressed.connect(_toggle_account_menu) _style_account_avatar(account_button) header.add_child(account_button) account_chevron_button = Button.new() account_chevron_button.icon = preload("res://assets/icons/lucide/chevron-down.svg") account_chevron_button.tooltip_text = "Open account menu" account_chevron_button.custom_minimum_size = Vector2(28, 44) account_chevron_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER account_chevron_button.add_theme_constant_override("icon_max_width", 17) account_chevron_button.pressed.connect(_toggle_account_menu) _style_account_chevron(account_chevron_button) header.add_child(account_chevron_button) var screens_container := MarginContainer.new() screens_margin = screens_container screens_host = screens_container screens_container.name = "Screens" screens_container.size_flags_vertical = Control.SIZE_EXPAND_FILL screens_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL screens_container.add_theme_constant_override("margin_left", 24) screens_container.add_theme_constant_override("margin_right", 24) screens_container.add_theme_constant_override("margin_top", 18) screens_container.add_theme_constant_override("margin_bottom", 22) main_column.add_child(screens_container) var menu := preload("res://scripts/ui/main_menu.gd").new() menu.name = "MainMenu" menu.main = self screens_container.add_child(menu) screens["menu"] = menu var login := preload("res://scripts/ui/login_screen.gd").new() login.name = "LoginScreen" login.main = self screens_container.add_child(login) screens["login"] = login var legal_consent := preload("res://scripts/ui/legal_consent_screen.gd").new() legal_consent.name = "LegalConsentScreen" legal_consent.main = self screens_container.add_child(legal_consent) screens["legal_consent"] = legal_consent var dashboard := preload("res://scripts/ui/dashboard_screen.gd").new() dashboard.name = "DashboardScreen" dashboard.main = self screens_container.add_child(dashboard) screens["dashboard"] = dashboard var selector := preload("res://scripts/ui/study_selector.gd").new() selector.name = "StudySelector" selector.main = self selector.stage_changed.connect(_on_selector_stage_changed) screens_container.add_child(selector) screens["selection"] = selector var map := preload("res://scripts/ui/study_map.gd").new() map.name = "StudyMap" map.main = self screens_container.add_child(map) screens["map"] = map var setup := preload("res://scripts/ui/block_setup.gd").new() setup.name = "BlockSetup" setup.main = self screens_container.add_child(setup) screens["setup"] = setup var player := preload("res://scripts/ui/question_player.gd").new() player.name = "QuestionPlayer" player.main = self screens_container.add_child(player) screens["player"] = player var review := preload("res://scripts/ui/review_results.gd").new() review.name = "ReviewResults" review.main = self screens_container.add_child(review) screens["review"] = review var review_queue_results := preload("res://scripts/ui/review_queue_results.gd").new() review_queue_results.name = "ReviewQueueResults" review_queue_results.main = self screens_container.add_child(review_queue_results) screens["review_queue_results"] = review_queue_results var simulation_break := preload("res://scripts/ui/simulation_break.gd").new() simulation_break.name = "SimulationBreak" simulation_break.main = self screens_container.add_child(simulation_break) screens["simulation_break"] = simulation_break var simulation_results := preload("res://scripts/ui/simulation_results.gd").new() simulation_results.name = "SimulationResults" simulation_results.main = self screens_container.add_child(simulation_results) screens["simulation_results"] = simulation_results var simulation_review := preload("res://scripts/ui/simulation_review.gd").new() simulation_review.name = "SimulationReview" simulation_review.main = self screens_container.add_child(simulation_review) screens["simulation_review"] = simulation_review var simulation_history := preload("res://scripts/ui/simulation_history.gd").new() simulation_history.name = "SimulationHistory" simulation_history.main = self screens_container.add_child(simulation_history) screens["simulation_history"] = simulation_history var ccs_hub := preload("res://scripts/ui/ccs_hub.gd").new() ccs_hub.name = "CcsHub"; ccs_hub.main = self; screens_container.add_child(ccs_hub); screens["ccs_hub"] = ccs_hub var ccs_player := preload("res://scripts/ui/ccs_player.gd").new() ccs_player.name = "CcsPlayer"; ccs_player.main = self; screens_container.add_child(ccs_player); screens["ccs_player"] = ccs_player var ccs_review := preload("res://scripts/ui/ccs_review.gd").new() ccs_review.name = "CcsReview"; ccs_review.main = self; screens_container.add_child(ccs_review); screens["ccs_review"] = ccs_review var ccs_history := preload("res://scripts/ui/ccs_history.gd").new() ccs_history.name = "CcsHistory"; ccs_history.main = self; screens_container.add_child(ccs_history); screens["ccs_history"] = ccs_history var profile := preload("res://scripts/ui/progress_profile.gd").new() profile.name = "ProgressProfile" profile.main = self screens_container.add_child(profile) screens["profile"] = profile var settings := preload("res://scripts/ui/settings_screen.gd").new() settings.name = "SettingsScreen" settings.main = self screens_container.add_child(settings) screens["settings"] = settings var notes := preload("res://scripts/ui/notes_screen.gd").new() notes.name = "NotesScreen" notes.main = self screens_container.add_child(notes) screens["notes"] = notes var bookmarks := preload("res://scripts/ui/bookmarks_screen.gd").new() bookmarks.name = "BookmarksScreen" bookmarks.main = self screens_container.add_child(bookmarks) screens["bookmarks"] = bookmarks var donations := preload("res://scripts/ui/donation_screen.gd").new() donations.name = "DonationScreen" donations.main = self screens_container.add_child(donations) screens["donations"] = donations feedback_dialog = preload("res://scripts/ui/app_feedback_dialog.gd").new() feedback_dialog.name = "AppFeedbackDialog" add_child(feedback_dialog) notes_dialog = preload("res://scripts/ui/notes_dialog.gd").new() notes_dialog.name = "NotesDialog" add_child(notes_dialog) session_expiry_dialog = preload("res://scripts/ui/session_expiry_dialog.gd").new() session_expiry_dialog.name = "SessionExpiryDialog" session_expiry_dialog.logout_requested.connect(_logout) add_child(session_expiry_dialog) legal_document_dialog = preload("res://scripts/ui/legal_document_dialog.gd").new() legal_document_dialog.name = "LegalDocumentDialog" add_child(legal_document_dialog) _build_account_menu() _build_drawer() AuthState.auth_changed.connect(_on_auth_changed) AuthState.session_expired.connect(_on_session_expired) _refresh_header_controls() _build_app_menu() func _build_sidebar() -> PanelContainer: var panel := PanelContainer.new() panel.name = "Sidebar" panel.custom_minimum_size = Vector2(304, 0) panel.add_theme_stylebox_override("panel", _sidebar_box()) sidebar_content = VBoxContainer.new() sidebar_content.add_theme_constant_override("separation", 8) panel.add_child(sidebar_content) _populate_navigation(sidebar_content, true) return panel func _populate_navigation(container: VBoxContainer, include_brand: bool) -> void: if include_brand: var brand_row := HBoxContainer.new() brand_row.custom_minimum_size = Vector2(0, 56) brand_row.add_theme_constant_override("separation", 10) container.add_child(brand_row) var mark := TextureRect.new() mark.texture = Brand.ranaze_mark_texture() mark.custom_minimum_size = Vector2(46, 46) mark.expand_mode = TextureRect.EXPAND_IGNORE_SIZE mark.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED brand_row.add_child(mark) var wordmark := HBoxContainer.new() wordmark.add_theme_constant_override("separation", 0) brand_row.add_child(wordmark) var rana := Label.new() rana.text = "Rana" rana.add_theme_font_override("font", Brand.font) rana.add_theme_font_size_override("font_size", 27) wordmark.add_child(rana) var ze := Label.new() ze.text = "Ze" ze.add_theme_font_override("font", Brand.font) ze.add_theme_font_size_override("font_size", 27) ze.add_theme_color_override("font_color", Brand.SKY_BLUE) wordmark.add_child(ze) container.add_child(HSeparator.new()) _add_nav_button(container, "home", "Dashboard", "chart-no-axes-column-increasing", true) _add_nav_button(container, "question_bank", "Question Bank", "clipboard-list", true) _add_nav_button(container, "ccs", "CCS Cases", "stethoscope", true) _add_nav_button(container, "exams", "Exam Simulations", "clipboard-list", true) _add_nav_button(container, "bookmarks", "Bookmarks", "bookmark", true) _add_nav_button(container, "notes", "Notes", "notebook-pen", true) var spacer := Control.new() spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL container.add_child(spacer) container.add_child(HSeparator.new()) _add_nav_button(container, "donations", "Support RanaZe", "heart-pulse", true) _add_nav_button(container, "performance", "Profile", "user-round", true) _add_nav_button(container, "settings", "Settings", "settings", true) _add_nav_button(container, "logout", "Log out", "log-out", true) func _add_nav_button(container: VBoxContainer, key: String, label_text: String, icon_name: String, enabled: bool) -> void: var button := Button.new() button.text = label_text button.name = "Nav%s" % key.to_pascal_case() button.icon = load("res://assets/icons/lucide/%s.svg" % icon_name) button.expand_icon = true button.alignment = HORIZONTAL_ALIGNMENT_LEFT button.custom_minimum_size = Vector2(0, 58) button.add_theme_constant_override("icon_max_width", 25) button.add_theme_constant_override("h_separation", 18) button.add_theme_font_override("font", Brand.font) button.add_theme_font_size_override("font_size", 18) button.disabled = not enabled button.tooltip_text = "Coming Soon" if not enabled else label_text button.pressed.connect(_on_navigation_pressed.bind(key)) container.add_child(button) if not nav_buttons.has(key): nav_buttons[key] = [] var instances: Array = nav_buttons[key] instances.append(button) nav_buttons[key] = instances _style_nav_button(button, key == current_nav_key) func _on_navigation_pressed(key: String) -> void: match key: "home": show_screen("dashboard") "question_bank": show_screen("selection", "question_bank") "exams": show_screen("simulation_history") "ccs": show_screen("ccs_hub") "performance": show_screen("profile") "study_plan": show_screen("map") "settings": show_screen("settings") "notes": show_screen("notes") "bookmarks": show_screen("bookmarks") "donations": show_screen("donations") "logout": _logout() func _update_header_title(screen_name: String) -> void: if header_title == null: return var titles := { "dashboard": "Dashboard", "selection": "Question Bank", "simulation_history": "Exam Simulations", "ccs_hub": "CCS Cases", "profile": "Profile", "settings": "Settings", "notes": "Notes", "bookmarks": "Bookmarks", "donations": "Support RanaZe" } header_title.text = str(titles.get(screen_name, "")) func _build_account_menu() -> void: account_menu = PanelContainer.new() account_menu.visible = false account_menu.top_level = true account_menu.custom_minimum_size = Vector2(230, 230) account_menu.add_theme_stylebox_override("panel", Brand.panel_box(UIThemeService.is_dark_mode())) add_child(account_menu) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 8) account_menu.add_child(content) for spec in [["Profile", "user-round", "performance"], ["Settings", "settings", "settings"], ["Log out", "log-out", "logout"]]: var button := Button.new() button.text = spec[0] button.icon = load("res://assets/icons/lucide/%s.svg" % spec[1]) button.add_theme_constant_override("icon_max_width", 22) button.alignment = HORIZONTAL_ALIGNMENT_LEFT button.custom_minimum_size = Vector2(0, 46) button.pressed.connect(_on_navigation_pressed.bind(spec[2])) content.add_child(button) func _build_drawer() -> void: drawer_backdrop = ColorRect.new() drawer_backdrop.visible = false drawer_backdrop.top_level = true drawer_backdrop.color = Color(0, 0, 0, 0.62) drawer_backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) drawer_backdrop.mouse_filter = Control.MOUSE_FILTER_STOP drawer_backdrop.gui_input.connect(_on_drawer_backdrop_input) add_child(drawer_backdrop) var drawer := PanelContainer.new() drawer.custom_minimum_size = Vector2(304, 0) drawer.set_anchors_preset(Control.PRESET_LEFT_WIDE) drawer.offset_right = 304 drawer.add_theme_stylebox_override("panel", _sidebar_box()) drawer.mouse_filter = Control.MOUSE_FILTER_STOP drawer_backdrop.add_child(drawer) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 8) drawer.add_child(content) _populate_navigation(content, true) func _open_drawer() -> void: if drawer_backdrop == null: return drawer_open = true drawer_backdrop.visible = true func _close_drawer() -> void: drawer_open = false if drawer_backdrop != null: drawer_backdrop.visible = false func _on_drawer_backdrop_input(event: InputEvent) -> void: var click := event as InputEventMouseButton if click != null and click.pressed: _close_drawer() func _toggle_account_menu() -> void: if account_menu == null: return if account_menu.visible: _close_account_menu() return account_menu.visible = true account_menu.size = account_menu.custom_minimum_size account_menu.position = Vector2( get_viewport_rect().size.x - account_menu.size.x - 24, 82 ) func _close_account_menu() -> void: if account_menu != null: account_menu.visible = false func _screen_uses_shell(screen_name: String) -> bool: return AuthState.is_logged_in and screen_name not in ["login", "legal_consent", "player", "ccs_player"] func _update_shell_visibility(screen_name: String) -> void: var show_shell := _screen_uses_shell(screen_name) if sidebar != null: sidebar.visible = show_shell and not compact_shell if header_panel != null: header_panel.visible = show_shell if menu_button != null: menu_button.visible = show_shell and compact_shell func _update_active_navigation(screen_name: String, payload: Variant = null) -> void: var key := NavigationRoutes.key_for_screen(screen_name, payload) current_nav_key = key for nav_key in nav_buttons: for button in nav_buttons[nav_key]: _style_nav_button(button, nav_key == current_nav_key) func _on_selector_stage_changed(stage_name: String) -> void: if current_screen_name == "selection": _update_active_navigation("selection", stage_name) func _style_nav_button(button: Button, active: bool) -> void: var dark := UIThemeService.is_dark_mode() var normal := StyleBoxFlat.new() normal.bg_color = (Color("14255b") if dark else Color("e5edff")) if active else Color(0, 0, 0, 0) normal.set_corner_radius_all(10) normal.content_margin_left = 18 normal.content_margin_right = 18 button.add_theme_stylebox_override("normal", normal) var hover := normal.duplicate() as StyleBoxFlat hover.bg_color = Color("172d68") if dark else Color("d8e5ff") button.add_theme_stylebox_override("hover", hover) var disabled := normal.duplicate() as StyleBoxFlat disabled.bg_color = Color(0, 0, 0, 0) button.add_theme_stylebox_override("disabled", disabled) button.add_theme_color_override("font_color", (Color("f8fafc") if dark else Color("0f172a")) if active else (Color("b8c2d6") if dark else Color("53627a"))) button.add_theme_color_override("icon_normal_color", Brand.ROYAL_BLUE if active else (Color("b8c2d6") if dark else Color("53627a"))) button.add_theme_color_override("icon_hover_color", Brand.SKY_BLUE) button.add_theme_color_override("font_disabled_color", Color("596982") if dark else Color("94a3b8")) button.add_theme_color_override("icon_disabled_color", Color("596982") if dark else Color("94a3b8")) func _sidebar_box() -> StyleBoxFlat: var dark := UIThemeService.is_dark_mode() var box := Brand.shell_box(dark, "right") box.content_margin_left = 28 box.content_margin_right = 28 box.content_margin_top = 12 box.content_margin_bottom = 26 return box func _top_bar_box() -> StyleBoxFlat: var dark := UIThemeService.is_dark_mode() var box := Brand.shell_box(dark, "bottom") box.content_margin_left = 28 box.content_margin_right = 28 return box func _environment_badge_box() -> StyleBoxFlat: var box := StyleBoxFlat.new() box.bg_color = Color("86efac") box.set_corner_radius_all(6) box.content_margin_left = 9 box.content_margin_right = 9 box.content_margin_top = 4 box.content_margin_bottom = 4 return box func _style_header_feedback_button(button: Button) -> void: var dark := UIThemeService.is_dark_mode() var normal := Style.button_box(Color(0, 0, 0, 0), Brand.ROYAL_BLUE) var hover := Style.button_box(Color(Brand.ROYAL_BLUE, 0.16), Brand.SKY_BLUE) var pressed := Style.button_box(Color(Brand.ROYAL_BLUE, 0.24), Brand.SKY_BLUE) button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) button.add_theme_stylebox_override("pressed", pressed) button.add_theme_color_override("font_color", Brand.ROYAL_BLUE if not dark else Brand.SKY_BLUE) button.add_theme_color_override("font_hover_color", Brand.SKY_BLUE) button.add_theme_color_override("icon_normal_color", Brand.ROYAL_BLUE if not dark else Brand.SKY_BLUE) button.add_theme_color_override("icon_hover_color", Brand.SKY_BLUE) func _style_account_avatar(button: Button) -> void: var dark := UIThemeService.is_dark_mode() var normal := StyleBoxFlat.new() normal.bg_color = Color("26324f") if dark else Color("dce5f5") normal.corner_radius_top_left = 22 normal.corner_radius_top_right = 22 normal.corner_radius_bottom_left = 22 normal.corner_radius_bottom_right = 22 var hover := normal.duplicate() as StyleBoxFlat hover.bg_color = Color("344465") if dark else Color("cad7ed") button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) button.add_theme_stylebox_override("pressed", hover) button.add_theme_color_override("font_color", Color("f8fafc") if dark else Color("172033")) button.add_theme_color_override("font_hover_color", Color("ffffff") if dark else Color("0f172a")) func _style_account_chevron(button: Button) -> void: var empty := StyleBoxEmpty.new() button.add_theme_stylebox_override("normal", empty) button.add_theme_stylebox_override("hover", empty) button.add_theme_stylebox_override("pressed", empty) button.add_theme_color_override("icon_normal_color", Style.MUTED) button.add_theme_color_override("icon_hover_color", Style.TEXT) func _style_shell_theme_button(button: Button) -> void: var dark := UIThemeService.is_dark_mode() var normal := StyleBoxFlat.new() normal.bg_color = Color("111b30") if dark else Brand.LIGHT_SURFACE normal.border_color = Color("33415d") if dark else Brand.LIGHT_BORDER normal.set_border_width_all(1) normal.set_corner_radius_all(22) normal.content_margin_left = 0 normal.content_margin_right = 0 normal.content_margin_top = 0 normal.content_margin_bottom = 0 var hover := normal.duplicate() as StyleBoxFlat hover.border_color = Brand.AMBER var focus := StyleBoxEmpty.new() button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) button.add_theme_stylebox_override("pressed", hover) button.add_theme_stylebox_override("focus", focus) button.add_theme_constant_override("h_separation", 0) button.add_theme_color_override("font_color", Brand.AMBER) button.add_theme_color_override("font_hover_color", Brand.AMBER) func _input(event: InputEvent) -> void: if not AuthState.is_logged_in or current_screen_name == "login": return if event.is_action_pressed("ui_cancel"): if drawer_open: _close_drawer() get_viewport().set_input_as_handled() return if account_menu != null and account_menu.visible: _close_account_menu() get_viewport().set_input_as_handled() return if current_screen_name in ["player", "ccs_player"]: _toggle_app_menu() get_viewport().set_input_as_handled() func _refresh_header_controls() -> void: if account_button != null: account_button.text = _account_initials(AuthState.username) if feedback_button != null: feedback_button.visible = AuthState.is_logged_in and size.x >= 560.0 feedback_button.icon = preload("res://assets/icons/lucide/message-square.svg") feedback_button.add_theme_constant_override("icon_max_width", 21) func _account_initials(username: String) -> String: var cleaned := "" for character in username: if character.to_lower() != character.to_upper(): cleaned += character elif character.is_valid_int(): cleaned += character if cleaned.length() >= 2: break return cleaned.to_upper() if not cleaned.is_empty() else "ME" func open_feedback_dialog() -> void: if not AuthState.is_logged_in or feedback_dialog == null: return feedback_dialog.show_dialog(FeedbackService.build_context(current_screen_name, get_viewport_rect().size)) func open_legal_document(kind: String) -> void: legal_document_dialog.show_document(kind) func _on_auth_changed() -> void: _refresh_header_controls() if AuthState.is_logged_in and AuthState.requires_legal_acceptance and current_screen_name not in ["login", "legal_consent"]: show_screen("legal_consent") func open_notes_dialog() -> void: if not AuthState.is_logged_in or notes_dialog == null: return if current_screen_name != "player" or BlockSessionService.session_kind == "simulation": return notes_dialog.show_dialog() func _on_theme_changed(is_dark_mode: bool) -> void: Style.refresh_tree(self, not is_dark_mode) _refresh_theme_button() if app_background != null: app_background.color = Brand.background_color(is_dark_mode) app_background.material = Brand.background_material(is_dark_mode) if account_menu != null: account_menu.add_theme_stylebox_override("panel", Brand.panel_box(is_dark_mode)) if feedback_button != null: _style_header_feedback_button(feedback_button) if account_button != null: _style_account_avatar(account_button) if account_chevron_button != null: _style_account_chevron(account_chevron_button) if sidebar != null: sidebar.add_theme_stylebox_override("panel", _sidebar_box()) if header_panel != null: header_panel.add_theme_stylebox_override("panel", _top_bar_box()) if header_title != null: header_title.add_theme_color_override("font_color", Brand.text_color(is_dark_mode)) if theme_button != null: _style_shell_theme_button(theme_button) for nav_key in nav_buttons: for button in nav_buttons[nav_key]: _style_nav_button(button, nav_key == current_nav_key) var selector = screens.get("selection") if selector != null and selector.has_method("refresh_theme_controls"): selector.refresh_theme_controls() var donations = screens.get("donations") if donations != null and donations.has_method("refresh_theme_controls"): donations.refresh_theme_controls() var player = screens.get("player") if player != null and player.has_method("refresh_theme_controls"): player.refresh_theme_controls() func _refresh_theme_button() -> void: if theme_button == null: return Brand.set_theme_toggle_icon(theme_button, theme_service.is_dark_mode()) theme_button.tooltip_text = "Switch to light mode" if theme_service.is_dark_mode() else "Switch to dark mode" _style_shell_theme_button(theme_button) func _logout() -> void: _perform_logout("") func _on_session_expired(_reason: String) -> void: _perform_logout("Your session expired. Sign in again.") func _perform_logout(login_message: String) -> void: _close_app_menu() if session_expiry_dialog != null and session_expiry_dialog.has_method("dismiss_for_logout"): session_expiry_dialog.dismiss_for_logout() if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation": BlockSessionService.finish_block() elif BlockSessionService.session_kind == "simulation": BlockSessionService.block_started = false QuestionBankService.clear_questions() if AuthState.is_logged_in or not AuthState.access_token.is_empty(): AuthState.clear_session() show_screen("login", login_message) func _build_app_menu() -> void: app_menu = PanelContainer.new() 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.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.CYAN)) add_child(app_menu) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 12) app_menu.add_child(content) var title := Label.new() title.text = "Paused" title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER Style.set_label(title, 28, Style.TEXT) content.add_child(title) var resume := Button.new() resume.text = "Resume" resume.custom_minimum_size = Vector2(0, 48) Style.style_primary_button(resume) UISoundService.set_button_sound(resume, "close") 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) menu_exit_button = Button.new() menu_exit_button.text = "Exit Run" menu_exit_button.custom_minimum_size = Vector2(0, 48) UISoundService.set_button_sound(menu_exit_button, "back") menu_exit_button.pressed.connect(_exit_run) content.add_child(menu_exit_button) var logout := Button.new() logout.text = "Log Out" logout.custom_minimum_size = Vector2(0, 48) Style.style_chip(logout, Style.RED) UISoundService.set_button_sound(logout, "back") logout.pressed.connect(_logout) content.add_child(logout) func _toggle_app_menu() -> void: if app_menu == null: return if app_menu.visible: UISoundService.play_sound("close") _close_app_menu() else: UISoundService.play_sound("open") app_menu.visible = true app_menu.size = app_menu.custom_minimum_size app_menu.position = (get_viewport_rect().size - app_menu.size) * 0.5 menu_exit_button.visible = (current_screen_name == "player" and BlockSessionService.block_started) or current_screen_name == "ccs_player" if menu_exit_button.visible: menu_exit_button.text = "Leave Case" if current_screen_name == "ccs_player" else "Leave (timer continues)" if BlockSessionService.session_kind == "simulation" else "Exit Run" Style.style_chip(menu_exit_button, Style.AMBER) if current_screen_name == "player": BlockSessionService.pause_current_question_timer() func _close_app_menu() -> void: if app_menu != null: app_menu.visible = false BlockSessionService.resume_current_question_timer() func _exit_run() -> void: _close_app_menu() if current_screen_name == "ccs_player": var ccs_player = screens.get("ccs_player") if ccs_player != null and ccs_player.has_method("leave_case"): ccs_player.leave_case() return var was_core_run := BlockSessionService.session_kind == "core" var was_review_run := BlockSessionService.session_kind == "review" if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation": BlockSessionService.finish_block() elif BlockSessionService.session_kind == "simulation": BlockSessionService.block_started = false if was_review_run: show_screen("dashboard") else: show_screen("selection", "exam" if was_core_run else "step3")