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