Pavel Alexandrov 5 роки тому
батько
коміт
856e2228ee
5 змінених файлів з 443 додано та 193 видалено
  1. 342 159
      h2d/HtmlText.hx
  2. 50 26
      h2d/Text.hx
  3. 2 2
      h2d/TextInput.hx
  4. 34 6
      samples/HtmlText.hx
  5. 15 0
      samples/Text.hx

+ 342 - 159
h2d/HtmlText.hx

@@ -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;
 }
 }

+ 50 - 26
h2d/Text.hx

@@ -164,26 +164,42 @@ class Text extends Drawable {
 		return splitRawText(text,0,0);
 		return splitRawText(text,0,0);
 	}
 	}
 
 
-	function splitRawText( text : String, leftMargin : Float, afterData : Float ) {
-		if( realMaxWidth < 0 )
-			return text;
-		var lines = [], rest = text, restPos = 0;
-		var x = leftMargin, prevChar = -1;
+	/**
+		Word-wrap the text based on this Text settings.  
+		@param text String to word-wrap.
+		@param leftMargin Starting x offset of the first line.
+		@param afterData Minimum remaining space required at the end of the line.
+		@param font Optional overriding font to use instead of currently set.
+		@param sizes Optional line width array. Will be populated with sizes of split lines if present. Sizes will include both `leftMargin` in it's first line entry.
+		@param prevChar Optional character code for concatenation purposes (proper kernings).
+	**/
+	public function splitRawText( text : String, leftMargin = 0., afterData = 0., ?font : Font, ?sizes:Array<Float>, ?prevChar:Int = -1 ) {
+		var maxWidth = realMaxWidth;
+		if( maxWidth < 0 ) {
+			if ( sizes == null ) 
+				return text;
+			else 
+				maxWidth = Math.POSITIVE_INFINITY;
+		}
+		if ( font == null ) font = this.font;
+		var lines = [], restPos = 0;
+		var x = leftMargin;
 		for( i in 0...text.length ) {
 		for( i in 0...text.length ) {
 			var cc = text.charCodeAt(i);
 			var cc = text.charCodeAt(i);
 			var e = font.getChar(cc);
 			var e = font.getChar(cc);
 			var newline = cc == '\n'.code;
 			var newline = cc == '\n'.code;
 			var esize = e.width + e.getKerningOffset(prevChar);
 			var esize = e.width + e.getKerningOffset(prevChar);
 			if( font.charset.isBreakChar(cc) ) {
 			if( font.charset.isBreakChar(cc) ) {
-				if( lines.length == 0 && leftMargin > 0 && x > realMaxWidth ) {
+				if( lines.length == 0 && leftMargin > 0 && x > maxWidth ) {
 					lines.push("");
 					lines.push("");
+					if ( sizes != null ) sizes.push(leftMargin);
 					x -= leftMargin;
 					x -= leftMargin;
 				}
 				}
 				var size = x + esize + letterSpacing; /* TODO : no letter spacing */
 				var size = x + esize + letterSpacing; /* TODO : no letter spacing */
 				var k = i + 1, max = text.length;
 				var k = i + 1, max = text.length;
 				var prevChar = prevChar;
 				var prevChar = prevChar;
 				var breakFound = false;
 				var breakFound = false;
-				while( size <= realMaxWidth && k < max ) {
+				while( size <= maxWidth && k < max ) {
 					var cc = text.charCodeAt(k++);
 					var cc = text.charCodeAt(k++);
 					if( font.charset.isSpace(cc) || cc == '\n'.code ) {
 					if( font.charset.isSpace(cc) || cc == '\n'.code ) {
 						breakFound = true;
 						breakFound = true;
@@ -194,7 +210,7 @@ class Text extends Drawable {
 					prevChar = cc;
 					prevChar = cc;
 					if( font.charset.isBreakChar(cc) ) break;
 					if( font.charset.isBreakChar(cc) ) break;
 				}
 				}
-				if( size > realMaxWidth || (!breakFound && size + afterData > realMaxWidth) ) {
+				if( size > maxWidth || (!breakFound && size + afterData > maxWidth) ) {
 					newline = true;
 					newline = true;
 					if( font.charset.isSpace(cc) ){
 					if( font.charset.isSpace(cc) ){
 						lines.push(text.substr(restPos, i - restPos));
 						lines.push(text.substr(restPos, i - restPos));
@@ -205,18 +221,23 @@ class Text extends Drawable {
 					restPos = i + 1;
 					restPos = i + 1;
 				}
 				}
 			}
 			}
-			if( e != null )
+			if( e != null && cc != '\n'.code )
 				x += esize + letterSpacing;
 				x += esize + letterSpacing;
 			if( newline ) {
 			if( newline ) {
+				if ( sizes != null ) sizes.push(x);
 				x = 0;
 				x = 0;
 				prevChar = -1;
 				prevChar = -1;
 			} else
 			} else
 				prevChar = cc;
 				prevChar = cc;
 		}
 		}
 		if( restPos < text.length ) {
 		if( restPos < text.length ) {
-			if( lines.length == 0 && leftMargin > 0 && x + afterData - letterSpacing > realMaxWidth )
+			if( lines.length == 0 && leftMargin > 0 && x + afterData - letterSpacing > maxWidth ) {
 				lines.push("");
 				lines.push("");
+				if ( sizes != null ) sizes.push(leftMargin);
+				x -= leftMargin;
+			}
 			lines.push(text.substr(restPos, text.length - restPos));
 			lines.push(text.substr(restPos, text.length - restPos));
+			if ( sizes != null ) sizes.push(x);
 		}
 		}
 		return lines.join("\n");
 		return lines.join("\n");
 	}
 	}
@@ -226,26 +247,31 @@ class Text extends Drawable {
 		return text.substr(0, Std.int(progress));
 		return text.substr(0, Std.int(progress));
 	}
 	}
 
 
-	function initGlyphs( text : String, rebuild = true, handleAlign = true, lines : Array<Int> = null ) : Void {
+	function initGlyphs( text : String, rebuild = true ) : Void {
 		if( rebuild ) glyphs.clear();
 		if( rebuild ) glyphs.clear();
-		var x = 0., y = 0., xMax = 0., xMin = 0., prevChar = -1;
-		var align = handleAlign ? textAlign : Left;
+		var x = 0., y = 0., xMax = 0., xMin = 0., yMin = 0., prevChar = -1, linei = 0;
+		var align = textAlign;
+		var lines = new Array<Float>();
+		var dl = font.lineHeight + lineSpacing;
+		var t = splitRawText(text, 0, 0, lines);
+
+		for ( lw in lines ) {
+			if ( lw > x ) x = lw;
+		}
+		calcWidth = x;
+
 		switch( align ) {
 		switch( align ) {
 		case Center, Right, MultilineCenter, MultilineRight:
 		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 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;
+			var k = align == Center || align == MultilineCenter ? 0.5 : 1;
 			for( i in 0...lines.length )
 			for( i in 0...lines.length )
-				lines[i] = (max - lines[i]) >> k;
-			x = lines.shift();
+				lines[i] = Math.ffloor((max - lines[i]) * k);
+			x = lines[0];
 			xMin = x;
 			xMin = x;
-		default:
+		case Left:
+			x = 0;
 		}
 		}
-		var dl = font.lineHeight + lineSpacing;
-		var calcLines = !handleAlign && !rebuild && lines != null;
-		var yMin = 0.;
-		var t = splitText(text);
+		
 		for( i in 0...t.length ) {
 		for( i in 0...t.length ) {
 			var cc = t.charCodeAt(i);
 			var cc = t.charCodeAt(i);
 			var e = font.getChar(cc);
 			var e = font.getChar(cc);
@@ -255,12 +281,11 @@ class Text extends Drawable {
 
 
 			if( cc == '\n'.code ) {
 			if( cc == '\n'.code ) {
 				if( x > xMax ) xMax = x;
 				if( x > xMax ) xMax = x;
-				if( calcLines ) lines.push(Math.ceil(x));
 				switch( align ) {
 				switch( align ) {
 				case Left:
 				case Left:
 					x = 0;
 					x = 0;
 				case Right, Center, MultilineCenter, MultilineRight:
 				case Right, Center, MultilineCenter, MultilineRight:
-					x = lines.shift();
+					x = lines[++linei];
 					if( x < xMin ) xMin = x;
 					if( x < xMin ) xMin = x;
 				}
 				}
 				y += dl;
 				y += dl;
@@ -274,7 +299,6 @@ class Text extends Drawable {
 				prevChar = cc;
 				prevChar = cc;
 			}
 			}
 		}
 		}
-		if( calcLines ) lines.push(Math.ceil(x));
 		if( x > xMax ) xMax = x;
 		if( x > xMax ) xMax = x;
 
 
 		calcXMin = xMin;
 		calcXMin = xMin;

+ 2 - 2
h2d/TextInput.hx

@@ -259,8 +259,8 @@ class TextInput extends Text {
 		return f;
 		return f;
 	}
 	}
 
 
-	override function initGlyphs(text:String, rebuild = true, handleAlign = true, lines:Array<Int> = null):Void {
-		super.initGlyphs(text, rebuild, handleAlign, lines);
+	override function initGlyphs(text:String, rebuild = true):Void {
+		super.initGlyphs(text, rebuild);
 		if( rebuild ) {
 		if( rebuild ) {
 			this.calcWidth += cursorTile.width; // cursor end pos
 			this.calcWidth += cursorTile.width; // cursor end pos
 			if( inputWidth != null && this.calcWidth > inputWidth ) this.calcWidth = inputWidth;
 			if( inputWidth != null && this.calcWidth > inputWidth ) this.calcWidth = inputWidth;

+ 34 - 6
samples/HtmlText.hx

@@ -59,13 +59,30 @@ class HtmlText extends hxd.App {
 	var resizeWidgets: Array<HtmlTextWidget> = [];
 	var resizeWidgets: Array<HtmlTextWidget> = [];
 
 
 	override function init() {
 	override function init() {
-		
+
 		// Enable global scaling
 		// Enable global scaling
 		// s2d.scale(1.25);
 		// s2d.scale(1.25);
 
 
 		var font = hxd.res.DefaultFont.get();
 		var font = hxd.res.DefaultFont.get();
 		// var font = hxd.Res.customFont.toFont();
 		// var font = hxd.Res.customFont.toFont();
 
 
+		h2d.HtmlText.defaultLoadFont = function( face : String ) : h2d.Font {
+			if ( face == 'myFontFace' ) {
+				var font = hxd.res.DefaultFont.get().clone();
+				font.resizeTo(font.size * 2);
+				return font;
+			}
+			return null;
+		}
+		h2d.HtmlText.defaultLoadImage = function( src : String ) : h2d.Tile {
+			if ( src == "logo" ) {
+				var t = hxd.Res.hxlogo.toTile();
+				t.scaleToSize(16, 16);
+				return t;
+			}
+			return null;
+		}
+
 		var multilineText = "This is a multiline <font color=\"#FF00FF\">text.<br/>Lorem</font> ipsum dolor";
 		var multilineText = "This is a multiline <font color=\"#FF00FF\">text.<br/>Lorem</font> ipsum dolor";
 		var singleText = "Hello simple text";
 		var singleText = "Hello simple text";
 
 
@@ -98,15 +115,14 @@ class HtmlText extends hxd.App {
 		// Resized widgets
 		// Resized widgets
 		xpos += 200;
 		xpos += 200;
 		yoffset = 10;
 		yoffset = 10;
-		var longText = "Lorem ipsum dolor sit amet, fabulas repudiare accommodare nec ut.<br />Ut nec facete maiestatis, <font color=\"#FF00FF\">partem debitis eos id</font>, perfecto ocurreret repudiandae cum no.";
-		//var longText = "Lorem ipsum dolor sit amet, fabulas repudiare accommodare nec ut.Ut nec facete maiestatis, <font color=\"#FF00FF\">partem debitis eos id</font>, perfecto ocurreret repudiandae cum no.";
+		var longText = "Long text long text. Icons like this one <img src='logo'/> are flowed separately, but they should <font color=\"#FF00FF\">stick</font> to the text when they appear <img src='logo'/>before or after<img src='logo'/>. We support different <font face='myFontFace'>font faces</font>";
 		for (a in [Align.Left, Align.Center, Align.Right, Align.MultilineCenter, Align.MultilineRight]) {
 		for (a in [Align.Left, Align.Center, Align.Right, Align.MultilineCenter, Align.MultilineRight]) {
 			var w = createWidget(longText, a);
 			var w = createWidget(longText, a);
 			w.setMaxWidth(200);
 			w.setMaxWidth(200);
 			resizeWidgets.push(w);
 			resizeWidgets.push(w);
-			yoffset += 100;
+			yoffset += 160;
 		}
 		}
-		
+
 		// Flows
 		// Flows
 		function createText(parent:Object, str : String, align:Align) {
 		function createText(parent:Object, str : String, align:Align) {
 			var tf = new h2d.HtmlText(font, parent);
 			var tf = new h2d.HtmlText(font, parent);
@@ -187,6 +203,18 @@ class HtmlText extends hxd.App {
 			var f2 = createFlow(flow);
 			var f2 = createFlow(flow);
 			createText(f2, multilineText, Align.Left);
 			createText(f2, multilineText, Align.Left);
 		}
 		}
+		yoffset += flow.getBounds().height + 10;
+
+		var tagShowcase = "HtmlText supports next tags:" +
+		"<p>&lt;p&gt; tag:<p align='center'>Forces line breaks before and after.</p><p align='right'>It also supports `align` property.</p></p>" +
+		"<p>&lt;br/&gt; tag: Makes a<br/>line break</p>" +
+		"<p>&lt;font&gt; tag: Allows to control <font color='#ff00ff'>color</font>, <font opacity='0.8'>op</font><font opacity='0.6'>ac</font><font opacity='0.4'>it</font><font opacity='0.2'>y</font> and <font face='myFontFace'>face</font> properties.</p>" +
+		"<p>&lt;img src='...'/&gt; tag: Allows to insert a Tile <img src='logo'/> into html text</p>";
+		var flow = createFlow(s2d);
+		flow.y = yoffset;
+		flow.maxWidth = 360;
+		var text = createText(flow, tagShowcase, Align.Left);
+		text.maxWidth = 360;
 
 
 		onResize();
 		onResize();
 	}
 	}
@@ -194,7 +222,7 @@ class HtmlText extends hxd.App {
 
 
 	override function update(dt:Float) {
 	override function update(dt:Float) {
 		for (w in resizeWidgets) {
 		for (w in resizeWidgets) {
-			w.setMaxWidth(Std.int(300 + Math.sin(haxe.Timer.stamp() * 0.5) * 100.0));
+			w.setMaxWidth(Std.int(300 + Math.sin(haxe.Timer.stamp() * 0.2) * 100.0));
 		}
 		}
 	}
 	}
 
 

+ 15 - 0
samples/Text.hx

@@ -218,6 +218,21 @@ class Text extends hxd.App {
 			sdfText.maxWidth = 400;
 			sdfText.maxWidth = 400;
 		}
 		}
 
 
+		yoffset += flow.getBounds().height + 35;
+
+		{
+			var flow = createFlow(s2d);
+			flow.y = yoffset;
+			flow.horizontalAlign = FlowAlign.Left;
+			flow.horizontalSpacing = 15;
+			flow.layout = Horizontal;
+			createText(flow, "LEFT: It is a text with a new line added after THAT\nto test the alignment", Align.Left);
+			createText(flow, "CENTER: It is a text with a new line added after THAT\nto test the alignment", Align.Center);
+			createText(flow, "RIGHT: It is a text with a new line added after THAT\nto test the alignment", Align.Right);
+			createText(flow, "MULTICENTER: It is a text with a new line added after THAT\nto test the alignment", Align.MultilineCenter);
+			createText(flow, "MULTIRIGHT: It is a text with a new line added after THAT\nto test the alignment", Align.MultilineRight);
+		}
+
 		onResize();
 		onResize();
 	}
 	}