Skip to content
This repository was archived by the owner on Apr 29, 2021. It is now read-only.

Commit 944fe51

Browse files
authored
Merge pull request #231 from UnityTech/emoji_new
Support Emoji Display and Edit
2 parents 63014be + c6f829c commit 944fe51

18 files changed

+919
-115
lines changed

README-ZH.md

+6
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ $JSEvents
211211
默认情况下,Unity会将导入图片的宽和高放缩为最近的等于2的幂的整数。
212212
在UIWidgets中使用图片时,记得将这一特性关闭,以免图片被意外放缩,方法如下:在Project面板中选中图片,在"Inspector"面板中将"Non Power of 2"(在"Advanced"中)设置为"None"。
213213

214+
#### 十、更新表情(Emoji)
215+
UIWidgets支持渲染文本中包含的表情。表情的图片来自[https://www.joypixels.com](https://www.joypixels.com/)提供的免费资源。
216+
如果您希望使用自己的表情图片,请更新纹理图`Tests/Resources/Emoji.png`,以及`Runtime/ui/txt/emoji.cs`中将Unicode映射到纹理图中具体位置的映射表。
217+
特别地,请记得更新Dictionary变量`emojiLookupTable`,纹理图的行数`rowCount`以及纹理图的列数`colCount`
218+
219+
214220
## 调试UIWidgets应用程序
215221

216222
#### 定义UIWidgets_DEBUG

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,15 @@ To learn about the original script in detail, please refer to `SystemInfo.js` an
248248
Unity, by default, resizes the width and height of an imported image to the nearest integer that is a power of 2.
249249
In UIWidgets, you should almost always disable this by selecting the image in the "Project" panel, then in the "Inspector" panel set the "Non Power of 2" option (in "Advanced") to "None", to prevent your image from being resized unexpectedly.
250250

251+
#### Update Emoji
252+
UIWidgets supports rendering emoji in (editable) texts. The emoji images comes from the free
253+
resources provided by [https://www.joypixels.com](https://www.joypixels.com/). If you would
254+
like to use your own images for emoji, please update the texture image `Tests/Resources/Emoji.png`,
255+
and the unicode-index table in `Runtime/ui/txt/emoji.cs` which maps unicodes to specific locations
256+
in the texture. Specifically, remember to update the Dictionary `emojiLookupTable`, number of rows
257+
in the texture `rowCount`, and number of columns `colCount`.
258+
259+
251260
## Debug UIWidgets Application
252261

253262
#### Define UIWidgets_DEBUG

Runtime/Plugins/platform/android/editing/InputConnectionAdaptor.java

+18
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ public boolean sendKeyEvent(KeyEvent event) {
130130
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
131131
int selStart = Selection.getSelectionStart(mEditable);
132132
int selEnd = Selection.getSelectionEnd(mEditable);
133+
String text = mEditable.toString();
134+
if(selStart >= 0 && selStart < text.length() && isTrailSurrogate(text.charAt(selStart))) {
135+
selStart++;
136+
}
137+
if(selEnd >= 0 && selEnd < text.length() && isTrailSurrogate(text.charAt(selEnd))) {
138+
selEnd++;
139+
}
133140
if (selEnd > selStart) {
134141
// Delete the selection.
135142
Selection.setSelection(mEditable, selStart);
@@ -139,6 +146,8 @@ public boolean sendKeyEvent(KeyEvent event) {
139146
} else if (selStart > 0) {
140147
// Delete to the left of the cursor.
141148
int newSel = Math.max(selStart - 1, 0);
149+
if(selStart >= 2 && isTrailSurrogate(text.charAt(selStart-1)))
150+
newSel = selStart - 2;
142151
Selection.setSelection(mEditable, newSel);
143152
mEditable.delete(newSel, selStart);
144153
updateEditingState();
@@ -212,4 +221,13 @@ public boolean performEditorAction(int actionCode) {
212221
}
213222
return true;
214223
}
224+
225+
private boolean isLeadSurrogate(int c) {
226+
return (c & 0xfffffc00) == 0xd800;
227+
}
228+
229+
230+
private boolean isTrailSurrogate(int c) {
231+
return (c & 0xfffffc00) == 0xdc00;
232+
}
215233
}

Runtime/painting/text_span.cs

+19-3
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ public class TextSpan : DiagnosticableTree, IEquatable<TextSpan> {
1313

1414
public readonly TextStyle style;
1515
public readonly string text;
16+
public List<string> splitedText;
1617
public readonly List<TextSpan> children;
1718
public readonly GestureRecognizer recognizer;
1819
public readonly HoverRecognizer hoverRecognizer;
1920

2021
public TextSpan(string text = "", TextStyle style = null, List<TextSpan> children = null,
2122
GestureRecognizer recognizer = null, HoverRecognizer hoverRecognizer = null) {
2223
this.text = text;
24+
this.splitedText = !string.IsNullOrEmpty(text) ? EmojiUtils.splitByEmoji(text) : null;
2325
this.style = style;
2426
this.children = children;
2527
this.recognizer = recognizer;
@@ -28,14 +30,27 @@ public TextSpan(string text = "", TextStyle style = null, List<TextSpan> childre
2830

2931
public void build(ParagraphBuilder builder, float textScaleFactor = 1.0f) {
3032
var hasStyle = this.style != null;
33+
3134
if (hasStyle) {
3235
builder.pushStyle(this.style, textScaleFactor);
3336
}
3437

35-
if (!string.IsNullOrEmpty(this.text)) {
36-
builder.addText(this.text);
38+
if (this.splitedText != null) {
39+
if (this.splitedText.Count == 1 && !char.IsHighSurrogate(this.splitedText[0][0]) &&
40+
!EmojiUtils.isSingleCharEmoji(this.splitedText[0][0])) {
41+
builder.addText(this.splitedText[0]);
42+
}
43+
else {
44+
TextStyle style = this.style ?? new TextStyle();
45+
for (int i = 0; i < this.splitedText.Count; i++) {
46+
builder.pushStyle(style, textScaleFactor);
47+
builder.addText(this.splitedText[i]);
48+
builder.pop();
49+
}
50+
}
3751
}
3852

53+
3954
if (this.children != null) {
4055
foreach (var child in this.children) {
4156
Assert.IsNotNull(child);
@@ -56,6 +71,7 @@ public bool hasHoverRecognizer {
5671
need = true;
5772
return false;
5873
}
74+
5975
return true;
6076
});
6177
return need;
@@ -173,7 +189,7 @@ public RenderComparison compareTo(TextSpan other) {
173189
if (!Equals(this.hoverRecognizer, other.hoverRecognizer)) {
174190
result = RenderComparison.function > result ? RenderComparison.function : result;
175191
}
176-
192+
177193
if (this.style != null) {
178194
var candidate = this.style.compareTo(other.style);
179195
if (candidate > result) {

Runtime/rendering/editable.cs

+42-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using Unity.UIWidgets.foundation;
43
using Unity.UIWidgets.gestures;
54
using Unity.UIWidgets.painting;
@@ -327,16 +326,34 @@ int _handleControl(bool rightArrow, bool leftArrow, bool ctrl, int newOffset) {
327326

328327
int _handleHorizontalArrows(bool rightArrow, bool leftArrow, bool shift, int newOffset) {
329328
if (rightArrow && this._extentOffset < this.text.text.Length) {
330-
newOffset += 1;
331-
if (shift) {
332-
this._previousCursorLocation += 1;
329+
if (newOffset < this.text.text.Length - 1 && char.IsHighSurrogate(this.text.text[newOffset])) {
330+
// handle emoji, which takes 2 bytes
331+
newOffset += 2;
332+
if (shift) {
333+
this._previousCursorLocation += 2;
334+
}
335+
}
336+
else {
337+
newOffset += 1;
338+
if (shift) {
339+
this._previousCursorLocation += 1;
340+
}
333341
}
334342
}
335343

336344
if (leftArrow && this._extentOffset > 0) {
337-
newOffset -= 1;
338-
if (shift) {
339-
this._previousCursorLocation -= 1;
345+
if (newOffset > 1 && char.IsLowSurrogate(this.text.text[newOffset - 1])) {
346+
// handle emoji, which takes 2 bytes
347+
newOffset -= 2;
348+
if (shift) {
349+
this._previousCursorLocation -= 2;
350+
}
351+
}
352+
else {
353+
newOffset -= 1;
354+
if (shift) {
355+
this._previousCursorLocation -= 1;
356+
}
340357
}
341358
}
342359

@@ -483,11 +500,20 @@ void _handleShortcuts(KeyCommand cmd) {
483500
void _handleDelete() {
484501
var selection = this.selection;
485502
if (selection.textAfter(this.text.text).isNotEmpty()) {
486-
this.textSelectionDelegate.textEditingValue = new TextEditingValue(
487-
text: selection.textBefore(this.text.text)
488-
+ selection.textAfter(this.text.text).Substring(1),
489-
selection: TextSelection.collapsed(offset: selection.start)
490-
);
503+
if (char.IsHighSurrogate(this.text.text[selection.end])) {
504+
this.textSelectionDelegate.textEditingValue = new TextEditingValue(
505+
text: selection.textBefore(this.text.text)
506+
+ selection.textAfter(this.text.text).Substring(2),
507+
selection: TextSelection.collapsed(offset: selection.start)
508+
);
509+
}
510+
else {
511+
this.textSelectionDelegate.textEditingValue = new TextEditingValue(
512+
text: selection.textBefore(this.text.text)
513+
+ selection.textAfter(this.text.text).Substring(1),
514+
selection: TextSelection.collapsed(offset: selection.start)
515+
);
516+
}
491517
}
492518
else {
493519
this.textSelectionDelegate.textEditingValue = new TextEditingValue(
@@ -877,7 +903,7 @@ public TextPosition getParagraphForward(TextPosition position, TextAffinity? aff
877903

878904
public TextPosition getParagraphBackward(TextPosition position, TextAffinity? affinity = null) {
879905
var lineCount = this._textPainter.getLineCount();
880-
906+
881907
Paragraph.LineRange line = null;
882908
for (int i = lineCount - 1; i >= 0; --i) {
883909
line = this._textPainter.getLineRange(i);
@@ -1160,7 +1186,8 @@ void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPositio
11601186
}
11611187
}
11621188

1163-
public void setFloatingCursor(FloatingCursorDragState? state, Offset boundedOffset, TextPosition lastTextPosition,
1189+
public void setFloatingCursor(FloatingCursorDragState? state, Offset boundedOffset,
1190+
TextPosition lastTextPosition,
11641191
float? resetLerpValue = null) {
11651192
D.assert(boundedOffset != null);
11661193
D.assert(lastTextPosition != null);

Runtime/service/keyboard.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ public void OnGUI() {
108108
currentEvent.Use();
109109
}
110110

111-
if (this._value != oldValue) {
111+
if (this._value != oldValue) {
112112
Window.instance.run(() => { TextInput._updateEditingState(this._client, this._value); });
113-
}
113+
}
114114
}
115115

116116
public void Dispose() {

Runtime/service/text_input.cs

+33-1
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,23 @@ public class TextEditingValue : IEquatable<TextEditingValue> {
203203

204204
public TextEditingValue(string text = "", TextSelection selection = null, TextRange composing = null) {
205205
this.text = text;
206-
this.selection = selection ?? TextSelection.collapsed(-1);
207206
this.composing = composing ?? TextRange.empty;
207+
208+
if (selection != null && selection.start >= 0 && selection.end >= 0) {
209+
// handle surrogate pair emoji, which takes 2 utf16 chars
210+
// if selection cuts in the middle of the emoji, move it to the end
211+
int start = selection.start, end = selection.end;
212+
if (start < text.Length && char.IsLowSurrogate(text[start])) {
213+
start++;
214+
}
215+
if (end < text.Length && char.IsLowSurrogate(text[end])) {
216+
end++;
217+
}
218+
this.selection = selection.copyWith(start, end);
219+
}
220+
else {
221+
this.selection = TextSelection.collapsed(-1);
222+
}
208223
}
209224

210225
public static TextEditingValue fromJson(JSONObject json) {
@@ -258,6 +273,23 @@ public TextEditingValue deleteSelection(bool backDelete = true) {
258273
return this;
259274
}
260275

276+
if (char.IsHighSurrogate(this.text[this.selection.start - 1])) {
277+
return this.copyWith(
278+
text: this.text.Substring(0, this.selection.start - 1) +
279+
this.text.Substring(this.selection.start + 1),
280+
selection: TextSelection.collapsed(this.selection.start - 1),
281+
composing: TextRange.empty);
282+
}
283+
284+
if (char.IsLowSurrogate(this.text[this.selection.start - 1])) {
285+
D.assert(this.selection.start > 1);
286+
return this.copyWith(
287+
text: this.text.Substring(0, this.selection.start - 2) +
288+
this.selection.textAfter(this.text),
289+
selection: TextSelection.collapsed(this.selection.start - 2),
290+
composing: TextRange.empty);
291+
}
292+
261293
return this.copyWith(
262294
text: this.text.Substring(0, this.selection.start - 1) + this.selection.textAfter(this.text),
263295
selection: TextSelection.collapsed(this.selection.start - 1),

0 commit comments

Comments
 (0)