extends Node3D ## VR start screen UI panel. ## Renders a 2D UI in a SubViewport on a QuadMesh in VR space. ## Allows user to enter server URL/port, connect, and launch AR mode. ## Includes an in-VR numpad since the Quest system keyboard doesn't work in XR mode. signal connect_requested(host: String, port: int) signal launch_ar_requested() @onready var ui_mesh: MeshInstance3D = $UIMesh @onready var viewport: SubViewport = $UIMesh/SubViewport @onready var host_input: LineEdit = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/ServerRow/HostInput @onready var port_input: LineEdit = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/PortRow/PortInput @onready var connect_button: Button = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/ConnectButton @onready var status_label: Label = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/StatusLabel @onready var launch_ar_button: Button = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/LaunchARButton @onready var vbox: VBoxContainer = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox var _is_connected: bool = false var _numpad_container: VBoxContainer var _active_input: LineEdit # Which input field the numpad types into func _ready() -> void: add_to_group("start_screen") connect_button.pressed.connect(_on_connect_pressed) launch_ar_button.pressed.connect(_on_launch_ar_pressed) # Show numpad when input fields are focused host_input.focus_entered.connect(_on_input_focused.bind(host_input)) port_input.focus_entered.connect(_on_input_focused.bind(port_input)) # Build the in-VR numpad _build_numpad() # Set up the mesh material to display the SubViewport var material := StandardMaterial3D.new() material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED material.albedo_texture = viewport.get_texture() material.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED ui_mesh.material_override = material print("[StartScreen] Ready") func _build_numpad() -> void: _numpad_container = VBoxContainer.new() _numpad_container.add_theme_constant_override("separation", 6) _numpad_container.visible = false # Button rows: [1,2,3] [4,5,6] [7,8,9] [.,0,<-] [Clear, Done] var rows := [ ["1", "2", "3"], ["4", "5", "6"], ["7", "8", "9"], [".", "0", "<"], ["Clear", "Done"], ] for row in rows: var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 6) hbox.alignment = BoxContainer.ALIGNMENT_CENTER for key in row: var btn := Button.new() btn.text = key btn.custom_minimum_size = Vector2(80, 55) btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL if key == "Clear" or key == "Done": btn.custom_minimum_size = Vector2(130, 55) btn.add_theme_font_size_override("font_size", 26) btn.pressed.connect(_on_numpad_key.bind(key)) hbox.add_child(btn) _numpad_container.add_child(hbox) # Insert numpad after the PortRow var port_row_idx := port_input.get_parent().get_index() vbox.add_child(_numpad_container) vbox.move_child(_numpad_container, port_row_idx + 1) func _on_input_focused(input: LineEdit) -> void: _active_input = input _numpad_container.visible = true func _on_numpad_key(key: String) -> void: if _active_input == null: return if key == "<": # Backspace var t := _active_input.text if t.length() > 0: _active_input.text = t.substr(0, t.length() - 1) _active_input.caret_column = _active_input.text.length() elif key == "Clear": _active_input.text = "" _active_input.caret_column = 0 elif key == "Done": _numpad_container.visible = false _active_input.release_focus() _active_input = null else: _active_input.text += key _active_input.caret_column = _active_input.text.length() func _on_connect_pressed() -> void: # Hide numpad if open _numpad_container.visible = false var host := host_input.text.strip_edges() if host.is_empty(): update_status("Please enter a server address") return var port := int(port_input.text.strip_edges()) if port <= 0 or port > 65535: update_status("Invalid port number") return update_status("Connecting to %s:%d..." % [host, port]) connect_requested.emit(host, port) func _on_launch_ar_pressed() -> void: launch_ar_requested.emit() func update_status(text: String) -> void: status_label.text = text func set_connected(connected: bool) -> void: _is_connected = connected if connected: update_status("Connected!") connect_button.text = "Disconnect" else: if connect_button.text == "Disconnect": update_status("Disconnected") connect_button.text = "Connect to Server" func show_screen() -> void: visible = true func hide_screen() -> void: visible = false