From bc9f68aaf200f95c06a00ee99d8ca601de4e0887 Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 1 Aug 2026 04:20:55 -0500 Subject: [PATCH] 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 --- scripts/ui/simulation_break.gd | 105 +++++++++-- scripts/ui/simulation_review.gd | 320 +++++++++++++++++++++++++------- 2 files changed, 338 insertions(+), 87 deletions(-) diff --git a/scripts/ui/simulation_break.gd b/scripts/ui/simulation_break.gd index a89393a..a9b6662 100644 --- a/scripts/ui/simulation_break.gd +++ b/scripts/ui/simulation_break.gd @@ -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) diff --git a/scripts/ui/simulation_review.gd b/scripts/ui/simulation_review.gd index 6636389..8527ab3 100644 --- a/scripts/ui/simulation_review.gd +++ b/scripts/ui/simulation_review.gd @@ -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 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 "" - var answer := Label.new() - answer.text = "%s. %s%s" % [choice_id, str(choice.get("text", "")), suffix] - 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, "")) - 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", [])) + 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 _add_heading(value: String) -> void: +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() + 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" % [choice_id, choice_text] + answer.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + 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 + 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 _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