From 8c32ef33020b17438f439712b50d3a02369aad50 Mon Sep 17 00:00:00 2001 From: RandomityGuy <31925790+RandomityGuy@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:05:18 +0530 Subject: [PATCH] finally the offset feature for HtmlText, making features of player count and readyness ez --- data/ui/mp/play/play_d.png | Bin 3363 -> 3841 bytes data/ui/mp/play/play_h.png | Bin 3346 -> 3784 bytes data/ui/mp/play/play_i.png | Bin 2661 -> 3545 bytes data/ui/mp/play/play_n.png | Bin 3328 -> 3845 bytes src/gui/GuiMLText.hx | 1 - src/gui/GuiMLTextListCtrl.hx | 2 +- src/gui/HtmlText.hx | 904 +++++++++++++++++++++++++++++++++++ src/gui/JoinServerGui.hx | 5 +- src/gui/MPPlayMissionGui.hx | 11 +- 9 files changed, 915 insertions(+), 8 deletions(-) create mode 100644 src/gui/HtmlText.hx diff --git a/data/ui/mp/play/play_d.png b/data/ui/mp/play/play_d.png index 2cbca65a01f8b5dec178eed1d5fa5ba3e8a18b9e..623b70c0b1e4874a8a5014c479d89937590ec272 100644 GIT binary patch literal 3841 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 DPaI5h literal 3363 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 diff --git a/data/ui/mp/play/play_h.png b/data/ui/mp/play/play_h.png index 651b4bff2b9a004bd684f7d1a5b02d7b2a6f7f47..6aa4d4aaf01c5b8294e5a87d9aeb1fd0fe9270dc 100644 GIT binary patch literal 3784 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 diff --git a/data/ui/mp/play/play_i.png b/data/ui/mp/play/play_i.png index cd6785a5952229623346cc4b47e0057f7a5271c6..92022e8cb666034d91ad77c99b7dfd0e88d9db5a 100644 GIT binary patch literal 3545 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 KMNUMnLSTZvc~H6l diff --git a/src/gui/GuiMLText.hx b/src/gui/GuiMLText.hx index 9ff8ddea..1def69c9 100644 --- a/src/gui/GuiMLText.hx +++ b/src/gui/GuiMLText.hx @@ -7,7 +7,6 @@ import h2d.Bitmap; import h3d.Engine; import h3d.Vector; import gui.GuiText.Justification; -import h2d.HtmlText; import h2d.Scene; import hxd.res.BitmapFont; import h2d.Text; diff --git a/src/gui/GuiMLTextListCtrl.hx b/src/gui/GuiMLTextListCtrl.hx index 6db54cce..99ec72e6 100644 --- a/src/gui/GuiMLTextListCtrl.hx +++ b/src/gui/GuiMLTextListCtrl.hx @@ -1,7 +1,6 @@ package gui; import h2d.filter.Filter; -import h2d.HtmlText; import h2d.Flow; import h3d.Engine; import h2d.Tile; @@ -84,6 +83,7 @@ class GuiMLTextListCtrl extends GuiControl { tobj.lineHeightMode = TextOnly; tobj.text = text; tobj.textColor = 0; + if (dropShadow != null) tobj.dropShadow = dropShadow; textObjs.push(tobj); diff --git a/src/gui/HtmlText.hx b/src/gui/HtmlText.hx new file mode 100644 index 00000000..4ad0763d --- /dev/null +++ b/src/gui/HtmlText.hx @@ -0,0 +1,904 @@ +package gui; + +import h2d.Bitmap; +import h2d.TileGroup; +import h2d.Font; +import h2d.Tile; +import h2d.RenderContext; +import h2d.Interactive; +import h2d.Object; +import h2d.Text; + +/** + The `HtmlText` line height calculation rules. +**/ +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 `` tags do not affect it (partial legacy behavior). + **/ + TextOnly; + + /** + Legacy line height mode. When used, line heights remain constant based on `Text.font` variable. + **/ + Constant; +} + +/** + `HtmlText` img tag vertical alignment rules. +**/ +enum ImageVerticalAlign { + /** + Align images along the top of the text line. + **/ + Top; + + /** + Align images to sit on the base line of the text. + **/ + Bottom; + + /** + Align images to the middle between the top of the text line its base line. + **/ + Middle; +} + +/** + A simple HTML text renderer. + + 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. +**/ +class HtmlText extends Text { + /** + A default method HtmlText uses to load images for `` 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 `` tags with `face` attribute. See `HtmlText.loadFont` for details. + **/ + public static dynamic function defaultLoadFont(name:String):h2d.Font { + return null; + } + + /** + A default method HtmlText uses to format assigned text. See `HtmlText.formatText` for details. + **/ + public static dynamic function defaultFormatText(text:String):String { + return text; + } + + /** + When enabled, condenses extra spaces (carriage-return, line-feed, tabulation and space character) to one space. + If not set, uncondensed whitespace is left as is, as well as line-breaks. + **/ + public var condenseWhite(default, set):Bool = true; + + /** + The spacing after `` tags in pixels. + **/ + public var imageSpacing(default, set):Float = 1; + + /** + Line height calculation mode controls how much space lines take up vertically. + Changing mode to `Constant` restores the legacy behavior of HtmlText. + **/ + public var lineHeightMode(default, set):LineHeightMode = Accurate; + + /** + Vertical alignment of the images in `` tag relative to the text. + **/ + public var imageVerticalAlign(default, set):ImageVerticalAlign = Bottom; + + var elements:Array = []; + var xPos:Float; + var yPos:Float; + var xMax:Float; + var xMin:Float; + var textXml:Xml; + var sizePos:Int; + var dropMatrix:h3d.shader.ColorMatrix; + var prevChar:Int; + var newLine:Bool; + var aHrefs:Array; + var aInteractive:Interactive; + + override function draw(ctx:RenderContext) { + if (dropShadow != null) { + var oldX = absX, oldY = absY; + absX += dropShadow.dx * matA + dropShadow.dy * matC; + absY += dropShadow.dx * matB + dropShadow.dy * matD; + if (dropMatrix == null) { + dropMatrix = new h3d.shader.ColorMatrix(); + addShader(dropMatrix); + } + dropMatrix.enabled = true; + var m = dropMatrix.matrix; + m.zero(); + m._41 = ((dropShadow.color >> 16) & 0xFF) / 255; + m._42 = ((dropShadow.color >> 8) & 0xFF) / 255; + m._43 = (dropShadow.color & 0xFF) / 255; + m._44 = dropShadow.alpha; + for (e in elements) { + if (e is TileGroup) + @:privateAccess (cast(e, TileGroup)).drawWith(ctx, this); + } + @:privateAccess glyphs.drawWith(ctx, this); + dropMatrix.enabled = false; + absX = oldX; + absY = oldY; + } else { + removeShader(dropMatrix); + dropMatrix = null; + } + @:privateAccess glyphs.drawWith(ctx, this); + } + + override function getShader(stype:Class):T { + if (shaders != null) + for (s in shaders) { + var c = Std.downcast(s, h3d.shader.ColorMatrix); + if (c != null && !c.enabled) + continue; + var s = hxd.impl.Api.downcast(s, stype); + if (s != null) + return s; + } + return null; + } + + /** + Method that should return an `h2d.Tile` instance for `` tags. By default calls `HtmlText.defaultLoadImage` method. + + HtmlText does not cache tile instances. + Due to internal structure, method should be deterministic and always return same Tile on consequent calls with same `url` input. + @param url A value contained in `src` attribute. + **/ + public dynamic function loadImage(url:String):Tile { + return defaultLoadImage(url); + } + + /** + Method that should return an `h2d.Font` instance for `` 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. + Due to internal structure, method should be deterministic and always return same Font instance on consequent calls with same `name` input. + @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 { + var f = defaultLoadFont(name); + if (f == null) + return this.font; + else + return f; + } + + /** + Called on a tag click + **/ + public dynamic function onHyperlink(url:String):Void {} + + /** + Called when text is assigned, allowing to process arbitrary text to a valid XHTML. + **/ + public dynamic function formatText(text:String):String { + return defaultFormatText(text); + } + + override function set_text(t:String) { + super.set_text(formatText(t)); + return t; + } + + function parseText(text:String) { + return try Xml.parse(text) catch (e:Dynamic) throw "Could not parse " + text + " (" + e + ")"; + } + + inline function makeLineInfo(width:Float, height:Float, baseLine:Float):LineInfo { + return {width: width, height: height, baseLine: baseLine}; + } + + override function validateText() { + textXml = parseText(text); + validateNodes(textXml); + } + + function validateNodes(xml:Xml) { + switch (xml.nodeType) { + case Element: + var nodeName = xml.nodeName.toLowerCase(); + switch (nodeName) { + case "img": + loadImage(xml.get("src")); + case "font": + if (xml.exists("face")) { + loadFont(xml.get("face")); + } + case "b", "bold": + loadFont("bold"); + case "i", "italic": + loadFont("italic"); + } + for (child in xml) + validateNodes(child); + case Document: + for (child in xml) + validateNodes(child); + default: + } + } + + override function initGlyphs(text:String, rebuild = true) { + if (rebuild) { + glyphs.clear(); + for (e in elements) + e.remove(); + elements = []; + } + glyphs.setDefaultColor(textColor); + + var doc:Xml; + if (textXml == null) { + doc = parseText(text); + } else { + doc = textXml; + } + + yPos = 0; + xMax = 0; + xMin = Math.POSITIVE_INFINITY; + sizePos = 0; + calcYMin = 0; + + var metrics:Array = [makeLineInfo(0, font.lineHeight, font.baseLine)]; + prevChar = -1; + newLine = true; + var splitNode:SplitNode = { + node: null, + pos: 0, + font: font, + prevChar: -1, + width: 0, + height: 0, + baseLine: 0 + }; + for (e in doc) + buildSizes(e, font, metrics, splitNode); + + var max = 0.; + for (info in metrics) { + if (info.width > max) + max = info.width; + } + calcWidth = max; + + prevChar = -1; + newLine = true; + nextLine(textAlign, metrics[0].width); + for (e in doc) + addNode(e, font, textAlign, rebuild, metrics); + + if (xPos > xMax) + xMax = xPos; + + textXml = null; + + var y = yPos; + calcXMin = xMin; + calcWidth = xMax - xMin; + calcHeight = y + metrics[sizePos].height; + calcSizeHeight = y + metrics[sizePos].baseLine; // (font.baseLine > 0 ? font.baseLine : font.lineHeight); + calcDone = true; + if (rebuild) + needsRebuild = false; + } + + function buildSizes(e:Xml, font:Font, metrics:Array, 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 [Image] texttext` - 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 (lineBreak && 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) { + 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(); + switch (nodeName) { + case "p": + if (!newLine) { + makeLineBreak(); + } + case "br": + makeLineBreak(); + case "img": + // TODO: Support width/height attributes + // Support max-width/max-height attributes (downscale) + // Support min-width/min-height attributes (upscale) + var i:Tile = loadImage(e.get("src")); + if (i == null) + i = Tile.fromColor(0xFF00FF, 8, 8); + + var size = metrics[metrics.length - 1].width + i.width + imageSpacing; + if (realMaxWidth >= 0 && size > realMaxWidth && metrics[metrics.length - 1].width > 0) { + if (splitNode.node != null) { + size = wordSplit() + i.width + imageSpacing; + var info = metrics[metrics.length - 1]; + // Bug: height/baseLine may be innacurate in case of sizeA sizeBsizeA 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) { + switch (imageVerticalAlign) { + case Top: + info.height += grow; + case Bottom: + info.baseLine += grow; + info.height += grow; + case Middle: + info.height += grow; + info.baseLine += Std.int(grow / 2); + } + } + grow = info.baseLine + i.dy; + if (info.height < grow) + info.height = grow; + } + } + newLine = false; + prevChar = -1; + case "font": + for (a in e.attributes()) { + var v = e.get(a); + switch (a.toLowerCase()) { + case "face": font = loadFont(v); + default: + } + } + case "b", "bold": + font = loadFont("bold"); + case "i", "italic": + font = loadFont("italic"); + default: + } + for (child in e) + buildSizes(child, font, metrics, splitNode); + switch (nodeName) { + case "p": + if (!newLine) { + makeLineBreak(); + } + default: + } + } else if (e.nodeValue.length != 0) { + newLine = false; + var text = htmlToText(e.nodeValue); + 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 g = font.getChar(cc); + var newline = cc == '\n'.code; + var esize = g.width + g.getKerningOffset(prevChar); + var nc = text.charCodeAt(i + 1); + if (font.charset.isBreakChar(cc) && (nc == null || !font.charset.isComplementChar(nc))) { + // 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(); + } + + var size = x + esize + letterSpacing; + var k = i + 1, max = text.length; + var prevChar = cc; + while (size <= maxWidth && k < max) { + var cc = text.charCodeAt(k++); + if (lineBreak && (font.charset.isSpace(cc) || cc == '\n'.code)) + break; + var e = font.getChar(cc); + size += e.width + letterSpacing + e.getKerningOffset(prevChar); + prevChar = cc; + var nc = text.charCodeAt(k); + if (font.charset.isBreakChar(cc) && (nc == null || !font.charset.isComplementChar(nc))) + break; + } + // Avoid empty line when last char causes line-break while being CJK + if (lineBreak && 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; + } + } + + 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"); + } + } + + static var REG_SPACES = ~/[\r\n\t ]+/g; + + function htmlToText(t:String) { + if (condenseWhite) + t = REG_SPACES.replace(t, " "); + return t; + } + + 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; + } + } + + override function splitText(text:String):String { + if (realMaxWidth < 0) + return text; + yPos = 0; + xMax = 0; + sizePos = 0; + calcYMin = 0; + + var doc = parseText(text); + + /* + This might require a global refactoring at some point. + We would need a way to somehow build an AST from the XML representation + with all sizes and word breaks so analysis is much more easy. + */ + + var splitNode:SplitNode = { + node: null, + font: font, + width: 0, + height: 0, + baseLine: 0, + pos: 0, + prevChar: -1 + }; + var metrics = [makeLineInfo(0, font.lineHeight, font.baseLine)]; + prevChar = -1; + newLine = true; + + for (e in doc) + buildSizes(e, font, metrics, splitNode); + xMax = 0; + function addBreaks(e:Xml) { + if (e.nodeType == Xml.Element) { + for (x in e) + addBreaks(x); + } else { + 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); + if (pre != "") + e.parent.insertChild(Xml.createPCData(pre), index++); + e.parent.insertChild(Xml.createElement("br"), index++); + startI = i + 1; + } + } + if (startI < text.length) { + e.nodeValue = text.substr(startI); + } else { + e.parent.removeChild(e); + } + } + } + for (d in doc) + addBreaks(d); + return doc.toString(); + } + + override function getTextProgress(text:String, progress:Float):String { + if (progress >= text.length) + return text; + var doc = parseText(text); + function progressRec(e:Xml) { + if (progress <= 0) { + e.parent.removeChild(e); + return; + } + if (e.nodeType == Xml.Element) { + for (x in [for (x in e) x]) + progressRec(x); + } else { + var text = htmlToText(e.nodeValue); + var len = text.length; + if (len > progress) { + text = text.substr(0, Std.int(progress)); + e.nodeValue = text; + } + progress -= len; + } + } + for (x in [for (x in doc) x]) + progressRec(x); + return doc.toString(); + } + + function addNode(e:Xml, font:Font, align:Align, rebuild:Bool, metrics:Array) { + inline function createInteractive() { + if (aHrefs == null || aHrefs.length == 0) + return; + aInteractive = new Interactive(0, metrics[sizePos].height, this); + var href = aHrefs[aHrefs.length - 1]; + aInteractive.onClick = function(event) { + onHyperlink(href); + } + aInteractive.x = xPos; + aInteractive.y = yPos; + elements.push(aInteractive); + } + + inline function finalizeInteractive() { + if (aInteractive != null) { + aInteractive.width = xPos - aInteractive.x; + aInteractive = null; + } + } + + inline function makeLineBreak() { + finalizeInteractive(); + if (xPos > xMax) + xMax = xPos; + yPos += metrics[sizePos].height + lineSpacing; + nextLine(align, metrics[++sizePos].width); + createInteractive(); + } + if (e.nodeType == Xml.Element) { + var prevColor = null, prevGlyphs = null; + var oldAlign = align; + var nodeName = e.nodeName.toLowerCase(); + inline function setFont(v:String) { + font = loadFont(v); + if (prevGlyphs == null) + prevGlyphs = glyphs; + var prev = glyphs; + glyphs = new TileGroup(font == null ? null : font.tile, this); + if (font != null) { + switch (font.type) { + case SignedDistanceField(channel, alphaCutoff, smoothing): + var shader = new h3d.shader.SignedDistanceField(); + shader.channel = channel; + shader.alphaCutoff = alphaCutoff; + shader.smoothing = smoothing; + shader.autoSmoothing = smoothing == -1; + glyphs.smooth = this.smooth; + glyphs.addShader(shader); + default: + } + } + @:privateAccess glyphs.curColor.load(prev.curColor); + elements.push(glyphs); + } + switch (nodeName) { + case "font": + for (a in e.attributes()) { + var v = e.get(a); + switch (a.toLowerCase()) { + case "color": + if (prevColor == null) + prevColor = @:privateAccess glyphs.curColor.clone(); + if (v.charCodeAt(0) == '#'.code && v.length == 4) + v = "#" + v.charAt(1) + v.charAt(1) + v.charAt(2) + v.charAt(2) + v.charAt(3) + v.charAt(3); + glyphs.setDefaultColor(Std.parseInt("0x" + v.substr(1))); + case "opacity": + if (prevColor == null) + prevColor = @:privateAccess glyphs.curColor.clone(); + @:privateAccess glyphs.curColor.a *= Std.parseFloat(v); + case "face": + setFont(v); + default: + } + } + case "p": + for (a in e.attributes()) { + switch (a.toLowerCase()) { + case "align": + var v = e.get(a); + if (v != null) switch (v.toLowerCase()) { + case "left": + align = Left; + case "center": + align = Center; + case "right": + align = Right; + case "multiline-center": + align = MultilineCenter; + case "multiline-right": + align = MultilineRight; + // ?justify + } + default: + } + } + if (!newLine) { + makeLineBreak(); + newLine = true; + prevChar = -1; + } else { + nextLine(align, metrics[sizePos].width); + } + case "offset": + for (a in e.attributes()) { + switch (a.toLowerCase()) { + case "value": + var v = e.get(a); + if (v != null) xPos = Std.parseFloat(v); + default: + } + } + // nextLine(align, metrics[sizePos].width); + + case "b", "bold": + setFont("bold"); + case "i", "italic": + setFont("italic"); + case "br": + makeLineBreak(); + newLine = true; + prevChar = -1; + case "img": + var i:Tile = loadImage(e.get("src")); + if (i == null) + i = Tile.fromColor(0xFF00FF, 8, 8); + var py = yPos; + switch (imageVerticalAlign) { + case Bottom: + py += metrics[sizePos].baseLine - i.height; + case Middle: + py += metrics[sizePos].baseLine - i.height / 2; + case Top: + } + if (py + i.dy < calcYMin) + calcYMin = py + i.dy; + if (rebuild) { + var b = new Bitmap(i, this); + b.x = xPos; + b.y = py; + elements.push(b); + } + newLine = false; + prevChar = -1; + xPos += i.width + imageSpacing; + case "a": + if (e.exists("href")) { + finalizeInteractive(); + if (aHrefs == null) + aHrefs = []; + aHrefs.push(e.get("href")); + createInteractive(); + } + default: + } + for (child in e) + addNode(child, font, align, rebuild, metrics); + align = oldAlign; + switch (nodeName) { + case "p": + if (newLine) { + nextLine(align, metrics[sizePos].width); + } else if (sizePos < metrics.length - 2 || metrics[sizePos + 1].width != 0) { + // Condition avoid extra empty line if

was the last tag. + makeLineBreak(); + newLine = true; + prevChar = -1; + } + case "a": + if (aHrefs.length > 0) { + finalizeInteractive(); + aHrefs.pop(); + createInteractive(); + } + default: + } + if (prevGlyphs != null) + glyphs = prevGlyphs; + if (prevColor != null) + @:privateAccess glyphs.curColor.load(prevColor); + } else if (e.nodeValue.length != 0) { + newLine = false; + var t = e.nodeValue; + var dy = metrics[sizePos].baseLine - font.baseLine; + for (i in 0...t.length) { + var cc = t.charCodeAt(i); + if (cc == "\n".code) { + makeLineBreak(); + dy = metrics[sizePos].baseLine - font.baseLine; + prevChar = -1; + continue; + } else { + var fc = font.getChar(cc); + if (fc != null) { + xPos += fc.getKerningOffset(prevChar); + if (rebuild) + glyphs.add(xPos, yPos + dy, fc.t); + if (yPos == 0 && fc.t.dy + dy < calcYMin) + calcYMin = fc.t.dy + dy; + xPos += fc.width + letterSpacing; + } + prevChar = cc; + } + } + } + } + + function set_imageSpacing(s) { + if (imageSpacing == s) + return s; + imageSpacing = s; + rebuild(); + return s; + } + + override function set_textColor(c) { + if (this.textColor == c) + return c; + this.textColor = c; + rebuild(); + return c; + } + + function set_condenseWhite(value:Bool) { + if (this.condenseWhite != value) { + this.condenseWhite = value; + rebuild(); + } + return value; + } + + function set_imageVerticalAlign(align) { + if (this.imageVerticalAlign != align) { + this.imageVerticalAlign = align; + rebuild(); + } + return align; + } + + 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) { + if (forSize) + for (i in elements) + if (hxd.impl.Api.isOfType(i, h2d.Bitmap)) + i.visible = false; + super.getBoundsRec(relativeTo, out, forSize); + if (forSize) + for (i in elements) + 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; +} diff --git a/src/gui/JoinServerGui.hx b/src/gui/JoinServerGui.hx index f7d4132e..f174f3e5 100644 --- a/src/gui/JoinServerGui.hx +++ b/src/gui/JoinServerGui.hx @@ -196,7 +196,8 @@ class JoinServerGui extends GuiImage { var platformToString = ["unknown", "pc", "mac", "web", "android"]; function updateServerListDisplay() { - serverDisplays = ourServerList.map(x -> '${x.name}'); + serverDisplays = ourServerList.map(x -> + '${x.name} ${x.players}/${x.maxPlayers}'); serverList.setTexts(serverDisplays); } @@ -324,7 +325,7 @@ class JoinServerGui extends GuiImage { alpha: 0.5, color: 0 }; - listTitle.text.text = " Server Name"; + listTitle.text.text = " Server Name Players"; window.addChild(listTitle); this.addChild(window); diff --git a/src/gui/MPPlayMissionGui.hx b/src/gui/MPPlayMissionGui.hx index ba99bcaa..09285f29 100644 --- a/src/gui/MPPlayMissionGui.hx +++ b/src/gui/MPPlayMissionGui.hx @@ -677,26 +677,29 @@ class MPPlayMissionGui extends GuiImage { if (Net.isHost) { playerListArr.push({ name: Settings.highscoreName, - platform: Net.getPlatform() + platform: Net.getPlatform(), + ready: Net.lobbyHostReady }); } if (Net.isClient) { playerListArr.push({ name: Settings.highscoreName, - platform: Net.getPlatform() + platform: Net.getPlatform(), + ready: Net.lobbyClientReady }); } if (Net.clientIdMap != null) { for (c => v in Net.clientIdMap) { playerListArr.push({ name: v.name, - platform: v.platform + platform: v.platform, + ready: v.lobbyReady }); } } var playerListCompiled = playerListArr.map(player -> - '${player.name}'); + '${player.name}${player.ready ? "Ready" : ""}'); playerListCtrl.setTexts(playerListCompiled); // if (!showingCustoms)