diff --git a/assets/icons/medmind/labs-darkmode.png b/assets/icons/medmind/labs-darkmode.png new file mode 100644 index 0000000..7b0bcb1 Binary files /dev/null and b/assets/icons/medmind/labs-darkmode.png differ diff --git a/assets/icons/medmind/labs-darkmode.png.import b/assets/icons/medmind/labs-darkmode.png.import new file mode 100644 index 0000000..1c40951 --- /dev/null +++ b/assets/icons/medmind/labs-darkmode.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ctqpsk2edv23" +path="res://.godot/imported/labs-darkmode.png-2db95305c902b01413a007879b51e169.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/medmind/labs-darkmode.png" +dest_files=["res://.godot/imported/labs-darkmode.png-2db95305c902b01413a007879b51e169.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/labs-lightmode.png b/assets/icons/medmind/labs-lightmode.png new file mode 100644 index 0000000..ac93f3a Binary files /dev/null and b/assets/icons/medmind/labs-lightmode.png differ diff --git a/assets/icons/medmind/labs-lightmode.png.import b/assets/icons/medmind/labs-lightmode.png.import new file mode 100644 index 0000000..5f2a8ed --- /dev/null +++ b/assets/icons/medmind/labs-lightmode.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b16pf3mq67md3" +path="res://.godot/imported/labs-lightmode.png-24b48bf8e309293bae3439edb1ab7c9b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/medmind/labs-lightmode.png" +dest_files=["res://.godot/imported/labs-lightmode.png-24b48bf8e309293bae3439edb1ab7c9b.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/autoload/block_session_service.gd b/scripts/autoload/block_session_service.gd index 7ebd77b..7b7bdc6 100644 --- a/scripts/autoload/block_session_service.gd +++ b/scripts/autoload/block_session_service.gd @@ -22,7 +22,6 @@ var current_question_started_at := 0.0 var current_question_pause_started_at := 0.0 var core_question_total_elapsed_seconds := 0.0 var core_question_total_active_since := 0.0 -var core_question_display_target_seconds := 90.0 var core_question_total_frozen := false var core_status_message := "" var simulation_block_index := 0 @@ -68,7 +67,6 @@ func start_core_run() -> bool: core_current_had_wrong_attempt = false core_question_total_elapsed_seconds = 0.0 core_question_total_active_since = 0.0 - core_question_display_target_seconds = core_target_seconds core_question_total_frozen = false core_status_message = "" if not advance_core_question(): @@ -320,7 +318,6 @@ func append_core_question(question: Dictionary) -> bool: current_question_started_at = Time.get_ticks_msec() / 1000.0 current_question_pause_started_at = 0.0 core_current_target_seconds = core_target_seconds - core_question_display_target_seconds = core_current_target_seconds core_question_total_elapsed_seconds = 0.0 core_question_total_active_since = current_question_started_at core_question_total_frozen = false @@ -361,12 +358,6 @@ func get_core_question_total_elapsed_seconds() -> float: return maxf(0.0, elapsed) -func get_core_question_elapsed_ratio() -> float: - if session_kind != "core" or core_question_display_target_seconds <= 0.0: - return 0.0 - return clampf(get_core_question_total_elapsed_seconds() / core_question_display_target_seconds, 0.0, 1.0) - - func get_session_timer_ratio() -> float: if session_kind == "core": return get_core_timer_ratio() diff --git a/scripts/ui/question_player.gd b/scripts/ui/question_player.gd index 01f7399..adbb2c5 100644 --- a/scripts/ui/question_player.gd +++ b/scripts/ui/question_player.gd @@ -4,6 +4,8 @@ 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 LABS_DARK_ICON := preload("res://assets/icons/medmind/labs-darkmode.png") +const LABS_LIGHT_ICON := preload("res://assets/icons/medmind/labs-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 @@ -207,7 +209,11 @@ func _ready() -> void: theme_button = Button.new() theme_button.custom_minimum_size = Vector2(44, 44) - theme_button.add_theme_font_size_override("font_size", 22) + 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 UISoundService.set_button_sound(theme_button, "toggle") theme_button.pressed.connect(theme_service.toggle_theme) question_toolbar.add_child(theme_button) @@ -233,9 +239,7 @@ func _ready() -> void: labs_button = Button.new() labs_button.tooltip_text = "Review lab results" - labs_button.icon = _make_labs_icon() labs_button.custom_minimum_size = Vector2(44, 44) - Style.style_chip(labs_button, Style.LIME) UISoundService.set_button_sound(labs_button, "open") labs_button.pressed.connect(_show_labs_modal) question_toolbar.add_child(labs_button) @@ -657,6 +661,10 @@ func _theme_toggle_box(dark: bool, hovered: bool = false) -> StyleBoxFlat: 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) + box.content_margin_left = 0 + box.content_margin_right = 0 + box.content_margin_top = 0 + box.content_margin_bottom = 0 return box @@ -669,20 +677,20 @@ func _style_theme_toggle_button(dark: bool) -> void: 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()) + theme_button.add_theme_constant_override("h_separation", 0) 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) + button.icon = null + button.focus_mode = Control.FOCUS_NONE + var texture_box := StyleBoxTexture.new() + texture_box.texture = texture + for state in ["normal", "hover", "pressed"]: + button.add_theme_stylebox_override(state, texture_box) + button.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) func _primary_action_box(dark: bool, state: String) -> StyleBoxFlat: @@ -806,7 +814,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 [text_smaller_button, text_larger_button, labs_button, flag_button, app_feedback_button]: + for control in [text_smaller_button, text_larger_button, flag_button, app_feedback_button]: if control == null: continue var active: bool = control == flag_button and flagged @@ -815,6 +823,7 @@ 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(labs_button, LABS_DARK_ICON if dark else LABS_LIGHT_ICON) _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) @@ -1637,26 +1646,18 @@ func _update_timer_bar() -> void: if timer_bar == null: return var is_core_timer := BlockSessionService.session_kind == "core" - var ratio := BlockSessionService.get_core_question_elapsed_ratio() if is_core_timer else BlockSessionService.get_session_timer_ratio() + var ratio := BlockSessionService.get_session_timer_ratio() timer_bar.value = ratio * 100.0 if is_core_timer and timer_label.visible: _set_timer_display(int(floor(BlockSessionService.get_core_question_total_elapsed_seconds()))) var color_state := "blue" var fill_color := Brand.ROYAL_BLUE - if is_core_timer: - if ratio >= 1.0: - color_state = "red" - fill_color = Style.RED - elif ratio >= 0.75: - color_state = "amber" - fill_color = Style.AMBER - else: - if ratio < 0.25: - color_state = "red" - fill_color = Style.RED - elif ratio <= 0.5: - color_state = "amber" - fill_color = Style.AMBER + if ratio < 0.25: + color_state = "red" + fill_color = Style.RED + elif ratio <= 0.5: + color_state = "amber" + fill_color = Style.AMBER if color_state != timer_color_state: timer_color_state = color_state timer_bar.add_theme_stylebox_override("fill", Style.button_box(fill_color, fill_color)) @@ -1856,8 +1857,6 @@ func _refresh_toolbar_state() -> void: func refresh_theme_controls() -> void: 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() _refresh_toolbar_state() @@ -3318,33 +3317,6 @@ func _update_question_elo_label(show_label: bool) -> void: question_elo_label.visible = true -func _make_labs_icon() -> Texture2D: - var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) - image.fill(Color(0, 0, 0, 0)) - var glass_color := Style.TEXT - var fluid_color := Style.LIME - for x in range(8, 16): - image.set_pixel(x, 4, glass_color) - for y in range(5, 10): - image.set_pixel(10, y, glass_color) - image.set_pixel(13, y, glass_color) - for y in range(10, 20): - var left := 10 - int(floor(float(y - 10) / 3.0)) - var right := 13 + int(floor(float(y - 10) / 3.0)) - image.set_pixel(left, y, glass_color) - image.set_pixel(right, y, glass_color) - for x in range(7, 17): - image.set_pixel(x, 20, glass_color) - for y in range(15, 20): - for x in range(8, 16): - if x >= 10 - int(floor(float(y - 10) / 3.0)) and x <= 13 + int(floor(float(y - 10) / 3.0)): - image.set_pixel(x, y, fluid_color) - image.set_pixel(7, 7, Style.CYAN) - image.set_pixel(17, 9, Style.CYAN) - image.set_pixel(18, 6, Style.CYAN) - return ImageTexture.create_from_image(image) - - func _make_flag_icon() -> Texture2D: var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) image.fill(Color(0, 0, 0, 0)) diff --git a/tests/choice_pool_test.gd b/tests/choice_pool_test.gd index 0e56732..c421969 100644 --- a/tests/choice_pool_test.gd +++ b/tests/choice_pool_test.gd @@ -29,11 +29,9 @@ func _initialize() -> void: service.seconds_remaining = 450.0 _assert(is_equal_approx(service.get_session_timer_ratio(), 0.5), "timed block progress should use the full session duration") service.session_kind = "core" - service.core_question_display_target_seconds = 100.0 service.core_question_total_elapsed_seconds = 25.0 service.core_question_total_active_since = 0.0 _assert(is_equal_approx(service.get_core_question_total_elapsed_seconds(), 25.0), "core question timer should expose cumulative elapsed time") - _assert(is_equal_approx(service.get_core_question_elapsed_ratio(), 0.25), "core question progress should fill toward its original target") service._start_core_retry_window() _assert(is_equal_approx(service.get_core_question_total_elapsed_seconds(), 25.0), "wrong-answer retry windows should not reset cumulative question time") service.core_question_total_active_since = 100.0 diff --git a/tests/post_answer_review_test.gd b/tests/post_answer_review_test.gd index e75b305..d5c3723 100644 --- a/tests/post_answer_review_test.gd +++ b/tests/post_answer_review_test.gd @@ -36,17 +36,20 @@ func _test_core_timer_counts_up_and_remains_visible() -> void: player.timer_footer = HBoxContainer.new() player.timer_color_state = "" block_service.session_kind = "core" - block_service.core_question_display_target_seconds = 100.0 + block_service.core_current_target_seconds = 100.0 + block_service.current_question_started_at = 0.001 + block_service.current_question_pause_started_at = 25.001 block_service.core_question_total_elapsed_seconds = 75.0 block_service.core_question_total_active_since = 0.0 player._set_answer_review_timer_visibility() _assert(player.timer_footer.visible and player.timer_bar.visible and player.timer_label.visible, "core timer should remain visible during correct-answer review") - _assert(is_equal_approx(player.timer_bar.value, 75.0), "core timer bar should fill with elapsed progress") + _assert(is_equal_approx(player.timer_bar.value, 75.0), "core timer bar should retain its original remaining-time progress") _assert(player.timer_label.text == "01:15", "core timer should display cumulative elapsed time") - _assert(player.timer_color_state == "amber", "core timer should warn when approaching its target") + _assert(player.timer_color_state == "blue", "core timer should retain its original remaining-time warning behavior") block_service.core_question_total_elapsed_seconds = 110.0 + block_service.current_question_pause_started_at = 110.001 player._update_timer_bar() - _assert(is_equal_approx(player.timer_bar.value, 100.0) and player.timer_color_state == "red", "core timer should remain full and red after exceeding its target") + _assert(is_equal_approx(player.timer_bar.value, 0.0) and player.timer_color_state == "red", "core timer bar should empty and turn red after exceeding its target") block_service.session_kind = "block" player._set_answer_review_timer_visibility() _assert(not player.timer_footer.visible, "non-core answer review should retain its existing hidden timer") @@ -342,12 +345,18 @@ func _test_player_runtime_construction() -> void: _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") + var clear_box := player.clear_highlight_button.get_theme_stylebox("normal") as StyleBoxTexture + var report_box := player.report_button.get_theme_stylebox("normal") as StyleBoxTexture + var labs_box := player.labs_button.get_theme_stylebox("normal") as StyleBoxTexture + _assert(clear_box != null and clear_box.texture != null, "clear highlights should use the supplied toolbar artwork") + _assert(report_box != null and report_box.texture != null, "report question should use the supplied toolbar artwork") + _assert(labs_box != null and labs_box.texture != null, "lab results 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") + clear_box = player.clear_highlight_button.get_theme_stylebox("normal") as StyleBoxTexture + _assert(clear_box.texture == 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") + report_box = player.report_button.get_theme_stylebox("normal") as StyleBoxTexture + _assert(report_box.texture == 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")