Restyle the simulation break and review screens to match the results screen
Both were still on the legacy flat-panel Style system, which made "Review Questions" on the redesigned results screen drop into a visibly older-looking screen. Rebuilt both with the same Brand card/ badge/accent-color language: a hero card for the break screen, and color-coded (green/red-tinted, bordered) choice cards, accent-colored filter chips, and icon headings for the review screen. Pure re-skin - same functions, same data flow, only what they render into changed. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -1,66 +1,105 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||
|
||||
var main: Control
|
||||
var card: PanelContainer
|
||||
var eyebrow: Label
|
||||
var title: Label
|
||||
var detail: Label
|
||||
var start_button: Button
|
||||
var abandon_button: Button
|
||||
var error_label: Label
|
||||
var note: Label
|
||||
var abandon_armed := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("separation", 18)
|
||||
add_theme_constant_override("separation", 16)
|
||||
if not ExamSimulationService.state_changed.is_connected(_on_state_changed):
|
||||
ExamSimulationService.state_changed.connect(_on_state_changed)
|
||||
if not ExamSimulationService.request_failed.is_connected(_on_failed):
|
||||
ExamSimulationService.request_failed.connect(_on_failed)
|
||||
|
||||
var eyebrow := Label.new()
|
||||
card = PanelContainer.new()
|
||||
add_child(card)
|
||||
var card_margin := MarginContainer.new()
|
||||
card_margin.add_theme_constant_override("margin_left", 24)
|
||||
card_margin.add_theme_constant_override("margin_right", 24)
|
||||
card_margin.add_theme_constant_override("margin_top", 22)
|
||||
card_margin.add_theme_constant_override("margin_bottom", 22)
|
||||
card.add_child(card_margin)
|
||||
var card_content := VBoxContainer.new()
|
||||
card_content.add_theme_constant_override("separation", 10)
|
||||
card_margin.add_child(card_content)
|
||||
|
||||
eyebrow = Label.new()
|
||||
eyebrow.text = "STEP 3 DAY 1 SIMULATION"
|
||||
Style.set_label(eyebrow, 14, Style.CYAN)
|
||||
add_child(eyebrow)
|
||||
eyebrow.add_theme_font_override("font", Brand.font)
|
||||
eyebrow.add_theme_font_size_override("font_size", 13)
|
||||
card_content.add_child(eyebrow)
|
||||
|
||||
title = Label.new()
|
||||
Style.set_label(title, 34, Style.TEXT)
|
||||
add_child(title)
|
||||
title.add_theme_font_override("font", Brand.font)
|
||||
title.add_theme_font_size_override("font_size", 26)
|
||||
card_content.add_child(title)
|
||||
|
||||
detail = Label.new()
|
||||
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(detail, 18, Style.MUTED)
|
||||
add_child(detail)
|
||||
detail.add_theme_font_override("font", Brand.font)
|
||||
detail.add_theme_font_size_override("font_size", 15)
|
||||
card_content.add_child(detail)
|
||||
|
||||
var actions := HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 12)
|
||||
card_content.add_child(actions)
|
||||
|
||||
start_button = Button.new()
|
||||
start_button.custom_minimum_size = Vector2(240, 54)
|
||||
start_button.custom_minimum_size = Vector2(220, 52)
|
||||
start_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_primary_button(start_button)
|
||||
_style_gradient_button(start_button)
|
||||
start_button.pressed.connect(_start_block)
|
||||
add_child(start_button)
|
||||
actions.add_child(start_button)
|
||||
|
||||
abandon_button = Button.new()
|
||||
abandon_button.text = "Abandon simulation"
|
||||
abandon_button.custom_minimum_size = Vector2(240, 44)
|
||||
abandon_button.custom_minimum_size = Vector2(200, 52)
|
||||
abandon_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_chip(abandon_button, Style.RED)
|
||||
abandon_button.pressed.connect(_abandon)
|
||||
add_child(abandon_button)
|
||||
actions.add_child(abandon_button)
|
||||
|
||||
error_label = Label.new()
|
||||
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(error_label, 16, Style.RED)
|
||||
error_label.add_theme_font_override("font", Brand.font)
|
||||
error_label.add_theme_font_size_override("font_size", 13)
|
||||
error_label.add_theme_color_override("font_color", Brand.MAGENTA)
|
||||
add_child(error_label)
|
||||
|
||||
var note := Label.new()
|
||||
note = Label.new()
|
||||
note.text = "Each block runs for 30 minutes. You may review and change answers within the active block, but closed blocks cannot be reopened. Leaving during a block does not stop its server timer."
|
||||
note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(note, 16, Style.MUTED)
|
||||
note.add_theme_font_override("font", Brand.font)
|
||||
note.add_theme_font_size_override("font_size", 12)
|
||||
add_child(note)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(spacer)
|
||||
|
||||
UIThemeService.theme_changed.connect(func(_dark: bool): refresh_theme_controls())
|
||||
refresh_theme_controls()
|
||||
|
||||
|
||||
func refresh_theme_controls() -> void:
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE))
|
||||
eyebrow.add_theme_color_override("font_color", Brand.SKY_BLUE)
|
||||
title.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
detail.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
note.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
_style_outline_button(abandon_button, Brand.MAGENTA, dark)
|
||||
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
abandon_armed = false
|
||||
@@ -123,3 +162,33 @@ func _on_failed(message: String) -> void:
|
||||
error_label.text = message
|
||||
start_button.disabled = false
|
||||
_refresh()
|
||||
|
||||
|
||||
func _style_gradient_button(button: Button) -> void:
|
||||
Brand.apply_font(button)
|
||||
button.add_theme_font_size_override("font_size", 15)
|
||||
button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
|
||||
button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
|
||||
button.add_theme_stylebox_override("pressed", Brand.gradient_button_box())
|
||||
button.add_theme_color_override("font_color", Color.WHITE)
|
||||
button.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
|
||||
|
||||
func _style_outline_button(button: Button, accent: Color, dark: bool) -> void:
|
||||
Brand.apply_font(button)
|
||||
button.add_theme_font_size_override("font_size", 14)
|
||||
var normal := StyleBoxFlat.new()
|
||||
normal.bg_color = Color(0, 0, 0, 0)
|
||||
normal.border_color = accent
|
||||
normal.set_border_width_all(1)
|
||||
normal.set_corner_radius_all(10)
|
||||
var hover := StyleBoxFlat.new()
|
||||
hover.bg_color = Color(accent, 0.12)
|
||||
hover.border_color = accent
|
||||
hover.set_border_width_all(1)
|
||||
hover.set_corner_radius_all(10)
|
||||
button.add_theme_stylebox_override("normal", normal)
|
||||
button.add_theme_stylebox_override("hover", hover)
|
||||
button.add_theme_stylebox_override("pressed", hover)
|
||||
button.add_theme_color_override("font_color", accent if dark else accent.darkened(0.08))
|
||||
button.add_theme_color_override("font_hover_color", accent)
|
||||
|
||||
+244
-62
@@ -1,7 +1,11 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||
const EXPLANATION_ICON := preload("res://assets/icons/lucide/book-open.svg")
|
||||
const LAB_ICON := preload("res://assets/icons/lucide/microscope.svg")
|
||||
const REFERENCE_ICON := preload("res://assets/icons/lucide/link.svg")
|
||||
const BLOCK_SIZES := [20, 19, 19, 20, 19, 19, 20, 19, 19, 20, 19, 19]
|
||||
const FILTER_DEFS := [["All", "all", 0], ["Incorrect", "incorrect", 1], ["Unanswered", "unanswered", 2], ["Flagged", "flagged", 3]]
|
||||
|
||||
var main: Control
|
||||
var simulation := {}
|
||||
@@ -10,7 +14,9 @@ var responses := {}
|
||||
var filtered_indices: Array[int] = []
|
||||
var review_position := 0
|
||||
var active_filter := "all"
|
||||
var back_button: Button
|
||||
var progress_label: Label
|
||||
var filter_buttons: Dictionary = {}
|
||||
var content: VBoxContainer
|
||||
var previous_button: Button
|
||||
var next_button: Button
|
||||
@@ -18,41 +24,49 @@ var empty_label: Label
|
||||
var media_requests: Array[HTTPRequest] = []
|
||||
var image_overlay: Control
|
||||
var image_texture: TextureRect
|
||||
var image_close_button: Button
|
||||
var return_screen := "selection"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("separation", 10)
|
||||
add_theme_constant_override("separation", 12)
|
||||
|
||||
var toolbar := HBoxContainer.new()
|
||||
toolbar.add_theme_constant_override("separation", 8)
|
||||
toolbar.add_theme_constant_override("separation", 12)
|
||||
add_child(toolbar)
|
||||
var back := Button.new()
|
||||
back.text = "< Results"
|
||||
Style.style_chip(back, Style.CYAN)
|
||||
back.pressed.connect(func(): main.show_screen("simulation_results", {"simulation": simulation, "return_screen": return_screen}))
|
||||
toolbar.add_child(back)
|
||||
back_button = Button.new()
|
||||
back_button.text = "< Results"
|
||||
back_button.custom_minimum_size = Vector2(0, 40)
|
||||
back_button.pressed.connect(func(): main.show_screen("simulation_results", {"simulation": simulation, "return_screen": return_screen}))
|
||||
toolbar.add_child(back_button)
|
||||
progress_label = Label.new()
|
||||
progress_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
Style.set_label(progress_label, 17, Style.TEXT)
|
||||
progress_label.add_theme_font_override("font", Brand.font)
|
||||
progress_label.add_theme_font_size_override("font_size", 15)
|
||||
toolbar.add_child(progress_label)
|
||||
var toolbar_spacer := Control.new()
|
||||
toolbar_spacer.custom_minimum_size.x = back_button.custom_minimum_size.x
|
||||
toolbar.add_child(toolbar_spacer)
|
||||
|
||||
var filters := GridContainer.new()
|
||||
filters.columns = 4
|
||||
filters.add_theme_constant_override("h_separation", 8)
|
||||
filters.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
add_child(filters)
|
||||
var group := ButtonGroup.new()
|
||||
for definition in [["All", "all"], ["Incorrect", "incorrect"], ["Unanswered", "unanswered"], ["Flagged", "flagged"]]:
|
||||
for definition in FILTER_DEFS:
|
||||
var button := Button.new()
|
||||
button.text = definition[0]
|
||||
button.toggle_mode = true
|
||||
button.button_group = group
|
||||
button.button_pressed = definition[1] == "all"
|
||||
Style.style_chip(button, Style.CYAN)
|
||||
button.custom_minimum_size = Vector2(0, 38)
|
||||
button.pressed.connect(_set_filter.bind(definition[1]))
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
filters.add_child(button)
|
||||
filter_buttons[definition[1]] = button
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
@@ -60,12 +74,13 @@ func _ready() -> void:
|
||||
add_child(scroll)
|
||||
content = VBoxContainer.new()
|
||||
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content.add_theme_constant_override("separation", 10)
|
||||
content.add_theme_constant_override("separation", 14)
|
||||
scroll.add_child(content)
|
||||
empty_label = Label.new()
|
||||
empty_label.visible = false
|
||||
empty_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
Style.set_label(empty_label, 19, Style.MUTED)
|
||||
empty_label.add_theme_font_override("font", Brand.font)
|
||||
empty_label.add_theme_font_size_override("font_size", 16)
|
||||
content.add_child(empty_label)
|
||||
|
||||
var navigation := HBoxContainer.new()
|
||||
@@ -73,8 +88,7 @@ func _ready() -> void:
|
||||
add_child(navigation)
|
||||
previous_button = Button.new()
|
||||
previous_button.text = "< Previous"
|
||||
previous_button.custom_minimum_size = Vector2(180, 48)
|
||||
Style.style_chip(previous_button, Style.CYAN)
|
||||
previous_button.custom_minimum_size = Vector2(160, 48)
|
||||
previous_button.pressed.connect(_move.bind(-1))
|
||||
navigation.add_child(previous_button)
|
||||
var spacer := Control.new()
|
||||
@@ -82,12 +96,28 @@ func _ready() -> void:
|
||||
navigation.add_child(spacer)
|
||||
next_button = Button.new()
|
||||
next_button.text = "Next >"
|
||||
next_button.custom_minimum_size = Vector2(180, 48)
|
||||
Style.style_primary_button(next_button)
|
||||
next_button.custom_minimum_size = Vector2(160, 48)
|
||||
next_button.pressed.connect(_move.bind(1))
|
||||
navigation.add_child(next_button)
|
||||
_build_image_overlay()
|
||||
|
||||
UIThemeService.theme_changed.connect(func(_dark: bool): refresh_theme_controls())
|
||||
refresh_theme_controls()
|
||||
|
||||
|
||||
func refresh_theme_controls() -> void:
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
_style_outline_button(back_button, Brand.ROYAL_BLUE, dark)
|
||||
progress_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
for filter_name in filter_buttons:
|
||||
var accent := _filter_accent(filter_name)
|
||||
_style_filter_chip(filter_buttons[filter_name], accent, dark)
|
||||
_style_outline_button(previous_button, Brand.SLATE, dark)
|
||||
_style_gradient_button(next_button)
|
||||
if is_instance_valid(image_close_button):
|
||||
_style_outline_button(image_close_button, Brand.SLATE, dark)
|
||||
_render_question()
|
||||
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
if typeof(payload) == TYPE_DICTIONARY:
|
||||
@@ -99,6 +129,7 @@ func on_show(payload: Variant = null) -> void:
|
||||
for response in simulation.get("responses", []):
|
||||
responses[str(response.get("questionExternalId", ""))] = response
|
||||
active_filter = "all"
|
||||
filter_buttons["all"].button_pressed = true
|
||||
review_position = 0
|
||||
_apply_filter()
|
||||
|
||||
@@ -151,11 +182,14 @@ func _move(direction: int) -> void:
|
||||
func _render_question() -> void:
|
||||
_clear_content()
|
||||
_clear_media_requests()
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
if filtered_indices.is_empty():
|
||||
empty_label = Label.new()
|
||||
empty_label.text = "No questions match this filter."
|
||||
empty_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
Style.set_label(empty_label, 19, Style.MUTED)
|
||||
empty_label.add_theme_font_override("font", Brand.font)
|
||||
empty_label.add_theme_font_size_override("font_size", 16)
|
||||
empty_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
content.add_child(empty_label)
|
||||
progress_label.text = "0 questions"
|
||||
previous_button.disabled = true
|
||||
@@ -166,70 +200,151 @@ func _render_question() -> void:
|
||||
var response: Dictionary = responses.get(str(question.get("externalId", "")), {})
|
||||
var selected := str(response.get("choiceId", ""))
|
||||
var correct := str(question.get("correctChoiceId", ""))
|
||||
progress_label.text = "Question %d of %d | Block %d | %s" % [review_position + 1, filtered_indices.size(), _block_for_index(question_index) + 1, "Correct" if selected == correct else "Unanswered" if selected == "" else "Incorrect"]
|
||||
var outcome := "Correct" if selected == correct else "Unanswered" if selected == "" else "Incorrect"
|
||||
progress_label.text = "Question %d of %d | Block %d | %s" % [review_position + 1, filtered_indices.size(), _block_for_index(question_index) + 1, outcome]
|
||||
previous_button.disabled = review_position == 0
|
||||
next_button.disabled = review_position + 1 >= filtered_indices.size()
|
||||
_add_text(str(question.get("stem", "")), 18, Style.TEXT)
|
||||
_add_media(question.get("media", []))
|
||||
_add_labs(question.get("labs", []))
|
||||
_add_text(str(question.get("leadIn", "")), 21, Style.TEXT)
|
||||
|
||||
var stem_card := _card(dark)
|
||||
var stem_column := VBoxContainer.new()
|
||||
stem_column.add_theme_constant_override("separation", 10)
|
||||
stem_card.get_child(0).add_child(stem_column)
|
||||
_add_text(stem_column, str(question.get("stem", "")), 15, Brand.text_color(dark), dark)
|
||||
_add_media(stem_column, question.get("media", []))
|
||||
_add_labs(stem_column, question.get("labs", []), dark)
|
||||
content.add_child(stem_card)
|
||||
_add_text(content, str(question.get("leadIn", "")), 17, Brand.text_color(dark), dark, true)
|
||||
|
||||
var explanations: Dictionary = question.get("choiceExplanations", {})
|
||||
for raw_choice in question.get("choices", []):
|
||||
var choice: Dictionary = raw_choice
|
||||
var choice_id := str(choice.get("id", ""))
|
||||
var is_correct := choice_id == correct
|
||||
var is_selected := choice_id == selected
|
||||
content.add_child(_build_choice_row(dark, choice_id, str(choice.get("text", "")), str(explanations.get(choice_id, "")), is_correct, is_selected))
|
||||
|
||||
_add_heading(content, "Explanation", EXPLANATION_ICON, dark)
|
||||
_add_text(content, str(question.get("explanation", "")), 14, Brand.text_color(dark), dark)
|
||||
_add_references(content, question.get("references", []), dark)
|
||||
|
||||
|
||||
func _build_choice_row(dark: bool, choice_id: String, choice_text: String, explanation: String, is_correct: bool, is_selected: bool) -> PanelContainer:
|
||||
var accent := Brand.SUCCESS if is_correct else (Brand.MAGENTA if is_selected else Brand.SLATE)
|
||||
var tinted := is_correct or is_selected
|
||||
var panel := PanelContainer.new()
|
||||
var border := Style.LIME if choice_id == correct else Style.RED if choice_id == selected else Style.BORDER
|
||||
var background := Color("1f7a3a") if choice_id == correct else Color("8f1d2c") if choice_id == selected else Style.SURFACE
|
||||
panel.add_theme_stylebox_override("panel", Style.panel_box(background, border))
|
||||
content.add_child(panel)
|
||||
var choice_content := VBoxContainer.new()
|
||||
panel.add_child(choice_content)
|
||||
var suffix := " [Correct answer]" if choice_id == correct else " [Your answer]" if choice_id == selected else ""
|
||||
panel.add_theme_stylebox_override("panel", _choice_box(dark, accent, tinted))
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 16)
|
||||
margin.add_theme_constant_override("margin_right", 16)
|
||||
margin.add_theme_constant_override("margin_top", 12)
|
||||
margin.add_theme_constant_override("margin_bottom", 12)
|
||||
panel.add_child(margin)
|
||||
var column := VBoxContainer.new()
|
||||
column.add_theme_constant_override("separation", 6)
|
||||
margin.add_child(column)
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
column.add_child(row)
|
||||
var answer := Label.new()
|
||||
answer.text = "%s. %s%s" % [choice_id, str(choice.get("text", "")), suffix]
|
||||
answer.text = "%s. %s" % [choice_id, choice_text]
|
||||
answer.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(answer, 17, Style.TEXT)
|
||||
choice_content.add_child(answer)
|
||||
var explanation := str(explanations.get(choice_id, ""))
|
||||
answer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
answer.add_theme_font_override("font", Brand.font)
|
||||
answer.add_theme_font_size_override("font_size", 14)
|
||||
answer.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
row.add_child(answer)
|
||||
if is_correct or is_selected:
|
||||
var badge := Label.new()
|
||||
badge.text = "Correct answer" if is_correct else "Your answer"
|
||||
badge.add_theme_font_override("font", Brand.font)
|
||||
badge.add_theme_font_size_override("font_size", 11)
|
||||
badge.add_theme_color_override("font_color", accent)
|
||||
badge.add_theme_stylebox_override("normal", Brand.badge_box(dark, accent))
|
||||
badge.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
row.add_child(badge)
|
||||
|
||||
if not explanation.is_empty():
|
||||
var detail := Label.new()
|
||||
detail.text = explanation
|
||||
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(detail, 15, Style.TEXT)
|
||||
choice_content.add_child(detail)
|
||||
_add_heading("Explanation")
|
||||
_add_text(str(question.get("explanation", "")), 17, Style.TEXT)
|
||||
_add_references(question.get("references", []))
|
||||
detail.add_theme_font_override("font", Brand.font)
|
||||
detail.add_theme_font_size_override("font_size", 12)
|
||||
detail.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
column.add_child(detail)
|
||||
|
||||
return panel
|
||||
|
||||
|
||||
func _add_heading(value: String) -> void:
|
||||
func _choice_box(dark: bool, accent: Color, tinted: bool) -> StyleBoxFlat:
|
||||
var box := StyleBoxFlat.new()
|
||||
if tinted:
|
||||
box.bg_color = Color(accent, 0.16 if dark else 0.10)
|
||||
box.border_color = accent
|
||||
else:
|
||||
box.bg_color = Color("11182e") if dark else Brand.LIGHT_SURFACE
|
||||
box.border_color = Color("2a365a") if dark else Brand.LIGHT_BORDER
|
||||
box.set_border_width_all(1)
|
||||
box.set_corner_radius_all(12)
|
||||
return box
|
||||
|
||||
|
||||
func _card(dark: bool) -> PanelContainer:
|
||||
var panel := PanelContainer.new()
|
||||
panel.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 16)
|
||||
margin.add_theme_constant_override("margin_right", 16)
|
||||
margin.add_theme_constant_override("margin_top", 14)
|
||||
margin.add_theme_constant_override("margin_bottom", 14)
|
||||
panel.add_child(margin)
|
||||
return panel
|
||||
|
||||
|
||||
func _add_heading(parent: Control, value: String, icon: Texture2D, dark: bool) -> void:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
var icon_rect := TextureRect.new()
|
||||
icon_rect.texture = icon
|
||||
icon_rect.custom_minimum_size = Vector2(18, 18)
|
||||
icon_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon_rect.modulate = Brand.ROYAL_BLUE
|
||||
row.add_child(icon_rect)
|
||||
var label := Label.new()
|
||||
label.text = value
|
||||
Style.set_label(label, 20, Style.CYAN)
|
||||
content.add_child(label)
|
||||
label.add_theme_font_override("font", Brand.font)
|
||||
label.add_theme_font_size_override("font_size", 16)
|
||||
label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
row.add_child(label)
|
||||
parent.add_child(row)
|
||||
|
||||
|
||||
func _add_text(value: String, size_value: int, color: Color) -> void:
|
||||
func _add_text(parent: Control, value: String, size_value: int, color: Color, _dark: bool, bold_ish: bool = false) -> void:
|
||||
if value.is_empty():
|
||||
return
|
||||
var label := Label.new()
|
||||
label.text = value
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(label, size_value, color)
|
||||
content.add_child(label)
|
||||
label.add_theme_font_override("font", Brand.font)
|
||||
label.add_theme_font_size_override("font_size", size_value + 2 if bold_ish else size_value)
|
||||
label.add_theme_color_override("font_color", color)
|
||||
parent.add_child(label)
|
||||
|
||||
|
||||
func _add_labs(labs: Array) -> void:
|
||||
func _add_labs(parent: Control, labs: Array, dark: bool) -> void:
|
||||
if labs.is_empty():
|
||||
return
|
||||
_add_heading("Laboratory studies")
|
||||
_add_heading(parent, "Laboratory studies", LAB_ICON, dark)
|
||||
for raw_lab in labs:
|
||||
var lab: Dictionary = raw_lab
|
||||
_add_text("%s: %s" % [str(lab.get("name", lab.get("test", ""))), str(lab.get("value", ""))], 15, Style.TEXT)
|
||||
_add_text(parent, "%s: %s" % [str(lab.get("name", lab.get("test", ""))), str(lab.get("value", ""))], 13, Brand.muted_color(dark), dark)
|
||||
|
||||
|
||||
func _add_references(references: Array) -> void:
|
||||
func _add_references(parent: Control, references: Array, dark: bool) -> void:
|
||||
if references.is_empty():
|
||||
return
|
||||
_add_heading("References")
|
||||
_add_heading(parent, "References", REFERENCE_ICON, dark)
|
||||
for raw_reference in references:
|
||||
var reference: Dictionary = raw_reference
|
||||
var url := str(reference.get("url", ""))
|
||||
@@ -240,12 +355,12 @@ func _add_references(references: Array) -> void:
|
||||
link.tooltip_text = url
|
||||
link.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
link.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
link.add_theme_color_override("font_color", Style.CYAN)
|
||||
link.add_theme_color_override("font_color", Brand.SKY_BLUE)
|
||||
link.pressed.connect(func(target_url: String = url): OS.shell_open(target_url))
|
||||
content.add_child(link)
|
||||
parent.add_child(link)
|
||||
|
||||
|
||||
func _add_media(media: Array) -> void:
|
||||
func _add_media(parent: Control, media: Array) -> void:
|
||||
for raw_url in media:
|
||||
var url := _resolve_media_url(str(raw_url))
|
||||
if url.is_empty():
|
||||
@@ -255,7 +370,7 @@ func _add_media(media: Array) -> void:
|
||||
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
preview.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
content.add_child(preview)
|
||||
parent.add_child(preview)
|
||||
var request := HTTPRequest.new()
|
||||
request.request_completed.connect(func(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray): _on_media_loaded(result, code, body, preview))
|
||||
media_requests.append(request)
|
||||
@@ -287,7 +402,7 @@ func _build_image_overlay() -> void:
|
||||
image_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(image_overlay)
|
||||
var background := ColorRect.new()
|
||||
background.color = Color(Style.BACKGROUND, 0.97)
|
||||
background.color = Color(Brand.background_color(UIThemeService.is_dark_mode()), 0.97)
|
||||
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
image_overlay.add_child(background)
|
||||
image_texture = TextureRect.new()
|
||||
@@ -299,13 +414,12 @@ func _build_image_overlay() -> void:
|
||||
image_texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
image_texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
image_overlay.add_child(image_texture)
|
||||
var close := Button.new()
|
||||
close.text = "X"
|
||||
close.position = Vector2(16, 16)
|
||||
close.custom_minimum_size = Vector2(44, 44)
|
||||
Style.style_chip(close, Style.CYAN)
|
||||
close.pressed.connect(func(): image_overlay.visible = false)
|
||||
image_overlay.add_child(close)
|
||||
image_close_button = Button.new()
|
||||
image_close_button.text = "X"
|
||||
image_close_button.position = Vector2(16, 16)
|
||||
image_close_button.custom_minimum_size = Vector2(44, 44)
|
||||
image_close_button.pressed.connect(func(): image_overlay.visible = false)
|
||||
image_overlay.add_child(image_close_button)
|
||||
|
||||
|
||||
func _resolve_media_url(value: String) -> String:
|
||||
@@ -337,6 +451,74 @@ func _block_for_index(index: int) -> int:
|
||||
return BLOCK_SIZES.size() - 1
|
||||
|
||||
|
||||
func _filter_accent(filter_name: String) -> Color:
|
||||
match filter_name:
|
||||
"incorrect": return Brand.MAGENTA
|
||||
"unanswered": return Brand.SLATE
|
||||
"flagged": return Brand.AMBER
|
||||
_: return Brand.ROYAL_BLUE
|
||||
|
||||
|
||||
func _style_filter_chip(button: Button, accent: Color, dark: bool) -> void:
|
||||
Brand.apply_font(button)
|
||||
button.add_theme_font_size_override("font_size", 13)
|
||||
var normal := StyleBoxFlat.new()
|
||||
normal.bg_color = Color(accent, 0.06 if dark else 0.045)
|
||||
normal.border_color = Color(accent, 0.35)
|
||||
normal.set_border_width_all(1)
|
||||
normal.set_corner_radius_all(9)
|
||||
var hover := StyleBoxFlat.new()
|
||||
hover.bg_color = Color(accent, 0.18)
|
||||
hover.border_color = accent
|
||||
hover.set_border_width_all(1)
|
||||
hover.set_corner_radius_all(9)
|
||||
var pressed := StyleBoxFlat.new()
|
||||
pressed.bg_color = Color(accent, 0.62)
|
||||
pressed.border_color = accent
|
||||
pressed.set_border_width_all(1)
|
||||
pressed.set_corner_radius_all(9)
|
||||
button.add_theme_stylebox_override("normal", normal)
|
||||
button.add_theme_stylebox_override("hover", hover)
|
||||
button.add_theme_stylebox_override("pressed", pressed)
|
||||
button.add_theme_color_override("font_color", accent if dark else accent.darkened(0.1))
|
||||
button.add_theme_color_override("font_hover_color", accent)
|
||||
button.add_theme_color_override("font_pressed_color", Brand.DARK_TEXT)
|
||||
|
||||
|
||||
func _style_gradient_button(button: Button) -> void:
|
||||
Brand.apply_font(button)
|
||||
button.add_theme_font_size_override("font_size", 15)
|
||||
button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
|
||||
button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
|
||||
button.add_theme_stylebox_override("pressed", Brand.gradient_button_box())
|
||||
button.add_theme_stylebox_override("disabled", Brand.gradient_button_box())
|
||||
button.add_theme_color_override("font_color", Color.WHITE)
|
||||
button.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
button.add_theme_color_override("font_disabled_color", Color(Color.WHITE, 0.5))
|
||||
|
||||
|
||||
func _style_outline_button(button: Button, accent: Color, dark: bool) -> void:
|
||||
Brand.apply_font(button)
|
||||
button.add_theme_font_size_override("font_size", 14)
|
||||
var normal := StyleBoxFlat.new()
|
||||
normal.bg_color = Color(0, 0, 0, 0)
|
||||
normal.border_color = accent
|
||||
normal.set_border_width_all(1)
|
||||
normal.set_corner_radius_all(10)
|
||||
var hover := StyleBoxFlat.new()
|
||||
hover.bg_color = Color(accent, 0.12)
|
||||
hover.border_color = accent
|
||||
hover.set_border_width_all(1)
|
||||
hover.set_corner_radius_all(10)
|
||||
button.add_theme_stylebox_override("normal", normal)
|
||||
button.add_theme_stylebox_override("hover", hover)
|
||||
button.add_theme_stylebox_override("pressed", hover)
|
||||
button.add_theme_stylebox_override("disabled", normal)
|
||||
button.add_theme_color_override("font_color", accent if dark else accent.darkened(0.08))
|
||||
button.add_theme_color_override("font_hover_color", accent)
|
||||
button.add_theme_color_override("font_disabled_color", Color(accent, 0.4))
|
||||
|
||||
|
||||
static func _matches_filter(filter_name: String, selected: String, correct: String, flagged: bool) -> bool:
|
||||
match filter_name:
|
||||
"incorrect": return selected != "" and selected != correct
|
||||
|
||||
Reference in New Issue
Block a user