|
@@ -2,15 +2,51 @@ package h2d;
|
|
|
|
|
|
import h2d.Text;
|
|
import h2d.Text;
|
|
|
|
|
|
|
|
+enum LineHeightMode {
|
|
|
|
+ /**
|
|
|
|
+ Accurate line height calculations. Each line will adjust it's height according to it's contents.
|
|
|
|
+ **/
|
|
|
|
+ Accurate;
|
|
|
|
+ /**
|
|
|
|
+ Only text adjusts line heights, and `<img>` tags do not affect it (partial legacy behavior).
|
|
|
|
+ **/
|
|
|
|
+ TextOnly;
|
|
|
|
+ /**
|
|
|
|
+ Legacy line height mode. When used, line heights are remain constant based on `HtmlText.font` variable.
|
|
|
|
+ **/
|
|
|
|
+ Constant;
|
|
|
|
+}
|
|
|
|
+
|
|
class HtmlText extends Text {
|
|
class HtmlText extends Text {
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ A default method HtmlText uses to load images for `<img>` tag. See `HtmlText.loadImage` for details.
|
|
|
|
+ **/
|
|
|
|
+ public static dynamic function defaultLoadImage( url : String ) : h2d.Tile {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ A default method HtmlText uses to load fonts for `<font>` tags with `face` attribute. See `HtmlText.loadFont` for details.
|
|
|
|
+ **/
|
|
|
|
+ public static dynamic function defaultLoadFont( name : String ) : h2d.Font {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
public var condenseWhite(default,set) : Bool = true;
|
|
public var condenseWhite(default,set) : Bool = true;
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ Line height calculation mode controls how much space lines take up vertically. ( default : Accurate )
|
|
|
|
+ Changing mode to `Constant` restores legacy behavior of HtmlText.
|
|
|
|
+ **/
|
|
|
|
+ public var lineHeightMode(default,set) : LineHeightMode = Accurate;
|
|
|
|
+
|
|
var elements : Array<Object> = [];
|
|
var elements : Array<Object> = [];
|
|
var xPos : Float;
|
|
var xPos : Float;
|
|
var yPos : Float;
|
|
var yPos : Float;
|
|
var xMax : Float;
|
|
var xMax : Float;
|
|
var xMin : Float;
|
|
var xMin : Float;
|
|
|
|
+ var imageCache : Map<String, Tile>;
|
|
var sizePos : Int;
|
|
var sizePos : Int;
|
|
var dropMatrix : h3d.shader.ColorMatrix;
|
|
var dropMatrix : h3d.shader.ColorMatrix;
|
|
var prevChar : Int;
|
|
var prevChar : Int;
|
|
@@ -39,19 +75,36 @@ class HtmlText extends Text {
|
|
glyphs.drawWith(ctx,this);
|
|
glyphs.drawWith(ctx,this);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ Method that should return `h2d.Tile` instance for `<img>` tags. By default calls `HtmlText.defaultLoadImage` method.
|
|
|
|
+ Loaded Tiles are temporary cached internally and if text contains multiple same images - this method will be called only once. Cache is invalidated whenever text changes.
|
|
|
|
+ @param url A value contained in `src` attribute.
|
|
|
|
+ **/
|
|
public dynamic function loadImage( url : String ) : Tile {
|
|
public dynamic function loadImage( url : String ) : Tile {
|
|
- return null;
|
|
|
|
|
|
+ return defaultLoadImage(url);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ Method that should return `h2d.Font` instance for `<font>` tags with `face` attribute. By default calls `HtmlText.defaultLoadFont` method.
|
|
|
|
+ HtmlText does not cache font instances and it's recommended to perform said caching from outside.
|
|
|
|
+ @param name A value contained in `face` attribute.
|
|
|
|
+ @returns Method should return loaded font instance or `null`. If `null` is returned - currently active font is used.
|
|
|
|
+ **/
|
|
public dynamic function loadFont( name : String ) : Font {
|
|
public dynamic function loadFont( name : String ) : Font {
|
|
- return font;
|
|
|
|
|
|
+ var f = defaultLoadFont(name);
|
|
|
|
+ if (f == null) return this.font;
|
|
|
|
+ else return f;
|
|
}
|
|
}
|
|
|
|
|
|
function parseText( text : String ) {
|
|
function parseText( text : String ) {
|
|
return try Xml.parse(text) catch( e : Dynamic ) throw "Could not parse " + text + " (" + e +")";
|
|
return try Xml.parse(text) catch( e : Dynamic ) throw "Could not parse " + text + " (" + e +")";
|
|
}
|
|
}
|
|
|
|
|
|
- override function initGlyphs( text : String, rebuild = true, handleAlign = true, ?lines : Array<Int> ) {
|
|
|
|
|
|
+ inline function makeLineInfo( width : Float, height : Float, baseLine : Float ) : LineInfo {
|
|
|
|
+ return { width: width, height: height, baseLine: baseLine };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ override function initGlyphs( text : String, rebuild = true ) {
|
|
if( rebuild ) {
|
|
if( rebuild ) {
|
|
glyphs.clear();
|
|
glyphs.clear();
|
|
for( e in elements ) e.remove();
|
|
for( e in elements ) e.remove();
|
|
@@ -59,70 +112,140 @@ class HtmlText extends Text {
|
|
}
|
|
}
|
|
glyphs.setDefaultColor(textColor);
|
|
glyphs.setDefaultColor(textColor);
|
|
|
|
|
|
- xPos = 0;
|
|
|
|
- xMin = 0;
|
|
|
|
-
|
|
|
|
- var align = handleAlign ? textAlign : Left;
|
|
|
|
- switch( align ) {
|
|
|
|
- case Center, Right, MultilineCenter, MultilineRight:
|
|
|
|
- lines = [];
|
|
|
|
- initGlyphs(text, false, false, lines);
|
|
|
|
- var max = if( align == MultilineCenter || align == MultilineRight ) hxd.Math.ceil(calcWidth) else realMaxWidth < 0 ? 0 : hxd.Math.ceil(realMaxWidth);
|
|
|
|
- var k = align == Center || align == MultilineCenter ? 1 : 0;
|
|
|
|
- for( i in 0...lines.length )
|
|
|
|
- lines[i] = (max - lines[i]) >> k;
|
|
|
|
- xPos = lines.shift();
|
|
|
|
- xMin = xPos;
|
|
|
|
- default:
|
|
|
|
- }
|
|
|
|
|
|
+ var doc = parseText(text);
|
|
|
|
+ imageCache = new Map();
|
|
|
|
|
|
yPos = 0;
|
|
yPos = 0;
|
|
xMax = 0;
|
|
xMax = 0;
|
|
|
|
+ xMin = Math.POSITIVE_INFINITY;
|
|
sizePos = 0;
|
|
sizePos = 0;
|
|
calcYMin = 0;
|
|
calcYMin = 0;
|
|
|
|
|
|
- var doc = parseText(text);
|
|
|
|
-
|
|
|
|
- var sizes = new Array<Float>();
|
|
|
|
|
|
+ var metrics : Array<LineInfo> = [ makeLineInfo(0, font.lineHeight, font.baseLine) ];
|
|
prevChar = -1;
|
|
prevChar = -1;
|
|
newLine = true;
|
|
newLine = true;
|
|
|
|
+ var splitNode : SplitNode = {
|
|
|
|
+ node: null, pos: 0, font: font, prevChar: -1,
|
|
|
|
+ width: 0, height: 0, baseLine: 0
|
|
|
|
+ };
|
|
for( e in doc )
|
|
for( e in doc )
|
|
- buildSizes(e, font, sizes, false);
|
|
|
|
|
|
+ buildSizes(e, font, metrics, splitNode);
|
|
|
|
+
|
|
|
|
+ var max = 0.;
|
|
|
|
+ for ( info in metrics ) {
|
|
|
|
+ if ( info.width > max ) max = info.width;
|
|
|
|
+ }
|
|
|
|
+ calcWidth = max;
|
|
|
|
|
|
prevChar = -1;
|
|
prevChar = -1;
|
|
newLine = true;
|
|
newLine = true;
|
|
- for( e in doc )
|
|
|
|
- addNode(e, font, rebuild, handleAlign, sizes, lines);
|
|
|
|
-
|
|
|
|
- if (!handleAlign && !rebuild && lines != null) lines.push(hxd.Math.ceil(xPos));
|
|
|
|
|
|
+ nextLine(textAlign, metrics[0].width);
|
|
|
|
+ for ( e in doc )
|
|
|
|
+ addNode(e, font, textAlign, rebuild, metrics);
|
|
|
|
+
|
|
if( xPos > xMax ) xMax = xPos;
|
|
if( xPos > xMax ) xMax = xPos;
|
|
|
|
|
|
|
|
+ imageCache = null;
|
|
var y = yPos;
|
|
var y = yPos;
|
|
calcXMin = xMin;
|
|
calcXMin = xMin;
|
|
calcWidth = xMax - xMin;
|
|
calcWidth = xMax - xMin;
|
|
- calcHeight = y + font.lineHeight;
|
|
|
|
- calcSizeHeight = y + (font.baseLine > 0 ? font.baseLine : font.lineHeight);
|
|
|
|
|
|
+ calcHeight = y + metrics[sizePos].height;
|
|
|
|
+ calcSizeHeight = y + metrics[sizePos].baseLine;//(font.baseLine > 0 ? font.baseLine : font.lineHeight);
|
|
calcDone = true;
|
|
calcDone = true;
|
|
}
|
|
}
|
|
|
|
|
|
- function buildSizes( e : Xml, font : Font, sizes : Array<Float>, forSplit ) {
|
|
|
|
|
|
+ function buildSizes( e : Xml, font : Font, metrics : Array<LineInfo>, splitNode:SplitNode ) {
|
|
|
|
+ function wordSplit() {
|
|
|
|
+ var fnt = splitNode.font;
|
|
|
|
+ var str = splitNode.node.nodeValue;
|
|
|
|
+ var info = metrics[metrics.length - 1];
|
|
|
|
+ var w = info.width;
|
|
|
|
+ var cc = str.charCodeAt(splitNode.pos);
|
|
|
|
+ // Restore line metrics to ones before split.
|
|
|
|
+ // Potential bug: `Text<split> [Image] text<split>text` - third line will use metrics as if image is present in the line.
|
|
|
|
+ info.width = splitNode.width;
|
|
|
|
+ info.height = splitNode.height;
|
|
|
|
+ info.baseLine = splitNode.baseLine;
|
|
|
|
+ var char = fnt.getChar(cc);
|
|
|
|
+ if (fnt.charset.isSpace(cc)) {
|
|
|
|
+ // Space characters are converted to \n
|
|
|
|
+ w -= (splitNode.width + letterSpacing + char.width + char.getKerningOffset(splitNode.prevChar));
|
|
|
|
+ splitNode.node.nodeValue = str.substr(0, splitNode.pos) + "\n" + str.substr(splitNode.pos + 1);
|
|
|
|
+ } else {
|
|
|
|
+ w -= (splitNode.width + letterSpacing + char.getKerningOffset(splitNode.prevChar));
|
|
|
|
+ splitNode.node.nodeValue = str.substr(0, splitNode.pos+1) + "\n" + str.substr(splitNode.pos+1);
|
|
|
|
+ }
|
|
|
|
+ splitNode.node = null;
|
|
|
|
+ return w;
|
|
|
|
+ }
|
|
|
|
+ inline function lineFont() {
|
|
|
|
+ return lineHeightMode == Constant ? this.font : font;
|
|
|
|
+ }
|
|
if( e.nodeType == Xml.Element ) {
|
|
if( e.nodeType == Xml.Element ) {
|
|
- var len = 0.;
|
|
|
|
|
|
+
|
|
|
|
+ inline function makeLineBreak() {
|
|
|
|
+ var fontInfo = lineFont();
|
|
|
|
+ metrics.push(makeLineInfo(0, fontInfo.lineHeight, fontInfo.baseLine));
|
|
|
|
+ splitNode.node = null;
|
|
|
|
+ newLine = true;
|
|
|
|
+ prevChar = -1;
|
|
|
|
+ }
|
|
|
|
+
|
|
var nodeName = e.nodeName.toLowerCase();
|
|
var nodeName = e.nodeName.toLowerCase();
|
|
switch( nodeName ) {
|
|
switch( nodeName ) {
|
|
case "p":
|
|
case "p":
|
|
- if ( !newLine )
|
|
|
|
- {
|
|
|
|
- len = -1; // break
|
|
|
|
- newLine = true;
|
|
|
|
|
|
+ if ( !newLine ) {
|
|
|
|
+ makeLineBreak();
|
|
}
|
|
}
|
|
case "br":
|
|
case "br":
|
|
- len = -1; // break
|
|
|
|
- newLine = true;
|
|
|
|
|
|
+ makeLineBreak();
|
|
case "img":
|
|
case "img":
|
|
- var i = loadImage(e.get("src"));
|
|
|
|
- len = (i == null ? 8 : i.width) + letterSpacing;
|
|
|
|
|
|
+ // TODO: Support width/height attributes
|
|
|
|
+ // Support max-width/max-height attributes (downscale)
|
|
|
|
+ // Support min-width/min-height attributes (upscale)
|
|
|
|
+ var src = e.get("src");
|
|
|
|
+ var i : Tile = imageCache.get(src);
|
|
|
|
+ if ( i == null ) {
|
|
|
|
+ i = loadImage(src);
|
|
|
|
+ if( i == null ) i = Tile.fromColor(0xFF00FF, 8, 8);
|
|
|
|
+ imageCache.set(src, i);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var size = metrics[metrics.length - 1].width + i.width + letterSpacing;
|
|
|
|
+ if (realMaxWidth >= 0 && size > realMaxWidth && metrics[metrics.length - 1].width > 0) {
|
|
|
|
+ if ( splitNode.node != null ) {
|
|
|
|
+ size = wordSplit() + i.width + letterSpacing;
|
|
|
|
+ var info = metrics[metrics.length - 1];
|
|
|
|
+ // Bug: height/baseLine may be innacurate in case of sizeA sizeB<split>sizeA where sizeB is larger.
|
|
|
|
+ switch ( lineHeightMode ) {
|
|
|
|
+ case Accurate:
|
|
|
|
+ var grow = i.height - i.dy - info.baseLine;
|
|
|
|
+ var h = info.height;
|
|
|
|
+ var bl = info.baseLine;
|
|
|
|
+ if (grow > 0) {
|
|
|
|
+ h += grow;
|
|
|
|
+ bl += grow;
|
|
|
|
+ }
|
|
|
|
+ metrics.push(makeLineInfo(size, Math.max(h, bl + i.dy), bl));
|
|
|
|
+ default:
|
|
|
|
+ metrics.push(makeLineInfo(size, info.height, info.baseLine));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ var info = metrics[metrics.length - 1];
|
|
|
|
+ info.width = size;
|
|
|
|
+ if ( lineHeightMode == Accurate ) {
|
|
|
|
+ var grow = i.height - i.dy - info.baseLine;
|
|
|
|
+ if ( grow > 0 ) {
|
|
|
|
+ info.baseLine += grow;
|
|
|
|
+ info.height += grow;
|
|
|
|
+ }
|
|
|
|
+ grow = info.baseLine + i.dy;
|
|
|
|
+ if ( info.height < grow ) info.height = grow;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
newLine = false;
|
|
newLine = false;
|
|
|
|
+ prevChar = -1;
|
|
case "font":
|
|
case "font":
|
|
for( a in e.attributes() ) {
|
|
for( a in e.attributes() ) {
|
|
var v = e.get(a);
|
|
var v = e.get(a);
|
|
@@ -137,39 +260,103 @@ class HtmlText extends Text {
|
|
font = loadFont("italic");
|
|
font = loadFont("italic");
|
|
default:
|
|
default:
|
|
}
|
|
}
|
|
- sizes.push(len);
|
|
|
|
for( child in e )
|
|
for( child in e )
|
|
- buildSizes(child, font, sizes, forSplit);
|
|
|
|
|
|
+ buildSizes(child, font, metrics, splitNode);
|
|
switch( nodeName ) {
|
|
switch( nodeName ) {
|
|
case "p":
|
|
case "p":
|
|
- sizes.push( -1);// break
|
|
|
|
- newLine = true;
|
|
|
|
|
|
+ if ( !newLine ) {
|
|
|
|
+ makeLineBreak();
|
|
|
|
+ }
|
|
default:
|
|
default:
|
|
}
|
|
}
|
|
} else {
|
|
} else {
|
|
newLine = false;
|
|
newLine = false;
|
|
var text = htmlToText(e.nodeValue);
|
|
var text = htmlToText(e.nodeValue);
|
|
- var xp = 0.;
|
|
|
|
- for( i in 0...text.length ) {
|
|
|
|
|
|
+ var fontInfo = lineFont();
|
|
|
|
+ var info : LineInfo = metrics.pop();
|
|
|
|
+ var leftMargin = info.width;
|
|
|
|
+ var maxWidth = realMaxWidth < 0 ? Math.POSITIVE_INFINITY : realMaxWidth;
|
|
|
|
+ var textSplit = [], restPos = 0;
|
|
|
|
+ var x = leftMargin;
|
|
|
|
+ var breakChars = 0;
|
|
|
|
+ for ( i in 0...text.length ) {
|
|
var cc = text.charCodeAt(i);
|
|
var cc = text.charCodeAt(i);
|
|
- var fc = font.getChar(cc);
|
|
|
|
- var sz = fc.getKerningOffset(prevChar) + fc.width;
|
|
|
|
- if( cc == "\n".code || font.charset.isBreakChar(cc) ) {
|
|
|
|
- if( cc != "\n".code && !font.charset.isSpace(cc) )
|
|
|
|
- xp += sz;
|
|
|
|
- if( !forSplit ) {
|
|
|
|
- sizes.push( -(xp + 1));
|
|
|
|
- return;
|
|
|
|
|
|
+ var g = font.getChar(cc);
|
|
|
|
+ var newline = cc == '\n'.code;
|
|
|
|
+ var esize = g.width + g.getKerningOffset(prevChar);
|
|
|
|
+ if ( font.charset.isBreakChar(cc) ) {
|
|
|
|
+ // Case: Very first word in text makes the line too long hence we want to start it off on a new line.
|
|
|
|
+ if (x > maxWidth && textSplit.length == 0 && splitNode.node != null) {
|
|
|
|
+ metrics.push(makeLineInfo(x, info.height, info.baseLine));
|
|
|
|
+ x = wordSplit();
|
|
}
|
|
}
|
|
- sizes.push(xp);
|
|
|
|
- if( font.charset.isSpace(cc) )
|
|
|
|
- sizes.push(sz);
|
|
|
|
- xp = 0;
|
|
|
|
- continue;
|
|
|
|
|
|
+
|
|
|
|
+ var size = x + esize + letterSpacing;
|
|
|
|
+ var k = i + 1, max = text.length;
|
|
|
|
+ var prevChar = prevChar;
|
|
|
|
+ while ( size <= maxWidth && k < max ) {
|
|
|
|
+ var cc = text.charCodeAt(k++);
|
|
|
|
+ if ( font.charset.isSpace(cc) || cc == '\n'.code ) break;
|
|
|
|
+ var e = font.getChar(cc);
|
|
|
|
+ size += e.width + letterSpacing + e.getKerningOffset(prevChar);
|
|
|
|
+ prevChar = cc;
|
|
|
|
+ if ( font.charset.isBreakChar(cc) ) break;
|
|
|
|
+ }
|
|
|
|
+ // Avoid empty line when last char causes line-break while being CJK
|
|
|
|
+ if ( size > maxWidth && i != max - 1 ) {
|
|
|
|
+ // Next word will reach maxWidth
|
|
|
|
+ newline = true;
|
|
|
|
+ if ( font.charset.isSpace(cc) ) {
|
|
|
|
+ textSplit.push(text.substr(restPos, i - restPos));
|
|
|
|
+ g = null;
|
|
|
|
+ } else {
|
|
|
|
+ textSplit.push(text.substr(restPos, i + 1 - restPos));
|
|
|
|
+ breakChars++;
|
|
|
|
+ }
|
|
|
|
+ splitNode.node = null;
|
|
|
|
+ restPos = i + 1;
|
|
|
|
+ } else {
|
|
|
|
+ splitNode.node = e;
|
|
|
|
+ splitNode.pos = i + breakChars;
|
|
|
|
+ splitNode.prevChar = this.prevChar;
|
|
|
|
+ splitNode.width = x;
|
|
|
|
+ splitNode.height = info.height;
|
|
|
|
+ splitNode.baseLine = info.baseLine;
|
|
|
|
+ splitNode.font = font;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if ( g != null && cc != '\n'.code )
|
|
|
|
+ x += esize + letterSpacing;
|
|
|
|
+ if ( newline ) {
|
|
|
|
+ metrics.push(makeLineInfo(x, info.height, info.baseLine));
|
|
|
|
+ info.height = fontInfo.lineHeight;
|
|
|
|
+ info.baseLine = fontInfo.baseLine;
|
|
|
|
+ x = 0;
|
|
|
|
+ prevChar = -1;
|
|
|
|
+ newLine = true;
|
|
|
|
+ } else {
|
|
|
|
+ prevChar = cc;
|
|
|
|
+ newLine = false;
|
|
}
|
|
}
|
|
- xp += sz + letterSpacing;
|
|
|
|
}
|
|
}
|
|
- sizes.push(xp);
|
|
|
|
|
|
+
|
|
|
|
+ if ( restPos < text.length ) {
|
|
|
|
+ if (x > maxWidth) {
|
|
|
|
+ if ( splitNode.node != null && splitNode.node != e ) {
|
|
|
|
+ metrics.push(makeLineInfo(x, info.height, info.baseLine));
|
|
|
|
+ x = wordSplit();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ textSplit.push(text.substr(restPos));
|
|
|
|
+ metrics.push(makeLineInfo(x, info.height, info.baseLine));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (newLine || metrics.length == 0) {
|
|
|
|
+ metrics.push(makeLineInfo(0, fontInfo.lineHeight, fontInfo.baseLine));
|
|
|
|
+ textSplit.push("");
|
|
|
|
+ }
|
|
|
|
+ // Save node value
|
|
|
|
+ e.nodeValue = textSplit.join("\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -180,17 +367,18 @@ class HtmlText extends Text {
|
|
return t;
|
|
return t;
|
|
}
|
|
}
|
|
|
|
|
|
- function remainingSize( sizes : Array<Float> ) {
|
|
|
|
- var size = 0.;
|
|
|
|
- for( i in sizePos...sizes.length ) {
|
|
|
|
- var s = sizes[i];
|
|
|
|
- if( s < 0 ) {
|
|
|
|
- size += -s - 1;
|
|
|
|
- return size;
|
|
|
|
- }
|
|
|
|
- size += s;
|
|
|
|
|
|
+ inline function nextLine( align : Align, size : Float )
|
|
|
|
+ {
|
|
|
|
+ switch( align ) {
|
|
|
|
+ case Left:
|
|
|
|
+ xPos = 0;
|
|
|
|
+ if (xMin > 0) xMin = 0;
|
|
|
|
+ case Right, Center, MultilineCenter, MultilineRight:
|
|
|
|
+ var max = if( align == MultilineCenter || align == MultilineRight ) hxd.Math.ceil(calcWidth) else calcWidth < 0 ? 0 : hxd.Math.ceil(realMaxWidth);
|
|
|
|
+ var k = align == Center || align == MultilineCenter ? 0.5 : 1;
|
|
|
|
+ xPos = Math.ffloor((max - size) * k);
|
|
|
|
+ if( xPos < xMin ) xMin = xPos;
|
|
}
|
|
}
|
|
- return size;
|
|
|
|
}
|
|
}
|
|
|
|
|
|
override function splitText(text:String):String {
|
|
override function splitText(text:String):String {
|
|
@@ -209,53 +397,34 @@ class HtmlText extends Text {
|
|
with all sizes and word breaks so analysis is much more easy.
|
|
with all sizes and word breaks so analysis is much more easy.
|
|
*/
|
|
*/
|
|
|
|
|
|
- var sizes = new Array<Float>();
|
|
|
|
|
|
+ var splitNode : SplitNode = { node: null, font: font, width: 0, height: 0, baseLine: 0, pos: 0, prevChar: -1 };
|
|
|
|
+ var metrics = new Array<LineInfo>();
|
|
prevChar = -1;
|
|
prevChar = -1;
|
|
newLine = true;
|
|
newLine = true;
|
|
|
|
+
|
|
for( e in doc )
|
|
for( e in doc )
|
|
- buildSizes(e, font, sizes, true);
|
|
|
|
|
|
+ buildSizes(e, font, metrics, splitNode);
|
|
xMax = 0;
|
|
xMax = 0;
|
|
function addBreaks( e : Xml ) {
|
|
function addBreaks( e : Xml ) {
|
|
if( e.nodeType == Xml.Element ) {
|
|
if( e.nodeType == Xml.Element ) {
|
|
- var sz = sizes[sizePos++];
|
|
|
|
- if( sz < 0 )
|
|
|
|
- xMax = 0;
|
|
|
|
- else
|
|
|
|
- xMax += sz;
|
|
|
|
for( x in e )
|
|
for( x in e )
|
|
addBreaks(x);
|
|
addBreaks(x);
|
|
- if( e.nodeName == "p" ) {
|
|
|
|
- sizePos++;
|
|
|
|
- xMax = 0;
|
|
|
|
- }
|
|
|
|
} else {
|
|
} else {
|
|
- var text = htmlToText(e.nodeValue);
|
|
|
|
- var startI = 0, prevI = 0;
|
|
|
|
- for( i in 0...text.length ) {
|
|
|
|
- var cc = text.charCodeAt(i);
|
|
|
|
- if( cc == "\n".code || font.charset.isBreakChar(cc) ) {
|
|
|
|
- var sz = sizes[sizePos++];
|
|
|
|
- var sp = font.charset.isSpace(cc) ? sizes[sizePos++] : 0;
|
|
|
|
- xMax += sz;
|
|
|
|
- if( xMax > realMaxWidth ) {
|
|
|
|
- var index = Lambda.indexOf(e.parent,e);
|
|
|
|
- var pre = text.substr(startI,prevI - startI);
|
|
|
|
- if( pre != "" )
|
|
|
|
- e.parent.insertChild(Xml.createPCData(pre),index++);
|
|
|
|
- e.parent.insertChild(Xml.createElement("br"),index);
|
|
|
|
- e.nodeValue = text.substr(prevI+1);
|
|
|
|
- startI = prevI+1;
|
|
|
|
- xMax = sz;
|
|
|
|
- }
|
|
|
|
- xMax += sp + letterSpacing;
|
|
|
|
- prevI = i;
|
|
|
|
|
|
+ var text = e.nodeValue;
|
|
|
|
+ var startI = 0;
|
|
|
|
+ var index = Lambda.indexOf(e.parent, e);
|
|
|
|
+ for (i in 0...text.length) {
|
|
|
|
+ if (text.charCodeAt(i) == '\n'.code) {
|
|
|
|
+ var pre = text.substring(startI, i - 1);
|
|
|
|
+ if (pre != "") e.parent.insertChild(Xml.createPCData(pre), index++);
|
|
|
|
+ e.parent.insertChild(Xml.createElement("br"),index++);
|
|
|
|
+ startI = i+1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- var sz = sizes[sizePos++];
|
|
|
|
- xMax += sz;
|
|
|
|
- if( xMax > realMaxWidth ) {
|
|
|
|
- e.parent.insertChild(Xml.createElement("br"),Lambda.indexOf(e.parent,e));
|
|
|
|
- xMax = sz;
|
|
|
|
|
|
+ if (startI < text.length) {
|
|
|
|
+ e.nodeValue = text.substr(startI);
|
|
|
|
+ } else {
|
|
|
|
+ e.parent.removeChild(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -290,27 +459,16 @@ class HtmlText extends Text {
|
|
return doc.toString();
|
|
return doc.toString();
|
|
}
|
|
}
|
|
|
|
|
|
- function addNode( e : Xml, font : Font, rebuild : Bool, handleAlign:Bool, sizes : Array<Float>, ?lines : Array<Int> = null ) {
|
|
|
|
- sizePos++;
|
|
|
|
- var calcLines = !handleAlign && !rebuild && lines != null;
|
|
|
|
- var align = handleAlign ? textAlign : Left;
|
|
|
|
|
|
+ function addNode( e : Xml, font : Font, align : Align, rebuild : Bool, metrics : Array<LineInfo> ) {
|
|
|
|
+ inline function makeLineBreak()
|
|
|
|
+ {
|
|
|
|
+ if( xPos > xMax ) xMax = xPos;
|
|
|
|
+ yPos += metrics[sizePos].height + lineSpacing;
|
|
|
|
+ nextLine(align, metrics[++sizePos].width);
|
|
|
|
+ }
|
|
if( e.nodeType == Xml.Element ) {
|
|
if( e.nodeType == Xml.Element ) {
|
|
var prevColor = null, prevGlyphs = null;
|
|
var prevColor = null, prevGlyphs = null;
|
|
- function makeLineBreak()
|
|
|
|
- {
|
|
|
|
- if( xPos > xMax ) xMax = xPos;
|
|
|
|
- if( calcLines ) lines.push(hxd.Math.ceil(xPos));
|
|
|
|
- switch( align ) {
|
|
|
|
- case Left:
|
|
|
|
- xPos = 0;
|
|
|
|
- case Right, Center, MultilineCenter, MultilineRight:
|
|
|
|
- xPos = lines.shift();
|
|
|
|
- if( xPos < xMin ) xMin = xPos;
|
|
|
|
- }
|
|
|
|
- yPos += font.lineHeight + lineSpacing;
|
|
|
|
- prevChar = -1;
|
|
|
|
- newLine = true;
|
|
|
|
- }
|
|
|
|
|
|
+ var oldAlign = align;
|
|
var nodeName = e.nodeName.toLowerCase();
|
|
var nodeName = e.nodeName.toLowerCase();
|
|
inline function setFont( v : String ) {
|
|
inline function setFont( v : String ) {
|
|
font = loadFont(v);
|
|
font = loadFont(v);
|
|
@@ -351,45 +509,45 @@ class HtmlText extends Text {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case "p":
|
|
case "p":
|
|
- /*
|
|
|
|
- ??need lines != null even if Left==textAlign
|
|
|
|
- for( a in e.attributes() ) {
|
|
|
|
- switch( a.toLowerCase() ) {
|
|
|
|
|
|
+ for( a in e.attributes() ) {
|
|
|
|
+ switch( a.toLowerCase() ) {
|
|
case "align":
|
|
case "align":
|
|
var v = e.get(a);
|
|
var v = e.get(a);
|
|
if ( v != null )
|
|
if ( v != null )
|
|
switch( v.toLowerCase() ) {
|
|
switch( v.toLowerCase() ) {
|
|
case "left":
|
|
case "left":
|
|
- new_align = Left;
|
|
|
|
|
|
+ align = Left;
|
|
case "center":
|
|
case "center":
|
|
- new_align = Center;
|
|
|
|
|
|
+ align = Center;
|
|
case "right":
|
|
case "right":
|
|
- new_align = Right;
|
|
|
|
|
|
+ align = Right;
|
|
|
|
+ case "multiline-center":
|
|
|
|
+ align = MultilineCenter;
|
|
|
|
+ case "multiline-right":
|
|
|
|
+ align = MultilineRight;
|
|
//?justify
|
|
//?justify
|
|
}
|
|
}
|
|
default:
|
|
default:
|
|
- }
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- */
|
|
|
|
- if ( !newLine )
|
|
|
|
|
|
+ if ( !newLine ) {
|
|
makeLineBreak();
|
|
makeLineBreak();
|
|
|
|
+ newLine = true;
|
|
|
|
+ prevChar = -1;
|
|
|
|
+ } else {
|
|
|
|
+ nextLine(align, metrics[sizePos].width);
|
|
|
|
+ }
|
|
case "b","bold":
|
|
case "b","bold":
|
|
setFont("bold");
|
|
setFont("bold");
|
|
case "i","italic":
|
|
case "i","italic":
|
|
setFont("italic");
|
|
setFont("italic");
|
|
case "br":
|
|
case "br":
|
|
makeLineBreak();
|
|
makeLineBreak();
|
|
|
|
+ newLine = true;
|
|
|
|
+ prevChar = -1;
|
|
case "img":
|
|
case "img":
|
|
- newLine = false;
|
|
|
|
- var i = loadImage(e.get("src"));
|
|
|
|
- if( i == null ) i = Tile.fromColor(0xFF00FF, 8, 8);
|
|
|
|
- if( realMaxWidth >= 0 && xPos + i.width + letterSpacing + remainingSize(sizes) > realMaxWidth && xPos > 0 ) {
|
|
|
|
- if( xPos > xMax ) xMax = xPos;
|
|
|
|
- xPos = 0;
|
|
|
|
- yPos += font.lineHeight + lineSpacing;
|
|
|
|
- }
|
|
|
|
- var py = yPos + font.baseLine - i.height;
|
|
|
|
|
|
+ var i : Tile = imageCache.get(e.get("src"));
|
|
|
|
+ var py = yPos + metrics[sizePos].baseLine - i.height;
|
|
if( py + i.dy < calcYMin )
|
|
if( py + i.dy < calcYMin )
|
|
calcYMin = py + i.dy;
|
|
calcYMin = py + i.dy;
|
|
if( rebuild ) {
|
|
if( rebuild ) {
|
|
@@ -398,15 +556,24 @@ class HtmlText extends Text {
|
|
b.y = py;
|
|
b.y = py;
|
|
elements.push(b);
|
|
elements.push(b);
|
|
}
|
|
}
|
|
|
|
+ newLine = false;
|
|
|
|
+ prevChar = -1;
|
|
xPos += i.width + letterSpacing;
|
|
xPos += i.width + letterSpacing;
|
|
default:
|
|
default:
|
|
}
|
|
}
|
|
for( child in e )
|
|
for( child in e )
|
|
- addNode(child, font, rebuild, handleAlign, sizes, lines);
|
|
|
|
|
|
+ addNode(child, font, align, rebuild, metrics);
|
|
|
|
+ align = oldAlign;
|
|
switch( nodeName ) {
|
|
switch( nodeName ) {
|
|
case "p":
|
|
case "p":
|
|
- sizePos++;
|
|
|
|
- makeLineBreak();
|
|
|
|
|
|
+ if ( newLine ) {
|
|
|
|
+ nextLine(align, metrics[sizePos].width);
|
|
|
|
+ } else if ( sizePos < metrics.length - 2 || metrics[sizePos + 1].width != 0 ) {
|
|
|
|
+ // Condition avoid extra empty line if <p> was the last tag.
|
|
|
|
+ makeLineBreak();
|
|
|
|
+ newLine = true;
|
|
|
|
+ prevChar = -1;
|
|
|
|
+ }
|
|
default:
|
|
default:
|
|
}
|
|
}
|
|
if( prevGlyphs != null )
|
|
if( prevGlyphs != null )
|
|
@@ -415,21 +582,13 @@ class HtmlText extends Text {
|
|
@:privateAccess glyphs.curColor.load(prevColor);
|
|
@:privateAccess glyphs.curColor.load(prevColor);
|
|
} else {
|
|
} else {
|
|
newLine = false;
|
|
newLine = false;
|
|
- var t = splitRawText(htmlToText(e.nodeValue), xPos, remainingSize(sizes));
|
|
|
|
- var dy = this.font.baseLine - font.baseLine;
|
|
|
|
|
|
+ var t = e.nodeValue;
|
|
|
|
+ var dy = metrics[sizePos].baseLine - font.baseLine;
|
|
for( i in 0...t.length ) {
|
|
for( i in 0...t.length ) {
|
|
var cc = t.charCodeAt(i);
|
|
var cc = t.charCodeAt(i);
|
|
if( cc == "\n".code ) {
|
|
if( cc == "\n".code ) {
|
|
- if( xPos > xMax ) xMax = xPos;
|
|
|
|
- if( calcLines ) lines.push(hxd.Math.ceil(xPos));
|
|
|
|
- switch( align ) {
|
|
|
|
- case Left:
|
|
|
|
- xPos = 0;
|
|
|
|
- case Right, Center, MultilineCenter, MultilineRight:
|
|
|
|
- xPos = lines.shift();
|
|
|
|
- if( xPos < xMin ) xMin = xPos;
|
|
|
|
- }
|
|
|
|
- yPos += font.lineHeight + lineSpacing;
|
|
|
|
|
|
+ makeLineBreak();
|
|
|
|
+ dy = metrics[sizePos].baseLine - font.baseLine;
|
|
prevChar = -1;
|
|
prevChar = -1;
|
|
continue;
|
|
continue;
|
|
}
|
|
}
|
|
@@ -462,6 +621,14 @@ class HtmlText extends Text {
|
|
return value;
|
|
return value;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ function set_lineHeightMode(v) {
|
|
|
|
+ if ( this.lineHeightMode != v ) {
|
|
|
|
+ this.lineHeightMode = v;
|
|
|
|
+ rebuild();
|
|
|
|
+ }
|
|
|
|
+ return v;
|
|
|
|
+ }
|
|
|
|
+
|
|
override function getBoundsRec( relativeTo : Object, out : h2d.col.Bounds, forSize : Bool ) {
|
|
override function getBoundsRec( relativeTo : Object, out : h2d.col.Bounds, forSize : Bool ) {
|
|
if( forSize )
|
|
if( forSize )
|
|
for( i in elements )
|
|
for( i in elements )
|
|
@@ -473,4 +640,20 @@ class HtmlText extends Text {
|
|
i.visible = true;
|
|
i.visible = true;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+private typedef LineInfo = {
|
|
|
|
+ var width : Float;
|
|
|
|
+ var height : Float;
|
|
|
|
+ var baseLine : Float;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+private typedef SplitNode = {
|
|
|
|
+ var node : Xml;
|
|
|
|
+ var prevChar : Int;
|
|
|
|
+ var pos : Int;
|
|
|
|
+ var width : Float;
|
|
|
|
+ var height : Float;
|
|
|
|
+ var baseLine : Float;
|
|
|
|
+ var font : h2d.Font;
|
|
}
|
|
}
|