Redesign flag/report icons as vector assets and dock review panels inside their choice card

Flag question now uses a clean vector flag (dark/light SVG variants,
replacing a hand-drawn pixel-art icon that got muddied by the shared
toolbar icon-tint override). Report question is now a megaphone
instead of a second flag glyph, since both toolbar buttons looked
identical before.

The correct-answer explanation and the wrong-answer follow-up quiz
previously rendered as separate bordered boxes sitting below the
entire choice list. Both now dock inside their own choice's card:
the explanation panel reparents onto the correct choice's row, the
follow-up panel was already there but visually detached. A shared
_attached_panel_box style drops the shared border/corner between the
choice button and its panel so they read as one continuous card.
This commit is contained in:
hz
2026-07-23 01:23:53 -05:00
parent b822fe5314
commit 5ba7af9d3e
9 changed files with 181 additions and 64 deletions
+79 -56
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 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 FLAG_DARK_ICON := preload("res://assets/icons/medmind/flag-dark.svg")
const FLAG_LIGHT_ICON := preload("res://assets/icons/medmind/flag-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
@@ -274,7 +276,6 @@ func _ready() -> void:
flag_button = Button.new()
flag_button.toggle_mode = true
flag_button.tooltip_text = "Flag question"
flag_button.icon = _make_flag_icon()
flag_button.custom_minimum_size = Vector2(44, 44)
Style.style_chip(flag_button, Style.AMBER)
UISoundService.set_button_sound(flag_button, "toggle")
@@ -434,7 +435,7 @@ func _ready() -> void:
for index in range(5):
var choice_row := VBoxContainer.new()
choice_row.add_theme_constant_override("separation", 6)
choice_row.add_theme_constant_override("separation", 0)
choices_container.add_child(choice_row)
choice_rows.append(choice_row)
@@ -701,7 +702,7 @@ func _question_content_box(dark: bool) -> StyleBoxFlat:
return box
func _answer_box(dark: bool, state: String) -> StyleBoxFlat:
func _answer_box(dark: bool, state: String, attached: bool = false) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.content_margin_left = 62
box.content_margin_right = 82
@@ -727,6 +728,46 @@ func _answer_box(dark: bool, state: String) -> StyleBoxFlat:
box.border_color = border
box.set_border_width_all(2)
box.set_corner_radius_all(14)
if attached:
# A review/follow-up panel is docked directly beneath this choice - drop the
# shared edge so the two read as one continuous card instead of two stacked boxes.
box.border_width_bottom = 0
box.corner_radius_bottom_left = 0
box.corner_radius_bottom_right = 0
return box
func _attached_panel_box(dark: bool, accent: Color) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("17183b") if dark else Color("eff4ff")
if accent == Brand.SUCCESS:
box.bg_color = Color("08251d") if dark else Color("ecfdf3")
elif accent == Color("fb7185"):
box.bg_color = Color("32131d") if dark else Color("fff1f2")
box.border_color = accent
box.border_width_left = 2
box.border_width_right = 2
box.border_width_bottom = 2
box.border_width_top = 0
box.corner_radius_top_left = 0
box.corner_radius_top_right = 0
box.corner_radius_bottom_left = 14
box.corner_radius_bottom_right = 14
box.content_margin_left = 12
box.content_margin_right = 12
box.content_margin_top = 10
box.content_margin_bottom = 10
return box
func _correct_review_panel_box(dark: bool) -> StyleBoxFlat:
# correct_review_panel already pads its content via an inner MarginContainer,
# so skip the shared attached-panel content margin to avoid doubling it up.
var box := _attached_panel_box(dark, Brand.SUCCESS)
box.content_margin_left = 0
box.content_margin_right = 0
box.content_margin_top = 0
box.content_margin_bottom = 0
return box
@@ -768,7 +809,7 @@ func _style_theme_toggle_button(dark: bool) -> void:
theme_button.add_theme_constant_override("h_separation", 0)
func _style_asset_toolbar_button(button: Button, texture: Texture2D, dark: bool) -> void:
func _style_asset_toolbar_button(button: Button, texture: Texture2D, dark: bool, active: bool = false) -> void:
if button == null:
return
button.text = ""
@@ -782,7 +823,7 @@ func _style_asset_toolbar_button(button: Button, texture: Texture2D, dark: bool)
button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
button.add_theme_constant_override("h_separation", 0)
button.add_theme_constant_override("icon_max_width", 23)
button.add_theme_stylebox_override("normal", _toolbar_box(dark))
button.add_theme_stylebox_override("normal", _toolbar_box(dark, active))
button.add_theme_stylebox_override("hover", _toolbar_box(dark, true))
button.add_theme_stylebox_override("pressed", _toolbar_box(dark, true))
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
@@ -813,15 +854,6 @@ func _primary_action_box(dark: bool, state: String) -> StyleBoxFlat:
return box
func _correct_review_box(dark: bool) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("08251d") if dark else Color("f0fdf4")
box.border_color = Color(Brand.SUCCESS, 0.62)
box.set_border_width_all(1)
box.set_corner_radius_all(7)
return box
func _teaching_card_box(dark: bool, accent: Color) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color(accent, 0.08 if dark else 0.045)
@@ -906,15 +938,15 @@ func _apply_question_theme() -> void:
if wrong_options_heading != null:
wrong_options_heading.add_theme_color_override("font_color", Brand.text_color(dark))
if correct_review_panel != null:
correct_review_panel.add_theme_stylebox_override("panel", _correct_review_box(dark))
correct_review_panel.add_theme_stylebox_override("panel", _correct_review_panel_box(dark))
question_elo_label.add_theme_color_override("font_color", Brand.AMBER)
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, flag_button, bookmark_button, notes_button, flashcard_button, app_feedback_button]:
for control in [text_smaller_button, text_larger_button, bookmark_button, notes_button, flashcard_button, app_feedback_button]:
if control == null:
continue
var active: bool = (control == flag_button and flagged) or (control == bookmark_button and BookmarkService.is_bookmarked(str(BlockSessionService.get_current_question().get("id", ""))))
var active: bool = control == bookmark_button and BookmarkService.is_bookmarked(str(BlockSessionService.get_current_question().get("id", "")))
control.add_theme_stylebox_override("normal", _toolbar_box(dark, active))
control.add_theme_stylebox_override("hover", _toolbar_box(dark, true))
control.add_theme_stylebox_override("pressed", _toolbar_box(dark, true))
@@ -923,6 +955,7 @@ func _apply_question_theme() -> void:
_style_asset_toolbar_button(labs_button, LABS_DARK_ICON if dark else LABS_LIGHT_ICON, dark)
_style_asset_toolbar_button(clear_highlight_button, CLEAR_HIGHLIGHT_DARK_ICON if dark else CLEAR_HIGHLIGHT_LIGHT_ICON, dark)
_style_asset_toolbar_button(report_button, REPORT_QUESTION_DARK_ICON if dark else REPORT_QUESTION_LIGHT_ICON, dark)
_style_asset_toolbar_button(flag_button, FLAG_DARK_ICON if dark else FLAG_LIGHT_ICON, dark, flagged)
_style_theme_toggle_button(dark)
if skip_button != null:
skip_button.add_theme_stylebox_override("normal", _toolbar_box(dark))
@@ -1485,6 +1518,10 @@ func _show_explanation() -> void:
var question := BlockSessionService.get_current_question()
for button in choice_buttons:
button.disabled = true
var correct_id := str(question.get("correct_choice_id", ""))
_dock_correct_review_panel(correct_id)
correct_review_panel.visible = true
correct_answer_heading.text = "Correct Answer: %s" % _display_label_for_choice_id(correct_id)
var choices := _get_presented_choices()
for index in range(choices.size()):
var choice: Dictionary = choices[index]
@@ -1495,14 +1532,19 @@ func _show_explanation() -> void:
explanation_plain_text = str(question.get("explanation", ""))
_render_explanation()
explanation_label.visible = true
correct_review_panel.visible = true
var correct_id := str(question.get("correct_choice_id", ""))
correct_answer_heading.text = "Correct Answer: %s" % _display_label_for_choice_id(correct_id)
_update_question_elo_label(true)
_refresh_nav_palette()
_update_content_vertical_alignment()
func _dock_correct_review_panel(correct_id: String) -> void:
var index := _choice_index_for_id(correct_id)
if index < 0 or index >= choice_rows.size():
return
if correct_review_panel.get_parent() != choice_rows[index]:
correct_review_panel.reparent(choice_rows[index])
func _render_answer_review_sections() -> void:
_render_teaching_cards()
_render_review_followups()
@@ -2110,8 +2152,6 @@ func _on_review_pass_completed(summary: Dictionary) -> void:
func refresh_theme_controls() -> void:
if media_viewer_background != null:
media_viewer_background.color = Style.BACKGROUND
if flag_button != null:
flag_button.icon = _make_flag_icon()
_refresh_toolbar_state()
_apply_saved_choice_state()
_apply_question_theme()
@@ -3035,11 +3075,12 @@ func _style_choice_button_for_state(index: int, choice_id: String) -> void:
elif eliminated.has(choice_id):
state = "eliminated"
var button := choice_buttons[index]
var normal_box := _answer_box(dark, state)
var attached := _row_has_attached_panel(index)
var normal_box := _answer_box(dark, state, attached)
button.add_theme_stylebox_override("normal", normal_box)
button.add_theme_stylebox_override("disabled", normal_box)
button.add_theme_stylebox_override("pressed", _answer_box(dark, "selected" if state == "default" else state))
button.add_theme_stylebox_override("hover", _answer_box(dark, "selected" if state == "default" else state))
button.add_theme_stylebox_override("pressed", _answer_box(dark, "selected" if state == "default" else state, attached))
button.add_theme_stylebox_override("hover", _answer_box(dark, "selected" if state == "default" else state, attached))
var text_color := Brand.muted_color(dark) if state == "eliminated" else Brand.text_color(dark)
button.add_theme_color_override("font_color", text_color)
button.add_theme_color_override("font_disabled_color", text_color)
@@ -3057,6 +3098,16 @@ func _style_choice_button_for_state(index: int, choice_id: String) -> void:
badge.add_theme_color_override("font_color", Color.WHITE if filled else Brand.muted_color(dark))
func _row_has_attached_panel(index: int) -> bool:
if index < 0 or index >= choice_rows.size():
return false
if index < choice_followup_panels.size() and choice_followup_panels[index].visible:
return true
if correct_review_panel != null and correct_review_panel.visible and correct_review_panel.get_parent() == choice_rows[index]:
return true
return false
func _mark_review_input_handled() -> void:
get_viewport().set_input_as_handled()
@@ -3266,6 +3317,7 @@ func _render_followup_state(choice_id: String) -> void:
else:
_build_resolved_followup(index, state)
choice_followup_panels[index].visible = true
_style_choice_button_for_state(index, choice_id)
func _build_resolved_followup(index: int, state: Dictionary) -> void:
@@ -3351,26 +3403,13 @@ func _add_followup_body(content: VBoxContainer, is_correct: bool) -> VBoxContain
func _style_followup_card(index: int, state: Dictionary) -> void:
if index < 0 or index >= choice_followup_panels.size():
return
var is_correct := bool(state.get("is_main_correct", false))
choice_followup_panels[index].add_theme_stylebox_override(
"panel",
_followup_card_box(_followup_is_dark(), bool(state.get("is_main_correct", false)))
_attached_panel_box(_followup_is_dark(), Brand.SUCCESS if is_correct else Color("fb7185"))
)
func _followup_card_box(dark: bool, is_correct: bool) -> StyleBoxFlat:
var accent := Brand.SUCCESS if is_correct else Color("fb5f6d")
var box := StyleBoxFlat.new()
box.bg_color = Color("0b172b") if dark else Color("ffffff")
box.border_color = Color(accent, 0.72)
box.set_border_width_all(1)
box.set_corner_radius_all(7)
box.content_margin_left = 12
box.content_margin_right = 12
box.content_margin_top = 10
box.content_margin_bottom = 10
return box
func _followup_body_box(dark: bool, is_correct: bool) -> StyleBoxFlat:
var accent := Brand.SUCCESS if is_correct else Color("fb5f6d")
var box := StyleBoxFlat.new()
@@ -3529,22 +3568,6 @@ func _update_question_elo_label(show_label: bool) -> void:
question_elo_label.visible = true
func _make_flag_icon() -> Texture2D:
var image := Image.create(24, 24, false, Image.FORMAT_RGBA8)
image.fill(Color(0, 0, 0, 0))
for y in range(4, 20):
image.set_pixel(6, y, Style.TEXT)
image.set_pixel(7, y, Style.TEXT)
for x in range(8, 18):
for y in range(4, 11):
if x < 16 or y < 9:
image.set_pixel(x, y, Style.AMBER)
for y in range(11, 14):
for x in range(8, 14 - (y - 11)):
image.set_pixel(x, y, Style.AMBER)
return ImageTexture.create_from_image(image)
func _refresh_nav_palette() -> void:
if nav_palette == null:
return