浏览代码

Merge branch 'text_edit' into 'master'

Text edit

See merge request upm-packages/ui-widgets/com.unity.uiwidgets!42
/main
Shenhua Gu 6 年前
当前提交
fba08ca2
共有 21 个文件被更改,包括 1047 次插入509 次删除
  1. 6
      Runtime/painting/text_painter.cs
  2. 2
      Runtime/painting/text_style.cs
  3. 15
      Runtime/rendering/editable.cs
  4. 6
      Runtime/rendering/paragraph.cs
  5. 3
      Runtime/ui/painting/canvas.cs
  6. 7
      Runtime/ui/painting/canvas_impl.cs
  7. 106
      Runtime/ui/painting/txt/mesh_generator.cs
  8. 56
      Runtime/ui/painting/txt/text_blob.cs
  9. 4
      Runtime/ui/text.cs
  10. 314
      Runtime/ui/txt/linebreaker.cs
  11. 721
      Runtime/ui/txt/paragraph.cs
  12. 6
      Runtime/ui/txt/word_separate.cs
  13. 4
      Runtime/widgets/basic.cs
  14. 2
      Runtime/widgets/routes.cs
  15. 8
      Tests/Editor/EditableTextWiget.cs
  16. 102
      Runtime/ui/txt/layout.cs
  17. 11
      Runtime/ui/txt/layout.cs.meta
  18. 14
      Runtime/ui/txt/layout_utils.cs
  19. 11
      Runtime/ui/txt/layout_utils.cs.meta
  20. 147
      Runtime/ui/txt/wordbreaker.cs
  21. 11
      Runtime/ui/txt/wordbreaker.cs.meta

6
Runtime/painting/text_painter.cs


Paragraph _layoutTemplate;
Paragraph _paragraph;
bool _needsLayout = true;
int _maxLines;
int? _maxLines;
string _ellipsis;
double _lastMinWidth;
double _lastMaxWidth;

TextDirection textDirection = TextDirection.ltr,
double textScaleFactor = 1.0,
int maxLines = 0,
int? maxLines = null,
string ellipsis = "") {
this._text = text;
this._textAlign = textAlign;

}
}
public int maxLines {
public int? maxLines {
get { return this._maxLines; }
set {
if (this._maxLines == value) {

2
Runtime/painting/text_style.cs


}
public ParagraphStyle getParagraphStyle(TextAlign textAlign,
TextDirection textDirection, string ellipsis, int maxLines, double textScaleFactor = 1.0) {
TextDirection textDirection, string ellipsis, int? maxLines, double textScaleFactor = 1.0) {
return new ParagraphStyle(
textAlign, textDirection, this.fontWeight, this.fontStyle,
maxLines, (this.fontSize ?? _defaultFontSize) * textScaleFactor, this.fontFamily, this.height,

15
Runtime/rendering/editable.cs


TextPainter _textPainter;
Color _cursorColor;
bool _hasFocus;
int _maxLines;
int? _maxLines;
Color _selectionColor;
ViewportOffset _offset;
ValueNotifier<bool> _showCursor;

public RenderEditable(TextSpan text, TextDirection textDirection, ViewportOffset offset,
ValueNotifier<bool> showCursor,
TextAlign textAlign = TextAlign.left, double textScaleFactor = 1.0, Color cursorColor = null,
bool? hasFocus = null, int maxLines = 1, Color selectionColor = null,
bool? hasFocus = null, int? maxLines = 1, Color selectionColor = null,
TextSelection selection = null, bool obscureText = false, SelectionChangedHandler onSelectionChanged = null,
CaretChangedHandler onCaretChanged = null, bool ignorePointer = false) {
this._textPainter = new TextPainter(text: text, textAlign: textAlign, textDirection: textDirection,

this.onCaretChanged = onCaretChanged;
this.onSelectionChanged = onSelectionChanged;
D.assert(this._maxLines == null || this._maxLines > 0);
D.assert(this._showCursor != null);
D.assert(!this._showCursor.value || cursorColor != null);

}
}
public int maxLines {
public int? maxLines {
D.assert(value > 0);
D.assert(value == null || value > 0);
if (this._maxLines == value) {
return;
}

}
double _preferredHeight(double width) {
if (this.maxLines <= 0) {
return this.preferredLineHeight * this.maxLines;
if (this.maxLines != null) {
return this.preferredLineHeight * this.maxLines.Value;
}
if (double.IsInfinity(width)) {

base.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<Color>("cursorColor", this.cursorColor));
properties.add(new DiagnosticsProperty<ValueNotifier<bool>>("showCursor", this.showCursor));
properties.add(new DiagnosticsProperty<int>("maxLines", this.maxLines));
properties.add(new DiagnosticsProperty<int?>("maxLines", this.maxLines));
properties.add(new DiagnosticsProperty<Color>("selectionColor", this.selectionColor));
properties.add(new DiagnosticsProperty<double>("textScaleFactor", this.textScaleFactor));
properties.add(new DiagnosticsProperty<TextSelection>("selection", this.selection));

6
Runtime/rendering/paragraph.cs


bool softWrap = true,
TextOverflow overflow = TextOverflow.clip,
double textScaleFactor = 1.0,
int maxLines = 0
int? maxLines = null
D.assert(maxLines == null || maxLines > 0);
this._softWrap = softWrap;
this._overflow = overflow;
this._textPainter = new TextPainter(

}
}
public int maxLines {
public int? maxLines {
D.assert(this.maxLines == null || this.maxLines > 0);
if (this._textPainter.maxLines == value) {
return;
}

3
Runtime/ui/painting/canvas.cs


}
public void drawRect(Rect rect, Paint paint) {
if (rect.size.isEmpty) {
return;
}
var path = new Path();
path.addRect(rect);

7
Runtime/ui/painting/canvas_impl.cs


XformUtils.transformTranslate(xform, (float) offset.dx, (float) offset.dy);
XformUtils.transformMultiply(xform, state.xform); // xform = state.xform * xform
var scale = XformUtils.getAverageScale(xform) * this._devicePixelRatio;
var mesh = MeshGenerator.generateMesh(textBlob, scale).transform(xform);
var scale = XformUtils.getAverageScale(xform) * this._devicePixelRatio;
var mesh = MeshGenerator.generateMesh(textBlob, scale)?.transform(xform);
if (mesh == null) {
return;
}
if (!this._applyClip(mesh.bounds)) {
return;
}

106
Runtime/ui/painting/txt/mesh_generator.cs


namespace Unity.UIWidgets.ui {
class MeshKey : IEquatable<MeshKey> {
public readonly string text;
public readonly int fontId;
public readonly int textureVersion;
public readonly int fontSizeToLoad;
public readonly UnityEngine.FontStyle fontStyle;
public readonly long textBlobId;
public MeshKey(string text, int fontId, int textureVersion, int fontSizeToLoad,
UnityEngine.FontStyle fontStyle, float scale) {
this.text = text;
this.fontId = fontId;
this.textureVersion = textureVersion;
this.fontSizeToLoad = fontSizeToLoad;
this.fontStyle = fontStyle;
public MeshKey(long textBlobId, float scale) {
this.textBlobId = textBlobId;
if (ReferenceEquals(null, other)) {
return false;
}
if (ReferenceEquals(this, other)) {
return true;
}
return string.Equals(this.text, other.text) && this.fontId == other.fontId &&
this.textureVersion == other.textureVersion && this.fontSizeToLoad == other.fontSizeToLoad &&
this.fontStyle == other.fontStyle && this.scale.Equals(other.scale);
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return this.textBlobId == other.textBlobId && this.scale.Equals(other.scale);
if (ReferenceEquals(null, obj)) {
return false;
}
if (ReferenceEquals(this, obj)) {
return true;
}
if (obj.GetType() != this.GetType()) {
return false;
}
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
var hashCode = (this.text != null ? this.text.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ this.fontId;
hashCode = (hashCode * 397) ^ this.textureVersion;
hashCode = (hashCode * 397) ^ this.fontSizeToLoad;
hashCode = (hashCode * 397) ^ (int) this.fontStyle;
hashCode = (hashCode * 397) ^ this.scale.GetHashCode();
return hashCode;
return (this.textBlobId.GetHashCode() * 397) ^ this.scale.GetHashCode();
}
}

}
public override string ToString() {
return
$"MeshKey(text: {this.text}, " +
$"fontId: {this.fontId}, " +
$"textureVersion: {this.textureVersion}, " +
$"fontSizeToLoad: {this.fontSizeToLoad}, " +
$"fontStyle: {this.fontStyle}, " +
$"scale: {this.scale})";
return $"{nameof(this.textBlobId)}: {this.textBlobId}, {nameof(this.scale)}: {this.scale}";
public readonly long textureVersion;
public MeshInfo(MeshKey key, MeshMesh mesh, int timeToLive = 5) {
public MeshInfo(MeshKey key, MeshMesh mesh, long textureVersion, int timeToLive = 5) {
this.textureVersion = textureVersion;
this.touch(timeToLive);
}

public static MeshMesh generateMesh(TextBlob textBlob, float scale) {
var style = textBlob.style;
var fontInfo = FontManager.instance.getOrCreate(style.fontFamily);
var key = new MeshKey(textBlob.instanceId, scale);
_meshes.TryGetValue(key, out var meshInfo);
if (meshInfo != null && meshInfo.textureVersion == fontInfo.textureVersion) {
meshInfo.touch();
return meshInfo.mesh;
}
var length = textBlob.end - textBlob.start;
var length = textBlob.textSize;
var subText = textBlob.text.Substring(textBlob.start, textBlob.end - textBlob.start);
var subText = textBlob.text.Substring(textBlob.textOffset, textBlob.textSize);
var key = new MeshKey(subText, font.GetInstanceID(), fontInfo.textureVersion, fontSizeToLoad,
style.UnityFontStyle, scale);
_meshes.TryGetValue(key, out var meshInfo);
if (meshInfo != null) {
meshInfo.touch();
return meshInfo.mesh;
}
var ch = text[charIndex + textBlob.start];
var ch = text[charIndex + textBlob.textOffset];
var position = textBlob.positions[charIndex + textBlob.start];
if (Paragraph.isWordSpace(ch) || Paragraph.isLineEndSpace(ch) || ch == '\t') {
var position = textBlob.positions[charIndex];
if (LayoutUtils.isWordSpace(ch) || LayoutUtils.isLineEndSpace(ch) || ch == '\t') {
var minX = charInfo.minX / scale;
var maxX = charInfo.maxX / scale;
var minY = charInfo.minY / scale;

uv.Add(charInfo.uvBottomLeft);
}
if (vertices.Count == 0) {
return null;
}
MeshMesh mesh = new MeshMesh(vertices, triangles, uv);
_meshes[key] = new MeshInfo(key, mesh);
MeshMesh mesh = vertices.Count > 0 ? new MeshMesh(vertices, triangles, uv) : null;
_meshes[key] = new MeshInfo(key, mesh, fontInfo.textureVersion);
}
}

56
Runtime/ui/painting/txt/text_blob.cs


using Unity.UIWidgets.foundation;
using System.Collections.Generic;
using Unity.UIWidgets.foundation;
public TextBlob(string text, int start, int end, Vector2d[] positions, TextStyle style, Rect bounds) {
D.assert(start < end);
public TextBlob(string text, int textOffset, int textSize, Vector2d[] positions, Rect bounds, TextStyle style) {
this.instanceId = ++_nextInstanceId;
this.positions = positions;
this.start = start;
this.end = end;
this.positions = positions;
this.textOffset = textOffset;
this.textSize = textSize;
get { return this.bounds.shift(new Offset(this.positions[this.start].x, this.positions[this.start].y)); }
get { return this.bounds.shift(new Offset(this.positions[0].x, this.positions[0].y)); }
static long _nextInstanceId = 0;
public readonly long instanceId;
public readonly int start;
public readonly int end;
public readonly int textOffset;
public readonly int textSize;
}
public class TextBlobBuilder {
TextStyle _style;
public Vector2d[] positions;
string _text;
int _textOffset;
int _size;
Rect _bounds;
public void allocRunPos(TextStyle style, string text, int offset, int size) {
this._style = style;
this._text = text;
this._textOffset = offset;
this._size = size;
if (this.positions == null || this.positions.Length < size) {
this.positions = new Vector2d[size];
}
}
public void setBounds(Rect bounds) {
this._bounds = bounds;
}
public TextBlob make() {
var result = new TextBlob(this._text, this._textOffset,
this._size, this.positions, this._bounds, this._style);
this.positions = null;
return result;
}
}
}

4
Runtime/ui/text.cs


public readonly string fontFamily;
public readonly double? lineHeight;
public readonly string ellipsis;
public bool ellipsized() {
return !string.IsNullOrEmpty(this.ellipsis);
}
}
public enum TextDecorationStyle {

314
Runtime/ui/txt/linebreaker.cs


using System;
using System.Collections.Generic;
using System.Linq;
public class TabStops {
int _tabWidth = int.MaxValue;
Font _font;
int _fontSize;
const int kTabSpaceCount = 4;
List<int> _stops = new List<int>();
public void set(List<int> stops, int tabWidth) {
this._stops.Clear();
if (stops != null) {
this._stops.AddRange(stops);
}
this._tabWidth = tabWidth;
}
public void setFont(Font font, int size) {
if (this._font != font || this._fontSize != size) {
this._tabWidth = int.MaxValue;
}
this._font = font;
this._fontSize = size;
}
public float nextTab(float widthSoFar) {
for (int i = 0; i < this._stops.Count; i++) {
if (this._stops[i] > widthSoFar) {
return this._stops[i];
}
}
if (this._tabWidth == int.MaxValue) {
this._font.RequestCharactersInTexture(" ", this._fontSize);
CharacterInfo characterInfo;
this._font.GetCharacterInfo(' ', out characterInfo, this._fontSize);
this._tabWidth = characterInfo.advance * kTabSpaceCount;
}
if (this._tabWidth == 0) {
return widthSoFar;
}
return (float) (Math.Floor(widthSoFar / this._tabWidth + 1) * this._tabWidth);
}
}
public class Candidate {
public int offset;
public int pre;
public double preBreak;
public float penalty;
public double postBreak;
public int preSpaceCount;
public int postSpaceCount;
}
public class LineInfo {
public int start;
public double width;
}
const float ScoreInfty = float.MaxValue;
const float ScoreDesperate = 1e10f;
StyledRuns _runs;
string _textBuf;
int _textOffset;
int _textLength;
List<float> _charWidths = new List<float>();
List<int> _breaks = new List<int>();
List<float> _widths = new List<float>();
WordBreaker _wordBreaker = new WordBreaker();
double _width = 0.0;
double _preBreak;
double _lineWidth;
int _lastBreak;
int _bestBreak;
float _bestScore;
int _spaceCount;
TabStops _tabStops;
int mFirstTabIndex;
List<Candidate> _candidates = new List<Candidate>();
public Vector2d[] _characterPositions;
public double[] _characterWidth;
string _text;
double _width;
int _lineStart;
int _wordStart;
int _spaceCount = 0;
int tabCount = 4;
double _lineLength;
public int computeBreaks() {
int nCand = this._candidates.Count;
if (nCand > 0 && (nCand == 1 || this._lastBreak != nCand - 1)) {
var cand = this._candidates[this._candidates.Count - 1];
this._pushBreak(cand.offset, (float) (cand.postBreak - this._preBreak));
}
List<LineInfo> _lines;
return this._breaks.Count;
}
public void setup(string text, StyledRuns runs, double width, Vector2d[] characterPositions,
double[] characterWidth) {
this._text = text;
this._runs = runs;
this._characterPositions = characterPositions;
this._characterWidth = characterWidth;
this._width = width;
public List<int> getBreaks() {
return this._breaks;
public List<LineInfo> getLines() {
return this._lines;
public void resize(int size) {
if (this._charWidths.Count < size) {
this._charWidths.AddRange(Enumerable.Repeat(0.0f, size - this._charWidths.Count));
}
public void doBreak(int blockStart, int blockEnd) {
this._lines = new List<LineInfo>();
this._lineStart = blockStart;
this._wordStart = blockStart;
public void setText(string text, int textOffset, int textLength) {
this._textBuf = text;
this._textOffset = textOffset;
this._textLength = textLength;
this._wordBreaker.setText(this._textBuf, textOffset, textLength);
this._wordBreaker.next();
this._candidates.Clear();
Candidate can = new Candidate {
offset = 0, postBreak = 0, preBreak = 0, postSpaceCount = 0, preSpaceCount = 0, pre = 0
};
this._candidates.Add(can);
this._lastBreak = 0;
this._bestBreak = 0;
this._bestScore = ScoreInfty;
this._preBreak = 0;
this.mFirstTabIndex = int.MaxValue;
}
double offsetX = 0.0;
var runIterator = this._runs.iterator();
for (var charIndex = blockStart; charIndex < blockEnd; charIndex++) {
runIterator.nextTo(charIndex);
var run = runIterator.run;
var font = FontManager.instance.getOrCreate(run.style.fontFamily).font;
public void setLineWidth(float lineWidth) {
this._lineWidth = lineWidth;
}
var style = run.style;
var charInfo = new CharacterInfo();
public float addStyleRun(TextStyle style, int start, int end) {
float width = 0.0f;
if (style != null) {
width = Layout.measureText(this._width - this._preBreak, this._textBuf,
start + this._textOffset, end - start, style,
this._charWidths, start, this._tabStops);
}
if (this._text[charIndex] == '\t') {
this._spaceCount++;
var font = FontManager.instance.getOrCreate(style.fontFamily).font;
int current = this._wordBreaker.current();
int afterWord = start;
int lastBreak = start;
font.GetCharacterInfo(' ', out charInfo,
style.UnityFontSize, style.UnityFontStyle);
double tabSize = charInfo.advance * this.tabCount;
var newX = Math.Floor(((offsetX / tabSize) + 1) * tabSize);
if (newX > this._width && this._lineStart != charIndex) {
this._characterWidth[charIndex] = tabSize;
this.makeLine(charIndex, charIndex);
}
else {
this._characterWidth[charIndex] = newX - offsetX;
this._characterPositions[charIndex].x = offsetX;
}
double lastBreakWidth = this._width;
double postBreak = this._width;
int postSpaceCount = this._spaceCount;
offsetX = this._characterPositions[charIndex].x + this._characterWidth[charIndex];
}
else if (this._text[charIndex] == ' ') {
font.GetCharacterInfo(this._text[charIndex], out charInfo, style.UnityFontSize,
run.style.UnityFontStyle);
this._spaceCount++;
this._characterPositions[charIndex].x = offsetX;
this._characterWidth[charIndex] = charInfo.advance;
offsetX = this._characterPositions[charIndex].x + this._characterWidth[charIndex];
// todo no wrap in space ?
for (int i = start; i < end; i++) {
char c = this._textBuf[i + this._textOffset];
if (c == '\t') {
this._width = this._preBreak + this._tabStops.nextTab((float) (this._width - this._preBreak));
if (this.mFirstTabIndex == int.MaxValue) {
this.mFirstTabIndex = i;
}
font.GetCharacterInfo(this._text[charIndex], out charInfo, style.UnityFontSize,
run.style.UnityFontStyle);
if (this._spaceCount > 0 || blockStart == charIndex) {
this._wordStart = charIndex;
if (LayoutUtils.isWordSpace(c)) {
this._spaceCount += 1;
this._characterPositions[charIndex].x = offsetX;
this._characterWidth[charIndex] = charInfo.advance;
this._width += this._charWidths[i];
if (!LayoutUtils.isLineEndSpace(c)) {
postBreak = this._width;
postSpaceCount = this._spaceCount;
afterWord = i + 1;
}
}
if (offsetX + charInfo.advance > this._width && this._lineStart != charIndex) {
if (this._lineStart == this._wordStart) {
this.makeLine(charIndex, charIndex);
this._wordStart = charIndex;
}
else {
this.makeLine(this._wordStart, charIndex);
}
if (i + 1 == current) {
int wordStart = this._wordBreaker.wordStart();
int wordEnd = this._wordBreaker.wordEnd();
if (style != null || current == end || this._charWidths[current] > 0) {
this._addWordBreak(current, this._width, postBreak, this._spaceCount, postSpaceCount, 0);
offsetX = this._characterPositions[charIndex].x + this._characterWidth[charIndex];
this._spaceCount = 0;
lastBreak = current;
lastBreakWidth = this._width;
current = this._wordBreaker.next();
this.makeLine(blockEnd, blockEnd);
return width;
}
public void finish() {
this._wordBreaker.finish();
this._width = 0;
this._candidates.Clear();
this._widths.Clear();
this._breaks.Clear();
this._textBuf = null;
public List<float> getWidths() {
return this._widths;
}
void makeLine(int end, int last) {
Debug.Assert(this._lineStart < end);
Debug.Assert(end <= last);
this._lines.Add(new LineInfo() {
start = this._lineStart,
width = this._characterPositions[end - 1].x + this._characterWidth[end - 1],
});
this._lineStart = end;
public void setTabStops(TabStops tabStops) {
this._tabStops = tabStops;
}
if (end >= this._characterPositions.Length) {
return;
void _addWordBreak(int offset, double preBreak, double postBreak, int preSpaceCount, int postSpaceCount,
float penalty) {
Candidate cand = new Candidate();
double width = this._candidates[this._candidates.Count - 1].preBreak;
if (postBreak - width > this._lineWidth) {
int i = this._candidates[this._candidates.Count - 1].offset;
width += this._charWidths[i++];
for (; i < offset; i++) {
float w = this._charWidths[i];
if (w > 0) {
cand.offset = i;
cand.preBreak = width;
cand.postBreak = width;
cand.preSpaceCount = postSpaceCount;
cand.preSpaceCount = postSpaceCount;
cand.penalty = ScoreDesperate;
this._addCandidate(cand);
width += w;
}
}
var offset = new Vector2d(-this._characterPositions[end].x, 0);
this._characterPositions[end].x = 0;
if (end < last) {
Paragraph.offsetCharacters(offset, this._characterPositions, end + 1, last + 1);
cand.offset = offset;
cand.preBreak = preBreak;
cand.postBreak = postBreak;
cand.penalty = penalty;
cand.preSpaceCount = preSpaceCount;
cand.preSpaceCount = postSpaceCount;
this._addCandidate(cand);
}
void _addCandidate(Candidate cand) {
int candIndex = this._candidates.Count;
this._candidates.Add(cand);
if (cand.postBreak - this._preBreak > this._lineWidth) {
this._pushGreedyBreak();
}
if (cand.penalty <= this._bestScore) {
this._bestBreak = candIndex;
this._bestScore = cand.penalty;
}
void _pushGreedyBreak() {
var bestCandidate = this._candidates[this._bestBreak];
this._pushBreak(bestCandidate.offset, (float) (bestCandidate.postBreak - this._preBreak));
this._bestScore = ScoreInfty;
this._lastBreak = this._bestBreak;
this._preBreak = bestCandidate.preBreak;
}
void _pushBreak(int offset, float width) {
this._breaks.Add(offset);
this._widths.Add(width);
}
}
}

721
Runtime/ui/txt/paragraph.cs


using System;
using System.Collections.Generic;
using System.Linq;
using Unity.UIWidgets.foundation;
using UnityEngine;

}
public class CodeUnitRun {
public int lineNumber;
public TextDirection direction;
public IndexRange codeUnits;
public FontMetrics fontMetrics;
public readonly int lineNumber;
public readonly TextDirection direction;
public readonly Range<int> codeUnits;
public readonly FontMetrics fontMetrics;
public Range<double> xPos;
public readonly List<GlyphPosition> positions;
public CodeUnitRun(IndexRange cu, int line, FontMetrics fontMetrics, TextDirection direction) {
public CodeUnitRun(List<GlyphPosition> positions, Range<int> cu, int line, Range<double> xPos,
FontMetrics fontMetrics, TextDirection direction) {
this.xPos = xPos;
this.positions = positions;
}
public void Shift(double shift) {
this.xPos = RangeUtils.shift(this.xPos, shift);
for (int i = 0; i < this.positions.Count; ++i) {
this.positions[i] = this.positions[i].shift(shift);
}
public readonly double leading = 0.0;
public readonly double descent;
public readonly double? underlineThickness;
public readonly double? underlinePosition;

this.fxHeight = fxHeight;
}
public static FontMetrics fromFont(Font font, int fontSize, double? height) {
var ascent = font.ascent * (height ?? 1.0) * fontSize / font.fontSize;
var descent = (font.lineHeight - font.ascent) * (height ?? 1.0) * fontSize / font.fontSize;
public static FontMetrics fromFont(Font font, int fontSize) {
var ascent = -font.ascent * fontSize / font.fontSize;
var descent = (font.lineHeight - font.ascent) * fontSize / font.fontSize;
double? fxHeight = null;
font.RequestCharactersInTexture("x", fontSize);
CharacterInfo charInfo;

}
}
public struct IndexRange : IEquatable<IndexRange> {
public int start, end;
public class LineStyleRun {
public readonly int start;
public readonly int end;
public readonly TextStyle style;
public LineStyleRun(int start, int end, TextStyle style) {
this.start = start;
this.end = end;
this.style = style;
}
}
public class PositionWithAffinity {
public readonly int position;
public readonly TextAffinity affinity;
public PositionWithAffinity(int p, TextAffinity a) {
this.position = p;
this.affinity = a;
}
}
public class GlyphPosition {
public readonly Range<double> xPos;
public readonly Range<int> codeUnits;
public IndexRange(int s, int e) {
this.start = s;
this.end = e;
public GlyphPosition(double start, double advance, Range<int> codeUnits) {
this.xPos = new Range<double>(start, start + advance);
this.codeUnits = codeUnits;
int width() {
return this.end - this.start;
public GlyphPosition shift(double shift) {
return new GlyphPosition(this.xPos.start + shift, this.xPos.end - this.xPos.start, this.codeUnits);
}
void shift(int delta) {
this.start += delta;
this.end += delta;
public class Range<T> : IEquatable<Range<T>> {
public Range(T start, T end) {
this.start = start;
this.end = end;
public bool Equals(IndexRange other) {
return this.start == other.start && this.end == other.end;
public bool Equals(Range<T> other) {
return EqualityComparer<T>.Default.Equals(this.start, other.start) &&
EqualityComparer<T>.Default.Equals(this.end, other.end);
}
public override bool Equals(object obj) {

return obj is IndexRange && this.Equals((IndexRange) obj);
return obj is Range<T> && this.Equals((Range<T>) obj);
return (this.start * 397) ^ this.end;
return (EqualityComparer<T>.Default.GetHashCode(this.start) * 397) ^
EqualityComparer<T>.Default.GetHashCode(this.end);
public static bool operator ==(IndexRange left, IndexRange right) {
public static bool operator ==(Range<T> left, Range<T> right) {
public static bool operator !=(IndexRange left, IndexRange right) {
public static bool operator !=(Range<T> left, Range<T> right) {
public readonly T start, end;
public class PositionWithAffinity {
public readonly int position;
public readonly TextAffinity affinity;
public PositionWithAffinity(int p, TextAffinity a) {
this.position = p;
this.affinity = a;
public static class RangeUtils {
public static Range<double> shift(Range<double> value, double shift) {
return new Range<double>(value.start + shift, value.end + shift);
public class Paragraph {
struct Range<T> : IEquatable<Range<T>> {
public Range(T start, T end) {
this.start = start;
this.end = end;
}
public bool Equals(Range<T> other) {
return EqualityComparer<T>.Default.Equals(this.start, other.start) &&
EqualityComparer<T>.Default.Equals(this.end, other.end);
}
public override bool Equals(object obj) {
if (ReferenceEquals(null, obj)) {
return false;
}
return obj is Range<T> && this.Equals((Range<T>) obj);
}
public override int GetHashCode() {
unchecked {
return (EqualityComparer<T>.Default.GetHashCode(this.start) * 397) ^
EqualityComparer<T>.Default.GetHashCode(this.end);
}
}
public static bool operator ==(Range<T> left, Range<T> right) {
return left.Equals(right);
}
public static bool operator !=(Range<T> left, Range<T> right) {
return !left.Equals(right);
}
public class GlyphLine {
public readonly List<GlyphPosition> positions;
public readonly int totalCountUnits;
public T start, end;
public GlyphLine(List<GlyphPosition> positions, int totalCountUnits) {
this.positions = positions;
this.totalCountUnits = totalCountUnits;
}
public class Paragraph {
public class LineRange {
public LineRange(int start, int end, int endExcludingWhitespace, int endIncludingNewLine, bool hardBreak) {
this.start = start;

public readonly int endIncludingNewLine;
public readonly bool hardBreak;
}
const int TabSpaceCount = 4;
bool _needsLayout = true;

List<LineRange> _lineRanges = new List<LineRange>();
List<double> _lineWidths = new List<double>();
List<double> _lineBaseLines = new List<double>();
Vector2d[] _characterPositions;
List<GlyphLine> _glyphLines = new List<GlyphLine>();
double _maxIntrinsicWidth;
double _minIntrinsicWidth;
double _alphabeticBaseline;

List<PaintRecord> _paintRecords = new List<PaintRecord>();
List<CodeUnitRun> _codeUnitRuns = new List<CodeUnitRun>();
bool _didExceedMaxLines;
TabStops _tabStops = new TabStops();
public const char CHAR_NBSP = '\u00A0';
public static bool isWordSpace(char ch) {
return ch == ' ' || ch == CHAR_NBSP;
}
// This function determines whether a character is a space that disappears at end of line.
// It is the Unicode set: [[:General_Category=Space_Separator:]-[:Line_Break=Glue:]],
// plus '\n'.
// Note: all such characters are in the BMP, so it's ok to use code units for this.
public static bool isLineEndSpace(char c) {
return c == '\n' || c == ' ' || c == 0x1680 || (0x2000 <= c && c <= 0x200A && c != 0x2007) ||
c == 0x205F || c == 0x3000;
}
public double height {
get { return this._lineHeights.Count == 0 ? 0 : this._lineHeights[this._lineHeights.Count - 1]; }
}

public void paint(Canvas canvas, Offset offset) {
foreach (var paintRecord in this._paintRecords) {
var paint = new Paint {
color = paintRecord.style.color,
filterMode = FilterMode.Bilinear,
color = paintRecord.style.color
canvas.drawTextBlob(paintRecord.text, offset, paint);
canvas.drawTextBlob(paintRecord.text, paintRecord.offset + offset, paint);
this.paintDecorations(canvas, paintRecord, offset);
}
}

return;
}
var textStyle = this._paragraphStyle.getTextStyle();
this._tabStops.setFont(FontManager.instance.getOrCreate(textStyle.fontFamily).font,
textStyle.UnityFontSize);
this.setup();
var maxLines = this._paragraphStyle.maxLines ?? 0;
this._didExceedMaxLines = !(maxLines == 0 || this._lineRanges.Count <= maxLines);
var lineLimits = maxLines == 0 ? this._lineRanges.Count : Math.Min(maxLines, this._lineRanges.Count);
this.layoutLines(lineLimits);
this._paintRecords.Clear();
this._lineHeights.Clear();
this._lineBaseLines.Clear();
this._codeUnitRuns.Clear();
this._glyphLines.Clear();
int styleMaxLines = this._paragraphStyle.maxLines ?? int.MaxValue;
var lineLimit = Math.Min(styleMaxLines, this._lineRanges.Count);
this._didExceedMaxLines = this._lineRanges.Count > styleMaxLines;
for (int lineNumber = 0; lineNumber < lineLimits; ++lineNumber) {
var line = this._lineRanges[lineNumber];
var words = this.findWords(line.start, line.end);
words.ForEach((word) => {
Debug.Assert(word.start < word.end);
double wordWidth = this._characterPositions[word.end - 1].x -
this._characterPositions[word.start].x + this._characterWidths[word.end - 1];
if (wordWidth > maxWordWidth) {
maxWordWidth = wordWidth;
Layout layout = new Layout();
layout.setTabStops(this._tabStops);
TextBlobBuilder builder = new TextBlobBuilder();
int styleRunIndex = 0;
double yOffset = 0;
double preMaxDescent = 0;
List<CodeUnitRun> lineCodeUnitRuns = new List<CodeUnitRun>();
List<GlyphPosition> glyphPositions = new List<GlyphPosition>();
for (int lineNumber = 0; lineNumber < lineLimit; ++lineNumber) {
var lineRange = this._lineRanges[lineNumber];
double wordGapWidth = 0;
// Break the line into words if justification should be applied.
int wordIndex = 0;
bool justifyLine = this._paragraphStyle.textAlign == TextAlign.justify &&
lineNumber != lineLimit - 1 &&
!lineRange.hardBreak;
var words = this.findWords(lineRange.start, lineRange.end);
if (justifyLine) {
if (words.Count > 1) {
wordGapWidth = (this._width - this._lineWidths[lineNumber]) / (words.Count - 1);
}
// Exclude trailing whitespace from right-justified lines so the last
// visible character in the line will be flush with the right margin.
int lineEndIndex = (this._paragraphStyle.textAlign == TextAlign.right ||
this._paragraphStyle.textAlign == TextAlign.center)
? lineRange.endExcludingWhitespace
: lineRange.end;
List<LineStyleRun> lineRuns = new List<LineStyleRun>();
while (styleRunIndex < this._runs.size) {
var styleRun = this._runs.getRun(styleRunIndex);
if (styleRun.start < lineEndIndex && styleRun.end > lineRange.start) {
lineRuns.Add(new LineStyleRun(Math.Max(styleRun.start, lineRange.start),
Math.Min(styleRun.end, lineEndIndex), styleRun.style));
}
if (styleRun.end >= lineEndIndex) {
break;
}
styleRunIndex++;
}
double runXOffset = 0;
double justifyXOffset = 0;
lineCodeUnitRuns.Clear();
List<GlyphPosition> lineGlyphPositions = new List<GlyphPosition>();
List<PaintRecord> paintRecords = new List<PaintRecord>();
for (int i = 0; i < lineRuns.Count; ++i) {
var run = lineRuns[i];
int textStart = Math.Max(run.start, lineRange.start);
int textEnd = Math.Min(run.end, lineEndIndex);
int textCount = textEnd - textStart;
layout.doLayout(runXOffset, this._text, textStart, textCount, run.style);
if (layout.nGlyphs() == 0) {
continue;
}
double wordStartPosition = double.NaN;
// var layoutAdvances = layout.getAdvances();
builder.allocRunPos(run.style, this._text, textStart, textCount);
builder.setBounds(layout.getBounds());
glyphPositions.Clear();
for (int glyphIndex = 0; glyphIndex < textCount; ++glyphIndex) {
double glyphXOffset = layout.getX(glyphIndex) + justifyXOffset;
builder.positions[glyphIndex] = new Vector2d(
glyphXOffset, layout.getY(glyphIndex)
);
float glyphAdvance = layout.getCharAdvance(glyphIndex);
glyphPositions.Add(new GlyphPosition(runXOffset + glyphXOffset, glyphAdvance,
new Range<int>(textStart + glyphIndex, textStart + glyphIndex + 1)));
if (wordIndex < words.Count && words[wordIndex].start == run.start + glyphIndex) {
wordStartPosition = runXOffset + glyphXOffset;
}
if (wordIndex < words.Count && words[wordIndex].end == run.start + glyphIndex + 1) {
if (justifyLine) {
justifyXOffset += wordGapWidth;
}
wordIndex++;
if (!double.IsNaN(wordStartPosition)) {
double wordWidth =
glyphPositions[glyphPositions.Count - 1].xPos.end - wordStartPosition;
maxWordWidth = Math.Max(wordWidth, maxWordWidth);
wordStartPosition = double.NaN;
}
}
}
if (glyphPositions.Count == 0) {
continue;
}
var font = FontManager.instance.getOrCreate(run.style.fontFamily).font;
var metrics = FontMetrics.fromFont(font, run.style.UnityFontSize);
paintRecords.Add(new PaintRecord(run.style, new Offset(runXOffset, 0),
builder.make(), metrics, lineNumber, layout.getAdvance()
));
lineGlyphPositions.AddRange(glyphPositions);
var codeUnitPositions = new List<GlyphPosition>(glyphPositions);
lineCodeUnitRuns.Add(new CodeUnitRun(codeUnitPositions, new Range<int>(run.start, run.end),
lineNumber,
new Range<double>(glyphPositions[0].xPos.start,
glyphPositions[glyphPositions.Count - 1].xPos.end),
metrics, TextDirection.ltr));
runXOffset += layout.getAdvance();
}
double lineXOffset = this.getLineXOffset(runXOffset);
if (lineXOffset != 0) {
foreach (var codeUnitRun in lineCodeUnitRuns) {
codeUnitRun.Shift(lineXOffset);
}
for (int i = 0; i < lineGlyphPositions.Count; ++i) {
lineGlyphPositions[i] = lineGlyphPositions[i].shift(lineXOffset);
}
}
int nextLineStart = (lineNumber < this._lineRanges.Count - 1)
? this._lineRanges[lineNumber + 1].start
: this._text.Length;
this._glyphLines.Add(new GlyphLine(lineGlyphPositions, nextLineStart - lineRange.start));
this._codeUnitRuns.AddRange(lineCodeUnitRuns);
double maxLineSpacing = 0;
double maxDescent = 0;
var updateLineMetrics = new Action<FontMetrics, TextStyle>((metrics, style) => {
double lineSpacing = (lineNumber == 0)
? -metrics.ascent * style.height
: (-metrics.ascent + metrics.leading) * (style.height);
if (lineSpacing > maxLineSpacing) {
maxLineSpacing = lineSpacing;
if (lineNumber == 0) {
this._alphabeticBaseline = lineSpacing;
this._ideographicBaseline =
(metrics.underlinePosition ?? 0.0 - metrics.ascent) * style.height;
}
}
double descent = metrics.descent * style.height;
maxDescent = Math.Max(descent, maxDescent);
if (this._paragraphStyle.TextAlign == TextAlign.justify && !this._lineRanges[lineNumber].hardBreak
&& lineNumber != lineLimits - 1) {
this.justifyLine(lineNumber, words);
foreach (var paintRecord in paintRecords) {
updateLineMetrics(paintRecord.metrics, paintRecord.style);
}
if (paintRecords.Count == 0) {
var defaultStyle = this._paragraphStyle.getTextStyle();
var defaultFont = FontManager.instance.getOrCreate(defaultStyle.fontFamily).font;
var metrics = FontMetrics.fromFont(defaultFont, defaultStyle.UnityFontSize);
updateLineMetrics(metrics, defaultStyle);
}
this._lineHeights.Add(
(this._lineHeights.Count == 0 ? 0 : this._lineHeights[this._lineHeights.Count - 1])
+ Math.Round(maxLineSpacing + maxDescent));
this._lineBaseLines.Add(this._lineHeights[this._lineHeights.Count - 1] - maxDescent);
yOffset += Math.Round(maxLineSpacing + preMaxDescent);
preMaxDescent = maxDescent;
foreach (var paintRecord in paintRecords) {
paintRecord.offset = new Offset(paintRecord.offset.dx + lineXOffset, yOffset);
this._paintRecords.Add(paintRecord);
else if (line.endExcludingWhitespace > line.start) {
Debug.Assert(!isLineEndSpace(this._text[line.endExcludingWhitespace - 1]));
var lineTotalAdvance = this._characterPositions[line.endExcludingWhitespace - 1].x +
this._characterWidths[line.endExcludingWhitespace - 1];
double xOffset = this.getLineXOffset(lineTotalAdvance);
if (xOffset > 0 || xOffset < 0) {
offsetCharacters(new Vector2d(xOffset, 0), this._characterPositions, line.start,
line.endExcludingWhitespace);
}
}
this._maxIntrinsicWidth = 0;
double lineBlockWidth = 0;
for (int i = 0; i < this._lineWidths.Count; ++i) {
lineBlockWidth += this._lineWidths[i];
if (this._lineRanges[i].hardBreak) {
this._maxIntrinsicWidth = Math.Max(lineBlockWidth, this._maxIntrinsicWidth);
lineBlockWidth = 0;
this.computeWidthMetrics(maxWordWidth);
this._maxIntrinsicWidth = Math.Max(lineBlockWidth, this._maxIntrinsicWidth);
if (this._paragraphStyle.maxLines == 1 || (this._paragraphStyle.maxLines == null &&
this._paragraphStyle.ellipsized())) {
this._minIntrinsicWidth = this.maxIntrinsicWidth;
}
else {
this._minIntrinsicWidth = Math.Min(maxWordWidth, this.maxIntrinsicWidth);
}
}

continue;
}
var baseLine = this._lineBaseLines[run.lineNumber];
double top = baseLine - run.fontMetrics.ascent;
double bottom = baseLine + run.fontMetrics.descent;
double top = (run.lineNumber == 0) ? 0 : this._lineHeights[run.lineNumber - 1];
double bottom = this._lineHeights[run.lineNumber];
double left, right;
if (run.codeUnits.start >= start && run.codeUnits.end <= end) {
left = run.xPos.start;
right = run.xPos.end;
}
else {
left = double.MaxValue;
right = double.MinValue;
foreach (var gp in run.positions) {
if (gp.codeUnits.start >= start && gp.codeUnits.end <= end) {
left = Math.Min(left, gp.xPos.start);
right = Math.Max(right, gp.xPos.end);
}
}
var from = Math.Max(start, run.codeUnits.start);
var to = Math.Min(end, run.codeUnits.end);
if (from < to) {
List<TextBox> boxs;
if (!lineBoxes.TryGetValue(run.lineNumber, out boxs)) {
boxs = new List<TextBox>();
lineBoxes.Add(run.lineNumber, boxs);
if (left == double.MaxValue || right == double.MinValue) {
continue;
}
double left = this._characterPositions[from].x;
double right = this._characterPositions[to - 1].x + this._characterWidths[to - 1];
boxs.Add(TextBox.fromLTBD(left, top, right, bottom, run.direction));
List<TextBox> boxs;
if (!lineBoxes.TryGetValue(run.lineNumber, out boxs)) {
boxs = new List<TextBox>();
lineBoxes.Add(run.lineNumber, boxs);
boxs.Add(TextBox.fromLTBD(left, top, right, bottom, run.direction));
}
for (int lineNumber = 0; lineNumber < this._lineRanges.Count; ++lineNumber) {

}
}
var line = this._lineRanges[yIndex];
if (line.start >= line.end) {
return new PositionWithAffinity(line.start, TextAffinity.downstream);
var lineGlyphPosition = this._glyphLines[yIndex].positions;
if (lineGlyphPosition.Count == 0) {
int lineStartIndex = this._glyphLines.Where((g, i) => i < yIndex).Sum((gl) => gl.totalCountUnits);
return new PositionWithAffinity(lineStartIndex, TextAffinity.downstream);
int index;
for (index = line.start; index < line.end; ++index) {
if (dx < this._characterPositions[index].x + this._characterWidths[index]) {
GlyphPosition gp = null;
for (int xIndex = 0; xIndex < lineGlyphPosition.Count; ++xIndex) {
double glyphEnd = xIndex < lineGlyphPosition.Count - 1
? lineGlyphPosition[xIndex + 1].xPos.start
: lineGlyphPosition[xIndex].xPos.end;
if (dx < glyphEnd) {
gp = lineGlyphPosition[xIndex];
if (index >= line.end) {
return new PositionWithAffinity(line.end, TextAffinity.upstream);
if (gp == null) {
GlyphPosition lastGlyph = lineGlyphPosition[lineGlyphPosition.Count - 1];
return new PositionWithAffinity(lastGlyph.codeUnits.end, TextAffinity.upstream);
var codeUnit = this._codeUnitRuns.Find((u) => u.codeUnits.start >= index && index < u.codeUnits.end);
if (codeUnit != null) {
direction = codeUnit.direction;
foreach (var run in this._codeUnitRuns) {
if (gp.codeUnits.start >= run.codeUnits.start && gp.codeUnits.end <= run.codeUnits.end) {
direction = run.direction;
break;
}
double glyphCenter = (this._characterPositions[index].x + this._characterPositions[index].x +
this._characterWidths[index]) / 2;
double glyphCenter = (gp.xPos.start + gp.xPos.end) / 2;
return new PositionWithAffinity(index, TextAffinity.downstream);
return new PositionWithAffinity(gp.codeUnits.start, TextAffinity.downstream);
return new PositionWithAffinity(index + 1, TextAffinity.upstream);
return new PositionWithAffinity(gp.codeUnits.end, TextAffinity.upstream);
}
}

return this._lineRanges[lineIndex];
}
public IndexRange getWordBoundary(int offset) {
public Range<int> getWordBoundary(int offset) {
public static void offsetCharacters(Vector2d offset, Vector2d[] characterPos, int start, int end) {
if (characterPos != null) {
for (int i = start; i < characterPos.Length && i < end; ++i) {
characterPos[i] = characterPos[i] + offset;
}
}
}
void computeWidthMetrics(double maxWordWidth) {
this._maxIntrinsicWidth = 0;
double lineBlockWidth = 0;
for (int i = 0; i < this._lineWidths.Count; ++i) {
var line = this._lineRanges[i];
lineBlockWidth += this._lineWidths[i];
if (line.hardBreak) {
this._maxIntrinsicWidth = Math.Max(lineBlockWidth, this._maxIntrinsicWidth);
lineBlockWidth = 0;
}
}
if (this._paragraphStyle.maxLines == 1 || (((this._paragraphStyle.maxLines ?? 0) == 0) &&
!string.IsNullOrEmpty(this._paragraphStyle.ellipsis))) {
this._minIntrinsicWidth = this._maxIntrinsicWidth;
}
else {
this._minIntrinsicWidth = Math.Min(maxWordWidth, this._maxIntrinsicWidth);
}
}
void setup() {
if (this._characterPositions == null || this._characterPositions.Length < this._text.Length) {
this._characterPositions = new Vector2d[this._text.Length];
}
this._lineHeights.Clear();
void computeLineBreak() {
this._lineBaseLines.Clear();
this._paintRecords.Clear();
this._codeUnitRuns.Clear();
this._characterWidths = new double[this._text.Length];
for (int i = 0; i < this._runs.size; ++i) {
var run = this._runs.getRun(i);
if (run.start < run.end) {
var font = FontManager.instance.getOrCreate(run.style.fontFamily).font;
font.RequestCharactersInTexture(this._text.Substring(run.start, run.end - run.start),
run.style.UnityFontSize,
run.style.UnityFontStyle);
}
}
}
void layoutLines(int lineLimits) {
double yOffset = 0;
var runIndex = 0;
double lastDescent = 0.0f;
var linePaintRecords = new List<PaintRecord>();
for (int lineNumber = 0; lineNumber < lineLimits; lineNumber++) {
var line = this._lineRanges[lineNumber];
double maxAscent = 0.0f;
double maxDescent = 0.0f;
linePaintRecords.Clear();
for (;;) {
var run = runIndex < this._runs.size ? this._runs.getRun(runIndex) : null;
if (run != null && run.start < run.end && run.start < line.end && run.end > line.start) {
var font = FontManager.instance.getOrCreate(run.style.fontFamily).font;
var metrics = FontMetrics.fromFont(font, run.style.UnityFontSize, run.style.height);
if (metrics.ascent > maxAscent) {
maxAscent = metrics.ascent;
}
if (metrics.descent > maxDescent) {
maxDescent = metrics.descent;
}
int start = Math.Max(run.start, line.start);
int end = Math.Min(run.end, line.end);
var width = this._characterPositions[end - 1].x + this._characterWidths[end - 1] -
this._characterPositions[start].x;
if (end > start) {
var bounds = Rect.fromLTWH(0, -metrics.ascent,
this._characterPositions[end - 1].x + this._characterWidths[end - 1] -
this._characterPositions[start].x,
metrics.ascent + metrics.descent);
linePaintRecords.Add(new PaintRecord(run.style,
new Offset(this._characterPositions[start].x, yOffset)
, new TextBlob(this._text, start, end, this._characterPositions, run.style, bounds),
metrics,
lineNumber, width));
this._codeUnitRuns.Add(new CodeUnitRun(
new IndexRange(start, end), lineNumber, metrics, TextDirection.ltr));
}
}
if (runIndex + 1 >= this._runs.size) {
break;
}
if (run.end < line.end) {
runIndex++;
}
else {
break;
}
}
if (lineNumber == 0) {
this._alphabeticBaseline = maxAscent;
this._ideographicBaseline = maxAscent; // todo Properly implement ideographic_baseline
}
yOffset = Utils.PixelCorrectRound(yOffset + maxAscent + lastDescent);
foreach (var record in linePaintRecords) {
record.offset = new Offset(record.offset.dx, yOffset);
}
this._maxIntrinsicWidth = 0;
this._paintRecords.AddRange(linePaintRecords);
for (var charIndex = line.start; charIndex < line.end; charIndex++) {
this._characterPositions[charIndex].y = yOffset;
}
this._lineHeights.Add(
(this._lineHeights.Count == 0 ? 0 : this._lineHeights[this._lineHeights.Count - 1]) +
Math.Round(maxAscent + maxDescent));
this._lineBaseLines.Add(yOffset);
lastDescent = maxDescent;
}
}
void computeLineBreak() {
var newLinePositions = new List<int>();
for (var i = 0; i < this._text.Length; i++) {
if (this._text[i] == '\n') {

newLinePositions.Add(this._text.Length);
var lineBreaker = new LineBreaker();
lineBreaker.setup(this._text, this._runs, this._width, this._characterPositions, this._characterWidths);
int runIndex = 0;
for (var newlineIndex = 0; newlineIndex < newLinePositions.Count; ++newlineIndex) {
var blockStart = newlineIndex > 0 ? newLinePositions[newlineIndex - 1] + 1 : 0;
var blockEnd = newLinePositions[newlineIndex];

continue;
}
lineBreaker.doBreak(blockStart, blockEnd);
var lines = lineBreaker.getLines();
for (int i = 0; i < lines.Count; ++i) {
var line = lines[i];
var end = i + 1 < lines.Count ? lines[i + 1].start : blockEnd;
lineBreaker.setLineWidth((float) this._width);
lineBreaker.resize(blockSize);
lineBreaker.setTabStops(this._tabStops);
lineBreaker.setText(this._text, blockStart, blockSize);
var nonSpaceEnd = end;
while (nonSpaceEnd > line.start && isLineEndSpace(this._text[nonSpaceEnd - 1])) {
nonSpaceEnd--;
while (runIndex < this._runs.size) {
var run = this._runs.getRun(runIndex);
if (run.start >= blockEnd) {
break;
this._lineRanges.Add(new LineRange(line.start, end, nonSpaceEnd,
end == blockEnd && end < this._text.Length ? end + 1 : end, end == blockEnd));
this._lineWidths.Add(line.width);
}
}
if (run.end < blockStart) {
runIndex++;
continue;
}
return;
}
int runStart = Math.Max(run.start, blockStart) - blockStart;
int runEnd = Math.Min(run.end, blockEnd) - blockStart;
lineBreaker.addStyleRun(run.style, runStart, runEnd);
double getLineXOffset(double lineTotalAdvance) {
if (double.IsInfinity(this._width)) {
return 0;
}
if (run.end > blockEnd) {
break;
}
var align = this._paragraphStyle.TextAlign;
if (align == TextAlign.right) {
return this._width - lineTotalAdvance;
}
else if (align == TextAlign.center) {
return Utils.PixelCorrectRound((this._width - lineTotalAdvance) / 2);
}
else {
return 0;
}
}
runIndex++;
}
void justifyLine(int lineNumber, List<Range<int>> words) {
if (words.Count <= 1) {
return;
}
int breaksCount = lineBreaker.computeBreaks();
List<int> breaks = lineBreaker.getBreaks();
List<float> widths = lineBreaker.getWidths();
for (int i = 0; i < breaksCount; ++i) {
var breakStart = (i > 0) ? breaks[i - 1] : 0;
var lineStart = breakStart + blockStart;
var lineEnd = breaks[i] + blockStart;
bool hardBreak = (i == breaksCount - 1);
var lineEndIncludingNewline =
(hardBreak && lineEnd < this._text.Length) ? lineEnd + 1 : lineEnd;
var lineEndExcludingWhitespace = lineEnd;
while (lineEndExcludingWhitespace > lineStart &&
LayoutUtils.isLineEndSpace(this._text[lineEndExcludingWhitespace - 1])) {
lineEndExcludingWhitespace--;
}
var line = this._lineRanges[lineNumber];
Debug.Assert(!isLineEndSpace(this._text[line.endExcludingWhitespace - 1]));
var lineTotalAdvance = this._characterPositions[line.endExcludingWhitespace - 1].x +
this._characterWidths[line.endExcludingWhitespace - 1];
double gapWidth = (this._width - lineTotalAdvance) / (words.Count - 1);
this._lineRanges.Add(new LineRange(lineStart, lineEnd,
lineEndExcludingWhitespace, lineEndIncludingNewline, hardBreak));
this._lineWidths.Add(widths[i]);
}
double justifyOffset = 0.0;
foreach (var word in words) {
offsetCharacters(new Vector2d(justifyOffset), this._characterPositions, word.start, word.end);
justifyOffset += gapWidth;
justifyOffset = Utils.PixelCorrectRound(justifyOffset);
lineBreaker.finish();
return;
}
List<Range<int>> findWords(int start, int end) {

for (int i = start; i < end; ++i) {
bool isSpace = isWordSpace(this._text[i]);
bool isSpace = LayoutUtils.isWordSpace(this._text[i]);
if (!inWord && !isSpace) {
wordStart = i;
inWord = true;

}
if (decoration != null && decoration.contains(TextDecoration.overline)) {
yOffset -= metrics.ascent;
yOffset += metrics.ascent;
canvas.drawLine(new Offset(x, y + yOffset), new Offset(x + width, y + yOffset), paint);
yOffset = yOffsetOriginal;
}

canvas.drawLine(new Offset(x, y + yOffset), new Offset(x + width, y + yOffset), paint);
yOffset = yOffsetOriginal;
}
}
}
double getLineXOffset(double lineTotalAdvance) {
if (double.IsInfinity(this._width)) {
return 0;
}
if (this._paragraphStyle.textAlign == TextAlign.right) {
return this._width - lineTotalAdvance;
}
else if (this._paragraphStyle.textAlign == TextAlign.center) {
return (this._width - lineTotalAdvance) / 2;
}
else {
return 0;
}
}

6
Runtime/ui/txt/word_separate.cs


this._text = text;
}
public IndexRange findWordRange(int index) {
public Range<int> findWordRange(int index) {
return new IndexRange(0, 0);
return new Range<int>(0, 0);
}
var t = this.classifyChar(index);

}
}
return new IndexRange(start, end + 1);
return new Range<int>(start, end + 1);
}

4
Runtime/widgets/basic.cs


softWrap: this.softWrap,
overflow: this.overflow,
textScaleFactor: this.textScaleFactor,
maxLines: this.maxLines ?? 0 // todo: maxLines should be nullable.
maxLines: this.maxLines
);
}

renderObject.softWrap = this.softWrap;
renderObject.overflow = this.overflow;
renderObject.textScaleFactor = this.textScaleFactor;
renderObject.maxLines = this.maxLines ?? 0; // todo: maxLines should be nullable.
renderObject.maxLines = this.maxLines;
}
public override void debugFillProperties(DiagnosticPropertiesBuilder properties) {

2
Runtime/widgets/routes.cs


}
}
abstract class PopupRoute : ModalRoute {
public abstract class PopupRoute : ModalRoute {
protected PopupRoute(
RouteSettings settings = null
) : base(settings: settings) {

8
Tests/Editor/EditableTextWiget.cs


cursorColor: Color.fromARGB(255, 0, 0, 0)
)
);
this.windowAdapter.attachRootWidget(this.root);
this.windowAdapter.attachRootWidget(new WidgetsApp(window: this.windowAdapter, home: this.root,
pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) =>
new PageRouteBuilder(
settings: settings,
pageBuilder: (BuildContext context, Unity.UIWidgets.animation.Animation<double> animation,
Unity.UIWidgets.animation.Animation<double> secondaryAnimation) => builder(context)
)));
this.titleContent = new GUIContent("EditableTextWidget");
}

102
Runtime/ui/txt/layout.cs


using System.Collections.Generic;
using UnityEngine;
namespace Unity.UIWidgets.ui {
public class Layout {
int _start;
int _count;
List<float> _advances = new List<float>();
List<float> _positions = new List<float>();
float _advance;
Rect _bounds;
string _text;
TabStops _tabStops;
public static float measureText(double offset, string buf, int start, int count, TextStyle style,
List<float> advances, int advanceOffset, TabStops tabStops) {
Layout layout = new Layout();
layout.setTabStops(tabStops);
layout.doLayout(offset, buf, start, count, style);
if (advances != null) {
var layoutAdv = layout.getAdvances();
for (int i = 0; i < count; i++) {
advances[i + advanceOffset] = layoutAdv[i];
}
}
return layout.getAdvance();
}
public void doLayout(double offset, string text, int start, int count, TextStyle style) {
this._text = text;
this._advances.Clear();
this._positions.Clear();
this._count = count;
var font = FontManager.instance.getOrCreate(style.fontFamily).font;
font.RequestCharactersInTexture(this._text.Substring(start, count),
style.UnityFontSize,
style.UnityFontStyle);
this._advance = 0;
this._bounds = null;
for (int i = 0; i < count; i++) {
int charIndex = start + i;
var ch = text[charIndex];
CharacterInfo characterInfo;
font.GetCharacterInfo(ch, out characterInfo, style.UnityFontSize, style.UnityFontStyle);
var rect = Rect.fromLTRB(characterInfo.minX, -characterInfo.maxY, characterInfo.maxX,
-characterInfo.minY);
rect = rect.translate(this._advance, 0);
if (this._bounds == null) {
this._bounds = rect;
}
else {
this._bounds = this._bounds.expandToInclude(rect);
}
this._positions.Add(this._advance);
float advance = characterInfo.advance;
if (ch == '\t') {
advance = this._tabStops.nextTab((float) (this._advance + offset)) - this._advance;
}
this._advances.Add(advance);
this._advance += advance;
}
}
public void setTabStops(TabStops tabStops) {
this._tabStops = tabStops;
}
public int nGlyphs() {
return this._count;
}
public List<float> getAdvances() {
return this._advances;
}
public float getAdvance() {
return this._advance;
}
public float getX(int index) {
return this._positions[index];
}
public float getY(int index) {
return 0;
}
public float getCharAdvance(int index) {
return this._advances[index];
}
public Rect getBounds() {
return this._bounds;
}
}
}

11
Runtime/ui/txt/layout.cs.meta


fileFormatVersion: 2
guid: 291468e4e150b495fa305f2d62b134e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

14
Runtime/ui/txt/layout_utils.cs


namespace Unity.UIWidgets.ui {
public static class LayoutUtils {
public const char CHAR_NBSP = '\u00A0';
public static bool isWordSpace(char ch) {
return ch == ' ' || ch == CHAR_NBSP;
}
public static bool isLineEndSpace(char c) {
return c == '\n' || c == ' ' || c == 0x1680 || (0x2000 <= c && c <= 0x200A && c != 0x2007) ||
c == 0x205F || c == 0x3000;
}
}
}

11
Runtime/ui/txt/layout_utils.cs.meta


fileFormatVersion: 2
guid: 1b1768a705ffe4cb7ab2638ac53c657e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

147
Runtime/ui/txt/wordbreaker.cs


namespace Unity.UIWidgets.ui {
public class WordBreaker {
public const uint U16_SURROGATE_OFFSET = ((0xd800 << 10) + 0xdc00 - 0x10000);
string _text;
int _offset;
int _size;
int _current;
int _last;
int _scanOffset;
bool _inEmailOrUrl;
public int next() {
this._last = this._current;
this._detectEmailOrUrl();
if (this._inEmailOrUrl) {
this._current = this._findNextBreakInEmailOrUrl();
}
else {
this._current = this._findNextBreakNormal();
}
return this._current;
}
public void setText(string data, int offset, int size) {
this._text = data;
this._offset = offset;
this._size = size;
this._last = 0;
this._current = 0;
this._scanOffset = 0;
this._inEmailOrUrl = false;
}
public int current() {
return this._current;
}
public int wordStart() {
if (this._inEmailOrUrl) {
return this._last;
}
var result = this._last;
while (result < this._current) {
int ix = result;
uint c = nextCode(this._text, ref ix, this._current);
if (!LayoutUtils.isLineEndSpace((char) c)) {
break;
}
result = ix;
}
return result;
}
public int wordEnd() {
if (this._inEmailOrUrl) {
return this._last;
}
int result = this._current;
while (result > this._last) {
int ix = result;
uint ch = preCode(this._text, ref ix, this._last);
if (!LayoutUtils.isLineEndSpace((char) ch)) {
break;
}
result = ix;
}
return result;
}
public int breakBadness() {
return (this._inEmailOrUrl && this._current < this._scanOffset) ? 1 : 0;
}
public void finish() {
this._text = null;
}
int _findNextBreakInEmailOrUrl() {
return 0;
}
int _findNextBreakNormal() {
if (this._current == this._size) {
return -1;
}
this._current++;
for (; this._current < this._size; ++this._current) {
char c = this._text[this._current + this._offset];
if (LayoutUtils.isWordSpace(c) || c == '\t') {
return this._current;
}
}
return this._current;
}
void _detectEmailOrUrl() {
}
static uint nextCode(string text, ref int index, int end) {
uint ch = text[index++];
if (isLeadSurrogate(ch)) {
if (index < end && isTrailSurrogate(text[index])) {
char ch2 = text[index];
index++;
ch = getSupplementary(ch, ch2);
}
}
return ch;
}
static uint preCode(string text, ref int index, int start) {
uint ch = text[--index];
if (isTrailSurrogate(ch)) {
if (index > start && isLeadSurrogate(text[index - 1])) {
ch = getSupplementary(text[index - 1], ch);
--index;
}
}
return ch;
}
public static bool isLeadSurrogate(uint c) {
return ((c) & 0xfffffc00) == 0xd800;
}
public static bool isTrailSurrogate(uint c) {
return ((c) & 0xfffffc00) == 0xdc00;
}
public static uint getSupplementary(uint lead, uint trail) {
return (char) (((uint) (lead) << 10) + (uint) (trail - U16_SURROGATE_OFFSET));
}
}
}

11
Runtime/ui/txt/wordbreaker.cs.meta


fileFormatVersion: 2
guid: 30b4907a8b76c4df9a243dcd8d3518fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
正在加载...
取消
保存