Redesign question bank screen to match new mockup

Adapts the quiz screen's header, toolbar, choice cards, and bottom bar
to the designer's new layout:

- Header: adds the RanaZe logo/wordmark and a question-progress bar +
  percentage alongside the existing "Question N of M" text.
- Toolbar: consolidates 12 icon-only/chip buttons into 6 labeled items
  (Text size, Calculator, Clear highlight, Notes, Bookmark, More), with
  a new "More" popup holding the rest (labs, flag, report, flashcard,
  theme toggle, feedback). The underlying buttons are reparented, not
  duplicated, so every existing theming/visibility/state call site
  keeps working unmodified.
- Category tag: shows the question's discipline (falling back to
  system) above the stem.
- Choice cards: every choice now renders as a subtly-bordered rounded
  card even before answering, not just a flat divider row; the
  currently-selected pre-submit choice shows a checkmark.
- Bottom bar: merges the Skip/Submit row and the timer row into one -
  Skip as a plain link, a centered "X of Y" progress bar, and a
  time slot that shows the existing countdown when a limit applies or
  a new count-up stopwatch ("Time elapsed") otherwise, plus a
  Ctrl+Enter/Cmd+Enter submit shortcut with a visible hint.
This commit is contained in:
hz
2026-07-24 02:28:22 -05:00
parent edcc0def5d
commit 974de67032
2 changed files with 354 additions and 43 deletions
+351 -42
View File
@@ -65,7 +65,14 @@ var awaiting_next_question := false
var simulation_submit_pending := false
var progress_button: Button
var progress_bar: ProgressBar
var progress_percent_label: Label
var bottom_progress_label: Label
var bottom_progress_bar: ProgressBar
var header_wordmark_label: Label
var category_label: Label
var timer_label: Label
var timer_caption_label: Label
var stem_label: RichTextLabel
var lead_label: Label
var explanation_label: RichTextLabel
@@ -92,6 +99,10 @@ var flashcard_prefill_dialog: Control
var bookmark_button: Button
var app_feedback_button: Button
var theme_button: Button
var more_button: Button
var more_menu_popup: PopupPanel
var text_size_button: Button
var text_size_popup: PopupPanel
var question_toolbar: HFlowContainer
var header_stack: VBoxContainer
var header_row: HBoxContainer
@@ -107,6 +118,7 @@ var media_container: VBoxContainer
var timer_bar: ProgressBar
var choice_badges: Array[Label] = []
var choice_badge_textures: Array[TextureRect] = []
var choice_selected_icons: Array[TextureRect] = []
var feedback_fog: ColorRect
var labs_overlay: Control
var labs_panel: PanelContainer
@@ -146,6 +158,7 @@ var green_fog_time := 0.0
var correct_advance_delay := 0.0
var shake_time := 0.0
var shake_origin := Vector2.ZERO
var untimed_elapsed_start_msec := 0
func _ready() -> void:
theme_service = get_node("/root/UIThemeService")
@@ -213,17 +226,53 @@ func _ready() -> void:
header_stack.add_theme_constant_override("separation", 8)
card_content.add_child(header_stack)
header_row = HBoxContainer.new()
header_row.add_theme_constant_override("separation", 10)
header_row.add_theme_constant_override("separation", 16)
header_stack.add_child(header_row)
var header_brand := HBoxContainer.new()
header_brand.add_theme_constant_override("separation", 8)
header_row.add_child(header_brand)
var header_brand_mark := TextureRect.new()
header_brand_mark.texture = Brand.ranaze_mark_texture()
header_brand_mark.custom_minimum_size = Vector2(28, 28)
header_brand_mark.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
header_brand_mark.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
header_brand.add_child(header_brand_mark)
header_wordmark_label = Label.new()
header_wordmark_label.text = "RanaZe"
header_wordmark_label.add_theme_font_override("font", Brand.font)
header_wordmark_label.add_theme_font_size_override("font_size", 18)
header_brand.add_child(header_wordmark_label)
var progress_group := VBoxContainer.new()
progress_group.add_theme_constant_override("separation", 2)
progress_group.size_flags_vertical = Control.SIZE_SHRINK_CENTER
header_row.add_child(progress_group)
progress_button = Button.new()
progress_button.flat = true
progress_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
progress_button.custom_minimum_size = Vector2(126, 34)
progress_button.custom_minimum_size = Vector2(126, 20)
progress_button.tooltip_text = "Open question navigator"
_set_scaled_button_font(progress_button, 14)
progress_button.pressed.connect(_toggle_question_navigator)
header_row.add_child(progress_button)
progress_group.add_child(progress_button)
var progress_bar_row := HBoxContainer.new()
progress_bar_row.add_theme_constant_override("separation", 8)
progress_group.add_child(progress_bar_row)
progress_bar = ProgressBar.new()
progress_bar.show_percentage = false
progress_bar.min_value = 0.0
progress_bar.max_value = 100.0
progress_bar.custom_minimum_size = Vector2(120, 6)
progress_bar.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
progress_bar_row.add_child(progress_bar)
progress_percent_label = Label.new()
_set_scaled_label(progress_percent_label, 13, Brand.SLATE)
progress_bar_row.add_child(progress_percent_label)
var header_spacer := Control.new()
header_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -235,10 +284,14 @@ func _ready() -> void:
timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
question_toolbar = HFlowContainer.new()
question_toolbar.add_theme_constant_override("h_separation", 8)
question_toolbar.add_theme_constant_override("h_separation", 14)
question_toolbar.add_theme_constant_override("v_separation", 8)
question_toolbar.alignment = FlowContainer.ALIGNMENT_END
# All the underlying buttons are constructed up front (same as before) so every
# other place in this file that mutates them by reference - theming, visibility,
# tooltips, badge state - keeps working unmodified. Only *where* each one is
# parented (directly in question_toolbar vs. reparented into a popup) changes.
theme_button = Button.new()
theme_button.custom_minimum_size = Vector2(44, 44)
theme_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
@@ -248,7 +301,6 @@ func _ready() -> void:
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)
_refresh_theme_button()
text_smaller_button = Button.new()
@@ -258,7 +310,6 @@ func _ready() -> void:
Style.style_chip(text_smaller_button, Style.CYAN)
UISoundService.set_button_sound(text_smaller_button, "toggle")
text_smaller_button.pressed.connect(func(): _adjust_question_text_scale(-TEXT_SCALE_STEP))
question_toolbar.add_child(text_smaller_button)
text_larger_button = Button.new()
text_larger_button.text = "A+"
@@ -267,7 +318,6 @@ func _ready() -> void:
Style.style_chip(text_larger_button, Style.CYAN)
UISoundService.set_button_sound(text_larger_button, "toggle")
text_larger_button.pressed.connect(func(): _adjust_question_text_scale(TEXT_SCALE_STEP))
question_toolbar.add_child(text_larger_button)
calculator_button = Button.new()
calculator_button.name = "CalculatorButton"
@@ -280,21 +330,18 @@ func _ready() -> void:
calculator_button.focus_mode = Control.FOCUS_NONE
UISoundService.set_button_sound(calculator_button, "open")
calculator_button.pressed.connect(_toggle_calculator)
question_toolbar.add_child(calculator_button)
labs_button = Button.new()
labs_button.tooltip_text = "Review lab results"
labs_button.custom_minimum_size = Vector2(44, 44)
UISoundService.set_button_sound(labs_button, "open")
labs_button.pressed.connect(_show_labs_modal)
question_toolbar.add_child(labs_button)
clear_highlight_button = Button.new()
clear_highlight_button.tooltip_text = "Clear highlights"
clear_highlight_button.custom_minimum_size = Vector2(44, 44)
UISoundService.set_button_sound(clear_highlight_button, "eliminate")
clear_highlight_button.pressed.connect(_clear_current_highlights)
question_toolbar.add_child(clear_highlight_button)
flag_button = Button.new()
flag_button.toggle_mode = true
@@ -308,7 +355,6 @@ func _ready() -> void:
if BlockSessionService.session_kind == "simulation":
_save_simulation_current_response()
)
question_toolbar.add_child(flag_button)
flashcard_button = Button.new()
flashcard_button.tooltip_text = "Save as flashcard (available once answered)"
@@ -321,7 +367,6 @@ func _ready() -> void:
flashcard_button.disabled = true
UISoundService.set_button_sound(flashcard_button, "toggle")
flashcard_button.pressed.connect(_toggle_flashcard_current_question)
question_toolbar.add_child(flashcard_button)
flashcard_prefill_dialog = FlashcardPrefillDialog.new()
flashcard_prefill_dialog.name = "FlashcardPrefillDialog"
add_child(flashcard_prefill_dialog)
@@ -336,14 +381,12 @@ func _ready() -> void:
bookmark_button.focus_mode = Control.FOCUS_NONE
UISoundService.set_button_sound(bookmark_button, "toggle")
bookmark_button.pressed.connect(_toggle_bookmark_current_question)
question_toolbar.add_child(bookmark_button)
report_button = Button.new()
report_button.tooltip_text = "Report question"
report_button.custom_minimum_size = Vector2(44, 44)
UISoundService.set_button_sound(report_button, "open")
report_button.pressed.connect(_toggle_report_current_question)
question_toolbar.add_child(report_button)
notes_button = Button.new()
notes_button.tooltip_text = "Open notes"
@@ -355,7 +398,6 @@ func _ready() -> void:
notes_button.focus_mode = Control.FOCUS_NONE
UISoundService.set_button_sound(notes_button, "open")
notes_button.pressed.connect(func(): main.open_notes_dialog())
question_toolbar.add_child(notes_button)
app_feedback_button = Button.new()
app_feedback_button.text = "Feedback"
@@ -363,15 +405,69 @@ func _ready() -> void:
app_feedback_button.custom_minimum_size = Vector2(92, 44)
Style.style_chip(app_feedback_button, Style.CYAN)
app_feedback_button.pressed.connect(func(): main.open_feedback_dialog())
question_toolbar.add_child(app_feedback_button)
question_toolbar.move_child(labs_button, 0)
question_toolbar.move_child(theme_button, question_toolbar.get_child_count() - 2)
# Text size: single entry point opening a small popup with the existing A-/A+
# controls untouched, instead of two always-visible buttons.
text_size_button = Button.new()
text_size_button.text = "Aa"
text_size_button.tooltip_text = "Text size"
text_size_button.custom_minimum_size = Vector2(44, 44)
text_size_button.focus_mode = Control.FOCUS_NONE
text_size_button.pressed.connect(_toggle_text_size_popup)
question_toolbar.add_child(_toolbar_labeled_button(text_size_button, "Text size"))
question_toolbar.add_child(_toolbar_labeled_button(calculator_button, "Calculator"))
question_toolbar.add_child(_toolbar_labeled_button(clear_highlight_button, "Clear highlight"))
question_toolbar.add_child(_toolbar_labeled_button(notes_button, "Notes"))
question_toolbar.add_child(_toolbar_labeled_button(bookmark_button, "Bookmark"))
more_button = Button.new()
more_button.text = "..."
more_button.tooltip_text = "More actions"
more_button.custom_minimum_size = Vector2(44, 44)
more_button.focus_mode = Control.FOCUS_NONE
more_button.pressed.connect(_toggle_more_menu)
question_toolbar.add_child(_toolbar_labeled_button(more_button, "More"))
header_toolbar_host = HBoxContainer.new()
header_toolbar_host.alignment = BoxContainer.ALIGNMENT_END
header_toolbar_host.add_child(question_toolbar)
header_row.add_child(header_toolbar_host)
text_size_popup = PopupPanel.new()
text_size_popup.name = "TextSizePopup"
add_child(text_size_popup)
var text_size_margin := MarginContainer.new()
for side in ["left", "right", "top", "bottom"]:
text_size_margin.add_theme_constant_override("margin_%s" % side, 12)
text_size_popup.add_child(text_size_margin)
var text_size_row := HBoxContainer.new()
text_size_row.add_theme_constant_override("separation", 8)
text_size_margin.add_child(text_size_row)
text_size_row.add_child(text_smaller_button)
text_size_row.add_child(text_larger_button)
more_menu_popup = PopupPanel.new()
more_menu_popup.name = "MoreMenu"
more_menu_popup.min_size = Vector2i(260, 0)
add_child(more_menu_popup)
var more_menu_margin := MarginContainer.new()
for side in ["left", "right", "top", "bottom"]:
more_menu_margin.add_theme_constant_override("margin_%s" % side, 10)
more_menu_popup.add_child(more_menu_margin)
var more_menu_content := VBoxContainer.new()
more_menu_content.add_theme_constant_override("separation", 4)
more_menu_margin.add_child(more_menu_content)
for entry in [
[labs_button, "Labs"],
[flag_button, "Flag question"],
[report_button, "Report question"],
[flashcard_button, "Save as flashcard"],
[theme_button, "Switch theme"],
[app_feedback_button, "Feedback"],
]:
more_menu_content.add_child(_more_menu_row(entry[0], entry[1]))
navigator_popup = PopupPanel.new()
navigator_popup.name = "QuestionNavigator"
navigator_popup.min_size = Vector2i(340, 180)
@@ -421,6 +517,11 @@ func _ready() -> void:
question_content.add_theme_constant_override("separation", 12)
content_margin.add_child(question_content)
category_label = Label.new()
category_label.visible = false
_set_scaled_label(category_label, 14, Brand.SKY_BLUE)
question_content.add_child(category_label)
stem_label = RichTextLabel.new()
stem_label.fit_content = true
stem_label.bbcode_enabled = true
@@ -521,6 +622,23 @@ func _ready() -> void:
button.add_child(percent_label)
choice_percent_labels.append(percent_label)
var selected_icon := TextureRect.new()
selected_icon.visible = false
selected_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
selected_icon.texture = preload("res://assets/icons/lucide/circle-check.svg")
selected_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
selected_icon.modulate = Brand.PURPLE
selected_icon.anchor_left = 1.0
selected_icon.anchor_top = 0.0
selected_icon.anchor_right = 1.0
selected_icon.anchor_bottom = 0.0
selected_icon.offset_left = -50
selected_icon.offset_top = 18
selected_icon.offset_right = -18
selected_icon.offset_bottom = 50
button.add_child(selected_icon)
choice_selected_icons.append(selected_icon)
var top_label := RichTextLabel.new()
top_label.visible = false
top_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
@@ -556,24 +674,59 @@ func _ready() -> void:
_build_answer_review_ui()
actions = HBoxContainer.new()
actions.add_theme_constant_override("separation", 10)
actions.add_theme_constant_override("separation", 16)
card_content.add_child(actions)
skip_button = Button.new()
skip_button.text = "Skip"
skip_button.custom_minimum_size = Vector2(120, 48)
Style.style_chip(skip_button, Style.AMBER)
skip_button.text = "Skip question"
skip_button.flat = true
skip_button.custom_minimum_size = Vector2(0, 40)
UISoundService.set_button_sound(skip_button, "back")
skip_button.pressed.connect(_skip_current_question)
actions.add_child(skip_button)
var bottom_progress_group := VBoxContainer.new()
bottom_progress_group.add_theme_constant_override("separation", 4)
bottom_progress_group.size_flags_horizontal = Control.SIZE_EXPAND_FILL
bottom_progress_group.alignment = BoxContainer.ALIGNMENT_CENTER
actions.add_child(bottom_progress_group)
bottom_progress_label = Label.new()
bottom_progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_set_scaled_label(bottom_progress_label, 13, Brand.SLATE)
bottom_progress_group.add_child(bottom_progress_label)
bottom_progress_bar = ProgressBar.new()
bottom_progress_bar.show_percentage = false
bottom_progress_bar.min_value = 0.0
bottom_progress_bar.max_value = 100.0
bottom_progress_bar.custom_minimum_size = Vector2(160, 6)
bottom_progress_bar.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
bottom_progress_group.add_child(bottom_progress_bar)
var time_group := VBoxContainer.new()
time_group.add_theme_constant_override("separation", 0)
actions.add_child(time_group)
timer_caption_label = Label.new()
timer_caption_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_set_scaled_label(timer_caption_label, 12, Brand.SLATE)
time_group.add_child(timer_caption_label)
time_group.add_child(timer_label)
next_button = Button.new()
next_button.text = "Submit Block" if BlockSessionService.session_kind == "simulation" and BlockSessionService.current_index + 1 == BlockSessionService.active_questions.size() else "Next" if BlockSessionService.session_kind == "simulation" else "Submit"
next_button.tooltip_text = ""
next_button.custom_minimum_size = Vector2(140, 48)
next_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
Style.style_primary_button(next_button)
next_button.pressed.connect(_submit_or_next)
actions.add_child(next_button)
card_content.add_child(actions)
var shortcut_hint_label := Label.new()
shortcut_hint_label.name = "SubmitShortcutHint"
shortcut_hint_label.text = "⌘ Enter" if OS.get_name() == "macOS" else "Ctrl+Enter"
_set_scaled_label(shortcut_hint_label, 11, Brand.SLATE)
actions.add_child(shortcut_hint_label)
timer_footer = HBoxContainer.new()
timer_footer.add_theme_constant_override("separation", 12)
@@ -588,7 +741,6 @@ func _ready() -> void:
timer_bar.add_theme_stylebox_override("background", Style.button_box(Color("232b3d"), Color("303b53")))
timer_bar.add_theme_stylebox_override("fill", Style.button_box(Style.LIME, Style.LIME))
timer_footer.add_child(timer_bar)
timer_footer.add_child(timer_label)
feedback_fog = ColorRect.new()
feedback_fog.color = Color(1.0, 0.0, 0.0, 0.0)
@@ -677,7 +829,10 @@ func _fit_modal_panels() -> void:
func _refresh_responsive_layout() -> void:
if header_toolbar_host == null or header_row == null or header_stack == null:
return
var compact := size.x < 1120.0
# The toolbar is now 6 labeled items (with "More" absorbing the rest) instead of
# 12 icon-only buttons, so it's meaningfully narrower - the compact-mode breakpoint
# is tuned down to match rather than reusing the old wider-toolbar threshold.
var compact := size.x < 860.0
var target_parent: Control = header_row
if compact:
target_parent = header_stack
@@ -685,9 +840,7 @@ func _refresh_responsive_layout() -> void:
header_toolbar_host.reparent(target_parent)
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
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
header_toolbar_host.custom_minimum_size.x = 0.0 if compact else 380.0
if size.x < 560.0:
nav_palette.columns = 5
else:
@@ -717,12 +870,12 @@ func _answer_box(dark: bool, state: String, attached: bool = false) -> StyleBoxF
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.
# Every choice reads as its own subtly-bordered card, even before answering -
# only selected/correct/wrong states get a stronger accent border/background.
box.bg_color = Color("0b172b") if dark else Color("ffffff")
box.border_color = Color("22304d") if dark else Color("e7ebf2")
box.border_width_bottom = 1
box.set_corner_radius_all(0)
box.set_border_width_all(1)
box.set_corner_radius_all(14)
return box
var background := Color("17183b") if dark else Color("eff4ff")
var border := Brand.PURPLE
@@ -916,10 +1069,37 @@ 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))
if text_size_popup != null:
text_size_popup.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
if more_menu_popup != null:
more_menu_popup.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
for label in more_menu_popup.find_children("MoreMenuLabel", "Label", true, false):
(label as Label).add_theme_color_override("font_color", Brand.text_color(dark))
if question_toolbar != null:
for label in question_toolbar.find_children("ToolbarLabel", "Label", true, false):
(label as Label).add_theme_color_override("font_color", Brand.muted_color(dark))
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))
if header_wordmark_label != null:
header_wordmark_label.add_theme_color_override("font_color", Brand.text_color(dark))
var progress_track := Style.button_box(Color("232b3d") if dark else Color("e2e8f0"), Color("303b53") if dark else Color("cbd5e1"))
var progress_fill := Style.button_box(Brand.ROYAL_BLUE, Brand.ROYAL_BLUE)
if progress_bar != null:
progress_bar.add_theme_stylebox_override("background", progress_track)
progress_bar.add_theme_stylebox_override("fill", progress_fill)
if progress_percent_label != null:
progress_percent_label.add_theme_color_override("font_color", Brand.muted_color(dark))
if bottom_progress_bar != null:
bottom_progress_bar.add_theme_stylebox_override("background", progress_track)
bottom_progress_bar.add_theme_stylebox_override("fill", progress_fill)
if bottom_progress_label != null:
bottom_progress_label.add_theme_color_override("font_color", Brand.muted_color(dark))
if category_label != null:
category_label.add_theme_color_override("font_color", Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE)
timer_label.add_theme_color_override("font_color", Brand.muted_color(dark))
if timer_caption_label != null:
timer_caption_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))
explanation_label.add_theme_color_override("default_color", Brand.text_color(dark))
@@ -935,7 +1115,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, calculator_button, bookmark_button, notes_button, flashcard_button, app_feedback_button]:
for control in [text_smaller_button, text_larger_button, calculator_button, bookmark_button, notes_button, flashcard_button, app_feedback_button, text_size_button, more_button]:
if control == null:
continue
var active: bool = control == bookmark_button and BookmarkService.is_bookmarked(str(BlockSessionService.get_current_question().get("id", "")))
@@ -952,9 +1132,17 @@ func _apply_question_theme() -> void:
if calculator_panel != null:
calculator_panel.refresh_theme_controls()
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))
skip_button.add_theme_color_override("font_color", Brand.text_color(dark))
var skip_link_color := Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE
skip_button.add_theme_stylebox_override("normal", StyleBoxEmpty.new())
skip_button.add_theme_stylebox_override("hover", StyleBoxEmpty.new())
skip_button.add_theme_stylebox_override("pressed", StyleBoxEmpty.new())
skip_button.add_theme_stylebox_override("disabled", StyleBoxEmpty.new())
skip_button.add_theme_color_override("font_color", skip_link_color)
skip_button.add_theme_color_override("font_hover_color", skip_link_color)
skip_button.add_theme_color_override("font_disabled_color", Brand.muted_color(dark))
var shortcut_hint := find_child("SubmitShortcutHint", true, false) as Label
if shortcut_hint != null:
shortcut_hint.add_theme_color_override("font_color", Brand.muted_color(dark))
if next_button != null:
next_button.add_theme_stylebox_override("normal", _primary_action_box(dark, "normal"))
next_button.add_theme_stylebox_override("hover", _primary_action_box(dark, "hover"))
@@ -1054,6 +1242,12 @@ func _process(delta: float) -> void:
_update_timer_bar()
if BlockSessionService.seconds_remaining <= 0:
_finish()
if visible and not _is_session_timer_visible() and (report_overlay == null or not report_overlay.visible):
# No real time limit applies here - show a simple local count-up stopwatch
# ("Time elapsed") so the bottom-bar time slot is never blank, without
# touching BlockSessionService's own (block-scoped, not per-question) timing.
var elapsed_seconds := int(floor(float(Time.get_ticks_msec() - untimed_elapsed_start_msec) / 1000.0))
_set_timer_display(maxi(0, elapsed_seconds))
func _input(event: InputEvent) -> void:
@@ -1085,6 +1279,15 @@ func _input(event: InputEvent) -> void:
if not visible or pending_correct_advance or stem_label == null:
return
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ENTER and (event.ctrl_pressed or event.meta_pressed):
var focus_owner := get_viewport().gui_get_focus_owner()
var focus_blocks_shortcut := focus_owner is TextEdit or focus_owner is LineEdit
var overlay_blocks_shortcut := (report_overlay != null and report_overlay.visible) or (labs_overlay != null and labs_overlay.visible)
if not focus_blocks_shortcut and not overlay_blocks_shortcut and next_button != null and next_button.visible and not next_button.disabled:
get_viewport().set_input_as_handled()
_submit_or_next()
return
var mouse_event := event as InputEventMouseButton
if mouse_event == null or mouse_event.button_index != MOUSE_BUTTON_LEFT:
return
@@ -1124,7 +1327,7 @@ func _load_question() -> void:
next_button.disabled = true
skip_button.disabled = false
skip_button.visible = BlockSessionService.session_kind not in ["review", "dynamic"]
skip_button.text = "Skip"
skip_button.text = "Skip question"
report_button.visible = BlockSessionService.session_kind != "simulation"
var question := BlockSessionService.get_current_question()
@@ -1141,21 +1344,35 @@ func _load_question() -> void:
progress_button.text = _question_progress_text()
progress_button.disabled = BlockSessionService.session_kind in ["core", "bookmark", "review", "dynamic"]
progress_button.tooltip_text = "" if BlockSessionService.session_kind in ["core", "bookmark", "review", "dynamic"] else "Open question navigator"
_update_progress_indicators()
untimed_elapsed_start_msec = Time.get_ticks_msec()
var show_timer := _is_session_timer_visible()
timer_label.visible = show_timer
# The time slot in the bottom bar is always visible - a countdown ("Time remaining")
# when a time limit applies, otherwise a simple count-up stopwatch ("Time elapsed")
# so the area is never blank for untimed practice.
timer_label.visible = true
timer_bar.visible = show_timer
timer_footer.visible = show_timer
if timer_caption_label != null:
timer_caption_label.text = "Time remaining" if show_timer else "Time elapsed"
if show_timer:
if BlockSessionService.session_kind == "core":
_set_timer_display(int(floor(BlockSessionService.get_core_question_total_elapsed_seconds())))
else:
_set_timer_display(int(ceil(BlockSessionService.seconds_remaining)))
else:
_set_timer_display(0)
_update_timer_bar()
stem_plain_text = str(question.get("stem", ""))
explanation_plain_text = str(question.get("explanation", ""))
_render_stem()
_load_question_media(question)
lead_label.text = question.get("lead_in", "")
var category_text := str(question.get("discipline", "")).strip_edges()
if category_text.is_empty():
category_text = str(question.get("system", "")).strip_edges()
category_label.text = category_text
category_label.visible = not category_text.is_empty()
_refresh_labs_button()
presented_choices = question.get("presented_choices", question.get("choices", [])).duplicate(true)
@@ -1247,9 +1464,11 @@ func _should_clear_selection_from_click(mouse_event: InputEventMouseButton) -> b
return false
if labs_button != null and labs_button.visible and labs_button.get_global_rect().has_point(pointer_position):
return false
if flag_button != null and flag_button.get_global_rect().has_point(pointer_position):
if flag_button != null and flag_button.visible and flag_button.get_global_rect().has_point(pointer_position):
return false
if report_button != null and report_button.get_global_rect().has_point(pointer_position):
if report_button != null and report_button.visible and report_button.get_global_rect().has_point(pointer_position):
return false
if more_menu_popup != null and more_menu_popup.visible and Rect2(more_menu_popup.position, more_menu_popup.size).has_point(pointer_position):
return false
for button in choice_buttons:
if button.visible and button.get_global_rect().has_point(pointer_position):
@@ -1533,7 +1752,7 @@ func _show_answer_review() -> void:
if skip_button != null:
skip_button.disabled = true
skip_button.visible = BlockSessionService.session_kind == "bookmark"
skip_button.text = "Back to Bookmarks" if BlockSessionService.session_kind == "bookmark" else "Skip"
skip_button.text = "Back to Bookmarks" if BlockSessionService.session_kind == "bookmark" else "Skip question"
skip_button.disabled = false if BlockSessionService.session_kind == "bookmark" else true
if question_scroll != null:
question_scroll.call_deferred("ensure_control_visible", explanation_label)
@@ -1928,6 +2147,34 @@ func _question_progress_text() -> String:
]
func _update_progress_indicators() -> void:
# "core" sessions have no fixed total (endless queue), so there's no meaningful
# ratio/percentage to show - hide the bars rather than showing a misleading value.
var show_progress := BlockSessionService.session_kind != "core"
if progress_bar != null:
progress_bar.visible = show_progress
if progress_percent_label != null:
progress_percent_label.visible = show_progress
if bottom_progress_bar != null:
bottom_progress_bar.visible = show_progress
if bottom_progress_label != null:
bottom_progress_label.visible = show_progress
if not show_progress:
return
var total := BlockSessionService.active_questions.size()
var current := mini(BlockSessionService.current_index + 1, maxi(total, 1))
var ratio := float(current) / float(maxi(total, 1))
var percent := int(round(ratio * 100.0))
if progress_bar != null:
progress_bar.value = ratio * 100.0
if progress_percent_label != null:
progress_percent_label.text = "%d%%" % percent
if bottom_progress_bar != null:
bottom_progress_bar.value = ratio * 100.0
if bottom_progress_label != null:
bottom_progress_label.text = "%d of %d" % [current, total]
func _toggle_question_navigator() -> void:
if navigator_popup == null or BlockSessionService.session_kind == "core":
return
@@ -1943,6 +2190,66 @@ func _toggle_question_navigator() -> void:
navigator_popup.popup(Rect2i(Vector2i(popup_x, popup_y), popup_size))
func _toolbar_labeled_button(button: Button, label_text: String) -> Control:
var wrap := VBoxContainer.new()
wrap.add_theme_constant_override("separation", 2)
wrap.alignment = BoxContainer.ALIGNMENT_CENTER
wrap.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
wrap.add_child(button)
var label := Label.new()
label.name = "ToolbarLabel"
label.text = label_text
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
_set_scaled_label(label, 11, Brand.SLATE)
wrap.add_child(label)
return wrap
func _more_menu_row(button: Button, label_text: String) -> Control:
# Reparents the *actual* button (not a duplicate/proxy) so its existing icon,
# tooltip, toggle state, and pressed connection keep working unmodified wherever
# it's mutated by reference elsewhere in this file.
var row := HBoxContainer.new()
row.name = "MoreMenuRow"
row.add_theme_constant_override("separation", 10)
row.add_child(button)
var label := Label.new()
label.name = "MoreMenuLabel"
label.text = label_text
_set_scaled_label(label, 14, Brand.SLATE)
row.add_child(label)
return row
func _toggle_text_size_popup() -> void:
if text_size_popup == null:
return
if text_size_popup.visible:
text_size_popup.hide()
return
var popup_size := Vector2i(140, 76)
var anchor := text_size_button.get_global_rect()
var viewport_size := get_viewport_rect().size
var popup_x := int(clampf(anchor.position.x, 8.0, maxf(8.0, viewport_size.x - popup_size.x - 8.0)))
var popup_y := int(minf(anchor.end.y + 6.0, viewport_size.y - popup_size.y - 8.0))
text_size_popup.popup(Rect2i(Vector2i(popup_x, popup_y), popup_size))
func _toggle_more_menu() -> void:
if more_menu_popup == null:
return
if more_menu_popup.visible:
more_menu_popup.hide()
return
var popup_size := Vector2i(260, 300)
var anchor := more_button.get_global_rect()
var viewport_size := get_viewport_rect().size
var popup_x := int(clampf(anchor.position.x - popup_size.x + anchor.size.x, 8.0, maxf(8.0, viewport_size.x - popup_size.x - 8.0)))
var popup_y := int(minf(anchor.end.y + 6.0, viewport_size.y - popup_size.y - 8.0))
more_menu_popup.popup(Rect2i(Vector2i(popup_x, popup_y), popup_size))
func _navigate_to_question(target_index: int) -> void:
if target_index == BlockSessionService.current_index:
navigator_popup.hide()
@@ -3147,6 +3454,8 @@ func _style_choice_button_for_state(index: int, choice_id: String) -> void:
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))
if index < choice_selected_icons.size():
choice_selected_icons[index].visible = state == "selected"
func _row_has_attached_panel(index: int) -> bool:
+3 -1
View File
@@ -357,7 +357,9 @@ func _test_player_runtime_construction() -> void:
_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.header_row.get_child(0) != player.progress_button, "the brand mark should be the first header item, ahead of question progress")
_assert(player.progress_button.get_parent().get_parent() == player.header_row, "question progress should still live in the header row")
_assert(player.progress_bar != null and player.progress_percent_label != null, "question player should construct a header progress bar and percentage label")
_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")
_assert(player.labs_button.icon != null, "lab results should use the supplied toolbar artwork")