HtmlText.hx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. package h2d;
  2. import h2d.Text;
  3. /**
  4. The `HtmlText` line height calculation rules.
  5. **/
  6. enum LineHeightMode {
  7. /**
  8. Accurate line height calculations. Each line will adjust it's height according to it's contents.
  9. **/
  10. Accurate;
  11. /**
  12. Only text adjusts line heights, and `<img>` tags do not affect it (partial legacy behavior).
  13. **/
  14. TextOnly;
  15. /**
  16. Legacy line height mode. When used, line heights remain constant based on `Text.font` variable.
  17. **/
  18. Constant;
  19. }
  20. /**
  21. `HtmlText` img tag vertical alignment rules.
  22. **/
  23. enum ImageVerticalAlign {
  24. /**
  25. Align images along the top of the text line.
  26. **/
  27. Top;
  28. /**
  29. Align images to sit on the base line of the text.
  30. **/
  31. Bottom;
  32. /**
  33. Align images to the middle between the top of the text line its base line.
  34. **/
  35. Middle;
  36. }
  37. /**
  38. A simple HTML text renderer.
  39. See the [Text](https://github.com/HeapsIO/heaps/wiki/Text) section of the manual for more details and a list of the supported HTML tags.
  40. **/
  41. class HtmlText extends Text {
  42. /**
  43. A default method HtmlText uses to load images for `<img>` tag. See `HtmlText.loadImage` for details.
  44. **/
  45. public static dynamic function defaultLoadImage( url : String ) : h2d.Tile {
  46. return null;
  47. }
  48. /**
  49. A default method HtmlText uses to load fonts for `<font>` tags with `face` attribute. See `HtmlText.loadFont` for details.
  50. **/
  51. public static dynamic function defaultLoadFont( name : String ) : h2d.Font {
  52. return null;
  53. }
  54. /**
  55. A default method HtmlText uses to format assigned text. See `HtmlText.formatText` for details.
  56. **/
  57. public static dynamic function defaultFormatText( text : String ) : String {
  58. return text;
  59. }
  60. /**
  61. When enabled, condenses extra spaces (carriage-return, line-feed, tabulation and space character) to one space.
  62. If not set, uncondensed whitespace is left as is, as well as line-breaks.
  63. **/
  64. public var condenseWhite(default,set) : Bool = true;
  65. /**
  66. The spacing after `<img>` tags in pixels.
  67. **/
  68. public var imageSpacing(default,set):Float = 1;
  69. /**
  70. Line height calculation mode controls how much space lines take up vertically.
  71. Changing mode to `Constant` restores the legacy behavior of HtmlText.
  72. **/
  73. public var lineHeightMode(default,set) : LineHeightMode = Accurate;
  74. /**
  75. Vertical alignment of the images in `<img>` tag relative to the text.
  76. **/
  77. public var imageVerticalAlign(default,set) : ImageVerticalAlign = Bottom;
  78. var elements : Array<Object> = [];
  79. var xPos : Float;
  80. var yPos : Float;
  81. var xMax : Float;
  82. var xMin : Float;
  83. var textXml : Xml;
  84. var sizePos : Int;
  85. var dropMatrix : h3d.shader.ColorMatrix;
  86. var prevChar : Int;
  87. var newLine : Bool;
  88. var aHrefs : Array<String>;
  89. var aInteractive : Interactive;
  90. override function draw(ctx:RenderContext) {
  91. if( dropShadow != null ) {
  92. var oldX = absX, oldY = absY;
  93. absX += dropShadow.dx * matA + dropShadow.dy * matC;
  94. absY += dropShadow.dx * matB + dropShadow.dy * matD;
  95. if( dropMatrix == null ) {
  96. dropMatrix = new h3d.shader.ColorMatrix();
  97. addShader(dropMatrix);
  98. }
  99. dropMatrix.enabled = true;
  100. var m = dropMatrix.matrix;
  101. m.zero();
  102. m._41 = ((dropShadow.color >> 16) & 0xFF) / 255;
  103. m._42 = ((dropShadow.color >> 8) & 0xFF) / 255;
  104. m._43 = (dropShadow.color & 0xFF) / 255;
  105. m._44 = dropShadow.alpha;
  106. glyphs.drawWith(ctx, this);
  107. dropMatrix.enabled = false;
  108. absX = oldX;
  109. absY = oldY;
  110. } else {
  111. removeShader(dropMatrix);
  112. dropMatrix = null;
  113. }
  114. glyphs.drawWith(ctx,this);
  115. }
  116. /**
  117. Method that should return an `h2d.Tile` instance for `<img>` tags. By default calls `HtmlText.defaultLoadImage` method.
  118. HtmlText does not cache tile instances.
  119. Due to internal structure, method should be deterministic and always return same Tile on consequent calls with same `url` input.
  120. @param url A value contained in `src` attribute.
  121. **/
  122. public dynamic function loadImage( url : String ) : Tile {
  123. return defaultLoadImage(url);
  124. }
  125. /**
  126. Method that should return an `h2d.Font` instance for `<font>` tags with `face` attribute. By default calls `HtmlText.defaultLoadFont` method.
  127. HtmlText does not cache font instances and it's recommended to perform said caching from outside.
  128. Due to internal structure, method should be deterministic and always return same Font instance on consequent calls with same `name` input.
  129. @param name A value contained in `face` attribute.
  130. @returns Method should return loaded font instance or `null`. If `null` is returned - currently active font is used.
  131. **/
  132. public dynamic function loadFont( name : String ) : Font {
  133. var f = defaultLoadFont(name);
  134. if (f == null) return this.font;
  135. else return f;
  136. }
  137. /**
  138. Called on a <a> tag click
  139. **/
  140. public dynamic function onHyperlink(url:String) : Void {
  141. }
  142. /**
  143. Called when text is assigned, allowing to process arbitrary text to a valid XHTML.
  144. **/
  145. public dynamic function formatText( text : String ) : String {
  146. return defaultFormatText(text);
  147. }
  148. override function set_text(t : String) {
  149. super.set_text(formatText(t));
  150. return t;
  151. }
  152. function parseText( text : String ) {
  153. return try Xml.parse(text) catch( e : Dynamic ) throw "Could not parse " + text + " (" + e +")";
  154. }
  155. inline function makeLineInfo( width : Float, height : Float, baseLine : Float ) : LineInfo {
  156. return { width: width, height: height, baseLine: baseLine };
  157. }
  158. override function validateText() {
  159. textXml = parseText(text);
  160. validateNodes(textXml);
  161. }
  162. function validateNodes( xml : Xml ) {
  163. switch( xml.nodeType ) {
  164. case Element:
  165. var nodeName = xml.nodeName.toLowerCase();
  166. switch ( nodeName ) {
  167. case "img":
  168. loadImage(xml.get("src"));
  169. case "font":
  170. if (xml.exists("face")) {
  171. loadFont(xml.get("face"));
  172. }
  173. case "b", "bold":
  174. loadFont("bold");
  175. case "i", "italic":
  176. loadFont("italic");
  177. }
  178. for( child in xml )
  179. validateNodes(child);
  180. case Document:
  181. for( child in xml )
  182. validateNodes(child);
  183. default:
  184. }
  185. }
  186. override function initGlyphs( text : String, rebuild = true ) {
  187. if( rebuild ) {
  188. glyphs.clear();
  189. for( e in elements ) e.remove();
  190. elements = [];
  191. }
  192. glyphs.setDefaultColor(textColor);
  193. var doc : Xml;
  194. if (textXml == null) {
  195. doc = parseText(text);
  196. } else {
  197. doc = textXml;
  198. }
  199. yPos = 0;
  200. xMax = 0;
  201. xMin = Math.POSITIVE_INFINITY;
  202. sizePos = 0;
  203. calcYMin = 0;
  204. var metrics : Array<LineInfo> = [ makeLineInfo(0, font.lineHeight, font.baseLine) ];
  205. prevChar = -1;
  206. newLine = true;
  207. var splitNode : SplitNode = {
  208. node: null, pos: 0, font: font, prevChar: -1,
  209. width: 0, height: 0, baseLine: 0
  210. };
  211. for( e in doc )
  212. buildSizes(e, font, metrics, splitNode);
  213. var max = 0.;
  214. for ( info in metrics ) {
  215. if ( info.width > max ) max = info.width;
  216. }
  217. calcWidth = max;
  218. prevChar = -1;
  219. newLine = true;
  220. nextLine(textAlign, metrics[0].width);
  221. for ( e in doc )
  222. addNode(e, font, textAlign, rebuild, metrics);
  223. if( xPos > xMax ) xMax = xPos;
  224. textXml = null;
  225. var y = yPos;
  226. calcXMin = xMin;
  227. calcWidth = xMax - xMin;
  228. calcHeight = y + metrics[sizePos].height;
  229. calcSizeHeight = y + metrics[sizePos].baseLine;//(font.baseLine > 0 ? font.baseLine : font.lineHeight);
  230. calcDone = true;
  231. if ( rebuild ) needsRebuild = false;
  232. }
  233. function buildSizes( e : Xml, font : Font, metrics : Array<LineInfo>, splitNode:SplitNode ) {
  234. function wordSplit() {
  235. var fnt = splitNode.font;
  236. var str = splitNode.node.nodeValue;
  237. var info = metrics[metrics.length - 1];
  238. var w = info.width;
  239. var cc = str.charCodeAt(splitNode.pos);
  240. // Restore line metrics to ones before split.
  241. // Potential bug: `Text<split> [Image] text<split>text` - third line will use metrics as if image is present in the line.
  242. info.width = splitNode.width;
  243. info.height = splitNode.height;
  244. info.baseLine = splitNode.baseLine;
  245. var char = fnt.getChar(cc);
  246. if (lineBreak && fnt.charset.isSpace(cc)) {
  247. // Space characters are converted to \n
  248. w -= (splitNode.width + letterSpacing + char.width + char.getKerningOffset(splitNode.prevChar));
  249. splitNode.node.nodeValue = str.substr(0, splitNode.pos) + "\n" + str.substr(splitNode.pos + 1);
  250. } else {
  251. w -= (splitNode.width + letterSpacing + char.getKerningOffset(splitNode.prevChar));
  252. splitNode.node.nodeValue = str.substr(0, splitNode.pos+1) + "\n" + str.substr(splitNode.pos+1);
  253. }
  254. splitNode.node = null;
  255. return w;
  256. }
  257. inline function lineFont() {
  258. return lineHeightMode == Constant ? this.font : font;
  259. }
  260. if( e.nodeType == Xml.Element ) {
  261. inline function makeLineBreak() {
  262. var fontInfo = lineFont();
  263. metrics.push(makeLineInfo(0, fontInfo.lineHeight, fontInfo.baseLine));
  264. splitNode.node = null;
  265. newLine = true;
  266. prevChar = -1;
  267. }
  268. var nodeName = e.nodeName.toLowerCase();
  269. switch( nodeName ) {
  270. case "p":
  271. if ( !newLine ) {
  272. makeLineBreak();
  273. }
  274. case "br":
  275. makeLineBreak();
  276. case "img":
  277. // TODO: Support width/height attributes
  278. // Support max-width/max-height attributes (downscale)
  279. // Support min-width/min-height attributes (upscale)
  280. var i : Tile = loadImage(e.get("src"));
  281. if ( i == null ) i = Tile.fromColor(0xFF00FF, 8, 8);
  282. var size = metrics[metrics.length - 1].width + i.width + imageSpacing;
  283. if (realMaxWidth >= 0 && size > realMaxWidth && metrics[metrics.length - 1].width > 0) {
  284. if ( splitNode.node != null ) {
  285. size = wordSplit() + i.width + imageSpacing;
  286. var info = metrics[metrics.length - 1];
  287. // Bug: height/baseLine may be innacurate in case of sizeA sizeB<split>sizeA where sizeB is larger.
  288. switch ( lineHeightMode ) {
  289. case Accurate:
  290. var grow = i.height - i.dy - info.baseLine;
  291. var h = info.height;
  292. var bl = info.baseLine;
  293. if (grow > 0) {
  294. h += grow;
  295. bl += grow;
  296. }
  297. metrics.push(makeLineInfo(size, Math.max(h, bl + i.dy), bl));
  298. default:
  299. metrics.push(makeLineInfo(size, info.height, info.baseLine));
  300. }
  301. }
  302. } else {
  303. var info = metrics[metrics.length - 1];
  304. info.width = size;
  305. if ( lineHeightMode == Accurate ) {
  306. var grow = i.height - i.dy - info.baseLine;
  307. if(grow > 0) {
  308. switch(imageVerticalAlign) {
  309. case Top:
  310. info.height += grow;
  311. case Bottom:
  312. info.baseLine += grow;
  313. info.height += grow;
  314. case Middle:
  315. info.height += grow;
  316. info.baseLine += Std.int(grow/2);
  317. }
  318. }
  319. grow = info.baseLine + i.dy;
  320. if ( info.height < grow ) info.height = grow;
  321. }
  322. }
  323. newLine = false;
  324. prevChar = -1;
  325. case "font":
  326. for( a in e.attributes() ) {
  327. var v = e.get(a);
  328. switch( a.toLowerCase() ) {
  329. case "face": font = loadFont(v);
  330. default:
  331. }
  332. }
  333. case "b", "bold":
  334. font = loadFont("bold");
  335. case "i", "italic":
  336. font = loadFont("italic");
  337. default:
  338. }
  339. for( child in e )
  340. buildSizes(child, font, metrics, splitNode);
  341. switch( nodeName ) {
  342. case "p":
  343. if ( !newLine ) {
  344. makeLineBreak();
  345. }
  346. default:
  347. }
  348. } else if (e.nodeValue.length != 0) {
  349. newLine = false;
  350. var text = htmlToText(e.nodeValue);
  351. var fontInfo = lineFont();
  352. var info : LineInfo = metrics.pop();
  353. var leftMargin = info.width;
  354. var maxWidth = realMaxWidth < 0 ? Math.POSITIVE_INFINITY : realMaxWidth;
  355. var textSplit = [], restPos = 0;
  356. var x = leftMargin;
  357. var breakChars = 0;
  358. for ( i in 0...text.length ) {
  359. var cc = text.charCodeAt(i);
  360. var g = font.getChar(cc);
  361. var newline = cc == '\n'.code;
  362. var esize = g.width + g.getKerningOffset(prevChar);
  363. var nc = text.charCodeAt(i+1);
  364. if ( font.charset.isBreakChar(cc) && (nc == null || !font.charset.isComplementChar(nc) )) {
  365. // Case: Very first word in text makes the line too long hence we want to start it off on a new line.
  366. if (x > maxWidth && textSplit.length == 0 && splitNode.node != null) {
  367. metrics.push(makeLineInfo(x, info.height, info.baseLine));
  368. x = wordSplit();
  369. }
  370. var size = x + esize + letterSpacing;
  371. var k = i + 1, max = text.length;
  372. var prevChar = cc;
  373. while ( size <= maxWidth && k < max ) {
  374. var cc = text.charCodeAt(k++);
  375. if ( lineBreak && (font.charset.isSpace(cc) || cc == '\n'.code ) ) break;
  376. var e = font.getChar(cc);
  377. size += e.width + letterSpacing + e.getKerningOffset(prevChar);
  378. prevChar = cc;
  379. var nc = text.charCodeAt(k);
  380. if ( font.charset.isBreakChar(cc) && (nc == null || !font.charset.isComplementChar(nc)) ) break;
  381. }
  382. // Avoid empty line when last char causes line-break while being CJK
  383. if ( lineBreak && size > maxWidth && i != max - 1 ) {
  384. // Next word will reach maxWidth
  385. newline = true;
  386. if ( font.charset.isSpace(cc) ) {
  387. textSplit.push(text.substr(restPos, i - restPos));
  388. g = null;
  389. } else {
  390. textSplit.push(text.substr(restPos, i + 1 - restPos));
  391. breakChars++;
  392. }
  393. splitNode.node = null;
  394. restPos = i + 1;
  395. } else {
  396. splitNode.node = e;
  397. splitNode.pos = i + breakChars;
  398. splitNode.prevChar = this.prevChar;
  399. splitNode.width = x;
  400. splitNode.height = info.height;
  401. splitNode.baseLine = info.baseLine;
  402. splitNode.font = font;
  403. }
  404. }
  405. if ( g != null && cc != '\n'.code )
  406. x += esize + letterSpacing;
  407. if ( newline ) {
  408. metrics.push(makeLineInfo(x, info.height, info.baseLine));
  409. info.height = fontInfo.lineHeight;
  410. info.baseLine = fontInfo.baseLine;
  411. x = 0;
  412. prevChar = -1;
  413. newLine = true;
  414. } else {
  415. prevChar = cc;
  416. newLine = false;
  417. }
  418. }
  419. if ( restPos < text.length ) {
  420. if (x > maxWidth) {
  421. if ( splitNode.node != null && splitNode.node != e ) {
  422. metrics.push(makeLineInfo(x, info.height, info.baseLine));
  423. x = wordSplit();
  424. }
  425. }
  426. textSplit.push(text.substr(restPos));
  427. metrics.push(makeLineInfo(x, info.height, info.baseLine));
  428. }
  429. if (newLine || metrics.length == 0) {
  430. metrics.push(makeLineInfo(0, fontInfo.lineHeight, fontInfo.baseLine));
  431. textSplit.push("");
  432. }
  433. // Save node value
  434. e.nodeValue = textSplit.join("\n");
  435. }
  436. }
  437. static var REG_SPACES = ~/[\r\n\t ]+/g;
  438. function htmlToText( t : String ) {
  439. if (condenseWhite)
  440. t = REG_SPACES.replace(t, " ");
  441. return t;
  442. }
  443. inline function nextLine( align : Align, size : Float )
  444. {
  445. switch( align ) {
  446. case Left:
  447. xPos = 0;
  448. if (xMin > 0) xMin = 0;
  449. case Right, Center, MultilineCenter, MultilineRight:
  450. var max = if( align == MultilineCenter || align == MultilineRight ) hxd.Math.ceil(calcWidth) else calcWidth < 0 ? 0 : hxd.Math.ceil(realMaxWidth);
  451. var k = align == Center || align == MultilineCenter ? 0.5 : 1;
  452. xPos = Math.ffloor((max - size) * k);
  453. if( xPos < xMin ) xMin = xPos;
  454. }
  455. }
  456. override function splitText(text:String):String {
  457. if( realMaxWidth < 0 )
  458. return text;
  459. yPos = 0;
  460. xMax = 0;
  461. sizePos = 0;
  462. calcYMin = 0;
  463. var doc = parseText(text);
  464. /*
  465. This might require a global refactoring at some point.
  466. We would need a way to somehow build an AST from the XML representation
  467. with all sizes and word breaks so analysis is much more easy.
  468. */
  469. var splitNode : SplitNode = { node: null, font: font, width: 0, height: 0, baseLine: 0, pos: 0, prevChar: -1 };
  470. var metrics = [makeLineInfo(0, font.lineHeight, font.baseLine)];
  471. prevChar = -1;
  472. newLine = true;
  473. for( e in doc )
  474. buildSizes(e, font, metrics, splitNode);
  475. xMax = 0;
  476. function addBreaks( e : Xml ) {
  477. if( e.nodeType == Xml.Element ) {
  478. for( x in e )
  479. addBreaks(x);
  480. } else {
  481. var text = e.nodeValue;
  482. var startI = 0;
  483. var index = Lambda.indexOf(e.parent, e);
  484. for (i in 0...text.length) {
  485. if (text.charCodeAt(i) == '\n'.code) {
  486. var pre = text.substring(startI, i);
  487. if (pre != "") e.parent.insertChild(Xml.createPCData(pre), index++);
  488. e.parent.insertChild(Xml.createElement("br"),index++);
  489. startI = i+1;
  490. }
  491. }
  492. if (startI < text.length) {
  493. e.nodeValue = text.substr(startI);
  494. } else {
  495. e.parent.removeChild(e);
  496. }
  497. }
  498. }
  499. for( d in doc )
  500. addBreaks(d);
  501. return doc.toString();
  502. }
  503. override function getTextProgress(text:String, progress:Float):String {
  504. if( progress >= text.length )
  505. return text;
  506. var doc = parseText(text);
  507. function progressRec(e:Xml) {
  508. if( progress <= 0 ) {
  509. e.parent.removeChild(e);
  510. return;
  511. }
  512. if( e.nodeType == Xml.Element ) {
  513. for( x in [for( x in e ) x] )
  514. progressRec(x);
  515. } else {
  516. var text = htmlToText(e.nodeValue);
  517. var len = text.length;
  518. if( len > progress ) {
  519. text = text.substr(0, Std.int(progress));
  520. e.nodeValue = text;
  521. }
  522. progress -= len;
  523. }
  524. }
  525. for( x in [for( x in doc ) x] )
  526. progressRec(x);
  527. return doc.toString();
  528. }
  529. function addNode( e : Xml, font : Font, align : Align, rebuild : Bool, metrics : Array<LineInfo> ) {
  530. inline function createInteractive() {
  531. if(aHrefs == null || aHrefs.length == 0)
  532. return;
  533. aInteractive = new Interactive(0, metrics[sizePos].height, this);
  534. var href = aHrefs[aHrefs.length-1];
  535. aInteractive.onClick = function(event) {
  536. onHyperlink(href);
  537. }
  538. aInteractive.x = xPos;
  539. aInteractive.y = yPos;
  540. elements.push(aInteractive);
  541. }
  542. inline function finalizeInteractive() {
  543. if(aInteractive != null) {
  544. aInteractive.width = xPos - aInteractive.x;
  545. aInteractive = null;
  546. }
  547. }
  548. inline function makeLineBreak()
  549. {
  550. finalizeInteractive();
  551. if( xPos > xMax ) xMax = xPos;
  552. yPos += metrics[sizePos].height + lineSpacing;
  553. nextLine(align, metrics[++sizePos].width);
  554. createInteractive();
  555. }
  556. if( e.nodeType == Xml.Element ) {
  557. var prevColor = null, prevGlyphs = null;
  558. var oldAlign = align;
  559. var nodeName = e.nodeName.toLowerCase();
  560. inline function setFont( v : String ) {
  561. font = loadFont(v);
  562. if( prevGlyphs == null ) prevGlyphs = glyphs;
  563. var prev = glyphs;
  564. glyphs = new TileGroup(font == null ? null : font.tile, this);
  565. if ( font != null ) {
  566. switch( font.type ) {
  567. case SignedDistanceField(channel, alphaCutoff, smoothing):
  568. var shader = new h3d.shader.SignedDistanceField();
  569. shader.channel = channel;
  570. shader.alphaCutoff = alphaCutoff;
  571. shader.smoothing = smoothing;
  572. shader.autoSmoothing = smoothing == -1;
  573. glyphs.smooth = this.smooth;
  574. glyphs.addShader(shader);
  575. default:
  576. }
  577. }
  578. @:privateAccess glyphs.curColor.load(prev.curColor);
  579. elements.push(glyphs);
  580. }
  581. switch( nodeName ) {
  582. case "font":
  583. for( a in e.attributes() ) {
  584. var v = e.get(a);
  585. switch( a.toLowerCase() ) {
  586. case "color":
  587. if( prevColor == null ) prevColor = @:privateAccess glyphs.curColor.clone();
  588. if( v.charCodeAt(0) == '#'.code && v.length == 4 )
  589. v = "#" + v.charAt(1) + v.charAt(1) + v.charAt(2) + v.charAt(2) + v.charAt(3) + v.charAt(3);
  590. glyphs.setDefaultColor(Std.parseInt("0x" + v.substr(1)));
  591. case "opacity":
  592. if( prevColor == null ) prevColor = @:privateAccess glyphs.curColor.clone();
  593. @:privateAccess glyphs.curColor.a *= Std.parseFloat(v);
  594. case "face":
  595. setFont(v);
  596. default:
  597. }
  598. }
  599. case "p":
  600. for( a in e.attributes() ) {
  601. switch( a.toLowerCase() ) {
  602. case "align":
  603. var v = e.get(a);
  604. if ( v != null )
  605. switch( v.toLowerCase() ) {
  606. case "left":
  607. align = Left;
  608. case "center":
  609. align = Center;
  610. case "right":
  611. align = Right;
  612. case "multiline-center":
  613. align = MultilineCenter;
  614. case "multiline-right":
  615. align = MultilineRight;
  616. //?justify
  617. }
  618. default:
  619. }
  620. }
  621. if ( !newLine ) {
  622. makeLineBreak();
  623. newLine = true;
  624. prevChar = -1;
  625. } else {
  626. nextLine(align, metrics[sizePos].width);
  627. }
  628. case "b","bold":
  629. setFont("bold");
  630. case "i","italic":
  631. setFont("italic");
  632. case "br":
  633. makeLineBreak();
  634. newLine = true;
  635. prevChar = -1;
  636. case "img":
  637. var i : Tile = loadImage(e.get("src"));
  638. if ( i == null ) i = Tile.fromColor(0xFF00FF, 8, 8);
  639. var py = yPos;
  640. switch(imageVerticalAlign) {
  641. case Bottom:
  642. py += metrics[sizePos].baseLine - i.height;
  643. case Middle:
  644. py += metrics[sizePos].baseLine - i.height/2;
  645. case Top:
  646. }
  647. if( py + i.dy < calcYMin )
  648. calcYMin = py + i.dy;
  649. if( rebuild ) {
  650. var b = new Bitmap(i, this);
  651. b.x = xPos;
  652. b.y = py;
  653. elements.push(b);
  654. }
  655. newLine = false;
  656. prevChar = -1;
  657. xPos += i.width + imageSpacing;
  658. case "a":
  659. if( e.exists("href") ) {
  660. finalizeInteractive();
  661. if( aHrefs == null )
  662. aHrefs = [];
  663. aHrefs.push(e.get("href"));
  664. createInteractive();
  665. }
  666. default:
  667. }
  668. for( child in e )
  669. addNode(child, font, align, rebuild, metrics);
  670. align = oldAlign;
  671. switch( nodeName ) {
  672. case "p":
  673. if ( newLine ) {
  674. nextLine(align, metrics[sizePos].width);
  675. } else if ( sizePos < metrics.length - 2 || metrics[sizePos + 1].width != 0 ) {
  676. // Condition avoid extra empty line if <p> was the last tag.
  677. makeLineBreak();
  678. newLine = true;
  679. prevChar = -1;
  680. }
  681. case "a":
  682. if( aHrefs.length > 0 ) {
  683. finalizeInteractive();
  684. aHrefs.pop();
  685. createInteractive();
  686. }
  687. default:
  688. }
  689. if( prevGlyphs != null )
  690. glyphs = prevGlyphs;
  691. if( prevColor != null )
  692. @:privateAccess glyphs.curColor.load(prevColor);
  693. } else if (e.nodeValue.length != 0) {
  694. newLine = false;
  695. var t = e.nodeValue;
  696. var dy = metrics[sizePos].baseLine - font.baseLine;
  697. for( i in 0...t.length ) {
  698. var cc = t.charCodeAt(i);
  699. if( cc == "\n".code ) {
  700. makeLineBreak();
  701. dy = metrics[sizePos].baseLine - font.baseLine;
  702. prevChar = -1;
  703. continue;
  704. }
  705. else {
  706. var fc = font.getChar(cc);
  707. if (fc != null) {
  708. xPos += fc.getKerningOffset(prevChar);
  709. if( rebuild ) glyphs.add(xPos, yPos + dy, fc.t);
  710. if( yPos == 0 && fc.t.dy+dy < calcYMin ) calcYMin = fc.t.dy + dy;
  711. xPos += fc.width + letterSpacing;
  712. }
  713. prevChar = cc;
  714. }
  715. }
  716. }
  717. }
  718. function set_imageSpacing(s) {
  719. if (imageSpacing == s) return s;
  720. imageSpacing = s;
  721. rebuild();
  722. return s;
  723. }
  724. override function set_textColor(c) {
  725. if( this.textColor == c ) return c;
  726. this.textColor = c;
  727. rebuild();
  728. return c;
  729. }
  730. function set_condenseWhite(value: Bool) {
  731. if ( this.condenseWhite != value ) {
  732. this.condenseWhite = value;
  733. rebuild();
  734. }
  735. return value;
  736. }
  737. function set_imageVerticalAlign(align) {
  738. if ( this.imageVerticalAlign != align ) {
  739. this.imageVerticalAlign = align;
  740. rebuild();
  741. }
  742. return align;
  743. }
  744. function set_lineHeightMode(v) {
  745. if ( this.lineHeightMode != v ) {
  746. this.lineHeightMode = v;
  747. rebuild();
  748. }
  749. return v;
  750. }
  751. override function getBoundsRec( relativeTo : Object, out : h2d.col.Bounds, forSize : Bool ) {
  752. if( forSize )
  753. for( i in elements )
  754. if( hxd.impl.Api.isOfType(i,h2d.Bitmap) )
  755. i.visible = false;
  756. super.getBoundsRec(relativeTo, out, forSize);
  757. if( forSize )
  758. for( i in elements )
  759. i.visible = true;
  760. }
  761. }
  762. private typedef LineInfo = {
  763. var width : Float;
  764. var height : Float;
  765. var baseLine : Float;
  766. }
  767. private typedef SplitNode = {
  768. var node : Xml;
  769. var prevChar : Int;
  770. var pos : Int;
  771. var width : Float;
  772. var height : Float;
  773. var baseLine : Float;
  774. var font : h2d.Font;
  775. }