Skip to content

Instantly share code, notes, and snippets.

@tomkail
Last active August 27, 2024 15:19
Show Gist options
  • Save tomkail/152cb3aca9ddf3942d97d7070c8c8518 to your computer and use it in GitHub Desktop.
Save tomkail/152cb3aca9ddf3942d97d7070c8c8518 to your computer and use it in GitHub Desktop.
Workaround for Unity's inability to keep an input field selected when you click a button
// Released under MIT License
using System.Reflection;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
// Workaround for Unity's inability to keep an input field selected when you click a button.
// Call ReactivateInputField to immediately activate and restore caret position/selection.
// This class also tracks the state of the last selected input field in Update, allowing easier restoration of the input field when it becomes deselected.
// Common use cases are to:
// - restore it immediately when it becomes deselected (in the callback for inputField.onEndEdit)
// - restore it immediately once the user clicks a particular button
// - restore it once the user returns to a particular state in your app
public class InputFieldActivationRestorer : MonoSingleton<InputFieldActivationRestorer> {
// The current restorable settings
public RestorableInputFieldSettings lastSelectedInputFieldSettings;
public bool lastSelectedInputFieldIsCurrentlySelected => lastSelectedInputFieldSettings.isRestorable && EventSystem.current != null && EventSystem.current.currentSelectedGameObject == lastSelectedInputFieldSettings.restorableGameObject;
// This struct contains all the properties that can be restored
[System.Serializable]
public struct RestorableInputFieldSettings {
public bool isRestorable => restorableInputField != null || restorableTMPInputField != null;
public GameObject restorableGameObject {
get {
if (restorableInputField != null) return restorableInputField.gameObject;
else if (restorableTMPInputField != null) return restorableTMPInputField.gameObject;
else return null;
}
}
public InputField restorableInputField;
public TMP_InputField restorableTMPInputField;
public int caretPosition;
public int selectionAnchorPosition;
public int selectionFocusPosition;
// Sets the input field to be restored automatically using whatever input field is currently selected
public static RestorableInputFieldSettings FromCurrentSelection() {
var selectedInputField = GetSelectedUGUIInputField();
if (selectedInputField != null) {
return new RestorableInputFieldSettings(selectedInputField);
}
var selectedTMPInputField = GetSelectedTMPInputField();
if(selectedTMPInputField != null) {
return new RestorableInputFieldSettings(selectedTMPInputField);
}
return default;
static TMP_InputField GetSelectedTMPInputField () {
if(EventSystem.current == null) return null;
var selectedGO = EventSystem.current.currentSelectedGameObject;
if (selectedGO == null) return null;
var inputField = selectedGO.GetComponentInParent<TMP_InputField>();
return inputField != null && inputField.isFocused ? inputField : null;
}
static InputField GetSelectedUGUIInputField () {
if(EventSystem.current == null) return null;
var selectedGO = EventSystem.current.currentSelectedGameObject;
if (selectedGO == null) return null;
var inputField = selectedGO.GetComponentInParent<InputField>();
return inputField != null && inputField.isFocused ? inputField : null;
}
}
public RestorableInputFieldSettings(InputField inputField) {
restorableInputField = inputField;
restorableTMPInputField = null;
caretPosition = inputField.caretPosition;
selectionAnchorPosition = inputField.selectionAnchorPosition;
selectionFocusPosition = inputField.selectionFocusPosition;
}
public RestorableInputFieldSettings(TMP_InputField tmpInputField) {
restorableInputField = null;
restorableTMPInputField = tmpInputField;
caretPosition = tmpInputField.caretPosition;
selectionAnchorPosition = tmpInputField.selectionAnchorPosition;
selectionFocusPosition = tmpInputField.selectionFocusPosition;
}
public override bool Equals(object obj) {
if (obj == null || GetType() != obj.GetType()) {
return false;
}
RestorableInputFieldSettings other = (RestorableInputFieldSettings)obj;
return (restorableInputField == other.restorableInputField &&
restorableTMPInputField == other.restorableTMPInputField &&
caretPosition == other.caretPosition &&
selectionAnchorPosition == other.selectionAnchorPosition &&
selectionFocusPosition == other.selectionFocusPosition);
}
public override int GetHashCode() {
unchecked {
int hash = 17;
hash = hash * 23 + (restorableInputField != null ? restorableInputField.GetHashCode() : 0);
hash = hash * 23 + (restorableTMPInputField != null ? restorableTMPInputField.GetHashCode() : 0);
hash = hash * 23 + caretPosition.GetHashCode();
hash = hash * 23 + selectionAnchorPosition.GetHashCode();
hash = hash * 23 + selectionFocusPosition.GetHashCode();
return hash;
}
}
}
public void ReactivateLastSelectedInputField() {
ReactivateInputField(lastSelectedInputFieldSettings);
}
// Selects the previously selected input field and restores the selection state.
public static void ReactivateInputField(RestorableInputFieldSettings restorableState) {
if (!restorableState.isRestorable) return;
// Copy the struct just in case something here causes it to be updated while we're restoring it
if (restorableState.restorableInputField != null) {
restorableState.restorableInputField.ActivateInputField();
// This forces TMP to immediately set itself selectable again.
MethodInfo mInfoMethod = typeof(InputField).GetMethod("LateUpdate", BindingFlags.Instance | BindingFlags.NonPublic);
mInfoMethod.Invoke(restorableState.restorableInputField, null);
}
else if (restorableState.restorableTMPInputField != null) {
restorableState.restorableTMPInputField.ActivateInputField();
// This forces TMP to immediately set itself selectable again.
MethodInfo mInfoMethod = typeof(TMP_InputField).GetMethod("LateUpdate", BindingFlags.Instance | BindingFlags.NonPublic);
mInfoMethod.Invoke(restorableState.restorableTMPInputField, null);
}
if (restorableState.restorableInputField != null && EventSystem.current.currentSelectedGameObject == restorableState.restorableInputField.gameObject) {
restorableState.restorableInputField.caretPosition = restorableState.caretPosition;
restorableState.restorableInputField.selectionAnchorPosition = restorableState.selectionAnchorPosition;
restorableState.restorableInputField.selectionFocusPosition = restorableState.selectionFocusPosition;
restorableState.restorableInputField.ForceLabelUpdate();
}
else if (restorableState.restorableTMPInputField != null && EventSystem.current.currentSelectedGameObject == restorableState.restorableTMPInputField.gameObject) {
restorableState.restorableTMPInputField.caretPosition = restorableState.caretPosition;
restorableState.restorableTMPInputField.selectionAnchorPosition = restorableState.selectionAnchorPosition;
restorableState.restorableTMPInputField.selectionFocusPosition = restorableState.selectionFocusPosition;
restorableState.restorableTMPInputField.ForceLabelUpdate();
}
}
void Update() {
if (EventSystem.current != null) {
var currentSelectionState = RestorableInputFieldSettings.FromCurrentSelection();
if (currentSelectionState.isRestorable) lastSelectedInputFieldSettings = currentSelectionState;
}
}
}
@nitz
Copy link

nitz commented Aug 27, 2024

Hey Tom, thanks for sharing this!

Is there a license you're sharing this with?

Cheers

@tomkail
Copy link
Author

tomkail commented Aug 27, 2024

MIT is fine by me. Edited the snippet!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment