diff --git a/Sources/zui/Ext.hx b/Sources/zui/Ext.hx new file mode 100644 index 000000000..b9534502a --- /dev/null +++ b/Sources/zui/Ext.hx @@ -0,0 +1,517 @@ +package zui; + +import zui.Zui; +import kha.input.KeyCode; + +@:access(zui.Zui) +class Ext { + + public static function floatInput(ui: Zui, handle: Handle, label = "", align: Align = Left, precision = 1000.0): Float { + handle.text = Std.string(Math.round(handle.value * precision) / precision); + var text = ui.textInput(handle, label, align); + handle.value = Std.parseFloat(text); + return handle.value; + } + + static function initPath(handle: Handle, systemId: String) { + handle.text = systemId == "Windows" ? "C:\\Users" : "/"; + // %HOMEDRIVE% + %HomePath% + // ~ + } + + public static var dataPath = ""; + static var lastPath = ""; + public static function fileBrowser(ui: Zui, handle: Handle, foldersOnly = false): String { + var sep = "/"; + + #if kha_krom + + var cmd = "ls "; + var systemId = kha.System.systemId; + if (systemId == "Windows") { + cmd = "dir /b "; + if (foldersOnly) cmd += "/ad "; + sep = "\\"; + handle.text = StringTools.replace(handle.text, "\\\\", "\\"); + handle.text = StringTools.replace(handle.text, "\r", ""); + } + if (handle.text == "") initPath(handle, systemId); + + var save = Krom.getFilesLocation() + sep + dataPath + "dir.txt"; + if (handle.text != lastPath) Krom.sysCommand(cmd + '"' + handle.text + '"' + " > " + '"' + save + '"'); + lastPath = handle.text; + var str = haxe.io.Bytes.ofData(Krom.loadBlob(save)).toString(); + var files = str.split("\n"); + + #elseif kha_kore + + if (handle.text == "") initPath(handle, kha.System.systemId); + var files = sys.FileSystem.isDirectory(handle.text) ? sys.FileSystem.readDirectory(handle.text) : []; + + #elseif kha_webgl + + var files: Array = []; + + var userAgent = untyped navigator.userAgent.toLowerCase(); + if (userAgent.indexOf(" electron/") > -1) { + if (handle.text == "") { + var pp = untyped window.process.platform; + var systemId = pp == "win32" ? "Windows" : (pp == "darwin" ? "OSX" : "Linux"); + initPath(handle, systemId); + } + try { + files = untyped require("fs").readdirSync(handle.text); + } + catch (e: Dynamic) { + // Non-directory item selected + } + } + + #else + + var files: Array = []; + + #end + + // Up directory + var i1 = handle.text.indexOf("/"); + var i2 = handle.text.indexOf("\\"); + var nested = + (i1 > -1 && handle.text.length - 1 > i1) || + (i2 > -1 && handle.text.length - 1 > i2); + handle.changed = false; + if (nested && ui.button("..", Align.Left)) { + handle.changed = ui.changed = true; + handle.text = handle.text.substring(0, handle.text.lastIndexOf(sep)); + // Drive root + if (handle.text.length == 2 && handle.text.charAt(1) == ":") handle.text += sep; + } + + // Directory contents + for (f in files) { + if (f == "" || f.charAt(0) == ".") continue; // Skip hidden + if (ui.button(f, Align.Left)) { + handle.changed = ui.changed = true; + if (handle.text.charAt(handle.text.length - 1) != sep) handle.text += sep; + handle.text += f; + } + } + + return handle.text; + } + + public static function inlineRadio(ui: Zui, handle: Handle, texts: Array, align: Align = Left): Int { + if (!ui.isVisible(ui.ELEMENT_H())) { + ui.endElement(); + return handle.position; + } + var step = ui._w / texts.length; + var hovered = -1; + if (ui.getHover()) { + var ix = Std.int(ui.inputX - ui._x - ui._windowX); + for (i in 0...texts.length) { + if (ix < i * step + step) { + hovered = i; + break; + } + } + } + if (ui.getReleased()) { + handle.position = hovered; + handle.changed = ui.changed = true; + } + else handle.changed = false; + + for (i in 0...texts.length) { + if (handle.position == i) { + ui.g.color = ui.t.ACCENT_HOVER_COL; + if (!ui.enabled) ui.fadeColor(); + ui.g.fillRect(ui._x + step * i, ui._y + ui.buttonOffsetY, step, ui.BUTTON_H()); + } + else if (hovered == i) { + ui.g.color = ui.t.ACCENT_COL; + if (!ui.enabled) ui.fadeColor(); + ui.g.drawRect(ui._x + step * i, ui._y + ui.buttonOffsetY, step, ui.BUTTON_H()); + } + ui.g.color = ui.t.TEXT_COL; // Text + ui._x += step * i; + var _w = ui._w; + ui._w = Std.int(step); + ui.drawString(ui.g, texts[i], null, 0, align); + ui._x -= step * i; + ui._w = _w; + } + ui.endElement(); + return handle.position; + } + + static var wheelSelectedHandle: Handle = null; + static var gradientSelectedHandle: Handle = null; + public static function colorWheel(ui: Zui, handle: Handle, alpha = false, w: Null = null, h: Null = null, colorPreview = true, picker: Void->Void = null): kha.Color { + if (w == null) w = ui._w; + rgbToHsv(handle.color.R, handle.color.G, handle.color.B, ar); + var chue = ar[0]; + var csat = ar[1]; + var cval = ar[2]; + var calpha = handle.color.A; + // Wheel + var px = ui._x; + var py = ui._y; + var scroll = ui.currentWindow != null ? ui.currentWindow.scrollEnabled : false; + if (!scroll) { + w -= ui.SCROLL_W(); + px += ui.SCROLL_W() / 2; + } + var _x = ui._x; + var _y = ui._y; + var _w = ui._w; + ui._w = Std.int(28 * ui.SCALE()); + if (picker != null && ui.button("P")) { + picker(); + ui.changed = false; + handle.changed = false; + return handle.color; + } + ui._x = _x; + ui._y = _y; + ui._w = _w; + ui.image(ui.ops.color_wheel, kha.Color.fromFloats(cval, cval, cval)); + // Picker + var ph = ui._y - py; + var ox = px + w / 2; + var oy = py + ph / 2; + var cw = w * 0.7; + var cwh = cw / 2; + var cx = ox; + var cy = oy + csat * cwh; // Sat is distance from center + var gradTx = px + 0.897 * w ; + var gradTy = oy - cwh; + var gradW = 0.0777 * w; + var gradH = cw; + // Rotate around origin by hue + var theta = chue * (Math.PI * 2.0); + var cx2 = Math.cos(theta) * (cx - ox) - Math.sin(theta) * (cy - oy) + ox; + var cy2 = Math.sin(theta) * (cx - ox) + Math.cos(theta) * (cy - oy) + oy; + cx = cx2; + cy = cy2; + + ui._x = px - (scroll ? 0 : ui.SCROLL_W() / 2); + ui._y = py; + ui.image(ui.ops.black_white_gradient); + + ui.g.color = 0xff000000; + ui.g.fillRect(cx - 3 * ui.SCALE(), cy - 3 * ui.SCALE(), 6 * ui.SCALE(), 6 * ui.SCALE()); + ui.g.color = 0xffffffff; + ui.g.fillRect(cx - 2 * ui.SCALE(), cy - 2 * ui.SCALE(), 4 * ui.SCALE(), 4 * ui.SCALE()); + + ui.g.color = 0xff000000; + ui.g.fillRect(gradTx + gradW / 2 - 3 * ui.SCALE(), gradTy + (1 - cval) * gradH - 3 * ui.SCALE(), 6 * ui.SCALE(), 6 * ui.SCALE()); + ui.g.color = 0xffffffff; + ui.g.fillRect(gradTx + gradW / 2 - 2 * ui.SCALE(), gradTy + (1 - cval) * gradH - 2 * ui.SCALE(), 4 * ui.SCALE(), 4 * ui.SCALE()); + + if (alpha) { + var alphaHandle = handle.nest(1, {value: Math.round(calpha * 100) / 100}); + calpha = ui.slider(alphaHandle, "Alpha", 0.0, 1.0, true); + if (alphaHandle.changed) handle.changed = ui.changed = true; + } + // Mouse picking for color wheel + var gx = ox + ui._windowX; + var gy = oy + ui._windowY; + if (ui.inputStarted && ui.getInputInRect(gx - cwh, gy - cwh, cw, cw)) wheelSelectedHandle = handle; + if (ui.inputReleased && wheelSelectedHandle != null) { wheelSelectedHandle = null; handle.changed = ui.changed = true; } + if (ui.inputDown && wheelSelectedHandle == handle) { + csat = Math.min(dist(gx, gy, ui.inputX, ui.inputY), cwh) / cwh; + var angle = Math.atan2(ui.inputX - gx, ui.inputY - gy); + if (angle < 0) angle = Math.PI + (Math.PI - Math.abs(angle)); + angle = Math.PI * 2 - angle; + chue = angle / (Math.PI * 2); + handle.changed = ui.changed = true; + } + // Mouse picking for cval + if (ui.inputStarted && ui.getInputInRect(gradTx + ui._windowX, gradTy + ui._windowY, gradW, gradH)) gradientSelectedHandle = handle; + if (ui.inputReleased && gradientSelectedHandle != null) { gradientSelectedHandle = null; handle.changed = ui.changed = true; } + if (ui.inputDown && gradientSelectedHandle == handle) { + cval = Math.max(0.01, Math.min(1, 1 - (ui.inputY - gradTy - ui._windowY) / gradH)); + handle.changed = ui.changed = true; + } + // Save as rgb + hsvToRgb(chue, csat, cval, ar); + handle.color = kha.Color.fromFloats(ar[0], ar[1], ar[2], calpha); + + if (colorPreview) ui.text("", Right, handle.color); + + var pos = Ext.inlineRadio(ui, Id.handle(), ["RGB", "HSV", "Hex"]); + var h0 = handle.nest(0).nest(0); + var h1 = handle.nest(0).nest(1); + var h2 = handle.nest(0).nest(2); + if (pos == 0) { + h0.value = handle.color.R; + + handle.color.R = ui.slider(h0, "R", 0, 1, true); + h1.value = handle.color.G; + + handle.color.G = ui.slider(h1, "G", 0, 1, true); + h2.value = handle.color.B; + handle.color.B = ui.slider(h2, "B", 0, 1, true); + } + else if (pos == 1) { + rgbToHsv(handle.color.R, handle.color.G, handle.color.B, ar); + h0.value = ar[0]; + h1.value = ar[1]; + h2.value = ar[2]; + var chue = ui.slider(h0, "H", 0, 1, true); + var csat = ui.slider(h1, "S", 0, 1, true); + var cval = ui.slider(h2, "V", 0, 1, true); + hsvToRgb(chue, csat, cval, ar); + handle.color = kha.Color.fromFloats(ar[0], ar[1], ar[2]); + } + else if (pos == 2) { + #if js + handle.text = untyped (handle.color >>> 0).toString(16); + var hexCode = ui.textInput(handle, "#"); + + if (hexCode.length >= 1 && hexCode.charAt(0) == "#") // Allow # at the beginning + hexCode = hexCode.substr(1); + if (hexCode.length == 3) // 3 digit CSS style values like fa0 --> ffaa00 + hexCode = hexCode.charAt(0) + hexCode.charAt(0) + hexCode.charAt(1) + hexCode.charAt(1) + hexCode.charAt(2) + hexCode.charAt(2); + if (hexCode.length == 4) // 4 digit CSS style values + hexCode = hexCode.charAt(0) + hexCode.charAt(0) + hexCode.charAt(1) + hexCode.charAt(1) + hexCode.charAt(2) + hexCode.charAt(2) + hexCode.charAt(3) + hexCode.charAt(3); + if (hexCode.length == 6) // Make the alpha channel optional + hexCode = "ff" + hexCode; + + handle.color = untyped parseInt(hexCode, 16); + #end + } + if (h0.changed || h1.changed || h2.changed) handle.changed = ui.changed = true; + + if (ui.getInputInRect(ui._windowX + px, ui._windowY + py, w, h == null ? (ui._y - py) : h) && ui.inputReleased) // Do not close if user clicks + ui.changed = true; + + return handle.color; + } + + static function rightAlignNumber(number: Int, length: Int): String { + var s = number + ""; + while (s.length < length) + s = " " + s; + return s; + } + + public static var textAreaLineNumbers = false; + public static var textAreaScrollPastEnd = false; + public static var textAreaColoring: TTextColoring = null; + + public static function textArea(ui: Zui, handle: Handle, align = Align.Left, editable = true, label = "", wordWrap = false): String { + handle.text = StringTools.replace(handle.text, "\t", " "); + var selected = ui.textSelectedHandle == handle; // Text being edited + var lines = handle.text.split("\n"); + var showLabel = (lines.length == 1 && lines[0] == ""); + var keyPressed = selected && ui.isKeyPressed; + ui.highlightOnSelect = false; + ui.tabSwitchEnabled = false; + + if (wordWrap && handle.text != "") { + var cursorSet = false; + var cursorPos = ui.cursorX; + for (i in 0...handle.position) cursorPos += lines[i].length + 1; // + \n + var words = lines.join(" ").split(" "); + lines = []; + var line = ""; + for (w in words) { + var linew = ui.ops.font.width(ui.fontSize, line + " " + w); + var wordw = ui.ops.font.width(ui.fontSize, " " + w); + if (linew > ui._w - 10 && linew > wordw) { + lines.push(line); + line = ""; + } + line = line == "" ? w : line + " " + w; + + var linesLen = lines.length; + for (l in lines) linesLen += l.length; + if (selected && !cursorSet && cursorPos <= linesLen + line.length) { + cursorSet = true; + handle.position = lines.length; + ui.cursorX = ui.highlightAnchor = cursorPos - linesLen; + } + } + lines.push(line); + if (selected) { + ui.textSelected = handle.text = lines[handle.position]; + } + } + var cursorStartX = ui.cursorX; + + if (textAreaLineNumbers) { + var _y = ui._y; + var _TEXT_COL = ui.t.TEXT_COL; + ui.t.TEXT_COL = ui.t.ACCENT_COL; + var maxLength = Math.ceil(Math.log(lines.length + 0.5) / Math.log(10)); // Express log_10 with natural log + for (i in 0...lines.length) { + ui.text(rightAlignNumber(i + 1, maxLength)); + ui._y -= ui.ELEMENT_OFFSET(); + } + ui.t.TEXT_COL = _TEXT_COL; + ui._y = _y; + ui._x += (lines.length + "").length * 16 + 4; + } + + ui.g.color = ui.t.SEPARATOR_COL; // Background + ui.drawRect(ui.g, true, ui._x + ui.buttonOffsetY, ui._y + ui.buttonOffsetY, ui._w - ui.buttonOffsetY * 2, lines.length * ui.ELEMENT_H() - ui.buttonOffsetY * 2); + + var _textColoring = ui.textColoring; + ui.textColoring = textAreaColoring; + + for (i in 0...lines.length) { // Draw lines + if ((!selected && ui.getHover()) || (selected && i == handle.position)) { + handle.position = i; // Set active line + handle.text = lines[i]; + ui.submitTextHandle = null; + ui.textInput(handle, showLabel ? label : "", align, editable); + if (keyPressed && ui.key != KeyCode.Return && ui.key != KeyCode.Escape) { // Edit text + lines[i] = ui.textSelected; + } + } + else { + if (showLabel) { + var TEXT_COL = ui.t.TEXT_COL; + ui.t.TEXT_COL = ui.t.LABEL_COL; + ui.text(label, Right); + ui.t.TEXT_COL = TEXT_COL; + } + else { + ui.text(lines[i], align); + } + } + ui._y -= ui.ELEMENT_OFFSET(); + } + ui._y += ui.ELEMENT_OFFSET(); + ui.textColoring = _textColoring; + + if (textAreaScrollPastEnd) { + ui._y += ui._h - ui.windowHeaderH - ui.ELEMENT_H() - ui.ELEMENT_OFFSET(); + } + + if (keyPressed) { + // Move cursor vertically + if (ui.key == KeyCode.Down && handle.position < lines.length - 1) { + handle.position++; + scrollAlign(ui, handle); + } + if (ui.key == KeyCode.Up && handle.position > 0) { + handle.position--; + scrollAlign(ui, handle); + } + // New line + if (editable && ui.key == KeyCode.Return && !wordWrap) { + handle.position++; + lines.insert(handle.position, lines[handle.position - 1].substr(ui.cursorX)); + lines[handle.position - 1] = lines[handle.position - 1].substr(0, ui.cursorX); + ui.startTextEdit(handle); + ui.cursorX = ui.highlightAnchor = 0; + scrollAlign(ui, handle); + } + // Delete line + if (editable && ui.key == KeyCode.Backspace && cursorStartX == 0 && handle.position > 0) { + handle.position--; + ui.cursorX = ui.highlightAnchor = lines[handle.position].length; + lines[handle.position] += lines[handle.position + 1]; + lines.splice(handle.position + 1, 1); + scrollAlign(ui, handle); + } + ui.textSelected = lines[handle.position]; + } + + ui.highlightOnSelect = true; + ui.tabSwitchEnabled = true; + handle.text = lines.join("\n"); + return handle.text; + } + + static function scrollAlign(ui: Zui, handle: Handle) { + // Scroll down + if ((handle.position + 1) * ui.ELEMENT_H() + ui.currentWindow.scrollOffset > ui._h - ui.windowHeaderH) { + ui.currentWindow.scrollOffset -= ui.ELEMENT_H(); + } + // Scroll up + else if ((handle.position + 1) * ui.ELEMENT_H() + ui.currentWindow.scrollOffset < ui.windowHeaderH) { + ui.currentWindow.scrollOffset += ui.ELEMENT_H(); + } + } + + static var _ELEMENT_OFFSET = 0; + static var _BUTTON_COL = 0; + public static function beginMenu(ui: Zui) { + _ELEMENT_OFFSET = ui.t.ELEMENT_OFFSET; + _BUTTON_COL = ui.t.BUTTON_COL; + ui.t.ELEMENT_OFFSET = 0; + ui.t.BUTTON_COL = ui.t.SEPARATOR_COL; + ui.g.color = ui.t.SEPARATOR_COL; + ui.g.fillRect(0, 0, ui._windowW, MENUBAR_H(ui)); + } + + public static function endMenu(ui: Zui) { + ui.t.ELEMENT_OFFSET = _ELEMENT_OFFSET; + ui.t.BUTTON_COL = _BUTTON_COL; + } + + public static function menuButton(ui: Zui, text: String): Bool { + ui._w = Std.int(ui.ops.font.width(ui.fontSize, text) + 25 * ui.SCALE()); + return ui.button(text); + } + + public static inline function MENUBAR_H(ui: Zui): Float { + return ui.BUTTON_H() * 1.1 + 2 + ui.buttonOffsetY; + } + + static inline function dist(x1: Float, y1: Float, x2: Float, y2: Float): Float { + var vx = x1 - x2; + var vy = y1 - y2; + return Math.sqrt(vx * vx + vy * vy); + } + static inline function fract(f: Float): Float { + return f - Std.int(f); + } + static inline function mix(x: Float, y: Float, a: Float): Float { + return x * (1.0 - a) + y * a; + } + static inline function clamp(x: Float, minVal: Float, maxVal: Float): Float { + return Math.min(Math.max(x, minVal), maxVal); + } + static inline function step(edge: Float, x: Float): Float { + return x < edge ? 0.0 : 1.0; + } + + static inline var kx = 1.0; + static inline var ky = 2.0 / 3.0; + static inline var kz = 1.0 / 3.0; + static inline var kw = 3.0; + static var ar = [0.0, 0.0, 0.0]; + static function hsvToRgb(cR: Float, cG: Float, cB: Float, out: Array) { + var px = Math.abs(fract(cR + kx) * 6.0 - kw); + var py = Math.abs(fract(cR + ky) * 6.0 - kw); + var pz = Math.abs(fract(cR + kz) * 6.0 - kw); + out[0] = cB * mix(kx, clamp(px - kx, 0.0, 1.0), cG); + out[1] = cB * mix(kx, clamp(py - kx, 0.0, 1.0), cG); + out[2] = cB * mix(kx, clamp(pz - kx, 0.0, 1.0), cG); + } + + static inline var Kx = 0.0; + static inline var Ky = -1.0 / 3.0; + static inline var Kz = 2.0 / 3.0; + static inline var Kw = -1.0; + static inline var e = 1.0e-10; + static function rgbToHsv(cR: Float, cG: Float, cB: Float, out: Array) { + var px = mix(cB, cG, step(cB, cG)); + var py = mix(cG, cB, step(cB, cG)); + var pz = mix(Kw, Kx, step(cB, cG)); + var pw = mix(Kz, Ky, step(cB, cG)); + var qx = mix(px, cR, step(px, cR)); + var qy = mix(py, py, step(px, cR)); + var qz = mix(pw, pz, step(px, cR)); + var qw = mix(cR, px, step(px, cR)); + var d = qx - Math.min(qw, qy); + out[0] = Math.abs(qz + (qw - qy) / (6.0 * d + e)); + out[1] = d / (qx + e); + out[2] = qx; + } +} diff --git a/Sources/zui/GraphicsExtension.hx b/Sources/zui/GraphicsExtension.hx new file mode 100644 index 000000000..170f08f6b --- /dev/null +++ b/Sources/zui/GraphicsExtension.hx @@ -0,0 +1,363 @@ +/** + Copied from deprecated + https://github.com/Kode/Kha/blob/15bfbdc2b3c363fe70ac3d178d9d3439f717861a/Sources/kha/graphics2/GraphicsExtension.hx +**/ + +package zui; + +import kha.math.Vector2; +import kha.math.FastVector2; +import kha.graphics2.Graphics; +import kha.graphics2.VerTextAlignment; +import kha.graphics2.HorTextAlignment; + +/** + * Static extension functions for Graphics2. + * Usage: "using zui.GraphicsExtension;" + */ +class GraphicsExtension { + /** + * Draws a arc. + * @param ccw (optional) Specifies whether the drawing should be counterclockwise. + * @param segments (optional) The amount of lines that should be used to draw the arc. + */ + public static function drawArc(g2: Graphics, cx: Float, cy: Float, radius: Float, sAngle: Float, eAngle: Float, strength: Float = 1, ccw: Bool = false, + segments: Int = 0): Void { + #if kha_html5 + if (kha.SystemImpl.gl == null) { + var g: kha.js.CanvasGraphics = cast g2; + radius -= strength / 2; // reduce radius to fit the line thickness within image width/height + g.drawArc(cx, cy, radius, sAngle, eAngle, strength, ccw); + return; + } + #end + + sAngle = sAngle % (Math.PI * 2); + eAngle = eAngle % (Math.PI * 2); + + if (ccw) { + if (eAngle > sAngle) + eAngle -= Math.PI * 2; + } + else if (eAngle < sAngle) + eAngle += Math.PI * 2; + + radius += strength / 2; + if (segments <= 0) + segments = Math.floor(10 * Math.sqrt(radius)); + + var theta = (eAngle - sAngle) / segments; + var c = Math.cos(theta); + var s = Math.sin(theta); + + var x = Math.cos(sAngle) * radius; + var y = Math.sin(sAngle) * radius; + + for (n in 0...segments) { + var px = x + cx; + var py = y + cy; + + var t = x; + x = c * x - s * y; + y = c * y + s * t; + + drawInnerLine(g2, x + cx, y + cy, px, py, strength); + } + } + + /** + * Draws a filled arc. + * @param ccw (optional) Specifies whether the drawing should be counterclockwise. + * @param segments (optional) The amount of lines that should be used to draw the arc. + */ + public static function fillArc(g2: Graphics, cx: Float, cy: Float, radius: Float, sAngle: Float, eAngle: Float, ccw: Bool = false, + segments: Int = 0): Void { + #if kha_html5 + if (kha.SystemImpl.gl == null) { + var g: kha.js.CanvasGraphics = cast g2; + g.fillArc(cx, cy, radius, sAngle, eAngle, ccw); + return; + } + #end + + sAngle = sAngle % (Math.PI * 2); + eAngle = eAngle % (Math.PI * 2); + + if (ccw) { + if (eAngle > sAngle) + eAngle -= Math.PI * 2; + } + else if (eAngle < sAngle) + eAngle += Math.PI * 2; + + if (segments <= 0) + segments = Math.floor(10 * Math.sqrt(radius)); + + var theta = (eAngle - sAngle) / segments; + var c = Math.cos(theta); + var s = Math.sin(theta); + + var x = Math.cos(sAngle) * radius; + var y = Math.sin(sAngle) * radius; + var sx = x + cx; + var sy = y + cy; + + for (n in 0...segments) { + var px = x + cx; + var py = y + cy; + + var t = x; + x = c * x - s * y; + y = c * y + s * t; + + g2.fillTriangle(px, py, x + cx, y + cy, sx, sy); + } + } + + /** + * Draws a circle. + * @param segments (optional) The amount of lines that should be used to draw the circle. + */ + public static function drawCircle(g2: Graphics, cx: Float, cy: Float, radius: Float, strength: Float = 1, segments: Int = 0): Void { + #if kha_html5 + if (kha.SystemImpl.gl == null) { + var g: kha.js.CanvasGraphics = cast g2; + radius -= strength / 2; // reduce radius to fit the line thickness within image width/height + g.drawCircle(cx, cy, radius, strength); + return; + } + #end + radius += strength / 2; + + if (segments <= 0) + segments = Math.floor(10 * Math.sqrt(radius)); + + var theta = 2 * Math.PI / segments; + var c = Math.cos(theta); + var s = Math.sin(theta); + + var x = radius; + var y = 0.0; + + for (n in 0...segments) { + var px = x + cx; + var py = y + cy; + + var t = x; + x = c * x - s * y; + y = c * y + s * t; + drawInnerLine(g2, x + cx, y + cy, px, py, strength); + } + } + + static function drawInnerLine(g2: Graphics, x1: Float, y1: Float, x2: Float, y2: Float, strength: Float): Void { + var side = y2 > y1 ? 1 : 0; + if (y2 == y1) + side = x2 - x1 > 0 ? 1 : 0; + + var vec = new FastVector2(); + if (y2 == y1) + vec.setFrom(new FastVector2(0, -1)); + else + vec.setFrom(new FastVector2(1, -(x2 - x1) / (y2 - y1))); + vec.length = strength; + var p1 = new FastVector2(x1 + side * vec.x, y1 + side * vec.y); + var p2 = new FastVector2(x2 + side * vec.x, y2 + side * vec.y); + var p3 = p1.sub(vec); + var p4 = p2.sub(vec); + g2.fillTriangle(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + g2.fillTriangle(p3.x, p3.y, p2.x, p2.y, p4.x, p4.y); + } + + /** + * Draws a filled circle. + * @param segments (optional) The amount of lines that should be used to draw the circle. + */ + public static function fillCircle(g2: Graphics, cx: Float, cy: Float, radius: Float, segments: Int = 0): Void { + #if kha_html5 + if (kha.SystemImpl.gl == null) { + var g: kha.js.CanvasGraphics = cast g2; + g.fillCircle(cx, cy, radius); + return; + } + #end + + if (segments <= 0) { + segments = Math.floor(10 * Math.sqrt(radius)); + } + + var theta = 2 * Math.PI / segments; + var c = Math.cos(theta); + var s = Math.sin(theta); + + var x = radius; + var y = 0.0; + + for (n in 0...segments) { + var px = x + cx; + var py = y + cy; + + var t = x; + x = c * x - s * y; + y = c * y + s * t; + + g2.fillTriangle(px, py, x + cx, y + cy, cx, cy); + } + } + + /** + * Draws a convex polygon. + */ + public static function drawPolygon(g2: Graphics, x: Float, y: Float, vertices: Array, strength: Float = 1) { + var iterator = vertices.iterator(); + var v0 = iterator.next(); + var v1 = v0; + + while (iterator.hasNext()) { + var v2 = iterator.next(); + g2.drawLine(v1.x + x, v1.y + y, v2.x + x, v2.y + y, strength); + v1 = v2; + } + g2.drawLine(v1.x + x, v1.y + y, v0.x + x, v0.y + y, strength); + } + + /** + * Draws a filled convex polygon. + */ + public static function fillPolygon(g2: Graphics, x: Float, y: Float, vertices: Array) { + var iterator = vertices.iterator(); + + if (!iterator.hasNext()) + return; + var v0 = iterator.next(); + + if (!iterator.hasNext()) + return; + var v1 = iterator.next(); + + while (iterator.hasNext()) { + var v2 = iterator.next(); + g2.fillTriangle(v0.x + x, v0.y + y, v1.x + x, v1.y + y, v2.x + x, v2.y + y); + v1 = v2; + } + } + + /** + * Draws a cubic bezier using 4 pairs of points. If the x and y arrays have a length bigger then 4, the additional + * points will be ignored. With a length smaller of 4 a error will occur, there is no check for this. + * You can construct the curves visually in Inkscape with a path using default nodes. + * Provide x and y in the following order: startPoint, controlPoint1, controlPoint2, endPoint + * Reference: http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/ + */ + public static function drawCubicBezier(g2: Graphics, x: Array, y: Array, segments: Int = 20, strength: Float = 1.0): Void { + var t: Float; + + var q0 = calculateCubicBezierPoint(0, x, y); + var q1: Array; + + for (i in 1...(segments + 1)) { + t = i / segments; + q1 = calculateCubicBezierPoint(t, x, y); + g2.drawLine(q0[0], q0[1], q1[0], q1[1], strength); + q0 = q1; + } + } + + /** + * Draws multiple cubic beziers joined by the end point. The minimum size is 4 pairs of points (a single curve). + */ + public static function drawCubicBezierPath(g2: Graphics, x: Array, y: Array, segments: Int = 20, strength: Float = 1.0): Void { + var i = 0; + var t: Float; + var q0: Array = null; + var q1: Array = null; + + while (i < x.length - 3) { + if (i == 0) + q0 = calculateCubicBezierPoint(0, [x[i], x[i + 1], x[i + 2], x[i + 3]], [y[i], y[i + 1], y[i + 2], y[i + 3]]); + + for (j in 1...(segments + 1)) { + t = j / segments; + q1 = calculateCubicBezierPoint(t, [x[i], x[i + 1], x[i + 2], x[i + 3]], [y[i], y[i + 1], y[i + 2], y[i + 3]]); + g2.drawLine(q0[0], q0[1], q1[0], q1[1], strength); + q0 = q1; + } + + i += 3; + } + } + + static function calculateCubicBezierPoint(t: Float, x: Array, y: Array): Array { + var u: Float = 1 - t; + var tt: Float = t * t; + var uu: Float = u * u; + var uuu: Float = uu * u; + var ttt: Float = tt * t; + + // first term + var p: Array = [uuu * x[0], uuu * y[0]]; + + // second term + p[0] += 3 * uu * t * x[1]; + p[1] += 3 * uu * t * y[1]; + + // third term + p[0] += 3 * u * tt * x[2]; + p[1] += 3 * u * tt * y[2]; + + // fourth term + p[0] += ttt * x[3]; + p[1] += ttt * y[3]; + + return p; + } + + static public function drawAlignedString(g2: Graphics, text: String, x: Float, y: Float, horAlign: HorTextAlignment, verAlign: VerTextAlignment): Void { + var xoffset = 0.0; + if (horAlign == TextCenter || horAlign == TextRight) { + var width = g2.font.width(g2.fontSize, text); + if (horAlign == TextCenter) { + xoffset = -width * 0.5; + } + else { + xoffset = -width; + } + } + var yoffset = 0.0; + if (verAlign == TextMiddle || verAlign == TextBottom) { + var height = g2.font.height(g2.fontSize); + if (verAlign == TextMiddle) { + yoffset = -height * 0.5; + } + else { + yoffset = -height; + } + } + g2.drawString(text, x + xoffset, y + yoffset); + } + + static public function drawAlignedCharacters(g2: Graphics, text: Array, start: Int, length: Int, x: Float, y: Float, horAlign: HorTextAlignment, + verAlign: VerTextAlignment): Void { + var xoffset = 0.0; + if (horAlign == TextCenter || horAlign == TextRight) { + var width = g2.font.widthOfCharacters(g2.fontSize, text, start, length); + if (horAlign == TextCenter) { + xoffset = -width * 0.5; + } + else { + xoffset = -width; + } + } + var yoffset = 0.0; + if (verAlign == TextMiddle || verAlign == TextBottom) { + var height = g2.font.height(g2.fontSize); + if (verAlign == TextMiddle) { + yoffset = -height * 0.5; + } + else { + yoffset = -height; + } + } + g2.drawCharacters(text, start, length, x + xoffset, y + yoffset); + } +} diff --git a/Sources/zui/Id.hx b/Sources/zui/Id.hx new file mode 100644 index 000000000..126526014 --- /dev/null +++ b/Sources/zui/Id.hx @@ -0,0 +1,19 @@ +package zui; + +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.ExprTools; + +class Id { + + static var i = 0; + + macro public static function pos(): Expr { + return macro $v{i++}; + } + + macro public static function handle(ops: Expr = null): Expr { + var code = "zui.Zui.Handle.global.nest(zui.Id.pos()," + ExprTools.toString(ops) + ")"; + return Context.parse(code, Context.currentPos()); + } +} diff --git a/Sources/zui/Nodes.hx b/Sources/zui/Nodes.hx new file mode 100644 index 000000000..afa2ae88e --- /dev/null +++ b/Sources/zui/Nodes.hx @@ -0,0 +1,971 @@ +package zui; + +using zui.GraphicsExtension; + +@:access(zui.Zui) +class Nodes { + + public var nodesDrag = false; + public var nodesSelected: Array = []; + public var panX = 0.0; + public var panY = 0.0; + public var zoom = 1.0; + public var uiw = 0; + public var uih = 0; + public var _inputStarted = false; + public var colorPickerCallback: kha.Color->Void = null; + var scaleFactor = 1.0; + var ELEMENT_H = 25; + var dragged = false; + var moveOnTop: TNode = null; + var linkDrag: TNodeLink = null; + var isNewLink = false; + var snapFromId = -1; + var snapToId = -1; + var snapSocket = 0; + var snapX = 0.0; + var snapY = 0.0; + var handle = new Zui.Handle(); + static var elementsBaked = false; + static var socketImage: kha.Image = null; + static var socketReleased = false; + static var clipboard = ""; + static var boxSelect = false; + static var boxSelectX = 0; + static var boxSelectY = 0; + static inline var maxButtons = 9; + + public static var excludeRemove: Array = []; // No removal for listed node types + public static var onLinkDrag: TNodeLink->Bool->Void = null; + public static var onHeaderReleased: TNode->Void = null; + public static var onSocketReleased: TNodeSocket->Void = null; + public static var onCanvasReleased: Void->Void = null; + public static var onNodeRemove: TNode->Void = null; + public static var onCanvasControl: Void->CanvasControl = null; // Pan, zoom + + #if zui_translate + public static dynamic function tr(id: String, vars: Map = null) { + return id; + } + #else + public static inline function tr(id: String, vars: Map = null) { + return id; + } + #end + + public function new() {} + + public inline function SCALE(): Float { + return scaleFactor * zoom; + } + public inline function PAN_X(): Float { + var zoomPan = (1.0 - zoom) * uiw / 2.5; + return panX * SCALE() + zoomPan; + } + public inline function PAN_Y(): Float { + var zoomPan = (1.0 - zoom) * uih / 2.5; + return panY * SCALE() + zoomPan; + } + public inline function LINE_H(): Int { + return Std.int(ELEMENT_H * SCALE()); + } + function BUTTONS_H(node: TNode): Int { + var h = 0.0; + for (but in node.buttons) { + if (but.type == "RGBA") h += 102 * SCALE() + LINE_H() * 5; // Color wheel + controls + else if (but.type == "VECTOR") h += LINE_H() * 4; + else if (but.type == "CUSTOM") h += LINE_H() * but.height; + else h += LINE_H(); + } + return Std.int(h); + } + function OUTPUTS_H(sockets: Array, length: Null = null): Int { + var h = 0.0; + for (i in 0...(length == null ? sockets.length : length)) { + h += LINE_H(); + } + return Std.int(h); + } + function INPUTS_H(canvas: TNodeCanvas, sockets: Array, length: Null = null): Int { + var h = 0.0; + for (i in 0...(length == null ? sockets.length : length)) { + if (sockets[i].type == "VECTOR" && sockets[i].display == 1 && !inputLinked(canvas, sockets[i].node_id, i)) h += LINE_H() * 4; + else h += LINE_H(); + } + return Std.int(h); + } + inline function NODE_H(canvas: TNodeCanvas, node: TNode): Int { + return Std.int(LINE_H() * 1.2 + INPUTS_H(canvas, node.inputs) + OUTPUTS_H(node.outputs) + BUTTONS_H(node)); + } + inline function NODE_W(node: TNode): Int { + return Std.int((node.width != null ? node.width : 140) * SCALE()); + } + inline function NODE_X(node: TNode): Float { + return node.x * SCALE() + PAN_X(); + } + inline function NODE_Y(node: TNode): Float { + return node.y * SCALE() + PAN_Y(); + } + inline function INPUT_Y(canvas: TNodeCanvas, sockets: Array, pos: Int): Int { + return Std.int(LINE_H() * 1.62) + INPUTS_H(canvas, sockets, pos); + } + inline function OUTPUT_Y(sockets: Array, pos: Int): Int { + return Std.int(LINE_H() * 1.62) + OUTPUTS_H(sockets, pos); + } + public inline function p(f: Float): Float { + return f * SCALE(); + } + + public function getNode(nodes: Array, id: Int): TNode { + for (node in nodes) if (node.id == id) return node; + return null; + } + + var nodeId = -1; + public function getNodeId(nodes: Array): Int { + if (nodeId == -1) for (n in nodes) if (nodeId < n.id) nodeId = n.id; + return ++nodeId; + } + + public function getLinkId(links: Array): Int { + var id = 0; + for (l in links) if (l.id >= id) id = l.id + 1; + return id; + } + + public function getSocketId(nodes: Array): Int { + var id = 0; + for (n in nodes) { + for (s in n.inputs) if (s.id >= id) id = s.id + 1; + for (s in n.outputs) if (s.id >= id) id = s.id + 1; + } + return id; + } + + function inputLinked(canvas: TNodeCanvas, node_id: Int, i: Int): Bool { + for (l in canvas.links) if (l.to_id == node_id && l.to_socket == i) return true; + return false; + } + + function bakeElements(ui: Zui) { + ui.g.end(); + elementsBaked = true; + socketImage = kha.Image.createRenderTarget(24, 24); + var g = socketImage.g2; + g.begin(true, 0x00000000); + g.color = 0xff000000; + zui.GraphicsExtension.fillCircle(g, 12, 12, 12); + g.color = 0xffffffff; + zui.GraphicsExtension.fillCircle(g, 12, 12, 9); + g.end(); + ui.g.begin(false); + } + + public function nodeCanvas(ui: Zui, canvas: TNodeCanvas) { + if (!elementsBaked) bakeElements(ui); + + var wx = ui._windowX; + var wy = ui._windowY; + var _inputEnabled = ui.inputEnabled; + ui.inputEnabled = _inputEnabled && popupCommands == null; + var controls = onCanvasControl != null ? onCanvasControl() : { + panX: ui.inputDownR ? ui.inputDX : 0.0, + panY: ui.inputDownR ? ui.inputDY : 0.0, + zoom: -ui.inputWheelDelta / 10.0 + }; + socketReleased = false; + + // Pan canvas + if (ui.inputEnabled && (controls.panX != 0.0 || controls.panY != 0.0)) { + panX += controls.panX / SCALE(); + panY += controls.panY / SCALE(); + } + + // Zoom canvas + if (ui.inputEnabled && controls.zoom != 0.0) { + zoom += controls.zoom; + if (zoom < 0.1) zoom = 0.1; + else if (zoom > 1.0) zoom = 1.0; + zoom = Math.round(zoom * 10) / 10; + uiw = ui._w; + uih = ui._h; + } + scaleFactor = ui.SCALE(); + ELEMENT_H = ui.t.ELEMENT_H + 2; + ui.setScale(SCALE()); // Apply zoomed scale + ui.elementsBaked = true; + ui.g.font = ui.ops.font; + ui.g.fontSize = ui.fontSize; + + for (link in canvas.links) { + var from = getNode(canvas.nodes, link.from_id); + var to = getNode(canvas.nodes, link.to_id); + var fromX = from == null ? ui.inputX : wx + NODE_X(from) + NODE_W(from); + var fromY = from == null ? ui.inputY : wy + NODE_Y(from) + OUTPUT_Y(from.outputs, link.from_socket); + var toX = to == null ? ui.inputX : wx + NODE_X(to); + var toY = to == null ? ui.inputY : wy + NODE_Y(to) + INPUT_Y(canvas, to.inputs, link.to_socket) + OUTPUTS_H(to.outputs) + BUTTONS_H(to); + + // Cull + var left = toX > fromX ? fromX : toX; + var right = toX > fromX ? toX : fromX; + var top = toY > fromY ? fromY : toY; + var bottom = toY > fromY ? toY : fromY; + if (right < 0 || left > wx + ui._windowW || + bottom < 0 || top > wy + ui._windowH) { + continue; + } + + // Snap to nearest socket + if (linkDrag == link) { + if (snapFromId != -1) { + fromX = snapX; + fromY = snapY; + } + if (snapToId != -1) { + toX = snapX; + toY = snapY; + } + snapFromId = snapToId = -1; + + for (node in canvas.nodes) { + var inps = node.inputs; + var outs = node.outputs; + var nodeh = NODE_H(canvas, node); + var rx = wx + NODE_X(node) - LINE_H() / 2; + var ry = wy + NODE_Y(node) - LINE_H() / 2; + var rw = NODE_W(node) + LINE_H(); + var rh = nodeh + LINE_H(); + if (ui.getInputInRect(rx, ry, rw, rh)) { + if (from == null && node.id != to.id) { // Snap to output + for (i in 0...outs.length) { + var sx = wx + NODE_X(node) + NODE_W(node); + var sy = wy + NODE_Y(node) + OUTPUT_Y(outs, i); + var rx = sx - LINE_H() / 2; + var ry = sy - LINE_H() / 2; + if (ui.getInputInRect(rx, ry, LINE_H(), LINE_H())) { + snapX = sx; + snapY = sy; + snapFromId = node.id; + snapSocket = i; + break; + } + } + } + else if (to == null && node.id != from.id) { // Snap to input + for (i in 0...inps.length) { + var sx = wx + NODE_X(node); + var sy = wy + NODE_Y(node) + INPUT_Y(canvas, inps, i) + OUTPUTS_H(outs) + BUTTONS_H(node); + var rx = sx - LINE_H() / 2; + var ry = sy - LINE_H() / 2; + if (ui.getInputInRect(rx, ry, LINE_H(), LINE_H())) { + snapX = sx; + snapY = sy; + snapToId = node.id; + snapSocket = i; + break; + } + } + } + } + } + } + + var selected = false; + for (n in nodesSelected) { + if (link.from_id == n.id || link.to_id == n.id) { + selected = true; + break; + } + } + + drawLink(ui, fromX - wx, fromY - wy, toX - wx, toY - wy, selected); + } + + for (node in canvas.nodes) { + // Cull + if (NODE_X(node) > ui._windowW || NODE_X(node) + NODE_W(node) < 0 || + NODE_Y(node) > ui._windowH || NODE_Y(node) + NODE_H(canvas, node) < 0) { + if (!isSelected(node)) continue; + } + + var inps = node.inputs; + var outs = node.outputs; + + // Drag node + var nodeh = NODE_H(canvas, node); + if (ui.inputEnabled && ui.getInputInRect(wx + NODE_X(node) - LINE_H() / 2, wy + NODE_Y(node), NODE_W(node) + LINE_H(), LINE_H())) { + if (ui.inputStarted) { + if (ui.isShiftDown || ui.isCtrlDown) { + // Add to selection or deselect + isSelected(node) ? + nodesSelected.remove(node) : + nodesSelected.push(node); + } + else if (nodesSelected.length <= 1) { + // Selecting single node, otherwise wait for input release + nodesSelected = [node]; + } + moveOnTop = node; // Place selected node on top + nodesDrag = true; + dragged = false; + } + else if (ui.inputReleased && !ui.isShiftDown && !ui.isCtrlDown && !dragged) { + // No drag performed, select single node + nodesSelected = [node]; + if (onHeaderReleased != null) { + onHeaderReleased(node); + } + } + } + if (ui.inputStarted && ui.getInputInRect(wx + NODE_X(node) - LINE_H() / 2, wy + NODE_Y(node) - LINE_H() / 2, NODE_W(node) + LINE_H(), nodeh + LINE_H())) { + // Check sockets + if (linkDrag == null) { + for (i in 0...outs.length) { + var sx = wx + NODE_X(node) + NODE_W(node); + var sy = wy + NODE_Y(node) + OUTPUT_Y(outs, i); + if (ui.getInputInRect(sx - LINE_H() / 2, sy - LINE_H() / 2, LINE_H(), LINE_H())) { + // New link from output + var l: TNodeLink = { id: getLinkId(canvas.links), from_id: node.id, from_socket: i, to_id: -1, to_socket: -1 }; + canvas.links.push(l); + linkDrag = l; + isNewLink = true; + break; + } + } + } + if (linkDrag == null) { + for (i in 0...inps.length) { + var sx = wx + NODE_X(node); + var sy = wy + NODE_Y(node) + INPUT_Y(canvas, inps, i) + OUTPUTS_H(outs) + BUTTONS_H(node); + if (ui.getInputInRect(sx - LINE_H() / 2, sy - LINE_H() / 2, LINE_H(), LINE_H())) { + // Already has a link - disconnect + for (l in canvas.links) { + if (l.to_id == node.id && l.to_socket == i) { + l.to_id = l.to_socket = -1; + linkDrag = l; + isNewLink = false; + break; + } + } + if (linkDrag != null) break; + // New link from input + var l: TNodeLink = { + id: getLinkId(canvas.links), + from_id: -1, + from_socket: -1, + to_id: node.id, + to_socket: i + }; + canvas.links.push(l); + linkDrag = l; + isNewLink = true; + break; + } + } + } + } + else if (ui.inputReleased) { + if (snapToId != -1) { // Connect to input + // Force single link per input + for (l in canvas.links) { + if (l.to_id == snapToId && l.to_socket == snapSocket) { + canvas.links.remove(l); + break; + } + } + linkDrag.to_id = snapToId; + linkDrag.to_socket = snapSocket; + ui.changed = true; + } + else if (snapFromId != -1) { // Connect to output + linkDrag.from_id = snapFromId; + linkDrag.from_socket = snapSocket; + ui.changed = true; + } + else if (linkDrag != null) { // Remove dragged link + canvas.links.remove(linkDrag); + ui.changed = true; + if (onLinkDrag != null) { + onLinkDrag(linkDrag, isNewLink); + } + } + snapToId = snapFromId = -1; + linkDrag = null; + nodesDrag = false; + } + if (nodesDrag && isSelected(node) && !ui.inputDownR) { + if (ui.inputDX != 0 || ui.inputDY != 0) { + dragged = true; + node.x += Std.int(ui.inputDX / SCALE()); + node.y += Std.int(ui.inputDY / SCALE()); + } + } + + drawNode(ui, node, canvas); + } + + if (onCanvasReleased != null && ui.inputEnabled && (ui.inputReleased || ui.inputReleasedR) && !socketReleased) { + onCanvasReleased(); + } + + if (boxSelect) { + ui.g.color = 0x223333dd; + ui.g.fillRect(boxSelectX, boxSelectY, ui.inputX - boxSelectX - ui._windowX, ui.inputY - boxSelectY - ui._windowY); + ui.g.color = 0x773333dd; + ui.g.drawRect(boxSelectX, boxSelectY, ui.inputX - boxSelectX - ui._windowX, ui.inputY - boxSelectY - ui._windowY); + ui.g.color = 0xffffffff; + } + if (ui.inputEnabled && ui.inputStarted && !ui.isAltDown && linkDrag == null && !nodesDrag && !ui.changed) { + boxSelect = true; + boxSelectX = Std.int(ui.inputX - ui._windowX); + boxSelectY = Std.int(ui.inputY - ui._windowY); + } + else if (boxSelect && !ui.inputDown) { + boxSelect = false; + var nodes: Array = []; + var left = boxSelectX; + var top = boxSelectY; + var right = Std.int(ui.inputX - ui._windowX); + var bottom = Std.int(ui.inputY - ui._windowY); + if (left > right) { + var t = left; + left = right; + right = t; + } + if (top > bottom) { + var t = top; + top = bottom; + bottom = t; + } + for (n in canvas.nodes) { + if (NODE_X(n) + NODE_W(n) > left && NODE_X(n) < right && + NODE_Y(n) + NODE_H(canvas, n) > top && NODE_Y(n) < bottom) { + nodes.push(n); + } + } + (ui.isShiftDown || ui.isCtrlDown) ? for (n in nodes) nodesSelected.push(n) : nodesSelected = nodes; + } + + // Place selected node on top + if (moveOnTop != null) { + canvas.nodes.remove(moveOnTop); + canvas.nodes.push(moveOnTop); + moveOnTop = null; + } + + // Node copy & paste + var cutSelected = false; + if (Zui.isCopy) { + var copyNodes: Array = []; + for (n in nodesSelected) { + if (excludeRemove.indexOf(n.type) >= 0) continue; + copyNodes.push(n); + } + var copyLinks: Array = []; + for (l in canvas.links) { + var from = getNode(nodesSelected, l.from_id); + var to = getNode(nodesSelected, l.to_id); + if (from != null && excludeRemove.indexOf(from.type) == -1 && + to != null && excludeRemove.indexOf(to.type) == -1) { + copyLinks.push(l); + } + } + if (copyNodes.length > 0) { + var copyCanvas: TNodeCanvas = { + name: canvas.name, + nodes: copyNodes, + links: copyLinks + }; + clipboard = haxe.Json.stringify(copyCanvas); + } + cutSelected = Zui.isCut; + } + if (Zui.isPaste && !ui.isTyping) { + var pasteCanvas: TNodeCanvas = null; + // Clipboard can contain non-json data + try { + pasteCanvas = haxe.Json.parse(clipboard); + } + catch(_) {} + if (pasteCanvas != null) { + for (l in pasteCanvas.links) { + // Assign unique link id + l.id = getLinkId(canvas.links); + canvas.links.push(l); + } + var offsetX = Std.int((Std.int(ui.inputX / ui.SCALE()) * SCALE() - wx - PAN_X()) / SCALE()) - pasteCanvas.nodes[pasteCanvas.nodes.length - 1].x; + var offsetY = Std.int((Std.int(ui.inputY / ui.SCALE()) * SCALE() - wy - PAN_Y()) / SCALE()) - pasteCanvas.nodes[pasteCanvas.nodes.length - 1].y; + for (n in pasteCanvas.nodes) { + // Assign unique node id + var old_id = n.id; + n.id = getNodeId(canvas.nodes); + for (soc in n.inputs) { + soc.id = getSocketId(canvas.nodes); + soc.node_id = n.id; + } + for (soc in n.outputs) { + soc.id = getSocketId(canvas.nodes); + soc.node_id = n.id; + } + for (l in pasteCanvas.links) { + if (l.from_id == old_id) l.from_id = n.id; + else if (l.to_id == old_id) l.to_id = n.id; + } + n.x += offsetX; + n.y += offsetY; + canvas.nodes.push(n); + } + nodesDrag = true; + nodesSelected = pasteCanvas.nodes; + ui.changed = true; + } + } + + // Select all nodes + if (ui.isCtrlDown && ui.key == kha.input.KeyCode.A && !ui.isTyping) { + nodesSelected = []; + for (n in canvas.nodes) nodesSelected.push(n); + } + + // Node removal + if (ui.inputEnabled && (ui.isBackspaceDown || ui.isDeleteDown || cutSelected) && !ui.isTyping) { + var i = nodesSelected.length - 1; + while (i >= 0) { + var n = nodesSelected[i--]; + if (excludeRemove.indexOf(n.type) >= 0) continue; + removeNode(n, canvas); + ui.changed = true; + } + } + + ui.setScale(scaleFactor); // Restore non-zoomed scale + ui.elementsBaked = true; + ui.inputEnabled = _inputEnabled; + + if (popupCommands != null) { + ui._x = popupX; + ui._y = popupY; + ui._w = popupW; + + ui.fill(-6, -6, ui._w / ui.SCALE() + 12, popupH + 12, ui.t.ACCENT_SELECT_COL); + ui.fill(-5, -5, ui._w / ui.SCALE() + 10, popupH + 10, ui.t.SEPARATOR_COL); + popupCommands(ui); + + var hide = (ui.inputStarted || ui.inputStartedR) && (ui.inputX - wx < popupX - 6 || ui.inputX - wx > popupX + popupW + 6 || ui.inputY - wy < popupY - 6 || ui.inputY - wy > popupY + popupH * ui.SCALE() + 6); + if (hide || ui.isEscapeDown) { + popupCommands = null; + } + } + } + + // Retrieve combo items for buttons of type ENUM + public static var enumTexts: String->Array = null; + + inline function isSelected(node: TNode): Bool { + return nodesSelected.indexOf(node) >= 0; + } + + public function drawNode(ui: Zui, node: TNode, canvas: TNodeCanvas) { + var wx = ui._windowX; + var wy = ui._windowY; + var uiX = ui._x; + var uiY = ui._y; + var uiW = ui._w; + var w = NODE_W(node); + var g = ui.g; + var h = NODE_H(canvas, node); + var nx = NODE_X(node); + var ny = NODE_Y(node); + var text = tr(node.name); + var lineh = LINE_H(); + + // Disallow input if node is overlapped by another node + _inputStarted = ui.inputStarted; + if (ui.inputStarted) { + for (i in (canvas.nodes.indexOf(node) + 1)...canvas.nodes.length) { + var n = canvas.nodes[i]; + if (NODE_X(n) < ui.inputX - ui._windowX && NODE_X(n) + NODE_W(n) > ui.inputX - ui._windowX && + NODE_Y(n) < ui.inputY - ui._windowY && NODE_Y(n) + NODE_H(canvas, n) > ui.inputY - ui._windowY) { + ui.inputStarted = false; + break; + } + } + } + + // Outline + g.color = isSelected(node) ? ui.t.LABEL_COL : ui.t.CONTEXT_COL; + g.fillRect(nx - 1, ny - 1, w + 2, h + 2); + + // Body + g.color = ui.t.WINDOW_BG_COL; + g.fillRect(nx, ny, w, h); + + // Header line + g.color = node.color; + g.fillRect(nx, ny + lineh - p(3), w, p(3)); + + // Title + g.color = ui.t.LABEL_COL; + var textw = g.font.width(ui.fontSize, text); + g.drawString(text, nx + p(10), ny + p(6)); + ui._x = nx; // Use the whole line for hovering and not just the drawn string. + ui._y = ny; + ui._w = w; + if (ui.getHover(lineh) && node.tooltip != null) ui.tooltip(tr(node.tooltip)); + + ny += lineh * 0.5; + + // Outputs + for (out in node.outputs) { + ny += lineh; + g.color = out.color; + g.drawScaledImage(socketImage, nx + w - p(6), ny - p(3), p(12), p(12)); + } + ny -= lineh * node.outputs.length; + g.color = ui.t.LABEL_COL; + for (out in node.outputs) { + ny += lineh; + var strw = ui.ops.font.width(ui.fontSize, tr(out.name)); + g.drawString(tr(out.name), nx + w - strw - p(12), ny - p(3)); + ui._x = nx; + ui._y = ny; + ui._w = w; + if (ui.getHover(lineh) && out.tooltip != null) ui.tooltip(tr(out.tooltip)); + + if (onSocketReleased != null && ui.inputEnabled && (ui.inputReleased || ui.inputReleasedR)) { + if (ui.inputX > wx + nx && ui.inputX < wx + nx + w && ui.inputY > wy + ny && ui.inputY < wy + ny + lineh) { + onSocketReleased(out); + socketReleased = true; + } + } + } + + // Buttons + var nhandle = handle.nest(node.id); + ny -= lineh / 3; // Fix align + for (buti in 0...node.buttons.length) { + var but = node.buttons[buti]; + + if (but.type == "RGBA") { + ny += lineh; // 18 + 2 separator + ui._x = nx; + ui._y = ny; + ui._w = w; + var val: kha.arrays.Float32Array = node.outputs[but.output].default_value; + nhandle.color = kha.Color.fromFloats(val[0], val[1], val[2]); + Ext.colorWheel(ui, nhandle, false, null, null, true, function () { + colorPickerCallback = function (color: kha.Color) { + node.outputs[but.output].default_value[0] = color.R; + node.outputs[but.output].default_value[1] = color.G; + node.outputs[but.output].default_value[2] = color.B; + ui.changed = true; + }; + }); + val[0] = nhandle.color.R; + val[1] = nhandle.color.G; + val[2] = nhandle.color.B; + } + else if (but.type == "VECTOR") { + ny += lineh; + ui._x = nx; + ui._y = ny; + ui._w = w; + var min = but.min != null ? but.min : 0.0; + var max = but.max != null ? but.max : 1.0; + var textOff = ui.t.TEXT_OFFSET; + ui.t.TEXT_OFFSET = 6; + ui.text(tr(but.name)); + if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); + var val: kha.arrays.Float32Array = but.default_value; + val[0] = ui.slider(nhandle.nest(buti).nest(0, {value: val[0]}), "X", min, max, true, 100, true, Left); + if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); + val[1] = ui.slider(nhandle.nest(buti).nest(1, {value: val[1]}), "Y", min, max, true, 100, true, Left); + if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); + val[2] = ui.slider(nhandle.nest(buti).nest(2, {value: val[2]}), "Z", min, max, true, 100, true, Left); + if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); + ui.t.TEXT_OFFSET = textOff; + if (but.output != null) node.outputs[but.output].default_value = but.default_value; + ny += lineh * 3; + } + else if (but.type == "VALUE") { + ny += lineh; + ui._x = nx; + ui._y = ny; + ui._w = w; + var soc = node.outputs[but.output]; + var min = but.min != null ? but.min : 0.0; + var max = but.max != null ? but.max : 1.0; + var prec = but.precision != null ? but.precision : 100.0; + var textOff = ui.t.TEXT_OFFSET; + ui.t.TEXT_OFFSET = 6; + soc.default_value = ui.slider(nhandle.nest(buti, {value: soc.default_value}), "Value", min, max, true, prec, true, Left); + if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); + ui.t.TEXT_OFFSET = textOff; + } + else if (but.type == "STRING") { + ny += lineh; + ui._x = nx; + ui._y = ny; + ui._w = w; + var soc = but.output != null ? node.outputs[but.output] : null; + but.default_value = ui.textInput(nhandle.nest(buti, {text: soc != null ? soc.default_value : but.default_value != null ? but.default_value : ""}), tr(but.name)); + if (soc != null) soc.default_value = but.default_value; + if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); + } + else if (but.type == "ENUM") { + ny += lineh; + ui._x = nx; + ui._y = ny; + ui._w = w; + var texts = Std.isOfType(but.data, Array) ? [for (s in cast(but.data, Array)) tr(s)] : enumTexts(node.type); + var buthandle = nhandle.nest(buti); + buthandle.position = but.default_value; + but.default_value = ui.combo(buthandle, texts, tr(but.name)); + if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); + } + else if (but.type == "BOOL") { + ny += lineh; + ui._x = nx; + ui._y = ny; + ui._w = w; + but.default_value = ui.check(nhandle.nest(buti, {selected: but.default_value}), tr(but.name)); + if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); + } + else if (but.type == "CUSTOM") { // Calls external function for custom button drawing + ny += lineh; + ui._x = nx; + ui._y = ny; + ui._w = w; + var dot = but.name.lastIndexOf("."); // TNodeButton.name specifies external function path + var fn = Reflect.field(Type.resolveClass(but.name.substr(0, dot)), but.name.substr(dot + 1)); + fn(ui, this, node); + ny += lineh * (but.height - 1); // TNodeButton.height specifies vertical button size + } + } + ny += lineh / 3; // Fix align + + // Inputs + for (i in 0...node.inputs.length) { + var inp = node.inputs[i]; + ny += lineh; + g.color = inp.color; + g.drawScaledImage(socketImage, nx - p(6), ny - p(3), p(12), p(12)); + var isLinked = inputLinked(canvas, node.id, i); + if (!isLinked && inp.type == "VALUE") { + ui._x = nx + p(6); + ui._y = ny - p(9); + ui._w = Std.int(w - p(6)); + var soc = inp; + var min = soc.min != null ? soc.min : 0.0; + var max = soc.max != null ? soc.max : 1.0; + var prec = soc.precision != null ? soc.precision : 100.0; + var textOff = ui.t.TEXT_OFFSET; + ui.t.TEXT_OFFSET = 6; + soc.default_value = ui.slider(nhandle.nest(maxButtons).nest(i, {value: soc.default_value}), tr(inp.name), min, max, true, prec, true, Left); + if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); + ui.t.TEXT_OFFSET = textOff; + } + else if (!isLinked && inp.type == "STRING") { + ui._x = nx + p(6); + ui._y = ny - p(9); + ui._w = Std.int(w - p(6)); + var soc = inp; + var textOff = ui.t.TEXT_OFFSET; + ui.t.TEXT_OFFSET = 6; + soc.default_value = ui.textInput(nhandle.nest(maxButtons).nest(i, {text: soc.default_value}), tr(inp.name), Left); + if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); + ui.t.TEXT_OFFSET = textOff; + } + else if (!isLinked && inp.type == "RGBA") { + g.color = ui.t.LABEL_COL; + g.drawString(tr(inp.name), nx + p(12), ny - p(3)); + ui._x = nx; + ui._y = ny; + ui._w = w; + if (ui.getHover(lineh) && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); + var soc = inp; + g.color = 0xff000000; + g.fillRect(nx + w - p(38), ny - p(6), p(36), p(18)); + var val: kha.arrays.Float32Array = soc.default_value; + g.color = kha.Color.fromFloats(val[0], val[1], val[2]); + var rx = nx + w - p(37); + var ry = ny - p(5); + var rw = p(34); + var rh = p(16); + g.fillRect(rx, ry, rw, rh); + var ix = ui.inputX - wx; + var iy = ui.inputY - wy; + if (ui.inputStarted && ix > rx && iy > ry && ix < rx + rw && iy < ry + rh) { + _inputStarted = ui.inputStarted = false; + rgbaPopup(ui, nhandle, soc.default_value, Std.int(rx), Std.int(ry + ui.ELEMENT_H())); + } + } + else if (!isLinked && inp.type == "VECTOR" && inp.display == 1) { + g.color = ui.t.LABEL_COL; + g.drawString(tr(inp.name), nx + p(12), ny - p(3)); + ui._x = nx; + ui._y = ny; + ui._w = w; + if (ui.getHover(lineh) && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); + + ny += lineh / 2; + ui._y = ny; + var min = inp.min != null ? inp.min : 0.0; + var max = inp.max != null ? inp.max : 1.0; + var textOff = ui.t.TEXT_OFFSET; + ui.t.TEXT_OFFSET = 6; + var val: kha.arrays.Float32Array = inp.default_value; + val[0] = ui.slider(nhandle.nest(maxButtons).nest(i).nest(0, {value: val[0]}), "X", min, max, true, 100, true, Left); + if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); + val[1] = ui.slider(nhandle.nest(maxButtons).nest(i).nest(1, {value: val[1]}), "Y", min, max, true, 100, true, Left); + if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); + val[2] = ui.slider(nhandle.nest(maxButtons).nest(i).nest(2, {value: val[2]}), "Z", min, max, true, 100, true, Left); + if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); + ui.t.TEXT_OFFSET = textOff; + ny += lineh * 2.5; + } + else { + g.color = ui.t.LABEL_COL; + g.drawString(tr(inp.name), nx + p(12), ny - p(3)); + ui._x = nx; + ui._y = ny; + ui._w = w; + if (ui.getHover(lineh) && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); + } + if (onSocketReleased != null && ui.inputEnabled && (ui.inputReleased || ui.inputReleasedR)) { + if (ui.inputX > wx + nx && ui.inputX < wx + nx + w && ui.inputY > wy + ny && ui.inputY < wy + ny + lineh) { + onSocketReleased(inp); + socketReleased = true; + } + } + } + + ui._x = uiX; + ui._y = uiY; + ui._w = uiW; + ui.inputStarted = _inputStarted; + } + + public function rgbaPopup(ui: Zui, nhandle: zui.Zui.Handle, val: kha.arrays.Float32Array, x: Int, y: Int) { + popup(x, y, Std.int(140 * scaleFactor), Std.int(ui.t.ELEMENT_H * 10), function(ui: Zui) { + nhandle.color = kha.Color.fromFloats(val[0], val[1], val[2]); + Ext.colorWheel(ui, nhandle, false, null, true, function () { + colorPickerCallback = function (color: kha.Color) { + val[0] = color.R; + val[1] = color.G; + val[2] = color.B; + ui.changed = true; + }; + }); + val[0] = nhandle.color.R; val[1] = nhandle.color.G; val[2] = nhandle.color.B; + }); + } + + public function drawLink(ui: Zui, x1: Float, y1: Float, x2: Float, y2: Float, highlight: Bool = false) { + var g = ui.g; + var c1: kha.Color = ui.t.LABEL_COL; + var c2: kha.Color = ui.t.ACCENT_SELECT_COL; + g.color = highlight ? kha.Color.fromBytes(c1.Rb, c1.Gb, c1.Bb, 210) : kha.Color.fromBytes(c2.Rb, c2.Gb, c2.Bb, 210); + if (ui.t.LINK_STYLE == Line) { + g.drawLine(x1, y1, x2, y2, 1.0); + g.color = highlight ? kha.Color.fromBytes(c1.Rb, c1.Gb, c1.Bb, 150) : kha.Color.fromBytes(c2.Rb, c2.Gb, c2.Bb, 150); // AA + g.drawLine(x1 + 0.5, y1, x2 + 0.5, y2, 1.0); + g.drawLine(x1 - 0.5, y1, x2 - 0.5, y2, 1.0); + g.drawLine(x1, y1 + 0.5, x2, y2 + 0.5, 1.0); + g.drawLine(x1, y1 - 0.5, x2, y2 - 0.5, 1.0); + } + else if (ui.t.LINK_STYLE == CubicBezier) { + g.drawCubicBezier([x1, x1 + Math.abs(x1 - x2) / 2, x2 - Math.abs(x1 - x2) / 2, x2], [y1, y1, y2, y2], 30, highlight ? 2.0 : 1.0); + } + } + + public function removeNode(n: TNode, canvas: TNodeCanvas) { + if (n == null) return; + var i = 0; + while (i < canvas.links.length) { + var l = canvas.links[i]; + if (l.from_id == n.id || l.to_id == n.id) { + canvas.links.splice(i, 1); + } + else i++; + } + canvas.nodes.remove(n); + if (onNodeRemove != null) { + onNodeRemove(n); + } + } + + var popupX = 0; + var popupY = 0; + var popupW = 0; + var popupH = 0; + var popupCommands: Zui->Void = null; + function popup(x: Int, y: Int, w: Int, h: Int, commands: Zui->Void) { + popupX = x; + popupY = y; + popupW = w; + popupH = h; + popupCommands = commands; + } +} + +typedef CanvasControl = { + var panX: Float; + var panY: Float; + var zoom: Float; +} + +typedef TNodeCanvas = { + var name: String; + var nodes: Array; + var links: Array; +} + +typedef TNode = { + var id: Int; + var name: String; + var type: String; + var x: Float; + var y: Float; + var inputs: Array; + var outputs: Array; + var buttons: Array; + var color: Int; + @:optional var width: Null; + @:optional var tooltip: String; +} + +typedef TNodeSocket = { + var id: Int; + var node_id: Int; + var name: String; + var type: String; + var color: Int; + var default_value: Dynamic; + @:optional var min: Null; + @:optional var max: Null; + @:optional var precision: Null; + @:optional var display: Null; + @:optional var tooltip: String; +} + +typedef TNodeLink = { + var id: Int; + var from_id: Int; + var from_socket: Int; + var to_id: Int; + var to_socket: Int; +} + +typedef TNodeButton = { + var name: String; + var type: String; + @:optional var output: Null; + @:optional var default_value: Dynamic; + @:optional var data: Dynamic; + @:optional var min: Null; + @:optional var max: Null; + @:optional var precision: Null; + @:optional var height: Null; + @:optional var tooltip: String; +} diff --git a/Sources/zui/Themes.hx b/Sources/zui/Themes.hx new file mode 100644 index 000000000..84aaabafa --- /dev/null +++ b/Sources/zui/Themes.hx @@ -0,0 +1,79 @@ +package zui; + +class Themes { + + public static var dark: TTheme = { + NAME: "Default Dark", + WINDOW_BG_COL: 0xff292929, + WINDOW_TINT_COL: 0xffffffff, + ACCENT_COL: 0xff393939, + ACCENT_HOVER_COL: 0xff434343, + ACCENT_SELECT_COL: 0xff505050, + BUTTON_COL: 0xff383838, + BUTTON_TEXT_COL: 0xffe8e7e5, + BUTTON_HOVER_COL: 0xff494949, + BUTTON_PRESSED_COL: 0xff1b1b1b, + TEXT_COL: 0xffe8e7e5, + LABEL_COL: 0xffc8c8c8, + SEPARATOR_COL: 0xff202020, + HIGHLIGHT_COL: 0xff205d9c, + CONTEXT_COL: 0xff222222, + PANEL_BG_COL: 0xff3b3b3b, + FONT_SIZE: 13, + ELEMENT_W: 100, + ELEMENT_H: 24, + ELEMENT_OFFSET: 4, + ARROW_SIZE: 5, + BUTTON_H: 22, + CHECK_SIZE: 15, + CHECK_SELECT_SIZE: 8, + SCROLL_W: 9, + TEXT_OFFSET: 8, + TAB_W: 6, + FILL_WINDOW_BG: false, + FILL_BUTTON_BG: true, + FILL_ACCENT_BG: false, + LINK_STYLE: Line, + FULL_TABS: false + }; +} + +typedef TTheme = { + var NAME: String; + var WINDOW_BG_COL: Int; + var WINDOW_TINT_COL: Int; + var ACCENT_COL: Int; + var ACCENT_HOVER_COL: Int; + var ACCENT_SELECT_COL: Int; + var BUTTON_COL: Int; + var BUTTON_TEXT_COL: Int; + var BUTTON_HOVER_COL: Int; + var BUTTON_PRESSED_COL: Int; + var TEXT_COL: Int; + var LABEL_COL: Int; + var SEPARATOR_COL: Int; + var HIGHLIGHT_COL: Int; + var CONTEXT_COL: Int; + var PANEL_BG_COL: Int; + var FONT_SIZE: Int; + var ELEMENT_W: Int; + var ELEMENT_H: Int; + var ELEMENT_OFFSET: Int; + var ARROW_SIZE: Int; + var BUTTON_H: Int; + var CHECK_SIZE: Int; + var CHECK_SELECT_SIZE: Int; + var SCROLL_W: Int; + var TEXT_OFFSET: Int; + var TAB_W: Int; // Indentation + var FILL_WINDOW_BG: Bool; + var FILL_BUTTON_BG: Bool; + var FILL_ACCENT_BG: Bool; + var LINK_STYLE: LinkStyle; + var FULL_TABS: Bool; // Make tabs take full window width +} + +@:enum abstract LinkStyle(Int) from Int { + var Line = 0; + var CubicBezier = 1; +} diff --git a/Sources/zui/Zui.hx b/Sources/zui/Zui.hx new file mode 100644 index 000000000..20299931a --- /dev/null +++ b/Sources/zui/Zui.hx @@ -0,0 +1,2098 @@ +package zui; + +// Immediate Mode UI Library +// https://github.com/armory3d/zui + +import kha.input.Mouse; +import kha.input.Pen; +import kha.input.Surface; +import kha.input.Keyboard; +import kha.input.KeyCode; +import kha.graphics2.Graphics; + +@:structInit +typedef ZuiOptions = { + font: kha.Font, + ?theme: zui.Themes.TTheme, + ?khaWindowId: Int, + ?scaleFactor: Float, + ?autoNotifyInput: Bool, + ?color_wheel: kha.Image, + ?black_white_gradient: kha.Image, +} + +class Zui { + public var isScrolling = false; // Use to limit other activities + public var isTyping = false; + public var enabled = true; // Current element state + public var isStarted = false; + public var isPushed = false; + public var isHovered = false; + public var isReleased = false; + public var changed = false; // Global elements change check + public var imageInvertY = false; + public var scrollEnabled = true; + public var alwaysRedraw = false; // Hurts performance + public var highlightOnSelect = true; // Highlight text edit contents on selection + public var tabSwitchEnabled = true; // Allow switching focus to the next element by pressing tab + public var textColoring: TTextColoring = null; // Set coloring scheme for drawString() calls + public var windowBorderTop = 0; + public var windowBorderBottom = 0; + public var windowBorderLeft = 0; + public var windowBorderRight = 0; + var highlightFullRow = false; + public static var current: Zui = null; + public static var onBorderHover: Handle->Int->Void = null; // Mouse over window border, use for resizing + public static var onTextHover: Void->Void = null; // Mouse over text input, use to set I-cursor + public static var onDeselectText: Void->Void = null; // Text editing finished + public static var alwaysRedrawWindow = true; // Redraw cached window texture each frame or on changes only + public static var keyRepeat = true; // Emulate key repeat for non-character keys + public static var dynamicGlyphLoad = true; // Allow text input fields to push new glyphs into the font atlas + public static var touchScroll = false; // Pan with finger to scroll + public static var touchHold = false; // Touch and hold finger for right click + public static var touchTooltip = false; // Show extra tooltips above finger / on-screen keyboard + var touchHoldActivated = false; + var sliderTooltip = false; + var sliderTooltipX = 0.0; + var sliderTooltipY = 0.0; + var sliderTooltipW = 0.0; + static var keyRepeatTime = 0.0; + + public var inputRegistered = false; + public var inputEnabled = true; + public var inputX: Float; // Input position + public var inputY: Float; + public var inputStartedX: Float; + public var inputStartedY: Float; + public var inputDX: Float; // Delta + public var inputDY: Float; + public var inputWheelDelta = 0; + public var inputStarted: Bool; // Buttons + public var inputStartedR: Bool; + public var inputReleased: Bool; + public var inputReleasedR: Bool; + public var inputDown: Bool; + public var inputDownR: Bool; + public var penInUse: Bool; + public var isKeyPressed = false; // Keys + public var isKeyDown = false; + public var isShiftDown = false; + public var isCtrlDown = false; + public var isAltDown = false; + public var isADown = false; + public var isBackspaceDown = false; + public var isDeleteDown = false; + public var isEscapeDown = false; + public var isReturnDown = false; + public var isTabDown = false; + public var key: Null = null; + public var char: String; + static var textToPaste = ""; + static var textToCopy = ""; + static var isCut = false; + static var isCopy = false; + static var isPaste = false; + static var copyReceiver: Zui = null; + static var copyFrame = 0; + + var inputStartedTime = 0.0; + var cursorX = 0; // Text input + var highlightAnchor = 0; + var ratios: Array; // Splitting rows + var curRatio = -1; + var xBeforeSplit: Float; + var wBeforeSplit: Int; + + public var g: Graphics; // Drawing + public var t: zui.Themes.TTheme; + public var ops: ZuiOptions; + var globalG: Graphics; + var rtTextPipeline: kha.graphics4.PipelineState; // Rendering text into rendertargets + + var fontSize: Int; + var fontOffsetY: Float; // Precalculated offsets + var arrowOffsetX: Float; + var arrowOffsetY: Float; + var titleOffsetX: Float; + var buttonOffsetY: Float; + var checkOffsetX: Float; + var checkOffsetY: Float; + var checkSelectOffsetX: Float; + var checkSelectOffsetY: Float; + var radioOffsetX: Float; + var radioOffsetY: Float; + var radioSelectOffsetX: Float; + var radioSelectOffsetY: Float; + var scrollAlign = 0.0; + var imageScrollAlign = true; + + var _x: Float; // Cursor(stack) position + var _y: Float; + var _w: Int; + var _h: Int; + + var _windowX = 0.0; // Window state + var _windowY = 0.0; + var _windowW: Float; + var _windowH: Float; + var currentWindow: Handle; + var windowEnded = true; + var scrollHandle: Handle = null; // Window or slider being scrolled + var dragHandle: Handle = null; // Window being dragged + var windowHeaderW = 0.0; + var windowHeaderH = 0.0; + var restoreX = -1.0; + var restoreY = -1.0; + + var textSelectedHandle: Handle = null; + var textSelected: String; + var submitTextHandle: Handle = null; + var textToSubmit = ""; + var tabPressed = false; + var tabPressedHandle: Handle = null; + var comboSelectedHandle: Handle = null; + var comboSelectedWindow: Handle = null; + var comboSelectedAlign: Align; + var comboSelectedTexts: Array; + var comboSelectedLabel: String; + var comboSelectedX: Int; + var comboSelectedY: Int; + var comboSelectedW: Int; + var comboSearchBar = false; + var submitComboHandle: Handle = null; + var comboToSubmit = 0; + var comboInitialValue = 0; + var tooltipText = ""; + var tooltipImg: kha.Image = null; + var tooltipImgMaxWidth: Null = null; + var tooltipInvertY = false; + var tooltipX = 0.0; + var tooltipY = 0.0; + var tooltipShown = false; + var tooltipWait = false; + var tooltipTime = 0.0; + var tabNames: Array = null; // Number of tab calls since window begin + var tabColors: Array = null; + var tabHandle: Handle = null; + var tabScroll = 0.0; + var tabVertical = false; + var sticky = false; + var scissor = false; + + var elementsBaked = false; + var checkSelectImage: kha.Image = null; + + public function new(ops: ZuiOptions) { + if (ops.theme == null) ops.theme = Themes.dark; + t = ops.theme; + if (ops.khaWindowId == null) ops.khaWindowId = 0; + if (ops.scaleFactor == null) ops.scaleFactor = 1.0; + if (ops.autoNotifyInput == null) ops.autoNotifyInput = true; + this.ops = ops; + setScale(ops.scaleFactor); + if (ops.autoNotifyInput) registerInput(); + if (copyReceiver == null) { + copyReceiver = this; + kha.System.notifyOnCutCopyPaste(onCut, onCopy, onPaste); + kha.System.notifyOnFrames(function(frames: Array) { + // Set isCopy to false on next frame + if ((isCopy || isPaste) && ++copyFrame > 1) { + isCopy = isCut = isPaste = false; + } + // Clear unpasted text on next frame + else if (copyFrame > 1 && ++copyFrame > 2) { + copyFrame = 0; + textToPaste = ""; + } + }); + } + var rtTextVS = kha.graphics4.Graphics2.createTextVertexStructure(); + rtTextPipeline = kha.graphics4.Graphics2.createTextPipeline(rtTextVS); + rtTextPipeline.alphaBlendSource = BlendOne; + rtTextPipeline.compile(); + } + + public function setScale(factor: Float) { + ops.scaleFactor = factor; + fontSize = FONT_SIZE(); + var fontHeight = ops.font.height(fontSize); + fontOffsetY = (ELEMENT_H() - fontHeight) / 2; // Precalculate offsets + arrowOffsetY = (ELEMENT_H() - ARROW_SIZE()) / 2; + arrowOffsetX = arrowOffsetY; + titleOffsetX = (arrowOffsetX * 2 + ARROW_SIZE()) / SCALE(); + buttonOffsetY = (ELEMENT_H() - BUTTON_H()) / 2; + checkOffsetY = (ELEMENT_H() - CHECK_SIZE()) / 2; + checkOffsetX = checkOffsetY; + checkSelectOffsetY = (CHECK_SIZE() - CHECK_SELECT_SIZE()) / 2; + checkSelectOffsetX = checkSelectOffsetY; + radioOffsetY = (ELEMENT_H() - CHECK_SIZE()) / 2; + radioOffsetX = radioOffsetY; + radioSelectOffsetY = (CHECK_SIZE() - CHECK_SELECT_SIZE()) / 2; + radioSelectOffsetX = radioSelectOffsetY; + elementsBaked = false; + } + + function bakeElements() { + if (checkSelectImage != null) { + checkSelectImage.unload(); + } + checkSelectImage = kha.Image.createRenderTarget(Std.int(CHECK_SELECT_SIZE()), Std.int(CHECK_SELECT_SIZE()), null, NoDepthAndStencil, 1); + var g = checkSelectImage.g2; + g.begin(true, 0x00000000); + g.color = t.ACCENT_SELECT_COL; + g.drawLine(0, 0, checkSelectImage.width, checkSelectImage.height, 2 * SCALE()); + g.drawLine(checkSelectImage.width, 0, 0, checkSelectImage.height, 2 * SCALE()); + g.end(); + elementsBaked = true; + } + + public function remove() { // Clean up + if (ops.autoNotifyInput) unregisterInput(); + } + + public function registerInput() { + if (inputRegistered) return; + Mouse.get().notifyWindowed(ops.khaWindowId, onMouseDown, onMouseUp, onMouseMove, onMouseWheel); + if (Pen.get() != null) Pen.get().notify(onPenDown, onPenUp, onPenMove); + Keyboard.get().notify(onKeyDown, onKeyUp, onKeyPress); + #if (kha_android || kha_ios) + if (Surface.get() != null) Surface.get().notify(onTouchDown, onTouchUp, onTouchMove); + #end + // Reset mouse delta on foreground + kha.System.notifyOnApplicationState(function() { inputDX = inputDY = 0; }, null, null, null, null); + inputRegistered = true; + } + + public function unregisterInput() { + if (!inputRegistered) return; + Mouse.get().removeWindowed(ops.khaWindowId, onMouseDown, onMouseUp, onMouseMove, onMouseWheel); + if (Pen.get() != null) Pen.get().remove(onPenDown, onPenUp, onPenMove); + Keyboard.get().remove(onKeyDown, onKeyUp, onKeyPress); + #if (kha_android || kha_ios) + if (Surface.get() != null) Surface.get().remove(onTouchDown, onTouchUp, onTouchMove); + #end + endInput(); + isShiftDown = isCtrlDown = isAltDown = false; + inputX = inputY = 0; + inputRegistered = false; + } + + public function begin(g: Graphics) { // Begin UI drawing + if (!elementsBaked) bakeElements(); + changed = false; + globalG = g; + current = this; + _x = 0; // Reset cursor + _y = 0; + _w = 0; + _h = 0; + } + + public function end(last = true) { // End drawing + if (!windowEnded) endWindow(); + drawCombo(); // Handle active combo + drawTooltip(true); + tabPressedHandle = null; + if (last) endInput(); + } + + public function beginRegion(g: Graphics, x: Int, y: Int, w: Int) { + if (!elementsBaked) { + g.end(); + bakeElements(); + g.begin(false); + } + changed = false; + globalG = g; + this.g = g; + currentWindow = null; + tooltipText = ""; + tooltipImg = null; + _windowX = 0; + _windowY = 0; + _windowW = w; + _x = x; + _y = y; + _w = w; + } + + public function endRegion(last = true) { + drawTooltip(false); + tabPressedHandle = null; + if (last) endInput(); + } + + // Sticky region ignores window scrolling + public function beginSticky() { + sticky = true; + _y -= currentWindow.scrollOffset; + } + + public function endSticky() { + sticky = false; + scissor = true; + g.scissor(0, Std.int(_y), Std.int(_windowW), Std.int(_windowH - _y)); + windowHeaderH += _y - windowHeaderH; + _y += currentWindow.scrollOffset; + isHovered = false; + } + + function endInput() { + isKeyPressed = false; + inputStarted = false; + inputStartedR = false; + inputReleased = false; + inputReleasedR = false; + inputDX = 0; + inputDY = 0; + inputWheelDelta = 0; + penInUse = false; + if (keyRepeat && isKeyDown && kha.Scheduler.time() - keyRepeatTime > 0.05) { + if (key == KeyCode.Backspace || key == KeyCode.Delete || key == KeyCode.Left || key == KeyCode.Right || key == KeyCode.Up || key == KeyCode.Down) { + keyRepeatTime = kha.Scheduler.time(); + isKeyPressed = true; + } + } + if (touchHold && inputDown && inputX == inputStartedX && inputY == inputStartedY && inputStartedTime > 0 && kha.Scheduler.time() - inputStartedTime > 0.7) { + touchHoldActivated = true; + inputReleasedR = true; + inputStartedTime = 0; + } + } + + function inputChanged(): Bool { + return inputDX != 0 || inputDY != 0 || inputWheelDelta != 0 || inputStarted || inputStartedR || inputReleased || inputReleasedR || inputDown || inputDownR || isKeyPressed; + } + + public function windowDirty(handle: Handle, x: Int, y: Int, w: Int, h: Int): Bool { + var wx = x + handle.dragX; + var wy = y + handle.dragY; + var inputChanged = getInputInRect(wx, wy, w, h) && inputChanged(); + return alwaysRedraw || isScrolling || inputChanged; + } + + // Returns true if redraw is needed + public function window(handle: Handle, x: Int, y: Int, w: Int, h: Int, drag = false): Bool { + if (handle.texture == null || w != handle.texture.width || h != handle.texture.height) { + resize(handle, w, h); + } + + if (!windowEnded) endWindow(); // End previous window if necessary + windowEnded = false; + + g = handle.texture.g2; // Set g + currentWindow = handle; + _windowX = x + handle.dragX; + _windowY = y + handle.dragY; + _windowW = w; + _windowH = h; + windowHeaderW = 0; + windowHeaderH = 0; + + if (windowDirty(handle, x, y, w, h)) { + handle.redraws = 2; + } + + if (onBorderHover != null) { + if (getInputInRect(_windowX - 4, _windowY, 8, _windowH)) { + onBorderHover(handle, 0); + } + else if (getInputInRect(_windowX + _windowW - 4, _windowY, 8, _windowH)) { + onBorderHover(handle, 1); + } + else if (getInputInRect(_windowX, _windowY - 4, _windowW, 8)) { + onBorderHover(handle, 2); + } + else if (getInputInRect(_windowX, _windowY + _windowH - 4, _windowW, 8)) { + onBorderHover(handle, 3); + } + } + + if (handle.redraws <= 0) { + return false; + } + + _x = 0; + _y = handle.scrollOffset; + if (handle.layout == Horizontal) w = Std.int(ELEMENT_W()); + _w = !handle.scrollEnabled ? w : w - SCROLL_W(); // Exclude scrollbar if present + _h = h; + tooltipText = ""; + tooltipImg = null; + tabNames = null; + + if (t.FILL_WINDOW_BG) { + g.begin(true, t.WINDOW_BG_COL); + } + else { + g.begin(true, 0x00000000); + g.color = t.WINDOW_BG_COL; + g.fillRect(_x, _y - handle.scrollOffset, handle.lastMaxX, handle.lastMaxY); + } + + handle.dragEnabled = drag; + if (drag) { + if (inputStarted && getInputInRect(_windowX, _windowY, _windowW, HEADER_DRAG_H())) { + dragHandle = handle; + } + else if (inputReleased) { + dragHandle = null; + } + if (handle == dragHandle) { + handle.redraws = 2; + handle.dragX += Std.int(inputDX); + handle.dragY += Std.int(inputDY); + } + _y += HEADER_DRAG_H(); // Header offset + windowHeaderH += HEADER_DRAG_H(); + } + + return true; + } + + public function endWindow(bindGlobalG = true) { + var handle = currentWindow; + if (handle == null) return; + if (handle.redraws > 0 || isScrolling) { + if (scissor) { + scissor = false; + g.disableScissor(); + } + + if (tabNames != null) drawTabs(); + + if (handle.dragEnabled) { // Draggable header + g.color = t.SEPARATOR_COL; + g.fillRect(0, 0, _windowW, HEADER_DRAG_H()); + } + + var wh = _windowH - windowHeaderH; // Exclude header + var fullHeight = _y - handle.scrollOffset - windowHeaderH; + if (fullHeight < wh || handle.layout == Horizontal || !scrollEnabled) { // Disable scrollbar + handle.scrollEnabled = false; + handle.scrollOffset = 0; + } + else { // Draw window scrollbar if necessary + handle.scrollEnabled = true; + if (tabScroll < 0) { // Restore tab + handle.scrollOffset = tabScroll; + tabScroll = 0; + } + var wy = _windowY + windowHeaderH; + var amountToScroll = fullHeight - wh; + var amountScrolled = -handle.scrollOffset; + var ratio = amountScrolled / amountToScroll; + var barH = wh * Math.abs(wh / fullHeight); + barH = Math.max(barH, ELEMENT_H()); + + var totalScrollableArea = wh - barH; + var e = amountToScroll / totalScrollableArea; + var barY = totalScrollableArea * ratio + windowHeaderH; + var barFocus = getInputInRect(_windowX + _windowW - SCROLL_W(), barY + _windowY, SCROLL_W(), barH); + + if (inputStarted && barFocus) { // Start scrolling + scrollHandle = handle; + isScrolling = true; + } + + var scrollDelta: Float = inputWheelDelta; + if (touchScroll && inputDown && inputDY != 0 && inputX > _windowX + windowHeaderW&& inputY > _windowY + windowHeaderH) { + isScrolling = true; + scrollDelta = -inputDY / 20; + } + if (handle == scrollHandle) { // Scroll + scroll(inputDY * e, fullHeight); + } + else if (scrollDelta != 0 && comboSelectedHandle == null && + getInputInRect(_windowX, wy, _windowW, wh)) { // Wheel + scroll(scrollDelta * ELEMENT_H(), fullHeight); + } + + // Stay in bounds + if (handle.scrollOffset > 0) { + handle.scrollOffset = 0; + } + else if (fullHeight + handle.scrollOffset < wh) { + handle.scrollOffset = wh - fullHeight; + } + + g.color = t.ACCENT_COL; // Bar + var scrollbarFocus = getInputInRect(_windowX + _windowW - SCROLL_W(), wy, SCROLL_W(), wh); + var barW = (scrollbarFocus || handle == scrollHandle) ? SCROLL_W() : SCROLL_W() / 3; + g.fillRect(_windowW - barW - scrollAlign, barY, barW, barH); + } + + handle.lastMaxX = _x; + handle.lastMaxY = _y; + if (handle.layout == Vertical) handle.lastMaxX += _windowW; + else handle.lastMaxY += _windowH; + handle.redraws--; + + g.end(); + } + + windowEnded = true; + + // Draw window texture + if (alwaysRedrawWindow || handle.redraws > -4) { + if (bindGlobalG) globalG.begin(false); + globalG.color = t.WINDOW_TINT_COL; + globalG.drawImage(handle.texture, _windowX, _windowY); + if (bindGlobalG) globalG.end(); + if (handle.redraws <= 0) handle.redraws--; + } + } + + function scroll(delta: Float, fullHeight: Float) { + currentWindow.scrollOffset -= delta; + } + + public function tab(handle: Handle, text: String, vertical = false, color: Int = -1): Bool { + if (tabNames == null) { // First tab + tabNames = []; + tabColors = []; + tabHandle = handle; + tabVertical = vertical; + _w -= tabVertical ? Std.int(ELEMENT_OFFSET() + ELEMENT_W() - 1 * SCALE()) : 0; // Shrink window area by width of vertical tabs + vertical ? + windowHeaderW += ELEMENT_W() : + windowHeaderH += BUTTON_H() + buttonOffsetY + ELEMENT_OFFSET(); + restoreX = inputX; // Mouse in tab header, disable clicks for tab content + restoreY = inputY; + if (!vertical && getInputInRect(_windowX, _windowY, _windowW, windowHeaderH)) { + inputX = inputY = -1; + } + vertical ? { _x += windowHeaderW + 6; _w -= 6; } : _y += windowHeaderH + 3; + } + tabNames.push(text); + tabColors.push(color); + return handle.position == tabNames.length - 1; + } + + function drawTabs() { + inputX = restoreX; + inputY = restoreY; + if (currentWindow == null) return; + var tabX = 0.0; + var tabY = 0.0; + var tabHMin = Std.int(BUTTON_H() * 1.1); + var headerH = currentWindow.dragEnabled ? HEADER_DRAG_H() : 0; + var tabH = t.FULL_TABS && tabVertical ? Std.int((_windowH - headerH) / tabNames.length) : tabHMin; + var origy = _y; + _y = headerH; + tabHandle.changed = false; + + if (isCtrlDown && isTabDown) { // Next tab + tabHandle.position++; + if (tabHandle.position >= tabNames.length) tabHandle.position = 0; + tabHandle.changed = true; + isTabDown = false; + } + + if (tabHandle.position >= tabNames.length) tabHandle.position = tabNames.length - 1; + + g.color = t.SEPARATOR_COL; // Tab background + tabVertical ? + g.fillRect(0, _y, ELEMENT_W(), _windowH) : + g.fillRect(0, _y, _windowW, buttonOffsetY + tabH + 2); + + g.color = t.ACCENT_COL; // Underline tab buttons + tabVertical ? + g.fillRect(ELEMENT_W(), _y, 1, _windowH) : + g.fillRect(buttonOffsetY, _y + buttonOffsetY + tabH + 2, _windowW - buttonOffsetY * 2, 1); + + var basey = tabVertical ? _y : _y + 2; + + for (i in 0...tabNames.length) { + _x = tabX; + _y = basey + tabY; + _w = tabVertical ? Std.int(ELEMENT_W() - 1 * SCALE()) : + t.FULL_TABS ? Std.int(_windowW / tabNames.length) : + Std.int(ops.font.width(fontSize, tabNames[i]) + buttonOffsetY * 2 + 18 * SCALE()); + var released = getReleased(tabH); + var pushed = getPushed(tabH); + var hover = getHover(tabH); + if (released) { + var h = tabHandle.nest(tabHandle.position); // Restore tab scroll + h.scrollOffset = currentWindow.scrollOffset; + h = tabHandle.nest(i); + tabScroll = h.scrollOffset; + tabHandle.position = i; // Set new tab + currentWindow.redraws = 3; + tabHandle.changed = true; + } + var selected = tabHandle.position == i; + + g.color = (pushed || hover) ? t.BUTTON_HOVER_COL : + tabColors[i] != -1 ? tabColors[i] : + selected ? t.WINDOW_BG_COL : + t.SEPARATOR_COL; + tabVertical ? + tabY += tabH + 1 : + tabX += _w + 1; + drawRect(g, true, _x + buttonOffsetY, _y + buttonOffsetY, _w, tabH); + g.color = selected ? t.BUTTON_TEXT_COL : t.LABEL_COL; + drawString(g, tabNames[i], null, (tabH - tabHMin) / 2, t.FULL_TABS ? Align.Center : Align.Left); + + if (selected) { // Hide underline for active tab + if (tabVertical) { + // g.color = t.WINDOW_BG_COL; + // g.fillRect(_x + buttonOffsetY + _w - 1, _y + buttonOffsetY - 1, 2, tabH + buttonOffsetY); + g.color = t.HIGHLIGHT_COL; + g.fillRect(_x + buttonOffsetY, _y + buttonOffsetY - 1, 2, tabH + buttonOffsetY); + } + else { + g.color = t.WINDOW_BG_COL; + g.fillRect(_x + buttonOffsetY + 1, _y + buttonOffsetY + tabH, _w - 1, 1); + g.color = t.HIGHLIGHT_COL; + g.fillRect(_x + buttonOffsetY, _y + buttonOffsetY, _w, 2); + } + } + } + + _x = 0; // Restore positions + _y = origy; + _w = Std.int(!currentWindow.scrollEnabled ? _windowW : _windowW - SCROLL_W()); + } + + public function panel(handle: Handle, text: String, isTree = false, filled = true, pack = true): Bool { + if (!isVisible(ELEMENT_H())) { + endElement(); + return handle.selected; + } + if (getReleased()) { + handle.selected = !handle.selected; + handle.changed = changed = true; + } + if (filled) { + g.color = t.PANEL_BG_COL; + drawRect(g, true, _x, _y, _w, ELEMENT_H()); + } + + isTree ? drawTree(handle.selected) : drawArrow(handle.selected); + + g.color = t.LABEL_COL; // Title + drawString(g, text, titleOffsetX, 0); + + endElement(); + if (pack && !handle.selected) _y -= ELEMENT_OFFSET(); + + return handle.selected; + } + + public function image(image: kha.Image, tint = 0xffffffff, h: Null = null, sx = 0, sy = 0, sw = 0, sh = 0): State { + var iw = (sw > 0 ? sw : image.width) * SCALE(); + var ih = (sh > 0 ? sh : image.height) * SCALE(); + var w = Math.min(iw, _w); + var x = _x; + var scroll = currentWindow != null ? currentWindow.scrollEnabled : false; + var r = curRatio == -1 ? 1.0 : getRatio(ratios[curRatio], 1); + if (imageScrollAlign) { // Account for scrollbar size + w = Math.min(iw, _w - buttonOffsetY * 2); + x += buttonOffsetY; + if (!scroll) { + w -= SCROLL_W() * r; + x += SCROLL_W() * r / 2; + } + } + else if (scroll) w += SCROLL_W() * r; + + // Image size + var ratio = h == null ? + w / iw : + h / ih; + h == null ? + h = ih * ratio : + w = iw * ratio; + + if (!isVisible(h)) { + endElement(h); + return State.Idle; + } + var started = getStarted(h); + var down = getPushed(h); + var released = getReleased(h); + var hover = getHover(h); + if (curRatio == -1 && (started || down || released || hover)) { + if (inputX < _windowX + _x || inputX > _windowX + _x + w) { + started = down = released = hover = false; + } + } + g.color = tint; + if (!enabled) fadeColor(); + var h_float: Float = h; // TODO: hashlink fix + if (sw > 0) { // Source rect specified + imageInvertY ? + g.drawScaledSubImage(image, sx, sy, sw, sh, x, _y + h_float, w, -h_float) : + g.drawScaledSubImage(image, sx, sy, sw, sh, x, _y, w, h_float); + } + else { + imageInvertY ? + g.drawScaledImage(image, x, _y + h_float, w, -h_float) : + g.drawScaledImage(image, x, _y, w, h_float); + } + + endElement(h); + return started ? State.Started : released ? State.Released : down ? State.Down : hover ? State.Hovered : State.Idle; + } + + public function text(text: String, align = Align.Left, bg = 0x00000000): State { + if (text.indexOf("\n") >= 0) { + splitText(text, align, bg); + return State.Idle; + } + var h = Math.max(ELEMENT_H(), ops.font.height(fontSize)); + if (!isVisible(h)) { + endElement(h + ELEMENT_OFFSET()); + return State.Idle; + } + var started = getStarted(h); + var down = getPushed(h); + var released = getReleased(h); + var hover = getHover(h); + if (bg != 0x0000000) { + g.color = bg; + g.fillRect(_x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H()); + } + g.color = t.TEXT_COL; + drawString(g, text, null, 0, align); + + endElement(h + ELEMENT_OFFSET()); + return started ? State.Started : released ? State.Released : down ? State.Down : State.Idle; + } + + inline function splitText(lines: String, align = Align.Left, bg = 0x00000000) { + for (line in lines.split("\n")) text(line, align, bg); + } + + function startTextEdit(handle: Handle, align = Align.Left) { + isTyping = true; + submitTextHandle = textSelectedHandle; + textToSubmit = textSelected; + textSelectedHandle = handle; + textSelected = handle.text; + cursorX = handle.text.length; + if (tabPressed) { + tabPressed = false; + isKeyPressed = false; // Prevent text deselect after tab press + } + else if (!highlightOnSelect) { // Set cursor to click location + setCursorToInput(align); + } + tabPressedHandle = handle; + highlightAnchor = highlightOnSelect ? 0 : cursorX; + if (Keyboard.get() != null) Keyboard.get().show(); + } + + function submitTextEdit() { + submitTextHandle.changed = submitTextHandle.text != textToSubmit; + submitTextHandle.text = textToSubmit; + submitTextHandle = null; + textToSubmit = ""; + textSelected = ""; + } + + function updateTextEdit(align = Align.Left, editable = true, liveUpdate = false) { + var text = textSelected; + if (isKeyPressed) { // Process input + if (key == KeyCode.Left) { // Move cursor + if (cursorX > 0) cursorX--; + } + else if (key == KeyCode.Right) { + if (cursorX < text.length) cursorX++; + } + else if (editable && key == KeyCode.Backspace) { // Remove char + if (cursorX > 0 && highlightAnchor == cursorX) { + text = text.substr(0, cursorX - 1) + text.substr(cursorX, text.length); + cursorX--; + } + else if (highlightAnchor < cursorX) { + text = text.substr(0, highlightAnchor) + text.substr(cursorX, text.length); + cursorX = highlightAnchor; + } + else { + text = text.substr(0, cursorX) + text.substr(highlightAnchor, text.length); + } + } + else if (editable && key == KeyCode.Delete) { + if (highlightAnchor == cursorX) { + text = text.substr(0, cursorX) + text.substr(cursorX + 1); + } + else if (highlightAnchor < cursorX) { + text = text.substr(0, highlightAnchor) + text.substr(cursorX, text.length); + cursorX = highlightAnchor; + } + else { + text = text.substr(0, cursorX) + text.substr(highlightAnchor, text.length); + } + } + else if (key == KeyCode.Return) { // Deselect + deselectText(); + } + else if (key == KeyCode.Escape) { // Cancel + textSelected = textSelectedHandle.text; + deselectText(); + } + else if (key == KeyCode.Tab && tabSwitchEnabled && !isCtrlDown) { // Next field + tabPressed = true; + deselectText(); + key = null; + } + else if (key == KeyCode.Home) { + cursorX = 0; + } + else if (key == KeyCode.End) { + cursorX = text.length; + } + else if (isCtrlDown && isADown) { // Select all + cursorX = text.length; + highlightAnchor = 0; + } + else if (editable && // Write + key != KeyCode.Shift && + key != KeyCode.CapsLock && + key != KeyCode.Control && + key != KeyCode.Meta && + key != KeyCode.Alt && + key != KeyCode.Up && + key != KeyCode.Down && + char != null && + char != "" && + char.charCodeAt(0) >= 32) { + text = text.substr(0, highlightAnchor) + char + text.substr(cursorX); + cursorX = cursorX + 1 > text.length ? text.length : cursorX + 1; + } + var selecting = isShiftDown && (key == KeyCode.Left || key == KeyCode.Right || key == KeyCode.Shift); + // isCtrlDown && isAltDown is the condition for AltGr was pressed + // AltGr is part of the German keyboard layout and part of key combinations like AltGr + e -> € + if (!selecting && (!isCtrlDown || (isCtrlDown && isAltDown))) highlightAnchor = cursorX; + } + + if (editable && textToPaste != "") { // Process cut copy paste + text = text.substr(0, highlightAnchor) + textToPaste + text.substr(cursorX); + cursorX += textToPaste.length; + highlightAnchor = cursorX; + textToPaste = ""; + isPaste = false; + } + if (highlightAnchor == cursorX) textToCopy = text; // Copy + else if (highlightAnchor < cursorX) textToCopy = text.substring(highlightAnchor, cursorX); + else textToCopy = text.substring(cursorX, highlightAnchor); + if (editable && isCut) { // Cut + if (highlightAnchor == cursorX) text = ""; + else if (highlightAnchor < cursorX) { + text = text.substr(0, highlightAnchor) + text.substr(cursorX, text.length); + cursorX = highlightAnchor; + } + else { + text = text.substr(0, cursorX) + text.substr(highlightAnchor, text.length); + } + } + + var off = TEXT_OFFSET(); + var lineHeight = ELEMENT_H(); + var cursorHeight = lineHeight - buttonOffsetY * 3.0; + // Draw highlight + if (highlightAnchor != cursorX) { + var istart = cursorX; + var iend = highlightAnchor; + if (highlightAnchor < cursorX) { + istart = highlightAnchor; + iend = cursorX; + } + var hlstr = text.substr(istart, iend - istart); + var hlstrw = ops.font.width(fontSize, hlstr); + var startoff = ops.font.width(fontSize, text.substr(0, istart)); + var hlStart = align == Align.Left ? _x + startoff + off : _x + _w - hlstrw - off; + if (align == Align.Right) { + hlStart -= ops.font.width(fontSize, text.substr(iend, text.length)); + } + g.color = t.ACCENT_SELECT_COL; + g.fillRect(hlStart, _y + buttonOffsetY * 1.5, hlstrw, cursorHeight); + } + + // Draw cursor + var str = align == Align.Left ? text.substr(0, cursorX) : text.substring(cursorX, text.length); + var strw = ops.font.width(fontSize, str); + var cursorX = align == Align.Left ? _x + strw + off : _x + _w - strw - off; + g.color = t.TEXT_COL; // Cursor + g.fillRect(cursorX, _y + buttonOffsetY * 1.5, 2 * SCALE(), cursorHeight); + + textSelected = text; + if (liveUpdate && textSelectedHandle != null) { + textSelectedHandle.changed = textSelectedHandle.text != textSelected; + textSelectedHandle.text = textSelected; + } + } + + public function textInput(handle: Handle, label = "", align = Align.Left, editable = true, liveUpdate = false): String { + if (!isVisible(ELEMENT_H())) { + endElement(); + return handle.text; + } + + var hover = getHover(); + if (hover && onTextHover != null) onTextHover(); + g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL; // Text bg + drawRect(g, t.FILL_ACCENT_BG, _x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H()); + + var released = getReleased(); + if (submitTextHandle == handle && released) { // Keep editing selected text + isTyping = true; + textSelectedHandle = submitTextHandle; + submitTextHandle = null; + setCursorToInput(align); + } + var startEdit = released || tabPressed; + handle.changed = false; + + if (textSelectedHandle != handle && startEdit) startTextEdit(handle, align); + if (textSelectedHandle == handle) updateTextEdit(align, editable, liveUpdate); + if (submitTextHandle == handle) submitTextEdit(); + + if (label != "") { + g.color = t.LABEL_COL; // Label + var labelAlign = align == Align.Right ? Align.Left : Align.Right; + drawString(g, label, labelAlign == Align.Left ? null : 0, 0, labelAlign); + } + + g.color = t.TEXT_COL; // Text + textSelectedHandle != handle ? drawString(g, handle.text, null, 0, align) : drawString(g, textSelected, null, 0, align, false); + + endElement(); + return handle.text; + } + + function setCursorToInput(align: Align) { + var off = align == Align.Left ? TEXT_OFFSET() : _w - ops.font.width(fontSize, textSelected); + var x = inputX - (_windowX + _x + off); + cursorX = 0; + while (cursorX < textSelected.length && ops.font.width(fontSize, textSelected.substr(0, cursorX)) < x) { + cursorX++; + } + highlightAnchor = cursorX; + } + + function deselectText() { + if (textSelectedHandle == null) return; + submitTextHandle = textSelectedHandle; + textToSubmit = textSelected; + textSelectedHandle = null; + isTyping = false; + if (currentWindow != null) currentWindow.redraws = 2; + if (Keyboard.get() != null) Keyboard.get().hide(); + highlightAnchor = cursorX; + if (onDeselectText != null) onDeselectText(); + } + + public function button(text: String, align = Align.Center, label = ""): Bool { + if (!isVisible(ELEMENT_H())) { + endElement(); + return false; + } + var released = getReleased(); + var pushed = getPushed(); + var hover = getHover(); + if (released) changed = true; + + g.color = pushed ? t.BUTTON_PRESSED_COL : + hover ? t.BUTTON_HOVER_COL : + t.BUTTON_COL; + + drawRect(g, t.FILL_BUTTON_BG, _x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H()); + + g.color = t.BUTTON_TEXT_COL; + drawString(g, text, null, 0, align); + if (label != "") { + g.color = t.LABEL_COL; + drawString(g, label, null, 0, align == Align.Right ? Align.Left : Align.Right); + } + + endElement(); + + return released; + } + + public function check(handle: Handle, text: String, label: String = ""): Bool { + if (!isVisible(ELEMENT_H())) { + endElement(); + return handle.selected; + } + if (getReleased()) { + handle.selected = !handle.selected; + handle.changed = changed = true; + } + else handle.changed = false; + + var hover = getHover(); + drawCheck(handle.selected, hover); // Check + + g.color = t.TEXT_COL; // Text + drawString(g, text, titleOffsetX, 0, Align.Left); + + if (label != "") { + g.color = t.LABEL_COL; + drawString(g, label, null, 0, Align.Right); + } + + endElement(); + + return handle.selected; + } + + public function radio(handle: Handle, position: Int, text: String, label: String = ""): Bool { + if (!isVisible(ELEMENT_H())) { + endElement(); + return handle.position == position; + } + if (position == 0) { + handle.changed = false; + } + if (getReleased()) { + handle.position = position; + handle.changed = changed = true; + } + + var hover = getHover(); + drawRadio(handle.position == position, hover); // Radio + + g.color = t.TEXT_COL; // Text + drawString(g, text, titleOffsetX, 0); + + if (label != "") { + g.color = t.LABEL_COL; + drawString(g, label, null, 0, Align.Right); + } + + endElement(); + + return handle.position == position; + } + + public function combo(handle: Handle, texts: Array, label = "", showLabel = false, align = Align.Left, searchBar = true): Int { + if (!isVisible(ELEMENT_H())) { + endElement(); + return handle.position; + } + if (getReleased()) { + if (comboSelectedHandle == null) { + inputEnabled = false; + comboSelectedHandle = handle; + comboSelectedWindow = currentWindow; + comboSelectedAlign = align; + comboSelectedTexts = texts; + comboSelectedLabel = label; + comboSelectedX = Std.int(_x + _windowX); + comboSelectedY = Std.int(_y + _windowY + ELEMENT_H()); + comboSelectedW = Std.int(_w); + comboSearchBar = searchBar; + for (t in texts) { // Adapt combo list width to combo item width + var w = Std.int(ops.font.width(fontSize, t)) + 10; + if (comboSelectedW < w) comboSelectedW = w; + } + if (comboSelectedW > _w * 2) comboSelectedW = Std.int(_w * 2); + if (comboSelectedW > _w) comboSelectedW += Std.int(TEXT_OFFSET()); + comboToSubmit = handle.position; + comboInitialValue = handle.position; + } + } + if (handle == comboSelectedHandle && (isEscapeDown || inputReleasedR)) { + handle.position = comboInitialValue; + handle.changed = changed = true; + submitComboHandle = null; + } + else if (handle == submitComboHandle) { + handle.position = comboToSubmit; + submitComboHandle = null; + handle.changed = changed = true; + } + else handle.changed = false; + + var hover = getHover(); + if (hover) { // Bg + g.color = t.ACCENT_HOVER_COL; + drawRect(g, t.FILL_ACCENT_BG, _x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H()); + } + else { + g.color = t.ACCENT_COL; + drawRect(g, t.FILL_ACCENT_BG, _x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H()); + } + + var x = _x + _w - arrowOffsetX - 8; + var y = _y + arrowOffsetY + 3; + g.fillTriangle(x, y, x + ARROW_SIZE(), y, x + ARROW_SIZE() / 2, y + ARROW_SIZE() / 2); + + if (showLabel && label != "") { + if (align == Align.Left) _x -= 15; + g.color = t.LABEL_COL; + drawString(g, label, null, 0, align == Align.Left ? Align.Right : Align.Left); + if (align == Align.Left) _x += 15; + } + + if (align == Align.Right) _x -= 15; + g.color = t.TEXT_COL; // Value + if (handle.position < texts.length) { + drawString(g, texts[handle.position], null, 0, align); + } + if (align == Align.Right) _x += 15; + + endElement(); + return handle.position; + } + + public function slider(handle: Handle, text: String, from = 0.0, to = 1.0, filled = false, precision = 100.0, displayValue = true, align = Align.Right, textEdit = true): Float { + if (!isVisible(ELEMENT_H())) { + endElement(); + return handle.value; + } + if (getStarted()) { + scrollHandle = handle; + isScrolling = true; + changed = handle.changed = true; + if (touchTooltip) { + sliderTooltip = true; + sliderTooltipX = _x + _windowX; + sliderTooltipY = _y + _windowY; + sliderTooltipW = _w; + } + } + else handle.changed = false; + + #if (!kha_android && !kha_ios) + if (handle == scrollHandle && inputDX != 0) { // Scroll + #else + if (handle == scrollHandle) { // Scroll + #end + var range = to - from; + var sliderX = _x + _windowX + buttonOffsetY; + var sliderW = _w - buttonOffsetY * 2; + var step = range / sliderW; + var value = from + (inputX - sliderX) * step; + handle.value = Math.round(value * precision) / precision; + if (handle.value < from) handle.value = from; // Stay in bounds + else if (handle.value > to) handle.value = to; + handle.changed = changed = true; + } + + var hover = getHover(); + drawSlider(handle.value, from, to, filled, hover); // Slider + + // Text edit + var startEdit = (getReleased() || tabPressed) && textEdit; + if (startEdit) { // Mouse did not move + handle.text = handle.value + ""; + startTextEdit(handle); + handle.changed = changed = true; + } + var lalign = align == Align.Left ? Align.Right : Align.Left; + if (textSelectedHandle == handle) { + updateTextEdit(lalign); + } + if (submitTextHandle == handle) { + submitTextEdit(); + #if js + try { + handle.value = js.Lib.eval(handle.text); + } + catch(_) {} + #else + handle.value = Std.parseFloat(handle.text); + #end + handle.changed = changed = true; + } + + g.color = t.LABEL_COL; // Text + drawString(g, text, null, 0, align); + + if (displayValue) { + g.color = t.TEXT_COL; // Value + textSelectedHandle != handle ? + drawString(g, (Math.round(handle.value * precision) / precision) + "", null, 0, lalign) : + drawString(g, textSelected, null, 0, lalign); + } + + endElement(); + return handle.value; + } + + public function separator(h = 4, fill = true) { + if (!isVisible(ELEMENT_H())) { + _y += h * SCALE(); + return; + } + if (fill) { + g.color = t.SEPARATOR_COL; + g.fillRect(_x, _y, _w, h * SCALE()); + } + _y += h * SCALE(); + } + + public function tooltip(text: String) { + tooltipText = text; + tooltipY = _y + _windowY; + } + + public function tooltipImage(image: kha.Image, maxWidth: Null = null) { + tooltipImg = image; + tooltipImgMaxWidth = maxWidth; + tooltipInvertY = imageInvertY; + tooltipY = _y + _windowY; + } + + function drawArrow(selected: Bool) { + var x = _x + arrowOffsetX; + var y = _y + arrowOffsetY; + g.color = t.TEXT_COL; + if (selected) { + g.fillTriangle(x, y, + x + ARROW_SIZE(), y, + x + ARROW_SIZE() / 2, y + ARROW_SIZE()); + } + else { + g.fillTriangle(x, y, + x, y + ARROW_SIZE(), + x + ARROW_SIZE(), y + ARROW_SIZE() / 2); + } + } + + function drawTree(selected: Bool) { + var SIGN_W = 7 * SCALE(); + var x = _x + arrowOffsetX + 1; + var y = _y + arrowOffsetY + 1; + g.color = t.TEXT_COL; + if (selected) { + g.fillRect(x, y + SIGN_W / 2 - 1, SIGN_W, SIGN_W / 8); + } + else { + g.fillRect(x, y + SIGN_W / 2 - 1, SIGN_W, SIGN_W / 8); + g.fillRect(x + SIGN_W / 2 - 1, y, SIGN_W / 8, SIGN_W); + } + } + + function drawCheck(selected: Bool, hover: Bool) { + var x = _x + checkOffsetX; + var y = _y + checkOffsetY; + + g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL; + drawRect(g, t.FILL_ACCENT_BG, x, y, CHECK_SIZE(), CHECK_SIZE()); // Bg + + if (selected) { // Check + g.color = kha.Color.White; + if (!enabled) fadeColor(); + var size = Std.int(CHECK_SELECT_SIZE()); + g.drawScaledImage(checkSelectImage, x + checkSelectOffsetX, y + checkSelectOffsetY, size, size); + } + } + + function drawRadio(selected: Bool, hover: Bool) { + var x = _x + radioOffsetX; + var y = _y + radioOffsetY; + g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL; + drawRect(g, t.FILL_ACCENT_BG, x, y, CHECK_SIZE(), CHECK_SIZE()); // Bg + + if (selected) { // Check + g.color = t.ACCENT_SELECT_COL; + if (!enabled) fadeColor(); + g.fillRect(x + radioSelectOffsetX, y + radioSelectOffsetY, CHECK_SELECT_SIZE(), CHECK_SELECT_SIZE()); + } + } + + function drawSlider(value: Float, from: Float, to: Float, filled: Bool, hover: Bool) { + var x = _x + buttonOffsetY; + var y = _y + buttonOffsetY; + var w = _w - buttonOffsetY * 2; + + g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL; + drawRect(g, t.FILL_ACCENT_BG, x, y, w, BUTTON_H()); // Bg + + g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL; + var offset = (value - from) / (to - from); + var barW = 8 * SCALE(); // Unfilled bar + var sliderX = filled ? x : x + (w - barW) * offset; + sliderX = Math.max(Math.min(sliderX, x + (w - barW)), x); + var sliderW = filled ? w * offset : barW; + sliderW = Math.max(Math.min(sliderW, w), 0); + drawRect(g, true, sliderX, y, sliderW, BUTTON_H()); + } + + static var comboFirst = true; + function drawCombo() { + if (comboSelectedHandle == null) return; + var _g = g; + globalG.color = t.SEPARATOR_COL; + globalG.begin(false); + + var comboH = (comboSelectedTexts.length + (comboSelectedLabel != "" ? 1 : 0) + (comboSearchBar ? 1 : 0)) * Std.int(ELEMENT_H()); + var distTop = comboSelectedY - comboH - Std.int(ELEMENT_H()) - windowBorderTop; + var distBottom = kha.System.windowHeight() - windowBorderBottom - (comboSelectedY + comboH ); + var unrollUp = distBottom < 0 && distBottom < distTop; + beginRegion(globalG, comboSelectedX, comboSelectedY, comboSelectedW); + if (isKeyPressed || inputWheelDelta != 0) { + var arrowUp = isKeyPressed && key == (unrollUp ? KeyCode.Down : KeyCode.Up); + var arrowDown = isKeyPressed && key == (unrollUp ? KeyCode.Up : KeyCode.Down); + var wheelUp = (unrollUp && inputWheelDelta > 0) || (!unrollUp && inputWheelDelta < 0); + var wheelDown = (unrollUp && inputWheelDelta < 0) || (!unrollUp && inputWheelDelta > 0); + if ((arrowUp || wheelUp) && comboToSubmit > 0) { + var step = 1; + if (comboSearchBar && textSelected.length > 0) { + var search = textSelected.toLowerCase(); + while (comboSelectedTexts[comboToSubmit - step].toLowerCase().indexOf(search) < 0 && comboToSubmit - step > 0) + ++step; + + // Corner case: Current position is the top one according to the search pattern. + if (comboSelectedTexts[comboToSubmit - step].toLowerCase().indexOf(search) < 0) step = 0; + } + comboToSubmit -= step; + submitComboHandle = comboSelectedHandle; + } + else if ((arrowDown || wheelDown) && comboToSubmit < comboSelectedTexts.length - 1) { + var step = 1; + if (comboSearchBar && textSelected.length > 0) { + var search = textSelected.toLowerCase(); + while (comboSelectedTexts[comboToSubmit + step].toLowerCase().indexOf(search) < 0 && comboToSubmit + step < comboSelectedTexts.length - 1) + ++step; + + // Corner case: Current position is the lowest one according to the search pattern. + if (comboSelectedTexts[comboToSubmit + step].toLowerCase().indexOf(search) < 0) step = 0; + } + + comboToSubmit += step; + submitComboHandle = comboSelectedHandle; + } + if (comboSelectedWindow != null) comboSelectedWindow.redraws = 2; + } + + inputEnabled = true; + var _BUTTON_COL = t.BUTTON_COL; + var _ELEMENT_OFFSET = t.ELEMENT_OFFSET; + t.ELEMENT_OFFSET = 0; + var unrollRight = _x + comboSelectedW * 2 < kha.System.windowWidth() - windowBorderRight ? 1 : -1; + var resetPosition = false; + var search = ""; + if (comboSearchBar) { + if (unrollUp) _y -= ELEMENT_H() * 2; + var comboSearchHandle = Id.handle(); + if (comboFirst) comboSearchHandle.text = ""; + fill(0, 0, _w / SCALE(), ELEMENT_H() / SCALE(), t.SEPARATOR_COL); + search = textInput(comboSearchHandle, "", Align.Left, true, true).toLowerCase(); + if (isReleased) comboFirst = true; // Keep combo open + if (comboFirst) { + #if (!kha_android && !kha_ios) + startTextEdit(comboSearchHandle); // Focus search bar + #end + } + resetPosition = comboSearchHandle.changed; + } + + for (i in 0...comboSelectedTexts.length) { + if (search.length > 0 && comboSelectedTexts[i].toLowerCase().indexOf(search) < 0) + continue; // Don't show items that don't fit the current search pattern + + if (resetPosition) { // The search has changed, select first entry that matches + comboToSubmit = comboSelectedHandle.position = i; + submitComboHandle = comboSelectedHandle; + resetPosition = false; + } + if (unrollUp) _y -= ELEMENT_H() * 2; + t.BUTTON_COL = i == comboSelectedHandle.position ? t.ACCENT_SELECT_COL : t.SEPARATOR_COL; + fill(0, 0, _w / SCALE(), ELEMENT_H() / SCALE(), t.SEPARATOR_COL); + if (button(comboSelectedTexts[i], comboSelectedAlign)) { + comboToSubmit = i; + submitComboHandle = comboSelectedHandle; + if (comboSelectedWindow != null) comboSelectedWindow.redraws = 2; + break; + } + if (_y + ELEMENT_H() > kha.System.windowHeight() - windowBorderBottom || _y - ELEMENT_H() * 2 < windowBorderTop) { + _x += comboSelectedW * unrollRight; // Next column + _y = comboSelectedY; + } + } + t.BUTTON_COL = _BUTTON_COL; + t.ELEMENT_OFFSET = _ELEMENT_OFFSET; + + if (comboSelectedLabel != "") { // Unroll down + if (unrollUp) { + _y -= ELEMENT_H() * 2; + fill(0, 0, _w / SCALE(), ELEMENT_H() / SCALE(), t.SEPARATOR_COL); + g.color = t.LABEL_COL; + drawString(g, comboSelectedLabel, null, 0, Align.Right); + _y += ELEMENT_H(); + fill(0, 0, _w / SCALE(), 1 * SCALE(), t.ACCENT_SELECT_COL); // Separator + } + else { + fill(0, 0, _w / SCALE(), ELEMENT_H() / SCALE(), t.SEPARATOR_COL); + fill(0, 0, _w / SCALE(), 1 * SCALE(), t.ACCENT_SELECT_COL); // Separator + g.color = t.LABEL_COL; + drawString(g, comboSelectedLabel, null, 0, Align.Right); + } + } + + if ((inputReleased || inputReleasedR || isEscapeDown || isReturnDown) && !comboFirst) { + comboSelectedHandle = null; + comboFirst = true; + } + else comboFirst = false; + inputEnabled = comboSelectedHandle == null; + endRegion(false); + globalG.end(); + g = _g; // Restore + } + + function drawTooltip(bindGlobalG: Bool) { + if (sliderTooltip) { + if (bindGlobalG) globalG.begin(false); + globalG.font = ops.font; + globalG.fontSize = fontSize * 2; + var text = (Math.round(scrollHandle.value * 100) / 100) + ""; + var xoff = ops.font.width(globalG.fontSize, text) / 2; + var yoff = ops.font.height(globalG.fontSize); + var x = Math.min(Math.max(sliderTooltipX, inputX), sliderTooltipX + sliderTooltipW); + globalG.color = t.ACCENT_COL; + globalG.fillRect(x - xoff, sliderTooltipY - yoff, xoff * 2, yoff); + globalG.color = t.TEXT_COL; + globalG.drawString(text, x - xoff, sliderTooltipY - yoff); + if (bindGlobalG) globalG.end(); + } + if (touchTooltip && textSelectedHandle != null) { + if (bindGlobalG) globalG.begin(false); + globalG.font = ops.font; + globalG.fontSize = fontSize * 2; + var xoff = ops.font.width(globalG.fontSize, textSelected) / 2; + var yoff = ops.font.height(globalG.fontSize) / 2; + var x = kha.System.windowWidth() / 2; + var y = kha.System.windowHeight() / 3; + globalG.color = t.ACCENT_COL; + globalG.fillRect(x - xoff, y - yoff, xoff * 2, yoff * 2); + globalG.color = t.TEXT_COL; + globalG.drawString(textSelected, x - xoff, y - yoff); + if (bindGlobalG) globalG.end(); + } + + if (tooltipText != "" || tooltipImg != null) { + if (inputChanged()) { + tooltipShown = false; + tooltipWait = inputDX == 0 && inputDY == 0; // Wait for movement before showing up again + } + if (!tooltipShown) { + tooltipShown = true; + tooltipX = inputX; + tooltipTime = kha.Scheduler.time(); + } + if (!tooltipWait && kha.Scheduler.time() - tooltipTime > TOOLTIP_DELAY()) { + if (tooltipImg != null) drawTooltipImage(bindGlobalG); + if (tooltipText != "") drawTooltipText(bindGlobalG); + } + } + else tooltipShown = false; + } + + function drawTooltipText(bindGlobalG: Bool) { + globalG.color = t.TEXT_COL; + var lines = tooltipText.split("\n"); + var tooltipW = 0.0; + for (line in lines) { + var lineTooltipW = ops.font.width(fontSize, line); + if (lineTooltipW > tooltipW) tooltipW = lineTooltipW; + } + tooltipX = Math.min(tooltipX, kha.System.windowWidth() - tooltipW - 20); + if (bindGlobalG) globalG.begin(false); + var fontHeight = ops.font.height(fontSize); + var off = 0; + if (tooltipImg != null) { + var w = tooltipImg.width; + if (tooltipImgMaxWidth != null && w > tooltipImgMaxWidth) w = tooltipImgMaxWidth; + off = Std.int(tooltipImg.height * (w / tooltipImg.width)); + } + globalG.fillRect(tooltipX, tooltipY + off, tooltipW + 20, fontHeight * lines.length); + globalG.font = ops.font; + globalG.fontSize = fontSize; + globalG.color = t.ACCENT_COL; + for (i in 0...lines.length) { + globalG.drawString(lines[i], tooltipX + 5, tooltipY + off + i * fontSize); + } + if (bindGlobalG) globalG.end(); + } + + function drawTooltipImage(bindGlobalG: Bool) { + var w = tooltipImg.width; + if (tooltipImgMaxWidth != null && w > tooltipImgMaxWidth) w = tooltipImgMaxWidth; + var h = tooltipImg.height * (w / tooltipImg.width); + tooltipX = Math.min(tooltipX, kha.System.windowWidth() - w - 20); + tooltipY = Math.min(tooltipY, kha.System.windowHeight() - h - 20); + if (bindGlobalG) globalG.begin(false); + globalG.color = 0xff000000; + globalG.fillRect(tooltipX, tooltipY, w, h); + globalG.color = 0xffffffff; + tooltipInvertY ? + globalG.drawScaledImage(tooltipImg, tooltipX, tooltipY + h, w, -h) : + globalG.drawScaledImage(tooltipImg, tooltipX, tooltipY, w, h); + if (bindGlobalG) globalG.end(); + } + + function drawString(g: Graphics, text: String, xOffset: Null = null, yOffset: Float = 0, align = Align.Left, truncation = true) { + var fullText = text; + if (truncation) { + while (text.length > 0 && ops.font.width(fontSize, text) > _w - 6 * SCALE()) { + text = text.substr(0, text.length - 1); + } + if (text.length < fullText.length) { + text += ".."; + // Strip more to fit ".." + while (text.length > 2 && ops.font.width(fontSize, text) > _w - 10 * SCALE()) { + text = text.substr(0, text.length - 3) + ".."; + } + if (isHovered) tooltip(fullText); + } + } + + if (dynamicGlyphLoad) { + for (i in 0...text.length) { + if (text.charCodeAt(i) > 126 && Graphics.fontGlyphs.indexOf(text.charCodeAt(i)) == -1) { + Graphics.fontGlyphs.push(text.charCodeAt(i)); + Graphics.fontGlyphs = Graphics.fontGlyphs.copy(); // Trigger atlas update + } + } + } + + if (xOffset == null) xOffset = t.TEXT_OFFSET; + xOffset *= SCALE(); + g.font = ops.font; + g.fontSize = fontSize; + if (align == Align.Center) xOffset = _w / 2 - ops.font.width(fontSize, text) / 2; + else if (align == Align.Right) xOffset = _w - ops.font.width(fontSize, text) - TEXT_OFFSET(); + + if (!enabled) fadeColor(); + g.pipeline = rtTextPipeline; + + if (textColoring == null) { + g.drawString(text, _x + xOffset, _y + fontOffsetY + yOffset); + } + else { + // Monospace fonts only for now + for (coloring in textColoring.colorings) { + var result = extractColoring(text, coloring); + if (result.colored != "") { + g.color = coloring.color; + g.drawString(result.colored, _x + xOffset, _y + fontOffsetY + yOffset); + } + text = result.uncolored; + } + g.color = textColoring.default_color; + g.drawString(text, _x + xOffset, _y + fontOffsetY + yOffset); + } + + g.pipeline = null; + } + + static function extractColoring(text: String, col: TColoring) { + var res = { colored: "", uncolored: "" }; + var coloring = false; + var startFrom = 0; + var startLength = 0; + for (i in 0...text.length) { + var skipFirst = false; + // Check if upcoming text should be colored + var length = checkStart(i, text, col.start); + // Not touching another character + var separatedLeft = i == 0 || !isChar(text.charCodeAt(i - 1)); + var separatedRight = i + length >= text.length || !isChar(text.charCodeAt(i + length)); + var isSeparated = separatedLeft && separatedRight; + // Start coloring + if (length > 0 && (!coloring || col.end == "") && (!col.separated || isSeparated)) { + coloring = true; + startFrom = i; + startLength = length; + if (col.end != "" && col.end != "\n") skipFirst = true; + } + // End coloring + else if (col.end == "") { + if (i == startFrom + startLength) coloring = false; + } + else if (text.substr(i, col.end.length) == col.end) { + coloring = false; + } + // If true, add current character to colored string + var b = coloring && !skipFirst; + res.colored += b ? text.charAt(i) : " "; + res.uncolored += b ? " " : text.charAt(i); + } + return res; + } + + static inline function isChar(code: Int): Bool { + return (code >= 65 && code <= 90) || (code >= 97 && code <= 122); + } + + static function checkStart(i:Int, text: String, start: Array): Int { + for (s in start) if (text.substr(i, s.length) == s) return s.length; + return 0; + } + + function endElement(elementSize: Null = null) { + if (elementSize == null) elementSize = ELEMENT_H() + ELEMENT_OFFSET(); + if (currentWindow == null || currentWindow.layout == Vertical) { + if (curRatio == -1 || (ratios != null && curRatio == ratios.length - 1)) { // New line + _y += elementSize; + + if ((ratios != null && curRatio == ratios.length - 1)) { // Last row element + curRatio = -1; + ratios = null; + _x = xBeforeSplit; + _w = wBeforeSplit; + highlightFullRow = false; + } + } + else { // Row + curRatio++; + _x += _w; // More row elements to place + _w = Std.int(getRatio(ratios[curRatio], wBeforeSplit)); + } + } + else { // Horizontal + _x += _w + ELEMENT_OFFSET(); + } + } + + /** + Highlight all upcoming elements in the next row on a `mouse-over` event. + **/ + public inline function highlightNextRow() { + highlightFullRow = true; + } + + inline function getRatio(ratio: Float, dyn: Float): Float { + return ratio < 0 ? -ratio : ratio * dyn; + } + + /** + Draw the upcoming elements in the same row. + Negative values will be treated as absolute, positive values as ratio to `window width`. + **/ + public function row(ratios: Array) { + this.ratios = ratios; + curRatio = 0; + xBeforeSplit = _x; + wBeforeSplit = _w; + _w = Std.int(getRatio(ratios[curRatio], _w)); + } + + public function indent(bothSides = true) { + _x += TAB_W(); + _w -= TAB_W(); + if (bothSides) _w -= TAB_W(); + } + + public function unindent(bothSides = true) { + _x -= TAB_W(); + _w += TAB_W(); + if (bothSides) _w += TAB_W(); + } + + function fadeColor() { + g.color = kha.Color.fromFloats(g.color.R, g.color.G, g.color.B, 0.25); + } + + public function fill(x: Float, y: Float, w: Float, h: Float, color: kha.Color) { + g.color = color; + if (!enabled) fadeColor(); + g.fillRect(_x + x * SCALE(), _y + y * SCALE() - 1, w * SCALE(), h * SCALE()); + g.color = 0xffffffff; + } + + public function rect(x: Float, y: Float, w: Float, h: Float, color: kha.Color, strength = 1.0) { + g.color = color; + if (!enabled) fadeColor(); + g.drawRect(_x + x * SCALE(), _y + y * SCALE(), w * SCALE(), h * SCALE(), strength); + g.color = 0xffffffff; + } + + inline function drawRect(g: Graphics, fill: Bool, x: Float, y: Float, w: Float, h: Float, strength = 0.0) { + if (strength == 0.0) strength = 1; + if (!enabled) fadeColor(); + fill ? g.fillRect(x, y - 1, w, h + 1) : g.drawRect(x, y, w, h, strength); + } + + function isVisible(elemH: Float): Bool { + if (currentWindow == null) return true; + return (_y + elemH > windowHeaderH && _y < currentWindow.texture.height); + } + + function getReleased(elemH = -1.0): Bool { // Input selection + isReleased = enabled && inputEnabled && inputReleased && getHover(elemH) && getInitialHover(elemH); + return isReleased; + } + + function getPushed(elemH = -1.0): Bool { + isPushed = enabled && inputEnabled && inputDown && getHover(elemH) && getInitialHover(elemH); + return isPushed; + } + + function getStarted(elemH = -1.0): Bool { + isStarted = enabled && inputEnabled && inputStarted && getHover(elemH); + return isStarted; + } + + function getInitialHover(elemH = -1.0): Bool { + if (scissor && inputY < _windowY + windowHeaderH) return false; + if (elemH == -1.0) elemH = ELEMENT_H(); + return enabled && inputEnabled && + inputStartedX >= _windowX + _x && inputStartedX < (_windowX + _x + _w) && + inputStartedY >= _windowY + _y && inputStartedY < (_windowY + _y + elemH); + } + + function getHover(elemH = -1.0): Bool { + if (scissor && inputY < _windowY + windowHeaderH) return false; + if (elemH == -1.0) elemH = ELEMENT_H(); + isHovered = enabled && inputEnabled && + inputX >= _windowX + (highlightFullRow ? 0 : _x) && inputX < (_windowX + _x + (highlightFullRow ? _windowW : _w)) && + inputY >= _windowY + _y && inputY < (_windowY + _y + elemH); + return isHovered; + } + + function getInputInRect(x: Float, y: Float, w: Float, h: Float, scale = 1.0): Bool { + return enabled && inputEnabled && + inputX >= x * scale && inputX < (x + w) * scale && + inputY >= y * scale && inputY < (y + h) * scale; + } + + public function onMouseDown(button: Int, x: Int, y: Int) { // Input events + if (penInUse) return; + button == 0 ? inputStarted = true : inputStartedR = true; + button == 0 ? inputDown = true : inputDownR = true; + inputStartedTime = kha.Scheduler.time(); + #if (kha_android || kha_ios) + setInputPosition(x, y); + #end + inputStartedX = x; + inputStartedY = y; + } + + public function onMouseUp(button: Int, x: Int, y: Int) { + if (penInUse) return; + + if (touchHoldActivated) { + touchHoldActivated = false; + return; + } + + if (isScrolling) { // Prevent action when scrolling is active + isScrolling = false; + scrollHandle = null; + sliderTooltip = false; + if (x == inputStartedX && y == inputStartedY) { // Mouse not moved + button == 0 ? inputReleased = true : inputReleasedR = true; + } + } + else { + button == 0 ? inputReleased = true : inputReleasedR = true; + } + button == 0 ? inputDown = false : inputDownR = false; + #if (kha_android || kha_ios) + setInputPosition(x, y); + #end + deselectText(); + } + + public function onMouseMove(x: Int, y: Int, movementX: Int, movementY: Int) { + #if (!kha_android && !kha_ios) + setInputPosition(x, y); + #end + } + + public function onMouseWheel(delta: Int) { + inputWheelDelta = delta; + } + + function setInputPosition(x: Int, y: Int) { + inputDX += x - inputX; + inputDY += y - inputY; + inputX = x; + inputY = y; + } + + public function onPenDown(x: Int, y: Int, pressure: Float) { + #if (kha_android || kha_ios) + return; + #end + + onMouseDown(0, x, y); + } + + public function onPenUp(x: Int, y: Int, pressure: Float) { + #if (kha_android || kha_ios) + return; + #end + + if (inputStarted) { inputStarted = false; penInUse = true; return; } + onMouseUp(0, x, y); + penInUse = true; // On pen release, additional mouse down & up events are fired at once - filter those out + } + + public function onPenMove(x: Int, y: Int, pressure: Float) { + #if (kha_android || kha_ios) + return; + #end + + onMouseMove(x, y, 0, 0); + } + + public function onKeyDown(code: KeyCode) { + this.key = code; + isKeyPressed = true; + isKeyDown = true; + keyRepeatTime = kha.Scheduler.time() + 0.4; + switch code { + case KeyCode.Shift: isShiftDown = true; + case KeyCode.Control: isCtrlDown = true; + #if kha_darwin + case KeyCode.Meta: isCtrlDown = true; + #end + case KeyCode.Alt: isAltDown = true; + case KeyCode.Backspace: isBackspaceDown = true; + case KeyCode.Delete: isDeleteDown = true; + case KeyCode.Escape: isEscapeDown = true; + case KeyCode.Return: isReturnDown = true; + case KeyCode.Tab: isTabDown = true; + case KeyCode.A: isADown = true; + case KeyCode.Space: char = " "; + #if kha_android_rmb // Detect right mouse button on Android.. + case KeyCode.Back: if (!inputDownR) onMouseDown(1, Std.int(inputX), Std.int(inputY)); + #end + default: + } + } + + public function onKeyUp(code: KeyCode) { + isKeyDown = false; + switch code { + case KeyCode.Shift: isShiftDown = false; + case KeyCode.Control: isCtrlDown = false; + #if kha_darwin + case KeyCode.Meta: isCtrlDown = false; + #end + case KeyCode.Alt: isAltDown = false; + case KeyCode.Backspace: isBackspaceDown = false; + case KeyCode.Delete: isDeleteDown = false; + case KeyCode.Escape: isEscapeDown = false; + case KeyCode.Return: isReturnDown = false; + case KeyCode.Tab: isTabDown = false; + case KeyCode.A: isADown = false; + #if kha_android_rmb + case KeyCode.Back: onMouseUp(1, Std.int(inputX), Std.int(inputY)); + #end + default: + } + } + + public function onKeyPress(char: String) { + this.char = char; + isKeyPressed = true; + } + + #if (kha_android || kha_ios) + public function onTouchDown(index: Int, x: Int, y: Int) { + // Reset movement delta on touch start + if (index == 0) { + inputDX = 0; + inputDY = 0; + inputX = x; + inputY = y; + } + // Two fingers down - right mouse button + else if (index == 1) { + inputDown = false; + onMouseDown(1, Std.int(inputX), Std.int(inputY)); + pinchStarted = true; + pinchTotal = 0.0; + pinchDistance = 0.0; + } + // Three fingers down - middle mouse button + else if (index == 2) { + inputDownR = false; + onMouseDown(2, Std.int(inputX), Std.int(inputY)); + } + } + + public function onTouchUp(index: Int, x: Int, y: Int) { + if (index == 1) onMouseUp(1, Std.int(inputX), Std.int(inputY)); + } + + var pinchDistance = 0.0; + var pinchTotal = 0.0; + var pinchStarted = false; + + public function onTouchMove(index: Int, x: Int, y: Int) { + if (index == 0) setInputPosition(x, y); + + // Pinch to zoom - mouse wheel + if (index == 1) { + var lastDistance = pinchDistance; + var dx = inputX - x; + var dy = inputY - y; + pinchDistance = Math.sqrt(dx * dx + dy * dy); + pinchTotal += lastDistance != 0 ? lastDistance - pinchDistance : 0; + if (!pinchStarted) { + inputWheelDelta = Std.int(pinchTotal / 50); + if (inputWheelDelta != 0) { + pinchTotal = 0.0; + } + } + pinchStarted = false; + } + } + #end + + public function onCut(): String { + isCut = true; + return onCopy(); + } + public function onCopy(): String { + isCopy = true; + return textToCopy; + } + public function onPaste(s: String) { + isPaste = true; + textToPaste = s; + } + + public inline function ELEMENT_W(): Float { + return t.ELEMENT_W * SCALE(); + } + public inline function ELEMENT_H(): Float { + return t.ELEMENT_H * SCALE(); + } + public inline function ELEMENT_OFFSET(): Float { + return t.ELEMENT_OFFSET * SCALE(); + } + public inline function ARROW_SIZE(): Float { + return t.ARROW_SIZE * SCALE(); + } + public inline function BUTTON_H(): Float { + return t.BUTTON_H * SCALE(); + } + public inline function CHECK_SIZE(): Float { + return t.CHECK_SIZE * SCALE(); + } + public inline function CHECK_SELECT_SIZE(): Float { + return t.CHECK_SELECT_SIZE * SCALE(); + } + public inline function FONT_SIZE(): Int { + return Std.int(t.FONT_SIZE * SCALE()); + } + public inline function SCROLL_W(): Int { + return Std.int(t.SCROLL_W * SCALE()); + } + public inline function TEXT_OFFSET(): Float { + return t.TEXT_OFFSET * SCALE(); + } + public inline function TAB_W(): Int { + return Std.int(t.TAB_W * SCALE()); + } + public inline function HEADER_DRAG_H(): Int { + return Std.int(15 * SCALE()); + } + public inline function SCALE(): Float { + return ops.scaleFactor; + } + inline function TOOLTIP_DELAY(): Float { + return 1.0; + } + + public function resize(handle: Handle, w: Int, h: Int) { + handle.redraws = 2; + if (handle.texture != null) handle.texture.unload(); + if (w < 1) w = 1; + if (h < 1) h = 1; + handle.texture = kha.Image.createRenderTarget(w, h, kha.graphics4.TextureFormat.RGBA32, kha.graphics4.DepthStencilFormat.NoDepthAndStencil, 1); + handle.texture.g2.imageScaleQuality = kha.graphics2.ImageScaleQuality.High; + } +} + +typedef HandleOptions = { + ?selected: Bool, + ?position: Int, + ?value: Float, + ?text: String, + ?color: kha.Color, + ?layout: Layout +} + +class Handle { + public var selected = false; + public var position = 0; + public var color = kha.Color.White; + public var value = 0.0; + public var text = ""; + public var texture: kha.Image = null; + public var redraws = 2; + public var scrollOffset = 0.0; + public var scrollEnabled = false; + public var layout: Layout = 0; + public var lastMaxX = 0.0; + public var lastMaxY = 0.0; + public var dragEnabled = false; + public var dragX = 0; + public var dragY = 0; + public var changed = false; + var children: Map; + + public function new(ops: HandleOptions = null) { + if (ops != null) { + if (ops.selected != null) selected = ops.selected; + if (ops.position != null) position = ops.position; + if (ops.value != null) value = ops.value; + if (ops.text != null) text = ops.text; + if (ops.color != null) color = ops.color; + if (ops.layout != null) layout = ops.layout; + } + } + + public function nest(i: Int, ops: HandleOptions = null): Handle { + if (children == null) children = []; + var c = children.get(i); + if (c == null) { + c = new Handle(ops); + children.set(i, c); + } + return c; + } + + public function unnest(i: Int) { + if (children != null) { + children.remove(i); + } + } + + public static var global = new Handle(); +} + +@:enum abstract Layout(Int) from Int { + var Vertical = 0; + var Horizontal = 1; +} + +@:enum abstract Align(Int) from Int { + var Left = 0; + var Center = 1; + var Right = 2; +} + +@:enum abstract State(Int) from Int { + var Idle = 0; + var Started = 1; + var Down = 2; + var Released = 3; + var Hovered = 4; +} + +typedef TColoring = { + var color: Int; + var start: Array; + var end: String; + @:optional var separated: Null; +} + +typedef TTextColoring = { + var colorings: Array; + var default_color: Int; +}