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

Commit 62b213a

Browse files
authored
Merge pull request #159 from UnityTech/textselection
Textselection
2 parents 7adeeab + 8d4fefc commit 62b213a

File tree

4 files changed

+140
-64
lines changed

4 files changed

+140
-64
lines changed

Runtime/painting/image_cache.cs

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ public class ImageCache {
77
const int _kDefaultSize = 1000;
88
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
99

10-
readonly Dictionary<object, ImageStreamCompleter> _pendingImages =
11-
new Dictionary<object, ImageStreamCompleter>();
10+
readonly Dictionary<object, _PendingImage> _pendingImages =
11+
new Dictionary<object, _PendingImage>();
1212

1313
readonly Dictionary<object, _CachedImage> _cache = new Dictionary<object, _CachedImage>();
1414
readonly LinkedList<object> _lruKeys = new LinkedList<object>();
@@ -65,28 +65,39 @@ public int currentSizeBytes {
6565

6666
public void clear() {
6767
this._cache.Clear();
68-
this._lruKeys.Clear();
68+
this._pendingImages.Clear();
6969
this._currentSizeBytes = 0;
70+
71+
this._lruKeys.Clear();
7072
}
7173

7274
public bool evict(object key) {
7375
D.assert(key != null);
7476

77+
if (this._pendingImages.TryGetValue(key, out var pendingImage)) {
78+
pendingImage.removeListener();
79+
this._pendingImages.Remove(key);
80+
return true;
81+
}
82+
7583
if (this._cache.TryGetValue(key, out var image)) {
7684
this._currentSizeBytes -= image.sizeBytes;
77-
this._cache.Remove(image.node);
85+
this._cache.Remove(key);
7886
this._lruKeys.Remove(image.node);
7987
return true;
8088
}
8189

8290
return false;
8391
}
8492

85-
public ImageStreamCompleter putIfAbsent(object key, Func<ImageStreamCompleter> loader) {
93+
public ImageStreamCompleter putIfAbsent(object key, Func<ImageStreamCompleter> loader,
94+
ImageErrorListener onError = null) {
8695
D.assert(key != null);
8796
D.assert(loader != null);
8897

89-
if (this._pendingImages.TryGetValue(key, out var result)) {
98+
ImageStreamCompleter result = null;
99+
if (this._pendingImages.TryGetValue(key, out var pendingImage)) {
100+
result = pendingImage.completer;
90101
return result;
91102
}
92103

@@ -97,40 +108,41 @@ public ImageStreamCompleter putIfAbsent(object key, Func<ImageStreamCompleter> l
97108
return image.completer;
98109
}
99110

100-
result = loader();
101-
102-
if (this._maximumSize > 0 && this._maximumSizeBytes > 0) {
103-
D.assert(!this._pendingImages.ContainsKey(key));
104-
this._pendingImages[key] = result;
105-
106-
ImageListener listener = null;
107-
listener = (info, syncCall) => {
108-
result.removeListener(listener);
111+
try {
112+
result = loader();
113+
}
114+
catch (Exception ex) {
115+
if (onError != null) {
116+
onError(ex);
117+
}
118+
else {
119+
throw;
120+
}
121+
}
109122

110-
D.assert(this._pendingImages.ContainsKey(key));
111-
this._pendingImages.Remove(key);
123+
void listener(ImageInfo info, bool syncCall) {
124+
int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
125+
_CachedImage cachedImage = new _CachedImage(result, imageSize);
112126

113-
int imageSize = info?.image == null ? 0 : info.image.width * info.image.height * 4;
114-
_CachedImage cachedImage = new _CachedImage {
115-
completer = result,
116-
sizeBytes = imageSize,
117-
};
127+
if (this.maximumSizeBytes > 0 && imageSize > this.maximumSizeBytes) {
128+
this._maximumSizeBytes = imageSize + 1000;
129+
}
118130

119-
// If the image is bigger than the maximum cache size, and the cache size
120-
// is not zero, then increase the cache size to the size of the image plus
121-
// some change.
122-
if (this._maximumSizeBytes > 0 && imageSize > this._maximumSizeBytes) {
123-
this._maximumSizeBytes = imageSize + 1000;
124-
}
131+
this._currentSizeBytes += imageSize;
125132

126-
this._currentSizeBytes += imageSize;
133+
if (this._pendingImages.TryGetValue(key, out var loadedPendingImage)) {
134+
loadedPendingImage.removeListener();
135+
this._pendingImages.Remove(key);
136+
}
127137

128-
D.assert(!this._cache.ContainsKey(key));
129-
this._cache[key] = cachedImage;
130-
cachedImage.node = this._lruKeys.AddLast(key);
138+
D.assert(!this._cache.ContainsKey(key));
139+
this._cache[key] = cachedImage;
140+
cachedImage.node = this._lruKeys.AddLast(key);
141+
this._checkCacheSize();
142+
}
131143

132-
this._checkCacheSize();
133-
};
144+
if (this.maximumSize > 0 && this.maximumSizeBytes > 0) {
145+
this._pendingImages[key] = new _PendingImage(result, listener);
134146
result.addListener(listener);
135147
}
136148

@@ -158,8 +170,31 @@ void _checkCacheSize() {
158170
}
159171

160172
class _CachedImage {
173+
public _CachedImage(ImageStreamCompleter completer, int sizeBytes) {
174+
this.completer = completer;
175+
this.sizeBytes = sizeBytes;
176+
}
177+
161178
public ImageStreamCompleter completer;
162179
public int sizeBytes;
163180
public LinkedListNode<object> node;
164181
}
182+
183+
class _PendingImage {
184+
public _PendingImage(
185+
ImageStreamCompleter completer,
186+
ImageListener listener
187+
) {
188+
this.completer = completer;
189+
this.listener = listener;
190+
}
191+
192+
public readonly ImageStreamCompleter completer;
193+
194+
public readonly ImageListener listener;
195+
196+
public void removeListener() {
197+
this.completer.removeListener(this.listener);
198+
}
199+
}
165200
}

Runtime/painting/image_stream.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ public abstract class ImageStreamCompleter : Diagnosticable {
159159
public ImageInfo currentImage;
160160
public UIWidgetsErrorDetails currentError;
161161

162+
protected bool hasListeners {
163+
get { return this._listeners.isNotEmpty(); }
164+
}
165+
162166
public virtual void addListener(ImageListener listener, ImageErrorListener onError = null) {
163167
this._listeners.Add(new _ImageListenerPair {listener = listener, errorListener = onError});
164168

@@ -245,7 +249,7 @@ protected void reportError(
245249
catch (Exception ex) {
246250
UIWidgetsError.reportError(
247251
new UIWidgetsErrorDetails(
248-
context: "by an image error listener",
252+
context: "when reporting an error to an image listener",
249253
library: "image resource service",
250254
exception: ex
251255
)
@@ -325,7 +329,7 @@ void _handleCodecReady(Codec codec) {
325329
}
326330

327331
void _handleAppFrame(TimeSpan timestamp) {
328-
if (!this._hasActiveListeners) {
332+
if (!this.hasListeners) {
329333
return;
330334
}
331335

@@ -384,12 +388,8 @@ void _emitFrame(ImageInfo imageInfo) {
384388
this._framesEmitted += 1;
385389
}
386390

387-
bool _hasActiveListeners {
388-
get { return this._listeners.isNotEmpty(); }
389-
}
390-
391391
public override void addListener(ImageListener listener, ImageErrorListener onError = null) {
392-
if (!this._hasActiveListeners && this._codec != null) {
392+
if (!this.hasListeners && this._codec != null) {
393393
this._decodeNextFrameAndSchedule();
394394
}
395395

@@ -398,7 +398,7 @@ public override void addListener(ImageListener listener, ImageErrorListener onEr
398398

399399
public override void removeListener(ImageListener listener) {
400400
base.removeListener(listener);
401-
if (!this._hasActiveListeners) {
401+
if (!this.hasListeners) {
402402
this._timer?.cancel();
403403
this._timer = null;
404404
}

Runtime/painting/text_painter.cs

Lines changed: 61 additions & 21 deletions
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.service;
54
using Unity.UIWidgets.ui;
@@ -66,7 +65,7 @@ public string ellipsis {
6665
public TextSpan text {
6766
get { return this._text; }
6867
set {
69-
if ((this._text == null && value == null) || (this._text != null && this.text.Equals(value))) {
68+
if ((this._text == null && value == null) || (this._text != null && this.text.Equals(value))) {
7069
return;
7170
}
7271

@@ -353,41 +352,82 @@ float _applyFloatingPointHack(float layoutValue) {
353352
return Mathf.Ceil(layoutValue);
354353
}
355354

355+
const int _zwjUtf16 = 0x200d;
356356

357357
Offset _getOffsetFromUpstream(int offset, Rect caretPrototype) {
358-
var prevCodeUnit = this._text.codeUnitAt(offset - 1);
358+
string flattenedText = this._text.toPlainText();
359+
var prevCodeUnit = this._text.codeUnitAt(Mathf.Max(0, offset - 1));
359360
if (prevCodeUnit == null) {
360361
return null;
361362
}
362363

363-
var prevRuneOffset = _isUtf16Surrogate((int) prevCodeUnit) ? offset - 2 : offset - 1;
364-
var boxes = this._paragraph.getRectsForRange(prevRuneOffset, offset);
365-
if (boxes.Count == 0) {
366-
return null;
364+
bool needsSearch = _isUtf16Surrogate(prevCodeUnit.Value) || this._text.codeUnitAt(offset) == _zwjUtf16;
365+
int graphemeClusterLength = needsSearch ? 2 : 1;
366+
List<TextBox> boxes = null;
367+
while ((boxes == null || boxes.isEmpty()) && flattenedText != null) {
368+
int prevRuneOffset = offset - graphemeClusterLength;
369+
boxes = this._paragraph.getRectsForRange(prevRuneOffset, offset);
370+
if (boxes.isEmpty()) {
371+
if (!needsSearch) {
372+
break;
373+
}
374+
375+
if (prevRuneOffset < -flattenedText.Length) {
376+
break;
377+
}
378+
379+
graphemeClusterLength *= 2;
380+
continue;
381+
}
382+
383+
TextBox box = boxes[0];
384+
const int NEWLINE_CODE_UNIT = 10;
385+
if (prevCodeUnit == NEWLINE_CODE_UNIT) {
386+
return new Offset(this._emptyOffset.dx, box.bottom);
387+
}
388+
389+
float caretEnd = box.end;
390+
float dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
391+
return new Offset(dx, box.top);
367392
}
368393

369-
var box = boxes[0];
370-
var caretEnd = box.end;
371-
var dx = box.direction == TextDirection.rtl ? caretEnd : caretEnd - caretPrototype.width;
372-
return new Offset(dx, box.top);
394+
return null;
373395
}
374396

375397
Offset _getOffsetFromDownstream(int offset, Rect caretPrototype) {
376-
var nextCodeUnit = this._text.codeUnitAt(offset - 1);
398+
string flattenedText = this._text.toPlainText();
399+
var nextCodeUnit =
400+
this._text.codeUnitAt(Mathf.Min(offset, flattenedText == null ? 0 : flattenedText.Length - 1));
377401
if (nextCodeUnit == null) {
378402
return null;
379403
}
380404

381-
var nextRuneOffset = _isUtf16Surrogate((int) nextCodeUnit) ? offset + 2 : offset + 1;
382-
var boxes = this._paragraph.getRectsForRange(offset, nextRuneOffset);
383-
if (boxes.Count == 0) {
384-
return null;
405+
bool needsSearch = _isUtf16Surrogate(nextCodeUnit.Value) || nextCodeUnit == _zwjUtf16;
406+
int graphemeClusterLength = needsSearch ? 2 : 1;
407+
List<TextBox> boxes = null;
408+
while ((boxes == null || boxes.isEmpty()) && flattenedText != null) {
409+
int nextRuneOffset = offset + graphemeClusterLength;
410+
boxes = this._paragraph.getRectsForRange(offset, nextRuneOffset);
411+
if (boxes.isEmpty()) {
412+
if (!needsSearch) {
413+
break;
414+
}
415+
416+
if (nextRuneOffset >= flattenedText.Length << 1) {
417+
break;
418+
}
419+
420+
graphemeClusterLength *= 2;
421+
continue;
422+
}
423+
424+
TextBox box = boxes[boxes.Count - 1];
425+
float caretStart = box.start;
426+
float dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
427+
return new Offset(dx, box.top);
385428
}
386429

387-
var box = boxes[0];
388-
var caretStart = box.start;
389-
var dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
390-
return new Offset(dx, box.top);
430+
return null;
391431
}
392432

393433
Offset _emptyOffset {

Samples/UIWidgetSample/AsScreenSample.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ Widget _buildHeader(BuildContext context) {
7373
fontSize: 16
7474
),
7575
selectionColor: Color.fromARGB(255, 255, 0, 0),
76-
cursorColor: Color.fromARGB(255, 0, 0, 0)
76+
cursorColor: Color.fromARGB(255, 0, 0, 0),
77+
backgroundCursorColor: Colors.blue
7778
)
7879
),
7980
new Container(

0 commit comments

Comments
 (0)