Skip to content

IME Events are now completely variable sized. #1120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
26 changes: 24 additions & 2 deletions Assets/Tests/InputSystem/CoreTests_Devices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3959,9 +3959,31 @@ public void Devices_CanListenForIMECompositionEvents()
Assert.AreEqual(composition.ToString(), imeCompositionCharacters);
};

var inputEvent = IMECompositionEvent.Create(keyboard.deviceId, imeCompositionCharacters,
IMECompositionEvent.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>();
keyboard.onIMECompositionChange += composition =>
{
Assert.That(callbackWasCalled, Is.False);
callbackWasCalled = true;
Assert.AreEqual(composition.Count, 0);
Assert.AreEqual(composition.ToString(), imeCompositionCharacters);
};

IMECompositionEvent.QueueEvent(keyboard.deviceId, imeCompositionCharacters,
InputRuntime.s_Instance.currentTime);
InputSystem.QueueEvent(ref inputEvent);
InputSystem.Update();

Assert.That(callbackWasCalled, Is.True);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,106 @@
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Utilities;

namespace UnityEngine.InputSystem.LowLevel
{
/// <summary>
/// Helper methods to convert NativeArray objects to C# strings.
/// </summary>
static class NativeArrayStringExtension
{
/// <summary>
/// Extension method to convert a NativeArray/<char/> to a C# string.
/// </summary>
/// <param name="c">The NativeArray containing the string data.</param>
/// <returns>A string representation of the Native Array character buffer.</returns>
public static unsafe string ToString(this NativeArray<char> c)
{
return new string((char*)NativeArrayUnsafeUtility.GetUnsafePtr(c), 0, c.Length);
}
}

/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = InputEvent.kBaseEventSize + sizeof(int) + (sizeof(char) * kIMECharBufferSize))]
[StructLayout(LayoutKind.Explicit, Size = InputEvent.kBaseEventSize + sizeof(int) + (sizeof(char)))]
public struct IMECompositionEvent : IInputEventTypeInfo
{
// These needs to match the native ImeCompositionStringInputEventData settings
internal const int kIMECharBufferSize = 64;
public const int Type = 0x494D4553;
public const int Type = 0x494D4543;

[FieldOffset(0)]
public InputEvent baseEvent;

[FieldOffset(InputEvent.kBaseEventSize)]
public IMECompositionString compositionString;
internal int length;

[FieldOffset(InputEvent.kBaseEventSize + sizeof(int))]
internal char bufferStart;

public FourCC typeStatic => Type;

public static IMECompositionEvent Create(int deviceId, string compositionString, double time)
internal IMECompositionString GetComposition()
{
var nativeArray = GetCharacters();
var str = new IMECompositionString(nativeArray);
nativeArray.Dispose();
return str;
}

/// <summary>
/// Gets the IME characters packed into a variable sized NativeArray.
/// </summary>
/// <remarks>
/// It is the callers responsibility to dispose of the NativeArray.
/// </remarks>
/// <returns>The IME Characters for this event.</returns>
public unsafe NativeArray<char> GetCharacters()
{
var characters = new NativeArray<char>(length + sizeof(char), Allocator.Temp);
var ptr = NativeArrayUnsafeUtility.GetUnsafePtr(characters);
fixed(char* buffer = &bufferStart)
{
UnsafeUtility.MemCpy(ptr, buffer, length + sizeof(char));
}
return characters;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="deviceId">ID of the device (see <see cref="InputDevice.deviceId") to which the composition event should be sent to. Should be an <see cref="ITextInputReceiver"/> device. Will trigger <see cref="ITextInputReceiver.OnIMECompositionChanged"/> call when processed.</param>
/// <param name="str">The IME characters to be sent. This can be any length, or left blank to represent a resetting of the IME dialog.</param>
/// <param name="time">The time in seconds, the event was generated at. This uses the same timeline as <see cref="Time.realtimeSinceStartup"/></param>
public static void QueueEvent(int deviceId, string str, double time)
{
var inputEvent = new IMECompositionEvent();
inputEvent.baseEvent = new InputEvent(Type, InputEvent.kBaseEventSize + sizeof(int) + (sizeof(char) * kIMECharBufferSize), deviceId, time);
inputEvent.compositionString = new IMECompositionString(compositionString);
return inputEvent;
unsafe
{
int sizeInBytes = (InputEvent.kBaseEventSize + sizeof(int) + sizeof(char)) + (sizeof(char) * str.Length);
NativeArray<Byte> eventBuffer = new NativeArray<byte>(sizeInBytes, Allocator.Temp, NativeArrayOptions.UninitializedMemory);

byte* ptr = (byte*)NativeArrayUnsafeUtility.GetUnsafePtr(eventBuffer);
InputEvent* evt = (InputEvent*)ptr;

*evt = new InputEvent(Type, sizeInBytes, deviceId, time);
ptr += InputEvent.kBaseEventSize;

int* lengthPtr = (int*)ptr;
*lengthPtr = str.Length;

ptr += sizeof(int);

fixed(char* p = str)
{
UnsafeUtility.MemCpy(ptr, p, str.Length * sizeof(char));
}

InputSystem.QueueEvent(new InputEventPtr(evt));
}
}
}

Expand Down Expand Up @@ -111,6 +182,17 @@ public char this[int index]
[FieldOffset(sizeof(int))]
fixed char buffer[IMECompositionEvent.kIMECharBufferSize];

public IMECompositionString(NativeArray<char> characters)
{
int copySize = IMECompositionEvent.kIMECharBufferSize < characters.Length ? IMECompositionEvent.kIMECharBufferSize : characters.Length;
fixed(char* ptr = buffer)
{
void* arrayPtr = NativeArrayUnsafeUtility.GetUnsafePtr(characters);
UnsafeUtility.MemCpy(ptr, arrayPtr, copySize * sizeof(char));
}
size = copySize;
}

public IMECompositionString(string characters)
{
if (string.IsNullOrEmpty(characters))
Expand Down
2 changes: 1 addition & 1 deletion Packages/com.unity.inputsystem/InputSystem/InputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2803,7 +2803,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev
{
var imeEventPtr = (IMECompositionEvent*)currentEventReadPtr;
var textInputReceiver = device as ITextInputReceiver;
textInputReceiver?.OnIMECompositionChanged(imeEventPtr->compositionString);
textInputReceiver?.OnIMECompositionChanged(imeEventPtr->GetComposition());
break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public struct PoseState : IInputStateTypeInfo

/// <summary>
/// Constructor for PoseStates.
///
///
/// Useful for creating PoseStates locally (not from <see cref="PoseControl"/>).
/// </summary>
/// <param name="isTracked">Value to use for <see cref="isTracked"/></param>
Expand Down Expand Up @@ -64,7 +64,7 @@ public PoseState(bool isTracked, TrackingState trackingState, Vector3 position,
/// <summary>
/// A Flags Enumeration specifying which other fields in the pose state are valid.
/// </summary>
[FieldOffset(4), InputControl( displayName = "Tracking State", layout = "Integer")]
[FieldOffset(4), InputControl(displayName = "Tracking State", layout = "Integer")]
public TrackingState trackingState;

/// <summary>
Expand Down Expand Up @@ -119,7 +119,7 @@ public PoseState(bool isTracked, TrackingState trackingState, Vector3 position,
/// will not work correctly with a different memory layouts. Additional fields may
/// be appended to the struct but what's there in the struct has to be located
/// at exactly those memory addresses.
///
///
/// For more information on tracking origins see <see cref="UnityEngine.XR.TrackingOriginModeFlags"/>.
/// </remarks>
[Preserve, InputControlLayout(stateType = typeof(PoseState))]
Expand Down