diff --git a/assets/icons/medmind/clear-highlight-darkmode.png b/assets/icons/medmind/clear-highlight-darkmode.png new file mode 100644 index 0000000..158e81e Binary files /dev/null and b/assets/icons/medmind/clear-highlight-darkmode.png differ diff --git a/assets/icons/medmind/clear-highlight-darkmode.png.import b/assets/icons/medmind/clear-highlight-darkmode.png.import new file mode 100644 index 0000000..95c87c2 --- /dev/null +++ b/assets/icons/medmind/clear-highlight-darkmode.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bv0vo5enee88m" +path="res://.godot/imported/clear-highlight-darkmode.png-f5af287fe98280a6ca2fcd51ab9af5a5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/medmind/clear-highlight-darkmode.png" +dest_files=["res://.godot/imported/clear-highlight-darkmode.png-f5af287fe98280a6ca2fcd51ab9af5a5.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 diff --git a/assets/icons/medmind/clear-highlight-lightmode.png b/assets/icons/medmind/clear-highlight-lightmode.png new file mode 100644 index 0000000..05ffb80 Binary files /dev/null and b/assets/icons/medmind/clear-highlight-lightmode.png differ diff --git a/assets/icons/medmind/clear-highlight-lightmode.png.import b/assets/icons/medmind/clear-highlight-lightmode.png.import new file mode 100644 index 0000000..38b6edb --- /dev/null +++ b/assets/icons/medmind/clear-highlight-lightmode.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://sfq2s5fx5vs4" +path="res://.godot/imported/clear-highlight-lightmode.png-dd2abfaa944c65e8d3e917cd1fc470e8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/medmind/clear-highlight-lightmode.png" +dest_files=["res://.godot/imported/clear-highlight-lightmode.png-dd2abfaa944c65e8d3e917cd1fc470e8.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 diff --git a/assets/icons/medmind/report-question-darkmode.png b/assets/icons/medmind/report-question-darkmode.png new file mode 100644 index 0000000..2f11475 Binary files /dev/null and b/assets/icons/medmind/report-question-darkmode.png differ diff --git a/assets/icons/medmind/report-question-darkmode.png.import b/assets/icons/medmind/report-question-darkmode.png.import new file mode 100644 index 0000000..c70de89 --- /dev/null +++ b/assets/icons/medmind/report-question-darkmode.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://duumt7vato28c" +path="res://.godot/imported/report-question-darkmode.png-47c7d15d7daf464037cdd7712879b6cc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/medmind/report-question-darkmode.png" +dest_files=["res://.godot/imported/report-question-darkmode.png-47c7d15d7daf464037cdd7712879b6cc.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 diff --git a/assets/icons/medmind/report-question-lightmode.png b/assets/icons/medmind/report-question-lightmode.png new file mode 100644 index 0000000..5df4743 Binary files /dev/null and b/assets/icons/medmind/report-question-lightmode.png differ diff --git a/assets/icons/medmind/report-question-lightmode.png.import b/assets/icons/medmind/report-question-lightmode.png.import new file mode 100644 index 0000000..23f1f0a --- /dev/null +++ b/assets/icons/medmind/report-question-lightmode.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cj8futh2o3ixp" +path="res://.godot/imported/report-question-lightmode.png-3006440fd905de3664fbd0b81dddda26.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/medmind/report-question-lightmode.png" +dest_files=["res://.godot/imported/report-question-lightmode.png-3006440fd905de3664fbd0b81dddda26.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 diff --git a/scripts/ui/question_player.gd b/scripts/ui/question_player.gd index 5ccefc9..01f7399 100644 --- a/scripts/ui/question_player.gd +++ b/scripts/ui/question_player.gd @@ -2,6 +2,10 @@ extends VBoxContainer const Style := preload("res://scripts/ui/ui_style.gd") const Brand := preload("res://scripts/ui/medmind_style.gd") +const CLEAR_HIGHLIGHT_DARK_ICON := preload("res://assets/icons/medmind/clear-highlight-darkmode.png") +const CLEAR_HIGHLIGHT_LIGHT_ICON := preload("res://assets/icons/medmind/clear-highlight-lightmode.png") +const REPORT_QUESTION_DARK_ICON := preload("res://assets/icons/medmind/report-question-darkmode.png") +const REPORT_QUESTION_LIGHT_ICON := preload("res://assets/icons/medmind/report-question-lightmode.png") const FEEDBACK_FADE_SECONDS := 0.45 const CORRECT_ADVANCE_DELAY_SECONDS := 1.25 const TEXT_SCALE_MIN := 0.85 @@ -54,7 +58,6 @@ var simulation_submit_pending := false var progress_button: Button var timer_label: Label -var player_stats_label: Label var stem_label: RichTextLabel var lead_label: Label var explanation_label: RichTextLabel @@ -75,6 +78,7 @@ var skip_button: Button var text_smaller_button: Button var text_larger_button: Button var labs_button: Button +var clear_highlight_button: Button var flag_button: Button var report_button: Button var app_feedback_button: Button @@ -142,12 +146,6 @@ func _ready() -> void: QuestionBankService.question_rating_updated.connect(_on_question_rating_updated) if not QuestionBankService.question_skipped.is_connected(_on_question_skipped): QuestionBankService.question_skipped.connect(_on_question_skipped) - if not ProfileService.profile_changed.is_connected(_refresh_player_stats): - ProfileService.profile_changed.connect(_refresh_player_stats) - if not QuestionBankService.ratings_changed.is_connected(_refresh_player_stats): - QuestionBankService.ratings_changed.connect(_refresh_player_stats) - if not AuthState.auth_changed.is_connected(_refresh_player_stats): - AuthState.auth_changed.connect(_refresh_player_stats) if not QuestionBankService.question_report_submitted.is_connected(_on_question_report_submitted): QuestionBankService.question_report_submitted.connect(_on_question_report_submitted) if not QuestionBankService.question_report_failed.is_connected(_on_question_report_failed): @@ -184,14 +182,6 @@ func _ready() -> void: header_row.add_theme_constant_override("separation", 10) header_stack.add_child(header_row) - var step_badge := Label.new() - step_badge.text = "STEP 3" - step_badge.name = "StepBadge" - step_badge.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - step_badge.custom_minimum_size = Vector2(70, 34) - _set_scaled_label(step_badge, 13, Brand.PURPLE) - header_row.add_child(step_badge) - progress_button = Button.new() progress_button.flat = true progress_button.alignment = HORIZONTAL_ALIGNMENT_LEFT @@ -210,11 +200,6 @@ func _ready() -> void: timer_label.visible = false timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT - player_stats_label = Label.new() - player_stats_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT - _set_scaled_label(player_stats_label, 13, Brand.SLATE) - header_row.add_child(player_stats_label) - question_toolbar = HFlowContainer.new() question_toolbar.add_theme_constant_override("h_separation", 8) question_toolbar.add_theme_constant_override("v_separation", 8) @@ -255,11 +240,9 @@ func _ready() -> void: labs_button.pressed.connect(_show_labs_modal) question_toolbar.add_child(labs_button) - var clear_highlight_button := Button.new() + clear_highlight_button = Button.new() clear_highlight_button.tooltip_text = "Clear highlights" - clear_highlight_button.icon = _make_clear_highlights_icon() clear_highlight_button.custom_minimum_size = Vector2(44, 44) - Style.style_chip(clear_highlight_button, Style.CYAN) UISoundService.set_button_sound(clear_highlight_button, "eliminate") clear_highlight_button.pressed.connect(_clear_current_highlights) question_toolbar.add_child(clear_highlight_button) @@ -281,9 +264,7 @@ func _ready() -> void: report_button = Button.new() report_button.tooltip_text = "Report question" - report_button.icon = _make_report_icon() report_button.custom_minimum_size = Vector2(44, 44) - Style.style_chip(report_button, Style.CYAN) UISoundService.set_button_sound(report_button, "open") report_button.pressed.connect(_toggle_report_current_question) question_toolbar.add_child(report_button) @@ -610,7 +591,6 @@ func _refresh_responsive_layout() -> void: header_toolbar_host.size_flags_horizontal = Control.SIZE_EXPAND_FILL if compact else Control.SIZE_SHRINK_END question_toolbar.size_flags_horizontal = Control.SIZE_EXPAND_FILL header_toolbar_host.custom_minimum_size.x = 0.0 if compact else 520.0 - player_stats_label.visible = size.x >= 720.0 app_feedback_button.text = "" if size.x < 760.0 else "Feedback" app_feedback_button.custom_minimum_size.x = 44 if size.x < 760.0 else 92 if size.x < 560.0: @@ -671,6 +651,40 @@ func _toolbar_box(dark: bool, active: bool = false) -> StyleBoxFlat: return box +func _theme_toggle_box(dark: bool, hovered: bool = false) -> StyleBoxFlat: + var box := StyleBoxFlat.new() + box.bg_color = Color("111b30") if dark else Brand.LIGHT_SURFACE + box.border_color = Brand.AMBER if hovered else (Color("33415d") if dark else Brand.LIGHT_BORDER) + box.set_border_width_all(1) + box.set_corner_radius_all(22) + return box + + +func _style_theme_toggle_button(dark: bool) -> void: + if theme_button == null: + return + Brand.set_theme_toggle_icon(theme_button, dark) + theme_button.tooltip_text = "Switch to light mode" if dark else "Switch to dark mode" + theme_button.add_theme_stylebox_override("normal", _theme_toggle_box(dark)) + theme_button.add_theme_stylebox_override("hover", _theme_toggle_box(dark, true)) + theme_button.add_theme_stylebox_override("pressed", _theme_toggle_box(dark, true)) + theme_button.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) + + +func _style_asset_toolbar_button(button: Button, texture: Texture2D) -> void: + if button == null: + return + button.text = "" + button.icon = texture + button.expand_icon = true + button.add_theme_constant_override("icon_max_width", 44) + var empty := StyleBoxEmpty.new() + for state in ["normal", "hover", "pressed", "focus"]: + button.add_theme_stylebox_override(state, empty) + for color_name in ["icon_normal_color", "icon_hover_color", "icon_pressed_color", "icon_focus_color"]: + button.add_theme_color_override(color_name, Color.WHITE) + + func _primary_action_box(dark: bool, state: String) -> StyleBoxFlat: var box := StyleBoxFlat.new() if state == "disabled": @@ -770,14 +784,9 @@ func _apply_question_theme() -> void: question_panel.add_theme_stylebox_override("panel", _question_content_box(dark)) if navigator_popup != null: navigator_popup.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false)) - var step_badge := header_row.get_node_or_null("StepBadge") as Label - if step_badge != null: - step_badge.add_theme_stylebox_override("normal", Brand.badge_box(dark, Brand.PURPLE)) - step_badge.add_theme_color_override("font_color", Brand.PURPLE if not dark else Color("c4b5fd")) progress_button.add_theme_color_override("font_color", Brand.text_color(dark)) progress_button.add_theme_color_override("font_hover_color", Brand.SKY_BLUE) progress_button.add_theme_color_override("font_disabled_color", Brand.text_color(dark)) - player_stats_label.add_theme_color_override("font_color", Brand.muted_color(dark)) timer_label.add_theme_color_override("font_color", Brand.muted_color(dark)) stem_label.add_theme_color_override("default_color", Brand.text_color(dark)) lead_label.add_theme_color_override("font_color", Brand.text_color(dark)) @@ -797,7 +806,7 @@ func _apply_question_theme() -> void: var navigator_heading := navigator_popup.find_child("NavigatorHeading", true, false) as Label if navigator_heading != null: navigator_heading.add_theme_color_override("font_color", Brand.text_color(dark)) - for control in [theme_button, text_smaller_button, text_larger_button, labs_button, flag_button, report_button, app_feedback_button]: + for control in [text_smaller_button, text_larger_button, labs_button, flag_button, app_feedback_button]: if control == null: continue var active: bool = control == flag_button and flagged @@ -806,6 +815,9 @@ func _apply_question_theme() -> void: control.add_theme_stylebox_override("pressed", _toolbar_box(dark, true)) control.add_theme_color_override("font_color", Brand.text_color(dark)) control.add_theme_color_override("icon_normal_color", Brand.SKY_BLUE) + _style_asset_toolbar_button(clear_highlight_button, CLEAR_HIGHLIGHT_DARK_ICON if dark else CLEAR_HIGHLIGHT_LIGHT_ICON) + _style_asset_toolbar_button(report_button, REPORT_QUESTION_DARK_ICON if dark else REPORT_QUESTION_LIGHT_ICON) + _style_theme_toggle_button(dark) if skip_button != null: skip_button.add_theme_stylebox_override("normal", _toolbar_box(dark)) skip_button.add_theme_stylebox_override("hover", _toolbar_box(dark, true)) @@ -1029,7 +1041,6 @@ func _load_question() -> void: _refresh_nav_palette() _refresh_toolbar_state() - _refresh_player_stats() _apply_question_theme() @@ -1843,19 +1854,12 @@ func _refresh_toolbar_state() -> void: func refresh_theme_controls() -> void: - _refresh_theme_button() if media_viewer_background != null: media_viewer_background.color = Style.BACKGROUND if labs_button != null: labs_button.icon = _make_labs_icon() if flag_button != null: flag_button.icon = _make_flag_icon() - if report_button != null: - report_button.icon = _make_report_icon() - if question_toolbar != null: - for child in question_toolbar.get_children(): - if child is Button and child.tooltip_text == "Clear highlights": - child.icon = _make_clear_highlights_icon() _refresh_toolbar_state() _apply_saved_choice_state() _apply_question_theme() @@ -1864,19 +1868,7 @@ func refresh_theme_controls() -> void: 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.style_chip(theme_button, Style.CYAN) - - -func _refresh_player_stats() -> void: - if player_stats_label == null: - return - var display_name := AuthState.role.to_upper() if AuthState.role != "" else AuthState.username - player_stats_label.text = "Rating: %d | %s" % [ - QuestionBankService.overall_elo, - display_name - ] + _style_theme_toggle_button(theme_service.is_dark_mode()) func _adjust_question_text_scale(delta: float) -> void: @@ -3369,67 +3361,6 @@ func _make_flag_icon() -> Texture2D: return ImageTexture.create_from_image(image) -func _make_clear_highlights_icon() -> Texture2D: - var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) - image.fill(Color(0, 0, 0, 0)) - var handle_color := Style.TEXT - var bristle_color := Style.AMBER - for offset in range(3): - _draw_icon_line(image, Vector2i(6 + offset, 16), Vector2i(16 + offset, 6), handle_color) - for x in range(13, 20): - for y in range(15, 19): - if x + y < 36: - image.set_pixel(x, y, bristle_color) - for x in range(12, 20): - image.set_pixel(x, 19, bristle_color) - return ImageTexture.create_from_image(image) - - -func _make_report_icon() -> Texture2D: - var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) - image.fill(Color(0, 0, 0, 0)) - var body_color := Style.TEXT - var accent_color := Style.RED - for x in range(5, 14): - for y in range(9, 15): - image.set_pixel(x, y, body_color) - for x in range(14, 20): - image.set_pixel(x, 7, accent_color) - image.set_pixel(x, 16, accent_color) - for y in range(8, 16): - image.set_pixel(20, y, accent_color) - for y in range(10, 14): - image.set_pixel(4, y, body_color) - for y in range(15, 21): - image.set_pixel(8, y, body_color) - if y < 20: - image.set_pixel(9, y, body_color) - _draw_icon_line(image, Vector2i(2, 5), Vector2i(5, 8), accent_color) - _draw_icon_line(image, Vector2i(2, 19), Vector2i(5, 16), accent_color) - return ImageTexture.create_from_image(image) - - -func _draw_icon_line(image: Image, from: Vector2i, to: Vector2i, color: Color) -> void: - var dx := int(abs(to.x - from.x)) - var sx := 1 if from.x < to.x else -1 - var dy := -int(abs(to.y - from.y)) - var sy := 1 if from.y < to.y else -1 - var err: int = dx + dy - var point := from - while true: - if point.x >= 0 and point.x < image.get_width() and point.y >= 0 and point.y < image.get_height(): - image.set_pixel(point.x, point.y, color) - if point == to: - break - var e2: int = 2 * err - if e2 >= dy: - err += dy - point.x += sx - if e2 <= dx: - err += dx - point.y += sy - - func _refresh_nav_palette() -> void: if nav_palette == null: return diff --git a/tests/post_answer_review_test.gd b/tests/post_answer_review_test.gd index 582e46c..e75b305 100644 --- a/tests/post_answer_review_test.gd +++ b/tests/post_answer_review_test.gd @@ -1,5 +1,7 @@ extends SceneTree +const Brand := preload("res://scripts/ui/medmind_style.gd") + var failures := 0 @@ -338,6 +340,18 @@ func _test_player_runtime_construction() -> void: await process_frame _assert(player.question_card != null, "question player should construct its branded card") _assert(player.navigator_popup != null, "question player should construct its popup navigator") + _assert(player.header_row.get_node_or_null("StepBadge") == null, "question player should not display a redundant STEP 3 badge") + _assert(player.header_row.get_child(0) == player.progress_button, "question progress should be the first header item") + _assert(player.clear_highlight_button.icon != null, "clear highlights should use the supplied toolbar artwork") + _assert(player.report_button.icon != null, "report question should use the supplied toolbar artwork") + player._style_asset_toolbar_button(player.clear_highlight_button, player.CLEAR_HIGHLIGHT_LIGHT_ICON) + _assert(player.clear_highlight_button.icon == player.CLEAR_HIGHLIGHT_LIGHT_ICON, "clear highlights should support its light-mode artwork") + player._style_asset_toolbar_button(player.report_button, player.REPORT_QUESTION_LIGHT_ICON) + _assert(player.report_button.icon == player.REPORT_QUESTION_LIGHT_ICON, "report question should support its light-mode artwork") + var toggle_box := player.theme_button.get_theme_stylebox("normal") as StyleBoxFlat + _assert(player.theme_button.custom_minimum_size == Vector2(44, 44), "question theme toggle should retain the shared square footprint") + _assert(toggle_box != null and toggle_box.corner_radius_top_left == 22, "question theme toggle should use the shared circular treatment") + _assert(player.theme_button.icon in [Brand.SUN_ICON, Brand.MOON_ICON], "question theme toggle should use the shared sun or moon icon") _assert(player.choice_badges.size() == 5, "question player should construct an answer badge for every visible choice slot") _assert(player.choice_followup_panels[2].get_parent() == player.choice_rows[2], "distractor remediation should remain nested inside its answer choice") player.choice_followup_panels[2].visible = true