From 9d18704ad07a9313022c762bb24ee54479aad28b Mon Sep 17 00:00:00 2001 From: hz Date: Thu, 23 Jul 2026 00:14:49 -0500 Subject: [PATCH] Render answer-choice badges as SVGs and give rows more breathing room StyleBoxFlat-drawn circles kept looking rough at this size even after tuning corner detail/antialiasing, so the badges are now two small SVG assets (outline and filled) tinted per-state via modulate instead - crisp regardless of size. Also increased row height, badge size, and margins across the board; the previous pass's padding bump was too timid and the rows still read as cramped. --- assets/icons/medmind/badge-circle-filled.svg | 3 + .../medmind/badge-circle-filled.svg.import | 43 ++++++++++++ assets/icons/medmind/badge-circle-outline.svg | 3 + .../medmind/badge-circle-outline.svg.import | 43 ++++++++++++ scripts/ui/question_player.gd | 67 +++++++++++++------ 5 files changed, 137 insertions(+), 22 deletions(-) create mode 100644 assets/icons/medmind/badge-circle-filled.svg create mode 100644 assets/icons/medmind/badge-circle-filled.svg.import create mode 100644 assets/icons/medmind/badge-circle-outline.svg create mode 100644 assets/icons/medmind/badge-circle-outline.svg.import diff --git a/assets/icons/medmind/badge-circle-filled.svg b/assets/icons/medmind/badge-circle-filled.svg new file mode 100644 index 0000000..5661376 --- /dev/null +++ b/assets/icons/medmind/badge-circle-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/medmind/badge-circle-filled.svg.import b/assets/icons/medmind/badge-circle-filled.svg.import new file mode 100644 index 0000000..d50f21c --- /dev/null +++ b/assets/icons/medmind/badge-circle-filled.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://neyyjsv1yi2y" +path="res://.godot/imported/badge-circle-filled.svg-c615dfa6e3ba91563385092d88757ba4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/medmind/badge-circle-filled.svg" +dest_files=["res://.godot/imported/badge-circle-filled.svg-c615dfa6e3ba91563385092d88757ba4.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 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/icons/medmind/badge-circle-outline.svg b/assets/icons/medmind/badge-circle-outline.svg new file mode 100644 index 0000000..5c299dc --- /dev/null +++ b/assets/icons/medmind/badge-circle-outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/medmind/badge-circle-outline.svg.import b/assets/icons/medmind/badge-circle-outline.svg.import new file mode 100644 index 0000000..3edb651 --- /dev/null +++ b/assets/icons/medmind/badge-circle-outline.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://2epi5x1l2k3k" +path="res://.godot/imported/badge-circle-outline.svg-48e5795ac9a851c7826f85026138d065.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/medmind/badge-circle-outline.svg" +dest_files=["res://.godot/imported/badge-circle-outline.svg-48e5795ac9a851c7826f85026138d065.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 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/scripts/ui/question_player.gd b/scripts/ui/question_player.gd index 1ca107a..96eeecc 100644 --- a/scripts/ui/question_player.gd +++ b/scripts/ui/question_player.gd @@ -9,6 +9,8 @@ const LABS_DARK_ICON := preload("res://assets/icons/medmind/labs-dark.svg") const LABS_LIGHT_ICON := preload("res://assets/icons/medmind/labs-light.svg") const REPORT_QUESTION_DARK_ICON := preload("res://assets/icons/medmind/report-question-dark.svg") const REPORT_QUESTION_LIGHT_ICON := preload("res://assets/icons/medmind/report-question-light.svg") +const BADGE_CIRCLE_OUTLINE_ICON := preload("res://assets/icons/medmind/badge-circle-outline.svg") +const BADGE_CIRCLE_FILLED_ICON := preload("res://assets/icons/medmind/badge-circle-filled.svg") const FEEDBACK_FADE_SECONDS := 0.45 const CORRECT_ADVANCE_DELAY_SECONDS := 1.25 const TEXT_SCALE_MIN := 0.85 @@ -104,6 +106,7 @@ var question_content: VBoxContainer var media_container: VBoxContainer var timer_bar: ProgressBar var choice_badges: Array[Label] = [] +var choice_badge_textures: Array[TextureRect] = [] var feedback_fog: ColorRect var labs_overlay: Control var labs_panel: PanelContainer @@ -424,7 +427,7 @@ func _ready() -> void: question_content.add_child(question_elo_label) var choices_container := VBoxContainer.new() - choices_container.add_theme_constant_override("separation", 8) + choices_container.add_theme_constant_override("separation", 10) question_content.add_child(choices_container) for index in range(5): @@ -434,7 +437,7 @@ func _ready() -> void: choice_rows.append(choice_row) var button := Button.new() - button.custom_minimum_size = Vector2(0, 54) + button.custom_minimum_size = Vector2(0, 68) button.alignment = HORIZONTAL_ALIGNMENT_LEFT Style.style_answer_button(button) UISoundService.set_button_sound(button, "select") @@ -443,20 +446,36 @@ func _ready() -> void: button.gui_input.connect(func(event: InputEvent, i := index): _choice_gui_input(event, i)) choice_buttons.append(button) + var badge_wrap := Control.new() + badge_wrap.mouse_filter = Control.MOUSE_FILTER_IGNORE + badge_wrap.anchor_left = 0.0 + badge_wrap.anchor_top = 0.0 + badge_wrap.anchor_right = 0.0 + badge_wrap.anchor_bottom = 0.0 + badge_wrap.offset_left = 16 + badge_wrap.offset_top = 17 + badge_wrap.offset_right = 50 + badge_wrap.offset_bottom = 51 + button.add_child(badge_wrap) + + # The badge circle is a pre-rendered SVG (tinted per-state via modulate) rather than a + # StyleBoxFlat corner-radius circle - a perfectly circular StyleBoxFlat is a known Godot + # degenerate case that can show a seam/faceting at this size even with AA tuned. + var badge_texture := TextureRect.new() + badge_texture.mouse_filter = Control.MOUSE_FILTER_IGNORE + badge_texture.texture = BADGE_CIRCLE_OUTLINE_ICON + badge_texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + badge_texture.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + badge_wrap.add_child(badge_texture) + choice_badge_textures.append(badge_texture) + var badge := Label.new() badge.mouse_filter = Control.MOUSE_FILTER_IGNORE badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER badge.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - badge.anchor_left = 0.0 - badge.anchor_top = 0.0 - badge.anchor_right = 0.0 - badge.anchor_bottom = 0.0 - badge.offset_left = 12 - badge.offset_top = 12 - badge.offset_right = 42 - badge.offset_bottom = 42 - _set_scaled_label(badge, 14, Brand.muted_color(true)) - button.add_child(badge) + badge.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _set_scaled_label(badge, 15, Brand.muted_color(true)) + badge_wrap.add_child(badge) choice_badges.append(badge) var percent_label := Label.new() @@ -469,9 +488,9 @@ func _ready() -> void: percent_label.anchor_right = 1.0 percent_label.anchor_bottom = 0.0 percent_label.offset_left = -82 - percent_label.offset_top = 8 + percent_label.offset_top = 18 percent_label.offset_right = -18 - percent_label.offset_bottom = 40 + percent_label.offset_bottom = 50 _set_scaled_label(percent_label, 18, Style.TEXT) button.add_child(percent_label) choice_percent_labels.append(percent_label) @@ -486,10 +505,10 @@ func _ready() -> void: top_label.anchor_top = 0.0 top_label.anchor_right = 1.0 top_label.anchor_bottom = 0.0 - top_label.offset_left = 54 - top_label.offset_top = 10 + top_label.offset_left = 62 + top_label.offset_top = 18 top_label.offset_right = -96 - top_label.offset_bottom = 42 + top_label.offset_bottom = 50 _set_scaled_rich_text(top_label, 16) top_label.add_theme_color_override("default_color", Style.RED) button.add_child(top_label) @@ -682,10 +701,10 @@ func _question_content_box(dark: bool) -> StyleBoxFlat: func _answer_box(dark: bool, state: String) -> StyleBoxFlat: var box := StyleBoxFlat.new() - box.content_margin_left = 54 + box.content_margin_left = 62 box.content_margin_right = 82 - box.content_margin_top = 16 - box.content_margin_bottom = 16 + box.content_margin_top = 22 + box.content_margin_bottom = 22 if state == "default" or state == "eliminated": # Flat, divider-style row - most choices sit in a plain list; only the # states below need to visually pop out as their own card. @@ -3015,8 +3034,12 @@ func _style_choice_button_for_state(index: int, choice_id: String) -> void: if state == "selected": accent = Brand.PURPLE elif state == "correct": accent = Brand.SUCCESS elif state == "wrong": accent = Color("fb7185") - badge.add_theme_stylebox_override("normal", Brand.icon_circle_box(dark, accent, 15, state in ["selected", "correct", "wrong"])) - badge.add_theme_color_override("font_color", Color.WHITE if state in ["selected", "correct", "wrong"] else Brand.muted_color(dark)) + var filled := state in ["selected", "correct", "wrong"] + if index < choice_badge_textures.size(): + var badge_texture := choice_badge_textures[index] + badge_texture.texture = BADGE_CIRCLE_FILLED_ICON if filled else BADGE_CIRCLE_OUTLINE_ICON + badge_texture.modulate = accent + badge.add_theme_color_override("font_color", Color.WHITE if filled else Brand.muted_color(dark)) func _mark_review_input_handled() -> void: