- add network NPC synchronization and dialogue interactions - add world bulletin publishing and display - improve authentication, session refresh, and appearance sync - synchronize player direction and movement animations - improve input focus and progressive Web loading - add macOS/Windows builds and deployment configuration - include required fonts, shaders, and runtime assets
202 lines
8.0 KiB
GDScript
202 lines
8.0 KiB
GDScript
extends Node
|
|
|
|
const FOCUS_BORDER_COLOR := Color(0.12, 0.49, 0.82, 1.0)
|
|
const CARET_COLOR := Color(0.04, 0.31, 0.60, 1.0)
|
|
const SELECTION_COLOR := Color(0.25, 0.61, 0.91, 0.30)
|
|
const STYLED_META := &"whaletown_input_focus_styled"
|
|
const INITIAL_PLACEHOLDER_META := &"whaletown_initial_placeholder"
|
|
const FOCUS_PLACEHOLDER_META := &"whaletown_focus_placeholder"
|
|
const NATIVE_INPUT_ID := "whaletown-native-input"
|
|
|
|
var _javascript_callback: Variant
|
|
var _native_inputs: Dictionary = {}
|
|
var _active_native_input: Control
|
|
var _bridge_initialized := false
|
|
|
|
func _ready() -> void:
|
|
get_tree().node_added.connect(_on_node_added)
|
|
call_deferred("_initialize_javascript_bridge")
|
|
call_deferred("_decorate_existing_inputs")
|
|
|
|
func _process(_delta: float) -> void:
|
|
if not _bridge_initialized:
|
|
_initialize_javascript_bridge()
|
|
|
|
func _initialize_javascript_bridge() -> void:
|
|
if _bridge_initialized or OS.get_name() != "Web":
|
|
return
|
|
var window: Variant = JavaScriptBridge.get_interface("window")
|
|
if window == null:
|
|
return
|
|
_javascript_callback = JavaScriptBridge.create_callback(_on_native_input_event)
|
|
window["__whaletownNativeInputCallback"] = _javascript_callback
|
|
_bridge_initialized = true
|
|
|
|
func _exit_tree() -> void:
|
|
_close_native_input()
|
|
if _bridge_initialized:
|
|
var window: Variant = JavaScriptBridge.get_interface("window")
|
|
if window != null:
|
|
window["__whaletownNativeInputCallback"] = null
|
|
|
|
func _on_node_added(node: Node) -> void:
|
|
if node is LineEdit or node is TextEdit:
|
|
_decorate_input.call_deferred(node)
|
|
|
|
func _decorate_existing_inputs() -> void:
|
|
_decorate_branch(get_tree().root)
|
|
|
|
func _decorate_branch(node: Node) -> void:
|
|
if node is LineEdit or node is TextEdit:
|
|
_decorate_input(node)
|
|
for child in node.get_children():
|
|
_decorate_branch(child)
|
|
|
|
func _decorate_input(input: Control) -> void:
|
|
if not is_instance_valid(input) or input.has_meta(STYLED_META):
|
|
return
|
|
input.set_meta(STYLED_META, true)
|
|
input.add_theme_stylebox_override("focus", _create_focus_style(input))
|
|
input.add_theme_color_override("caret_color", CARET_COLOR)
|
|
input.add_theme_color_override("selection_color", SELECTION_COLOR)
|
|
input.add_theme_constant_override("caret_width", 2)
|
|
input.set_meta(INITIAL_PLACEHOLDER_META, input.placeholder_text)
|
|
input.focus_entered.connect(_on_input_focus_entered.bind(input))
|
|
input.focus_exited.connect(_on_input_focus_exited.bind(input))
|
|
if input is LineEdit:
|
|
(input as LineEdit).text_changed.connect(func(_value: String) -> void: _sync_native_input(input))
|
|
else:
|
|
(input as TextEdit).text_changed.connect(func() -> void: _sync_native_input(input))
|
|
|
|
func _on_input_focus_entered(input: Control) -> void:
|
|
if not is_instance_valid(input):
|
|
return
|
|
if input.text.strip_edges().is_empty():
|
|
input.set_meta(FOCUS_PLACEHOLDER_META, input.placeholder_text)
|
|
input.placeholder_text = ""
|
|
_open_native_input(input)
|
|
|
|
func _on_input_focus_exited(input: Control) -> void:
|
|
if not is_instance_valid(input) or not input.text.strip_edges().is_empty():
|
|
return
|
|
var placeholder := str(input.get_meta(FOCUS_PLACEHOLDER_META, input.get_meta(INITIAL_PLACEHOLDER_META, "")))
|
|
if not placeholder.is_empty():
|
|
input.placeholder_text = placeholder
|
|
|
|
func _open_native_input(input: Control) -> void:
|
|
if not _bridge_initialized or not is_instance_valid(input):
|
|
return
|
|
_close_native_input()
|
|
_active_native_input = input
|
|
_native_inputs[input.get_instance_id()] = input
|
|
var callback_name := "window.__whaletownNativeInputCallback"
|
|
var input_id := str(input.get_instance_id())
|
|
var initial_text: String = input.text
|
|
var multiline := input is TextEdit
|
|
var secret := input is LineEdit and (input as LineEdit).secret
|
|
var rect := input.get_global_rect()
|
|
var viewport_size := get_viewport().get_visible_rect().size
|
|
var script := """
|
|
(() => {
|
|
const callback = %s;
|
|
if (typeof callback !== 'function') return false;
|
|
const previous = document.getElementById('%s');
|
|
if (previous) previous.remove();
|
|
const target = document.createElement(%s ? 'textarea' : 'input');
|
|
target.id = '%s';
|
|
target.value = %s;
|
|
target.spellcheck = false;
|
|
target.autocomplete = 'off';
|
|
if (%s) target.type = 'password';
|
|
const rect = {x: %f, y: %f, width: %f, height: %f};
|
|
const viewport = {width: %f, height: %f};
|
|
const canvas = document.getElementById('canvas');
|
|
const position = () => {
|
|
const bounds = canvas.getBoundingClientRect();
|
|
const sx = bounds.width / viewport.width;
|
|
const sy = bounds.height / viewport.height;
|
|
target.style.left = (bounds.left + rect.x * sx) + 'px';
|
|
target.style.top = (bounds.top + rect.y * sy) + 'px';
|
|
target.style.width = (rect.width * sx) + 'px';
|
|
target.style.height = (rect.height * sy) + 'px';
|
|
};
|
|
Object.assign(target.style, {
|
|
position: 'fixed', zIndex: '10000',
|
|
opacity: '0.01', color: 'transparent', caretColor: 'transparent',
|
|
background: 'transparent', border: '0', outline: '0', padding: '0',
|
|
margin: '0', resize: 'none', WebkitTextFillColor: 'transparent'
|
|
});
|
|
const send = (kind) => callback(kind, %s, target.value,
|
|
target.selectionStart || 0, target.selectionEnd || 0);
|
|
target.addEventListener('input', () => send('input'));
|
|
target.addEventListener('blur', () => send('blur'));
|
|
target.addEventListener('keydown', (event) => {
|
|
if (!%s && event.key === 'Enter') { event.preventDefault(); send('submit'); }
|
|
if (event.key === 'Escape') { event.preventDefault(); send('cancel'); }
|
|
});
|
|
document.querySelectorAll('.ime').forEach((element) => element.remove());
|
|
document.body.appendChild(target);
|
|
position();
|
|
window.addEventListener('resize', position, {passive: true});
|
|
target.__whaletownPosition = position;
|
|
target.focus({preventScroll: true});
|
|
target.setSelectionRange(target.value.length, target.value.length);
|
|
return true;
|
|
})()
|
|
""" % [callback_name, NATIVE_INPUT_ID, str(multiline).to_lower(), NATIVE_INPUT_ID,
|
|
JSON.stringify(initial_text), str(secret).to_lower(), rect.position.x, rect.position.y,
|
|
rect.size.x, rect.size.y, viewport_size.x, viewport_size.y, JSON.stringify(input_id), str(multiline).to_lower()]
|
|
var opened: Variant = JavaScriptBridge.eval(script, true)
|
|
if opened != true:
|
|
_native_inputs.erase(input.get_instance_id())
|
|
_active_native_input = null
|
|
|
|
func _close_native_input() -> void:
|
|
if _bridge_initialized:
|
|
JavaScriptBridge.eval("(() => { const el = document.getElementById('%s'); if (el) { if (el.__whaletownPosition) window.removeEventListener('resize', el.__whaletownPosition); el.remove(); } })()" % NATIVE_INPUT_ID, true)
|
|
_native_inputs.clear()
|
|
_active_native_input = null
|
|
|
|
func _sync_native_input(input: Control) -> void:
|
|
if not _bridge_initialized or input != _active_native_input or not is_instance_valid(input):
|
|
return
|
|
var value: String = input.text
|
|
var script := "(() => { const el = document.getElementById('%s'); if (el && el.value !== %s) el.value = %s; })()" % [NATIVE_INPUT_ID, JSON.stringify(value), JSON.stringify(value)]
|
|
JavaScriptBridge.eval(script, true)
|
|
|
|
func _on_native_input_event(arguments: Array) -> void:
|
|
if arguments.size() < 3:
|
|
return
|
|
var event_kind := str(arguments[0])
|
|
var input_id := int(arguments[1])
|
|
var input := _native_inputs.get(input_id) as Control
|
|
if not is_instance_valid(input):
|
|
return
|
|
var value := str(arguments[2])
|
|
if input.text != value:
|
|
input.text = value
|
|
if input is LineEdit:
|
|
(input as LineEdit).set_caret_column(value.length())
|
|
match event_kind:
|
|
"submit":
|
|
if input is LineEdit:
|
|
(input as LineEdit).text_submitted.emit(input.text)
|
|
"cancel":
|
|
_close_native_input()
|
|
|
|
func _create_focus_style(input: Control) -> StyleBoxFlat:
|
|
var focusStyle := StyleBoxFlat.new()
|
|
focusStyle.bg_color = Color.TRANSPARENT
|
|
focusStyle.border_color = FOCUS_BORDER_COLOR
|
|
focusStyle.set_border_width_all(2)
|
|
|
|
var normalStyle := input.get_theme_stylebox("normal") as StyleBoxFlat
|
|
if normalStyle != null:
|
|
focusStyle.corner_radius_top_left = normalStyle.corner_radius_top_left
|
|
focusStyle.corner_radius_top_right = normalStyle.corner_radius_top_right
|
|
focusStyle.corner_radius_bottom_right = normalStyle.corner_radius_bottom_right
|
|
focusStyle.corner_radius_bottom_left = normalStyle.corner_radius_bottom_left
|
|
else:
|
|
focusStyle.set_corner_radius_all(6)
|
|
return focusStyle
|