diff --git a/Assets/Tests/InputSystem/APIVerificationTests.cs b/Assets/Tests/InputSystem/APIVerificationTests.cs index 6bb3863dcc..edd05b7827 100644 --- a/Assets/Tests/InputSystem/APIVerificationTests.cs +++ b/Assets/Tests/InputSystem/APIVerificationTests.cs @@ -424,6 +424,14 @@ public class SwitchProControllerHID : UnityEngine.InputSystem.Gamepad [Property("Exclusions", @"1.0.0 public class DualShock4GamepadHID : UnityEngine.InputSystem.DualShock.DualShockGamepad ")] + // IMECompositionEvent added unsafe attribute + [Property("Exclusions", @"1.0.0 + public struct IMECompositionEvent : UnityEngine.InputSystem.LowLevel.IInputEventTypeInfo + ")] + // IMECompositionEvent.compositionString changed from field to property due to hard-to-avoid changes to IMECompositionString struct + [Property("Exclusions", @"1.0.0 + public UnityEngine.InputSystem.LowLevel.IMECompositionString compositionString; + ")] public void API_MinorVersionsHaveNoBreakingChanges() { var currentVersion = CoreTests.PackageJson.ReadVersion(); diff --git a/Assets/Tests/InputSystem/CoreTests_Devices.cs b/Assets/Tests/InputSystem/CoreTests_Devices.cs index 96c5f2b366..cff20f0b97 100644 --- a/Assets/Tests/InputSystem/CoreTests_Devices.cs +++ b/Assets/Tests/InputSystem/CoreTests_Devices.cs @@ -5235,9 +5235,31 @@ public void Devices_CanListenForIMECompositionEvents() Assert.AreEqual(composition.ToString(), imeCompositionCharacters); }; - var inputEvent = IMECompositionEvent.Create(keyboard.deviceId, imeCompositionCharacters, + IMECompositionEventVariableSize.QueueEvent(keyboard.deviceId, imeCompositionCharacters, + InputRuntime.s_Instance.currentTime); + InputSystem.Update(); + + Assert.That(callbackWasCalled, Is.True); + } + + [Test] + [Category("Devices")] + public void Devices_CanReadEmptyIMECompositionEvents() + { + const string imeCompositionCharacters = ""; + var callbackWasCalled = false; + + var keyboard = InputSystem.AddDevice(); + keyboard.onIMECompositionChange += composition => + { + Assert.That(callbackWasCalled, Is.False); + callbackWasCalled = true; + Assert.AreEqual(composition.Count, 0); + Assert.AreEqual(composition.ToString(), imeCompositionCharacters); + }; + + IMECompositionEventVariableSize.QueueEvent(keyboard.deviceId, imeCompositionCharacters, InputRuntime.s_Instance.currentTime); - InputSystem.QueueEvent(ref inputEvent); InputSystem.Update(); Assert.That(callbackWasCalled, Is.True); diff --git a/Packages/com.unity.inputsystem/Documentation~/Events.md b/Packages/com.unity.inputsystem/Documentation~/Events.md index 5c92c30f4f..e0d3aa2ee6 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Events.md +++ b/Packages/com.unity.inputsystem/Documentation~/Events.md @@ -70,7 +70,7 @@ There are three types of Device events: There are two types of text events: * [`TextEvent`](../api/UnityEngine.InputSystem.LowLevel.TextEvent.html) (`'TEXT'`) -* [`IMECompositionEvent`](../api/UnityEngine.InputSystem.LowLevel.IMECompositionEvent.html) (`'IMES'`) +* [`IMECompositionEventVariableSize`](../api/UnityEngine.InputSystem.LowLevel.IMECompositionEventVariableSize.html) (`'IMEC'`) ## Working with events diff --git a/Packages/com.unity.inputsystem/Documentation~/filter.yml b/Packages/com.unity.inputsystem/Documentation~/filter.yml index db361971be..a0649061be 100644 --- a/Packages/com.unity.inputsystem/Documentation~/filter.yml +++ b/Packages/com.unity.inputsystem/Documentation~/filter.yml @@ -1,4 +1,8 @@ apiRules: + - include: + # IMECompositionEvent is marked as obsolete + uid: UnityEngine.InputSystem.LowLevel.IMECompositionEvent + type: Type - exclude: # inherited Object methods uidRegex: ^System\.Object\..*$ diff --git a/Packages/com.unity.inputsystem/InputSystem/Devices/ITextInputReceiver.cs b/Packages/com.unity.inputsystem/InputSystem/Devices/ITextInputReceiver.cs index 5fb433c5f1..c88509c495 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Devices/ITextInputReceiver.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Devices/ITextInputReceiver.cs @@ -8,7 +8,7 @@ namespace UnityEngine.InputSystem.LowLevel /// input through . /// /// - /// + /// public interface ITextInputReceiver { /// @@ -32,7 +32,7 @@ public interface ITextInputReceiver /// Called when an IME composition is in-progress or finished. /// /// The current composition. - /// + /// /// /// /// The method will be repeatedly called with the current string while composition is in progress. diff --git a/Packages/com.unity.inputsystem/InputSystem/Events/IMECompositionEvent.cs b/Packages/com.unity.inputsystem/InputSystem/Events/IMECompositionEvent.cs index a9c886269f..6828637230 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Events/IMECompositionEvent.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Events/IMECompositionEvent.cs @@ -2,26 +2,42 @@ using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; using UnityEngine.InputSystem.Utilities; +//// TODO for v2 remove and replace with just string. + namespace UnityEngine.InputSystem.LowLevel { /// - /// A specialized event that contains the current IME Composition string, if IME is enabled and active. - /// This event contains the entire current string to date, and once a new composition is submitted will send a blank string event. + /// Deprecated variant of IME composition event. Please use for replacement. /// + [Obsolete("Use IMECompositionEventVariableSize instead.")] [StructLayout(LayoutKind.Explicit, Size = InputEvent.kBaseEventSize + sizeof(int) + (sizeof(char) * kIMECharBufferSize))] - public struct IMECompositionEvent : IInputEventTypeInfo + public unsafe struct IMECompositionEvent : IInputEventTypeInfo { // These needs to match the native ImeCompositionStringInputEventData settings - internal const int kIMECharBufferSize = 64; + private const int kIMECharBufferSize = 64; public const int Type = 0x494D4553; [FieldOffset(0)] public InputEvent baseEvent; [FieldOffset(InputEvent.kBaseEventSize)] - public IMECompositionString compositionString; + private int length; + + [FieldOffset(InputEvent.kBaseEventSize + sizeof(int))] + private fixed char buffer[kIMECharBufferSize]; + + public IMECompositionString compositionString + { + get + { + fixed(char* ptr = buffer) + return new IMECompositionString(ptr, length); + } + } public FourCC typeStatic => Type; @@ -29,11 +45,74 @@ public static IMECompositionEvent Create(int deviceId, string compositionString, { var inputEvent = new IMECompositionEvent(); inputEvent.baseEvent = new InputEvent(Type, InputEvent.kBaseEventSize + sizeof(int) + (sizeof(char) * kIMECharBufferSize), deviceId, time); - inputEvent.compositionString = new IMECompositionString(compositionString); + inputEvent.length = compositionString.Length > kIMECharBufferSize ? kIMECharBufferSize : compositionString.Length; + fixed(char* dst = compositionString) + fixed(char* src = compositionString) + UnsafeUtility.MemCpy(dst, src, inputEvent.length * sizeof(char)); return inputEvent; } } + /// + /// A specialized event that contains the current IME Composition string, if IME is enabled and active. + /// This event contains the entire current string to date, and once a new composition is submitted will send a blank string event. + /// + [StructLayout(LayoutKind.Explicit, Size = InputEvent.kBaseEventSize + sizeof(int))] + public struct IMECompositionEventVariableSize : IInputEventTypeInfo + { + // Before we had 0x494D4553 which corresponds to ImeCompositionStringInputEventData fixed size event with 64 character payload. + // 0x494D4543 corresponds to ImeCompositionInputEventData and is a different event which provides variable size array of characters after the event. + public const int Type = 0x494D4543; + + [FieldOffset(0)] + public InputEvent baseEvent; + + [FieldOffset(InputEvent.kBaseEventSize)] + internal int length; + + internal static unsafe char* GetCharsPtr(IMECompositionEventVariableSize* ev) + { + return (char*)((byte*)ev + InputEvent.kBaseEventSize + sizeof(int)); + } + + public FourCC typeStatic => Type; + + /// + /// Returns composition string for the given event. + /// + /// Pointer to the event. + /// Composition string. + public static unsafe IMECompositionString GetIMECompositionString(IMECompositionEventVariableSize* ev) + { + return new IMECompositionString(GetCharsPtr(ev), ev->length); + } + + /// + /// Queues up an IME Composition Event. IME Event sizes are variable, and this simplifies the process of aligning up the Input Event information and actual IME composition string. + /// + /// ID of the device (see ) to which the composition event should be sent to. Should be an device. Will trigger call when processed. + /// The IME characters to be sent. This can be any length, or left blank to represent a resetting of the IME dialog. + /// The time in seconds, the event was generated at. This uses the same timeline as + public static unsafe void QueueEvent(int deviceId, string str, double time) + { + var sizeInBytes = (InputEvent.kBaseEventSize + sizeof(int)) + sizeof(char) * str.Length; + var eventBuffer = new NativeArray(sizeInBytes, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + + var ev = (IMECompositionEventVariableSize*)eventBuffer.GetUnsafePtr(); + + ev->baseEvent = new InputEvent(Type, sizeInBytes, deviceId, time); + ev->length = str.Length; + + if (str.Length > 0) + fixed(char* p = str) + UnsafeUtility.MemCpy(GetCharsPtr(ev), p, str.Length * sizeof(char)); + + InputSystem.QueueEvent(new InputEventPtr((InputEvent*)ev)); + + eventBuffer.Dispose(); + } + } + /// /// A struct representing an string of characters generated by an IME for text input. /// @@ -42,36 +121,31 @@ public static IMECompositionEvent Create(int deviceId, string compositionString, /// method. It can easily be converted to a normal C# string using /// , but is exposed as the raw struct to avoid allocating memory by default. /// - [StructLayout(LayoutKind.Explicit, Size = sizeof(int) + sizeof(char) * LowLevel.IMECompositionEvent.kIMECharBufferSize)] public unsafe struct IMECompositionString : IEnumerable { - internal struct Enumerator : IEnumerator + private const int kLegacyIMEEventCharBufferSize = 64; + + private readonly string m_ManagedString; + private readonly int m_Size; + private fixed char m_FixedBuffer[kLegacyIMEEventCharBufferSize]; + + private struct FixedBufferEnumerator : IEnumerator { - IMECompositionString m_CompositionString; - char m_CurrentCharacter; - int m_CurrentIndex; + private IMECompositionString m_CompositionString; + private int m_CurrentIndex; - public Enumerator(IMECompositionString compositionString) + public FixedBufferEnumerator(IMECompositionString compositionString) { m_CompositionString = compositionString; - m_CurrentCharacter = '\0'; m_CurrentIndex = -1; } public bool MoveNext() { - int size = m_CompositionString.Count; - - m_CurrentIndex++; - - if (m_CurrentIndex == size) + if (m_CurrentIndex + 1 >= m_CompositionString.Count) return false; - fixed(char* ptr = m_CompositionString.buffer) - { - m_CurrentCharacter = *(ptr + m_CurrentIndex); - } - + m_CurrentIndex++; return true; } @@ -84,58 +158,73 @@ public void Dispose() { } - public char Current => m_CurrentCharacter; + public char Current => m_CompositionString[m_CurrentIndex]; object IEnumerator.Current => Current; } - public int Count => size; + public int Count => m_Size; public char this[int index] { get { + if (m_ManagedString != null) + return m_ManagedString[index]; + if (index >= Count || index < 0) throw new ArgumentOutOfRangeException(nameof(index)); - fixed(char* ptr = buffer) - { - return *(ptr + index); - } + return m_FixedBuffer[index]; } } - [FieldOffset(0)] - int size; - - [FieldOffset(sizeof(int))] - fixed char buffer[IMECompositionEvent.kIMECharBufferSize]; - - public IMECompositionString(string characters) + public IMECompositionString(char* characters, int length) { - if (string.IsNullOrEmpty(characters)) + // only allocate string if we can't fit into fixed buffer + if (length <= kLegacyIMEEventCharBufferSize) + { + m_ManagedString = null; + m_Size = length; + if (m_Size > 0) + { + Debug.Assert(characters != null); + fixed(char* dst = m_FixedBuffer) + UnsafeUtility.MemCpy(dst, characters, m_Size * sizeof(char)); + } + } + else { - size = 0; - return; + m_ManagedString = new string(characters, 0, length); + m_Size = length; } + } - Debug.Assert(characters.Length < IMECompositionEvent.kIMECharBufferSize); - size = characters.Length; - for (var i = 0; i < size; i++) - buffer[i] = characters[i]; + public IMECompositionString(string characters) + { + // string is already allocated on the heap, so reuse it + m_ManagedString = characters; + m_Size = characters.Length; } public override string ToString() { - fixed(char* ptr = buffer) - { - return new string(ptr, 0, size); - } + if (m_Size == 0) + return string.Empty; + + if (m_ManagedString != null) + return m_ManagedString; + + fixed(char* ptr = m_FixedBuffer) + return new string(ptr, 0, m_Size); } public IEnumerator GetEnumerator() { - return new Enumerator(this); + if (m_ManagedString != null) + return m_ManagedString.GetEnumerator(); + + return new FixedBufferEnumerator(this); } IEnumerator IEnumerable.GetEnumerator() diff --git a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs index 7d871b2dbb..cb7110892a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs @@ -3252,11 +3252,11 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev break; } - case IMECompositionEvent.Type: + case IMECompositionEventVariableSize.Type: { - var imeEventPtr = (IMECompositionEvent*)currentEventReadPtr; + var imeEventPtr = (IMECompositionEventVariableSize*)currentEventReadPtr; var textInputReceiver = device as ITextInputReceiver; - textInputReceiver?.OnIMECompositionChanged(imeEventPtr->compositionString); + textInputReceiver?.OnIMECompositionChanged(IMECompositionEventVariableSize.GetIMECompositionString(imeEventPtr)); break; }