From 5ed96ae3b8aea673d0e7c1676d8182a1d63ee8bc Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 11 Jul 2026 03:06:37 -0500 Subject: [PATCH] Add lab results modal --- scripts/ui/question_player.gd | 194 ++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) diff --git a/scripts/ui/question_player.gd b/scripts/ui/question_player.gd index 5c9cd89..d3e9fc6 100644 --- a/scripts/ui/question_player.gd +++ b/scripts/ui/question_player.gd @@ -34,12 +34,16 @@ var explanation_label: RichTextLabel var question_elo_label: Label var next_button: Button var skip_button: Button +var labs_button: Button var flag_button: Button var report_button: Button var nav_palette: HBoxContainer var question_content: VBoxContainer var timer_bar: ProgressBar var feedback_fog: ColorRect +var labs_overlay: Control +var labs_panel: PanelContainer +var labs_content: VBoxContainer var red_fog_time := 0.0 var green_fog_time := 0.0 var correct_advance_delay := 0.0 @@ -77,6 +81,14 @@ func _ready() -> void: toolbar.add_theme_constant_override("separation", 8) toolbar.alignment = BoxContainer.ALIGNMENT_END + labs_button = Button.new() + labs_button.tooltip_text = "Review lab results" + labs_button.icon = _make_labs_icon() + labs_button.custom_minimum_size = Vector2(44, 44) + Style.style_chip(labs_button, Style.LIME) + labs_button.pressed.connect(_show_labs_modal) + toolbar.add_child(labs_button) + var clear_highlight_button := Button.new() clear_highlight_button.tooltip_text = "Clear highlights" clear_highlight_button.icon = _make_clear_highlights_icon() @@ -254,6 +266,8 @@ func _ready() -> void: feedback_fog.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(feedback_fog) + _build_labs_modal() + func on_show(_payload: Variant = null) -> void: _load_question() @@ -273,6 +287,10 @@ func _process(delta: float) -> void: func _input(event: InputEvent) -> void: + if labs_overlay != null and labs_overlay.visible and event.is_action_pressed("ui_cancel"): + _hide_labs_modal() + get_viewport().set_input_as_handled() + return if not visible or pending_correct_advance or stem_label == null: return @@ -289,6 +307,7 @@ func _input(event: InputEvent) -> void: func _load_question() -> void: _reset_wrong_feedback() + _hide_labs_modal() eliminated.clear() locked_wrong_choice_ids.clear() _clear_followups() @@ -318,6 +337,7 @@ func _load_question() -> void: stem_plain_text = str(question.get("stem", "")) _render_stem() lead_label.text = question.get("lead_in", "") + _refresh_labs_button() var choices: Array = question.get("choices", []) for index in range(choice_buttons.size()): @@ -655,6 +675,7 @@ func _toggle_report_current_question() -> void: func _refresh_toolbar_state() -> void: + _refresh_labs_button() if flag_button != null: Style.style_chip(flag_button, Style.AMBER if flagged else Style.CYAN) if report_button != null: @@ -662,6 +683,152 @@ func _refresh_toolbar_state() -> void: Style.style_chip(report_button, Style.RED if reported_question_ids.has(question_id) else Style.CYAN) +func _refresh_labs_button() -> void: + if labs_button == null: + return + labs_button.visible = _current_question_has_labs() + + +func _current_question_has_labs() -> bool: + var question := BlockSessionService.get_current_question() + if question.is_empty(): + return false + var labs: Array = question.get("labs", []) + return not labs.is_empty() + + +func _build_labs_modal() -> void: + labs_overlay = Control.new() + labs_overlay.visible = false + labs_overlay.top_level = true + labs_overlay.mouse_filter = Control.MOUSE_FILTER_STOP + labs_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + labs_overlay.gui_input.connect(_labs_overlay_gui_input) + add_child(labs_overlay) + + var dim := ColorRect.new() + dim.color = Color(0.02, 0.03, 0.05, 0.72) + dim.mouse_filter = Control.MOUSE_FILTER_IGNORE + dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + labs_overlay.add_child(dim) + + var center := CenterContainer.new() + center.mouse_filter = Control.MOUSE_FILTER_IGNORE + center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + labs_overlay.add_child(center) + + labs_panel = PanelContainer.new() + labs_panel.custom_minimum_size = Vector2(760, 430) + labs_panel.mouse_filter = Control.MOUSE_FILTER_STOP + labs_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.LIME)) + center.add_child(labs_panel) + + var modal_body := VBoxContainer.new() + modal_body.add_theme_constant_override("separation", 14) + labs_panel.add_child(modal_body) + + var header := HBoxContainer.new() + header.add_theme_constant_override("separation", 10) + modal_body.add_child(header) + + var title := Label.new() + title.text = "Lab Results" + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + Style.set_label(title, 26, Style.TEXT) + header.add_child(title) + + var close_button := Button.new() + close_button.text = "X" + close_button.tooltip_text = "Close lab results" + close_button.custom_minimum_size = Vector2(42, 38) + Style.style_chip(close_button, Style.RED) + close_button.pressed.connect(_hide_labs_modal) + header.add_child(close_button) + + var scroll := ScrollContainer.new() + scroll.custom_minimum_size = Vector2(0, 320) + scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + modal_body.add_child(scroll) + + labs_content = VBoxContainer.new() + labs_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL + labs_content.add_theme_constant_override("separation", 8) + scroll.add_child(labs_content) + + +func _labs_overlay_gui_input(event: InputEvent) -> void: + var mouse_event := event as InputEventMouseButton + if mouse_event == null or not mouse_event.pressed or mouse_event.button_index != MOUSE_BUTTON_LEFT: + return + if labs_panel == null or not labs_panel.get_global_rect().has_point(mouse_event.global_position): + _hide_labs_modal() + get_viewport().set_input_as_handled() + + +func _show_labs_modal() -> void: + if labs_overlay == null or labs_content == null: + return + var question := BlockSessionService.get_current_question() + var labs: Array = question.get("labs", []) + if labs.is_empty(): + return + _populate_labs_modal(labs) + labs_overlay.visible = true + labs_overlay.move_to_front() + + +func _hide_labs_modal() -> void: + if labs_overlay != null: + labs_overlay.visible = false + + +func _populate_labs_modal(labs: Array) -> void: + for child in labs_content.get_children(): + labs_content.remove_child(child) + child.queue_free() + + var grid := GridContainer.new() + grid.columns = 3 + grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL + grid.add_theme_constant_override("h_separation", 16) + grid.add_theme_constant_override("v_separation", 8) + labs_content.add_child(grid) + + _add_lab_table_label(grid, "Test", Style.CYAN, true) + _add_lab_table_label(grid, "Value", Style.CYAN, true) + _add_lab_table_label(grid, "Reference", Style.CYAN, true) + + for lab in labs: + if typeof(lab) != TYPE_DICTIONARY: + continue + var lab_dict: Dictionary = lab + _add_lab_table_label(grid, str(lab_dict.get("name", "-")), Style.TEXT) + _add_lab_table_label(grid, _format_lab_value(lab_dict), Style.TEXT) + _add_lab_table_label(grid, str(lab_dict.get("reference", "-")), Style.MUTED) + + +func _add_lab_table_label(parent: GridContainer, text: String, color: Color, is_header: bool = false) -> void: + var label := Label.new() + label.text = text if text.strip_edges() != "" else "-" + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + Style.set_label(label, 16 if not is_header else 17, color) + parent.add_child(label) + + +func _format_lab_value(lab: Dictionary) -> String: + var value := str(lab.get("value", "")).strip_edges() + var unit := str(lab.get("unit", "")).strip_edges() + if value == "" and unit == "": + return "-" + if unit == "": + return value + if value == "": + return unit + return "%s %s" % [value, unit] + + func _render_stem() -> void: var question_id := str(BlockSessionService.get_current_question().get("id", "")) var ranges: Array = question_highlights.get(question_id, []).duplicate() @@ -1137,6 +1304,33 @@ func _strike_text(value: String) -> String: return result +func _make_labs_icon() -> Texture2D: + var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) + image.fill(Color(0, 0, 0, 0)) + var glass_color := Style.TEXT + var fluid_color := Style.LIME + for x in range(8, 16): + image.set_pixel(x, 4, glass_color) + for y in range(5, 10): + image.set_pixel(10, y, glass_color) + image.set_pixel(13, y, glass_color) + for y in range(10, 20): + var left := 10 - int(floor(float(y - 10) / 3.0)) + var right := 13 + int(floor(float(y - 10) / 3.0)) + image.set_pixel(left, y, glass_color) + image.set_pixel(right, y, glass_color) + for x in range(7, 17): + image.set_pixel(x, 20, glass_color) + for y in range(15, 20): + for x in range(8, 16): + if x >= 10 - int(floor(float(y - 10) / 3.0)) and x <= 13 + int(floor(float(y - 10) / 3.0)): + image.set_pixel(x, y, fluid_color) + image.set_pixel(7, 7, Style.CYAN) + image.set_pixel(17, 9, Style.CYAN) + image.set_pixel(18, 6, Style.CYAN) + return ImageTexture.create_from_image(image) + + func _make_flag_icon() -> Texture2D: var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) image.fill(Color(0, 0, 0, 0))