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.
This commit is contained in:
hz
2026-07-23 00:14:49 -05:00
parent aef287ac26
commit 9d18704ad0
5 changed files with 137 additions and 22 deletions
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="15" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 118 B

@@ -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
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="14.25" fill="#ffffff" fill-opacity="0.08" stroke="#ffffff" stroke-opacity="0.55" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 199 B

@@ -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
+45 -22
View File
@@ -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 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_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 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 FEEDBACK_FADE_SECONDS := 0.45
const CORRECT_ADVANCE_DELAY_SECONDS := 1.25 const CORRECT_ADVANCE_DELAY_SECONDS := 1.25
const TEXT_SCALE_MIN := 0.85 const TEXT_SCALE_MIN := 0.85
@@ -104,6 +106,7 @@ var question_content: VBoxContainer
var media_container: VBoxContainer var media_container: VBoxContainer
var timer_bar: ProgressBar var timer_bar: ProgressBar
var choice_badges: Array[Label] = [] var choice_badges: Array[Label] = []
var choice_badge_textures: Array[TextureRect] = []
var feedback_fog: ColorRect var feedback_fog: ColorRect
var labs_overlay: Control var labs_overlay: Control
var labs_panel: PanelContainer var labs_panel: PanelContainer
@@ -424,7 +427,7 @@ func _ready() -> void:
question_content.add_child(question_elo_label) question_content.add_child(question_elo_label)
var choices_container := VBoxContainer.new() 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) question_content.add_child(choices_container)
for index in range(5): for index in range(5):
@@ -434,7 +437,7 @@ func _ready() -> void:
choice_rows.append(choice_row) choice_rows.append(choice_row)
var button := Button.new() var button := Button.new()
button.custom_minimum_size = Vector2(0, 54) button.custom_minimum_size = Vector2(0, 68)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT button.alignment = HORIZONTAL_ALIGNMENT_LEFT
Style.style_answer_button(button) Style.style_answer_button(button)
UISoundService.set_button_sound(button, "select") 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)) button.gui_input.connect(func(event: InputEvent, i := index): _choice_gui_input(event, i))
choice_buttons.append(button) 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() var badge := Label.new()
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
badge.vertical_alignment = VERTICAL_ALIGNMENT_CENTER badge.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
badge.anchor_left = 0.0 badge.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
badge.anchor_top = 0.0 _set_scaled_label(badge, 15, Brand.muted_color(true))
badge.anchor_right = 0.0 badge_wrap.add_child(badge)
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)
choice_badges.append(badge) choice_badges.append(badge)
var percent_label := Label.new() var percent_label := Label.new()
@@ -469,9 +488,9 @@ func _ready() -> void:
percent_label.anchor_right = 1.0 percent_label.anchor_right = 1.0
percent_label.anchor_bottom = 0.0 percent_label.anchor_bottom = 0.0
percent_label.offset_left = -82 percent_label.offset_left = -82
percent_label.offset_top = 8 percent_label.offset_top = 18
percent_label.offset_right = -18 percent_label.offset_right = -18
percent_label.offset_bottom = 40 percent_label.offset_bottom = 50
_set_scaled_label(percent_label, 18, Style.TEXT) _set_scaled_label(percent_label, 18, Style.TEXT)
button.add_child(percent_label) button.add_child(percent_label)
choice_percent_labels.append(percent_label) choice_percent_labels.append(percent_label)
@@ -486,10 +505,10 @@ func _ready() -> void:
top_label.anchor_top = 0.0 top_label.anchor_top = 0.0
top_label.anchor_right = 1.0 top_label.anchor_right = 1.0
top_label.anchor_bottom = 0.0 top_label.anchor_bottom = 0.0
top_label.offset_left = 54 top_label.offset_left = 62
top_label.offset_top = 10 top_label.offset_top = 18
top_label.offset_right = -96 top_label.offset_right = -96
top_label.offset_bottom = 42 top_label.offset_bottom = 50
_set_scaled_rich_text(top_label, 16) _set_scaled_rich_text(top_label, 16)
top_label.add_theme_color_override("default_color", Style.RED) top_label.add_theme_color_override("default_color", Style.RED)
button.add_child(top_label) button.add_child(top_label)
@@ -682,10 +701,10 @@ func _question_content_box(dark: bool) -> StyleBoxFlat:
func _answer_box(dark: bool, state: String) -> StyleBoxFlat: func _answer_box(dark: bool, state: String) -> StyleBoxFlat:
var box := StyleBoxFlat.new() var box := StyleBoxFlat.new()
box.content_margin_left = 54 box.content_margin_left = 62
box.content_margin_right = 82 box.content_margin_right = 82
box.content_margin_top = 16 box.content_margin_top = 22
box.content_margin_bottom = 16 box.content_margin_bottom = 22
if state == "default" or state == "eliminated": if state == "default" or state == "eliminated":
# Flat, divider-style row - most choices sit in a plain list; only the # Flat, divider-style row - most choices sit in a plain list; only the
# states below need to visually pop out as their own card. # 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 if state == "selected": accent = Brand.PURPLE
elif state == "correct": accent = Brand.SUCCESS elif state == "correct": accent = Brand.SUCCESS
elif state == "wrong": accent = Color("fb7185") elif state == "wrong": accent = Color("fb7185")
badge.add_theme_stylebox_override("normal", Brand.icon_circle_box(dark, accent, 15, state in ["selected", "correct", "wrong"])) var filled := 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)) 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: func _mark_review_input_handled() -> void: