forked from xiangwang25/whale-town-front-v2
feat: expand multiplayer, chat, and release support
- 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
This commit is contained in:
201
_Core/managers/InputFocusManager.gd
Normal file
201
_Core/managers/InputFocusManager.gd
Normal file
@@ -0,0 +1,201 @@
|
||||
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
|
||||
Reference in New Issue
Block a user