Update Files

This commit is contained in:
2025-01-22 16:18:30 +01:00
parent ed4603cf95
commit a36294b518
16718 changed files with 2960346 additions and 0 deletions

View File

@ -0,0 +1,175 @@
package lnx2d;
// Zui
import leenkx.ui.Canvas;
// Editor
import lnx2d.Path;
import lnx2d.ui.UIProperties;
class Assets {
public static function getImage(asset:TAsset):kha.Image {
return Canvas.assetMap.get(asset.id);
}
public static function getFont(asset:TAsset):kha.Font {
return Canvas.assetMap.get(asset.id);
}
public static function importAsset(canvas:TCanvas, path:String) {
var abspath = Path.toAbsolute(path, Main.cwd);
abspath = kha.System.systemId == "Windows" ? StringTools.replace(abspath, "/", "\\") : abspath;
if (isPathImage(path)) {
kha.Assets.loadImageFromPath(abspath, false, function(image:kha.Image) {
var ar = path.split("/");
var name = ar[ar.length - 1];
var asset:TAsset = { name: name, file: path, id: Canvas.getAssetId(canvas) };
canvas.assets.push(asset);
Canvas.assetMap.set(asset.id, image);
Editor.assetNames.push(name);
UIProperties.hwin.redraws = 2;
});
}
else if (isPathFont(path)) {
kha.Assets.loadFontFromPath(abspath, function(font:kha.Font) {
var ar = path.split("/");
var name = ar[ar.length - 1];
var asset:TAsset = { name: name, file: path, id: Canvas.getAssetId(canvas) };
canvas.assets.push(asset);
Canvas.assetMap.set(asset.id, font);
Editor.assetNames.push(name);
UIProperties.hwin.redraws = 2;
});
}
}
/**
* Imports all themes from '_themes.json'. If the file doesn't exist, the
* default light theme is used instead.
*/
public static function importThemes() {
var themesDir = haxe.io.Path.directory(Main.prefs.path);
var themesPath = haxe.io.Path.join([themesDir, "_themes.json"]);
themesPath = kha.System.systemId == "Windows" ? StringTools.replace(themesPath, "/", "\\") : themesPath;
try {
kha.Assets.loadBlobFromPath(themesPath, function(b:kha.Blob) {
Canvas.themes = haxe.Json.parse(b.toString());
if (Canvas.themes.length == 0) {
Canvas.themes.push(Reflect.copy(leenkx.ui.Themes.light));
}
if (Main.inst != null) Editor.selectedTheme = Canvas.themes[0];
// Error handling for HTML5 target
}, function(a:kha.AssetError) {
Canvas.themes.push(Reflect.copy(leenkx.ui.Themes.light));
if (Main.inst != null) Editor.selectedTheme = Canvas.themes[0];
});
}
// Error handling for Krom, as the failed callback for loadBlobFromPath()
// is currently not implemented in Krom
catch (e: Dynamic) {
Canvas.themes.push(Reflect.copy(leenkx.ui.Themes.light));
if(Main.inst != null) Editor.selectedTheme = Canvas.themes[0];
}
}
public static function save(canvas: TCanvas) {
// Unpan
canvas.x = 0;
canvas.y = 0;
saveCanvas(canvas);
saveAssets(canvas);
saveThemes();
canvas.x = Editor.coffX;
canvas.y = Editor.coffY;
}
public static function load(done: TCanvas->Void) {
kha.Assets.loadBlobFromPath(Main.prefs.path, function(b: kha.Blob) {
done(Canvas.parseCanvasFromBlob(b));
});
}
static function saveCanvas(canvas: TCanvas) {
#if kha_krom
Krom.fileSaveBytes(Main.prefs.path, haxe.io.Bytes.ofString(haxe.Json.stringify(canvas)).getData());
#elseif kha_debug_html5
html5WriteFile(Main.prefs.path, haxe.Json.stringify(canvas));
#end
}
static function saveAssets(canvas: TCanvas) {
var filesPath = Main.prefs.path.substr(0, Main.prefs.path.length - 5); // .json
filesPath += '.files';
var filesList = '';
for (a in canvas.assets) filesList += a.file + '\n';
#if kha_krom
Krom.fileSaveBytes(filesPath, haxe.io.Bytes.ofString(filesList).getData());
#elseif kha_debug_html5
html5WriteFile(filesPath, filesList);
#end
}
static function saveThemes() {
var themesPath = haxe.io.Path.join([haxe.io.Path.directory(Main.prefs.path), "_themes.json"]);
#if kha_krom
Krom.fileSaveBytes(themesPath, haxe.io.Bytes.ofString(haxe.Json.stringify(Canvas.themes)).getData());
#elseif kha_debug_html5
html5WriteFile(themesPath, haxe.Json.stringify(Canvas.themes));
#end
}
#if kha_debug_html5
static function html5WriteFile(filePath: String, data: String) {
var fs = js.Syntax.code('require("fs");');
var path = js.Syntax.code('require("path")');
var filePath = path.resolve(js.Syntax.code('__dirname'), filePath);
try { fs.writeFileSync(filePath, data); }
catch (x: Dynamic) { trace('saving "${filePath}" failed'); }
}
#end
public static function getEnumTexts():Array<String> {
if(Main.inst==null) return [""];
return Editor.assetNames.length > 0 ? Editor.assetNames : [""];
}
public static function getAssetIndex(canvas:TCanvas, asset:String):Int {
for (i in 0...canvas.assets.length) if (asset == canvas.assets[i].name) return i + 1; // assetNames[0] = ""
return 0;
}
/**
* Returns if the given path is a path to an image file.
* @param path The path of the asset
* @return Bool
*/
public static function isPathImage(path: String): Bool {
var extension = haxe.io.Path.extension(path).toLowerCase();
return extension == "jpg" || extension == "png" || extension == "k" || extension == "hdr";
}
/**
* Returns if the given path is a path to a font file.
* @param path The path of the asset
* @return Bool
*/
public static inline function isPathFont(path: String): Bool {
return haxe.io.Path.extension(path).toLowerCase() == "ttf";
}
}

View File

@ -0,0 +1,470 @@
package lnx2d;
// Zui
import zui.Zui;
import zui.Themes;
import zui.Id;
using zui.Ext;
import leenkx.ui.Popup;
import leenkx.ui.Canvas;
// Editor
import lnx2d.Path;
import lnx2d.Assets;
import lnx2d.tools.Math;
import lnx2d.ui.UIToolBar;
import lnx2d.ui.UIProperties;
import lnx2d.tools.CanvasTools;
@:access(zui.Zui)
class Editor {
var ui:Zui;
public var cui:Zui;
public var canvas:TCanvas;
public static var defaultWindowW = 240;
public static var windowW = defaultWindowW;
static var uiw(get, null):Int;
static function get_uiw():Int {
return Std.int(windowW * Main.prefs.scaleFactor);
}
var toolbarw(get, null):Int;
function get_toolbarw():Int {
return Std.int(140 * ui.SCALE());
}
// Canvas offset from the editor window
// Should be a multiple of gridSize to ensure visual grid alignment
public static var coffX = 160.0;
public static var coffY = 40.0;
var dropPath = "";
public static var currentOperation = "";
public static var assetNames:Array<String> = [""];
public static var dragAsset:TAsset = null;
var resizeCanvas = false;
var zoom = 1.0;
public static var showFiles = false;
public static var foldersOnly = false;
public static var filesDone:String->Void = null;
var uimodal:Zui;
public static var gridSnapBounds:Bool = false;
public static var gridSnapPos:Bool = true;
public static var gridUseRelative:Bool = true;
public static var useRotationSteps:Bool = false;
public static var rotationSteps:Float = Math.toRadians(15);
public static var gridSize:Int = 20;
public static var redrawGrid = false;
static var grid:kha.Image = null;
static var timeline:kha.Image = null;
var selectedFrame = 0;
public static var selectedTheme:zui.Themes.TTheme = null;
public static var selectedElem:TElement = null;
var lastW = 0;
var lastH = 0;
var lastCanvasW = 0;
var lastCanvasH = 0;
public function new(canvas:TCanvas) {
this.canvas = canvas;
// Reimport assets
if (canvas.assets.length > 0) {
var assets = canvas.assets;
canvas.assets = [];
for (a in assets) Assets.importAsset(canvas, a.file);
}
Assets.importThemes();
kha.Assets.loadEverything(loaded);
}
function loaded() {
var t = Reflect.copy(Themes.dark);
t.FILL_WINDOW_BG = true;
ui = new Zui({scaleFactor: Main.prefs.scaleFactor, font: kha.Assets.fonts.font_default, theme: t, color_wheel: kha.Assets.images.color_wheel, black_white_gradient: kha.Assets.images.black_white_gradient});
cui = new Zui({scaleFactor: 1.0, font: kha.Assets.fonts.font_default, autoNotifyInput: true, theme: Reflect.copy(Canvas.getTheme(canvas.theme))});
uimodal = new Zui( { font: kha.Assets.fonts.font_default, scaleFactor: Main.prefs.scaleFactor } );
ElementController.initialize(ui, cui);
if (Canvas.getTheme(canvas.theme) == null) {
Popup.showMessage(new Zui(ui.ops), "Warning!",
'Theme "${canvas.theme}" was not found!'
+ '\nUsing first theme in list instead: "${Canvas.themes[0].NAME}"');
canvas.theme = Canvas.themes[0].NAME;
}
kha.System.notifyOnDropFiles(function(path:String) {
dropPath = StringTools.rtrim(path);
dropPath = Path.toRelative(dropPath, Main.cwd);
});
kha.System.notifyOnFrames(onFrames);
kha.Scheduler.addTimeTask(update, 0, 1 / 60);
}
function resize() {
if (grid != null) {
grid.unload();
grid = null;
}
if (timeline != null) {
timeline.unload();
timeline = null;
}
}
function drawGrid() {
redrawGrid = false;
var scaledGridSize = scaled(gridSize);
var doubleGridSize = scaled(gridSize * 2);
var ww = kha.System.windowWidth();
var wh = kha.System.windowHeight();
var w = ww + doubleGridSize * 2;
var h = wh + doubleGridSize * 2;
if (grid == null) {
grid = kha.Image.createRenderTarget(w, h);
}
grid.g2.begin(true, 0xff242424);
for (i in 0...Std.int(h / doubleGridSize) + 1) {
grid.g2.color = 0xff282828;
grid.g2.drawLine(0, i * doubleGridSize + scaledGridSize, w, i * doubleGridSize + scaledGridSize);
grid.g2.color = 0xff323232;
grid.g2.drawLine(0, i * doubleGridSize, w, i * doubleGridSize);
}
for (i in 0...Std.int(w / doubleGridSize) + 1) {
grid.g2.color = 0xff282828;
grid.g2.drawLine(i * doubleGridSize + scaledGridSize, 0, i * doubleGridSize + scaledGridSize, h);
grid.g2.color = 0xff323232;
grid.g2.drawLine(i * doubleGridSize, 0, i * doubleGridSize, h);
}
grid.g2.end();
}
function drawTimeline(timelineLabelsHeight:Int, timelineFramesHeight:Int) {
var sc = ui.SCALE();
var timelineHeight = timelineLabelsHeight + timelineFramesHeight;
timeline = kha.Image.createRenderTarget(kha.System.windowWidth() - uiw - toolbarw, timelineHeight);
var g = timeline.g2;
g.begin(true, 0xff222222);
g.font = kha.Assets.fonts.font_default;
g.fontSize = Std.int(16 * sc);
// Labels
var frames = Std.int(timeline.width / (11 * sc));
for (i in 0...Std.int(frames / 5) + 1) {
var frame = i * 5;
var frameTextWidth = kha.Assets.fonts.font_default.width(g.fontSize, frame + "");
g.drawString(frame + "", i * 55 * sc + 5 * sc - frameTextWidth / 2, timelineLabelsHeight / 2 - g.fontSize / 2);
}
// Frames
for (i in 0...frames) {
g.color = i % 5 == 0 ? 0xff444444 : 0xff333333;
g.fillRect(i * 11 * sc, timelineHeight - timelineFramesHeight, 10 * sc, timelineFramesHeight);
}
g.end();
}
public function onFrames(framebuffers: Array<kha.Framebuffer>): Void {
// Prevent crash when minimizing window
if (kha.System.windowWidth() == 0 || kha.System.windowHeight() == 0) return;
var framebuffer = framebuffers[0];
// Disable UI if a popup is displayed
if (Popup.show && ui.inputRegistered) {
ui.unregisterInput();
cui.unregisterInput();
} else if (!Popup.show && !ui.inputRegistered) {
ui.registerInput();
cui.registerInput();
}
// Update preview when choosing a color
if (Popup.show) UIProperties.hwin.redraws = 1;
if (dropPath != "") {
Assets.importAsset(canvas, dropPath);
dropPath = "";
}
var sc = ui.SCALE();
var timelineLabelsHeight = Std.int(30 * sc);
var timelineFramesHeight = Std.int(40 * sc);
// Bake and redraw if the UI scale has changed
if (grid == null || redrawGrid) drawGrid();
if (timeline == null || timeline.height != timelineLabelsHeight + timelineFramesHeight) drawTimeline(timelineLabelsHeight, timelineFramesHeight);
var g = framebuffer.g2;
g.begin();
g.color = 0xffffffff;
var doubleGridSize = scaled(gridSize * 2);
g.drawImage(grid, coffX % doubleGridSize - doubleGridSize, coffY % doubleGridSize - doubleGridSize);
// Canvas outline
canvas.x = coffX;
canvas.y = coffY;
g.drawRect(canvas.x, canvas.y, scaled(canvas.width), scaled(canvas.height), 1.0);
// Canvas resize
var handleSize = ElementController.handleSize;
if (Math.hitbox(cui, canvas.x + scaled(canvas.width) - handleSize / 2, canvas.y + scaled(canvas.height) - handleSize / 2, handleSize, handleSize)) {
g.color = 0xff205d9c;
g.fillRect(canvas.x + scaled(canvas.width) - handleSize / 2, canvas.y + scaled(canvas.height) - handleSize / 2, handleSize, handleSize);
g.color = 0xffffffff;
}
g.drawRect(canvas.x + scaled(canvas.width) - handleSize / 2, canvas.y + scaled(canvas.height) - handleSize / 2, handleSize, handleSize, 1);
Canvas.screenW = canvas.width;
Canvas.screenH = canvas.height;
Canvas.draw(cui, canvas, g);
ElementController.render(g, canvas);
if (currentOperation != "") {
g.fontSize = Std.int(14 * ui.SCALE());
g.color = 0xffaaaaaa;
g.drawString(currentOperation, toolbarw, kha.System.windowHeight() - timeline.height - g.fontSize);
}
// Timeline
var showTimeline = true;
if (showTimeline) {
g.color = 0xffffffff;
var ty = kha.System.windowHeight() - timeline.height;
g.drawImage(timeline, toolbarw, ty);
g.color = 0xff205d9c;
g.fillRect(toolbarw + selectedFrame * 11 * sc, ty + timelineLabelsHeight, 10 * sc, timelineFramesHeight);
// Show selected frame number
g.font = kha.Assets.fonts.font_default;
g.fontSize = Std.int(16 * sc);
var frameIndicatorMargin = 4 * sc;
var frameIndicatorPadding = 4 * sc;
var frameIndicatorWidth = 30 * sc;
var frameIndicatorHeight = timelineLabelsHeight - frameIndicatorMargin * 2;
var frameTextWidth = kha.Assets.fonts.font_default.width(g.fontSize, "" + selectedFrame);
// Scale the indicator if the contained text is too long
if (frameTextWidth > frameIndicatorWidth + frameIndicatorPadding) {
frameIndicatorWidth = frameTextWidth + frameIndicatorPadding;
}
g.fillRect(toolbarw + selectedFrame * 11 * sc + 5 * sc - frameIndicatorWidth / 2, ty + frameIndicatorMargin, frameIndicatorWidth, frameIndicatorHeight);
g.color = 0xffffffff;
g.drawString("" + selectedFrame, toolbarw + selectedFrame * 11 * sc + 5 * sc - frameTextWidth / 2, ty + timelineLabelsHeight / 2 - g.fontSize / 2);
}
g.end();
ui.begin(g);
UIToolBar.renderToolbar(ui, cui, canvas, toolbarw);
if (ui.window(Id.handle(), toolbarw, 0, kha.System.windowWidth() - uiw - toolbarw, Std.int((ui.t.ELEMENT_H + 2) * ui.SCALE()))) {
ui.tab(Id.handle(), canvas.name);
}
UIProperties.renderProperties(ui, uiw, canvas);
ui.end();
if (ui.changed && !ui.inputDown) drawGrid();
g.begin(false);
if (dragAsset != null) {
var w = std.Math.min(128, Assets.getImage(dragAsset).width);
var ratio = w / Assets.getImage(dragAsset).width;
var h = Assets.getImage(dragAsset).height * ratio;
g.drawScaledImage(Assets.getImage(dragAsset), ui.inputX, ui.inputY, w, h);
}
g.end();
if (lastW > 0 && (lastW != kha.System.windowWidth() || lastH != kha.System.windowHeight())) {
resize();
}
else if (lastCanvasW > 0 && (lastCanvasW != canvas.width || lastCanvasH != canvas.height)) {
resize();
}
lastW = kha.System.windowWidth();
lastH = kha.System.windowHeight();
lastCanvasW = canvas.width;
lastCanvasH = canvas.height;
if (showFiles) renderFiles(g);
if (Popup.show) Popup.render(g);
}
function acceptDrag(index:Int) {
var elem = CanvasTools.makeElem(cui, canvas, ElementType.Image);
elem.asset = assetNames[index + 1]; // assetNames[0] == ""
elem.x = ui.inputX - canvas.x;
elem.y = ui.inputY - canvas.y;
elem.width = Assets.getImage(canvas.assets[index]).width;
elem.height = Assets.getImage(canvas.assets[index]).height;
selectedElem = elem;
}
public function update() {
// Drag from assets panel
if (ui.inputReleased && dragAsset != null) {
if (ui.inputX < kha.System.windowWidth() - uiw) {
var index = 0;
for (i in 0...canvas.assets.length) if (canvas.assets[i] == dragAsset) { index = i; break; }
acceptDrag(index);
}
dragAsset = null;
}
if (dragAsset != null) return;
updateCanvas();
// Select frame
if (timeline != null) {
var ty = kha.System.windowHeight() - timeline.height;
if (ui.inputDown && ui.inputY > ty && ui.inputX < kha.System.windowWidth() - uiw && ui.inputX > toolbarw) {
selectedFrame = Std.int((ui.inputX - toolbarw) / 11 / ui.SCALE());
}
}
ElementController.update(ui, cui, canvas);
if (Popup.show) Popup.update();
updateFiles();
}
function updateCanvas() {
if (showFiles || ui.inputX > kha.System.windowWidth() - uiw) return;
ElementController.selectElement(canvas);
if (!ElementController.isManipulating) {
// Pan canvas
if (ui.inputDownR) {
coffX += Std.int(ui.inputDX);
coffY += Std.int(ui.inputDY);
}
// Zoom canvas
if (ui.inputWheelDelta != 0) {
var prevZoom = zoom;
zoom += -ui.inputWheelDelta / 10;
if (zoom < 0.4) zoom = 0.4;
else if (zoom > 1.0) zoom = 1.0;
zoom = std.Math.round(zoom * 10) / 10;
cui.setScale(zoom);
// Update the grid only when necessary, this prevents lag from scrolling too fast
if (prevZoom != zoom) {
drawGrid();
}
}
}
// Canvas resize
var handleSize = ElementController.handleSize;
if (ui.inputStarted && Math.hitbox(cui, canvas.x + scaled(canvas.width) - handleSize / 2, canvas.y + scaled(canvas.height) - handleSize / 2, handleSize, handleSize)) {
resizeCanvas = true;
}
if (ui.inputReleased && resizeCanvas) {
resizeCanvas = false;
}
if (resizeCanvas) {
canvas.width += Std.int(ui.inputDX);
canvas.height += Std.int(ui.inputDY);
if (canvas.width < 1) canvas.width = 1;
if (canvas.height < 1) canvas.height = 1;
}
}
function updateFiles() {
if (!showFiles) return;
if (ui.inputReleased) {
var appw = kha.System.windowWidth();
var apph = kha.System.windowHeight();
var left = appw / 2 - modalRectW / 2;
var right = appw / 2 + modalRectW / 2;
var top = apph / 2 - modalRectH / 2;
var bottom = apph / 2 + modalRectH / 2;
if (ui.inputX < left || ui.inputX > right || ui.inputY < top + modalHeaderH || ui.inputY > bottom) {
showFiles = false;
}
}
}
static var modalW = 625;
static var modalH = 545;
static var modalHeaderH = 66;
static var modalRectW = 625; // No shadow
static var modalRectH = 545;
static var path = '/';
function renderFiles(g:kha.graphics2.Graphics) {
var appw = kha.System.windowWidth();
var apph = kha.System.windowHeight();
var left = appw / 2 - modalW / 2;
var top = apph / 2 - modalH / 2;
g.begin(false);
g.color = 0xff202020;
g.fillRect(left, top, modalW, modalH);
g.end();
var leftRect = Std.int(appw / 2 - modalRectW / 2);
var rightRect = Std.int(appw / 2 + modalRectW / 2);
var topRect = Std.int(apph / 2 - modalRectH / 2);
var bottomRect = Std.int(apph / 2 + modalRectH / 2);
topRect += modalHeaderH;
uimodal.begin(g);
if (uimodal.window(Id.handle(), leftRect, topRect, modalRectW, modalRectH - 100)) {
var pathHandle = Id.handle();
pathHandle.text = uimodal.textInput(pathHandle);
path = uimodal.fileBrowser(pathHandle, foldersOnly);
}
uimodal.end(false);
g.begin(false);
uimodal.beginRegion(g, rightRect - 100, bottomRect - 30, 100);
if (uimodal.button("OK")) {
showFiles = false;
filesDone(path);
}
uimodal.endRegion(false);
uimodal.beginRegion(g, rightRect - 200, bottomRect - 30, 100);
if (uimodal.button("Cancel")) {
showFiles = false;
}
uimodal.endRegion();
g.end();
}
inline function scaled(f: Float): Int { return Std.int(f * cui.SCALE()); }
}

View File

@ -0,0 +1,399 @@
package lnx2d;
// Kha
import kha.math.Vector2;
import kha.input.KeyCode;
import kha.graphics2.Graphics;
using kha.graphics2.GraphicsExtension;
// Zui
import zui.Zui;
import leenkx.ui.Canvas;
// Editor
import lnx2d.tools.Math;
import lnx2d.ui.UIProperties;
import lnx2d.tools.CanvasTools;
class ElementController {
static var ui:Zui;
static var cui:Zui;
public static var isManipulating = false;
static var transformInitInput:Vector2;
static var transformInitPos:Vector2;
static var transformInitRot:Float;
static var transformInitSize:Vector2;
// Was the transformation editing started by dragging the mouse
static var transformStartedMouse = false;
static var drag = false;
static var dragLeft = false;
static var dragTop = false;
static var dragRight = false;
static var dragBottom = false;
static var grab = false;
static var grabX = false;
static var grabY = false;
static var rotate = false;
static var newElementSelected = false;
public static var handleSize(get, null):Int;
static inline function get_handleSize():Int { return Std.int(8 * ui.SCALE()); }
public static function initialize(ui: Zui, cui: Zui) {
ElementController.ui = ui;
ElementController.cui = cui;
}
public static function selectElement(canvas:TCanvas) {
if (ui == null) return;
var selectButton = Main.prefs.keyMap.selectMouseButton;
if (selectButton == "Left" && ui.inputStarted && ui.inputDown ||
selectButton == "Right" && ui.inputStartedR && ui.inputDownR) {
// Deselect
var lastSelected = Editor.selectedElem;
Editor.selectedElem = null;
newElementSelected = false;
// Elements are sorted by z position (descending), so the topmost element will get
// selected if multiple elements overlap each other at the mouse position
var sorted_elements = canvas.elements.copy();
sorted_elements.reverse();
for (elem in sorted_elements) {
var anchorOffset = Canvas.getAnchorOffset(canvas, elem);
var ex = scaled(Math.absx(canvas, elem)) + anchorOffset[0];
var ey = scaled(Math.absy(canvas, elem)) + anchorOffset[1];
var ew = scaled(elem.width);
var eh = scaled(elem.height);
// Element center
var cx = canvas.x + ex + ew / 2;
var cy = canvas.y + ey + eh / 2;
var rotHandleX = cx - handleSize / 2;
var rotHandleY = canvas.y + ey - handleSize * 2 - handleSize / 2;
var rotHandleH = handleSize * 2 + handleSize / 2;
if (Math.hitbox(cui, canvas.x + ex - handleSize / 2, canvas.y + ey - handleSize / 2, ew + handleSize, eh + handleSize, elem.rotation)
|| (Math.hitbox(cui, rotHandleX, rotHandleY, handleSize, rotHandleH, elem.rotation, [cx, cy]) // Rotation handle hitbox
&& lastSelected == elem)) { // Don't select elements other than the currently selected by their rotation handle
Editor.selectedElem = elem;
if (lastSelected != elem)
newElementSelected = true;
break;
}
}
// force properties redraw to show selection
UIProperties.hwin.redraws = 2;
}
}
public static function render(g:Graphics, canvas:TCanvas) {
// Outline selected elem
if (Editor.selectedElem != null) {
var anchorOffset = Canvas.getAnchorOffset(canvas, Editor.selectedElem);
// Resize rects
var ex = scaled(Math.absx(canvas, Editor.selectedElem)) + anchorOffset[0];
var ey = scaled(Math.absy(canvas, Editor.selectedElem)) + anchorOffset[1];
var ew = scaled(Editor.selectedElem.width);
var eh = scaled(Editor.selectedElem.height);
// Element center
var cx = canvas.x + ex + ew / 2;
var cy = canvas.y + ey + eh / 2;
g.pushRotation(Editor.selectedElem.rotation, cx, cy);
// Draw element outline
g.color = 0xffffffff;
g.drawRect(canvas.x + ex, canvas.y + ey, ew, eh);
g.color = 0xff000000;
g.drawRect(canvas.x + ex + 1, canvas.y + ey + 1, ew, eh);
g.color = 0xffffffff;
// Rotate mouse coords in opposite direction as the element
var rotatedInput:Vector2 = Math.rotatePoint(ui.inputX, ui.inputY, cx, cy, -Editor.selectedElem.rotation);
// Draw corner drag handles
for (handlePosX in 0...3) {
// 0 = Left, 0.5 = Center, 1 = Right
var handlePosX:Float = handlePosX / 2;
for (handlePosY in 0...3) {
// 0 = Top, 0.5 = Center, 1 = Bottom
var handlePosY:Float = handlePosY / 2;
if (handlePosX == 0.5 && handlePosY == 0.5) {
continue;
}
var hX = canvas.x + ex + ew * handlePosX - handleSize / 2;
var hY = canvas.y + ey + eh * handlePosY - handleSize / 2;
// Check if the handle is currently dragged (not necessarily hovered!)
var dragged = false;
if (handlePosX == 0 && dragLeft) {
if (handlePosY == 0 && dragTop) dragged = true;
else if (handlePosY == 0.5 && !(dragTop || dragBottom)) dragged = true;
else if (handlePosY == 1 && dragBottom) dragged = true;
} else if (handlePosX == 0.5 && !(dragLeft || dragRight)) {
if (handlePosY == 0 && dragTop) dragged = true;
else if (handlePosY == 1 && dragBottom) dragged = true;
} else if (handlePosX == 1 && dragRight) {
if (handlePosY == 0 && dragTop) dragged = true;
else if (handlePosY == 0.5 && !(dragTop || dragBottom)) dragged = true;
else if (handlePosY == 1 && dragBottom) dragged = true;
}
dragged = dragged && drag;
// Hover
if (rotatedInput.x > hX && rotatedInput.x < hX + handleSize || dragged) {
if (rotatedInput.y > hY && rotatedInput.y < hY + handleSize || dragged) {
g.color = 0xff205d9c;
g.fillRect(hX, hY, handleSize, handleSize);
g.color = 0xffffffff;
}
}
g.drawRect(hX, hY, handleSize, handleSize);
}
}
// Draw rotation handle
g.drawLine(cx, canvas.y + ey, cx, canvas.y + ey - handleSize * 2);
var rotHandleCenter = new Vector2(cx, canvas.y + ey - handleSize * 2);
if (rotatedInput.sub(rotHandleCenter).length <= handleSize / 2 || rotate) {
g.color = 0xff205d9c;
g.fillCircle(rotHandleCenter.x, rotHandleCenter.y, handleSize / 2);
g.color = 0xffffffff;
}
g.drawCircle(rotHandleCenter.x, rotHandleCenter.y, handleSize / 2);
g.popTransformation();
}
}
public static function update(ui:Zui, cui:Zui, canvas:TCanvas) {
lnx2d.ElementController.ui = ui;
lnx2d.ElementController.cui = cui;
if (newElementSelected)
return;
if (Editor.selectedElem != null) {
var elem = Editor.selectedElem;
var anchorOffset = Canvas.getAnchorOffset(canvas, elem);
var ex = scaled(Math.absx(canvas, elem)) + anchorOffset[0];
var ey = scaled(Math.absy(canvas, elem)) + anchorOffset[1];
var ew = scaled(elem.width);
var eh = scaled(elem.height);
var rotatedInput:Vector2 = Math.rotatePoint(ui.inputX, ui.inputY, canvas.x + ex + ew / 2, canvas.y + ey + eh / 2, -elem.rotation);
if (ui.inputStarted && ui.inputDown) {
// Drag selected element
if (Math.hitbox(ui, canvas.x + ex - handleSize / 2, canvas.y + ey - handleSize / 2, ew + handleSize, eh + handleSize, Editor.selectedElem.rotation)) {
drag = true;
// Resize
dragLeft = dragRight = dragTop = dragBottom = false;
if (rotatedInput.x > canvas.x + ex + ew - handleSize) dragRight = true;
else if (rotatedInput.x < canvas.x + ex + handleSize) dragLeft = true;
if (rotatedInput.y > canvas.y + ey + eh - handleSize) dragBottom = true;
else if (rotatedInput.y < canvas.y + ey + handleSize) dragTop = true;
startElementManipulation(true);
} else {
var rotHandleCenter = new Vector2(canvas.x + ex + ew / 2, canvas.y + ey - handleSize * 2);
var inputPos = rotatedInput.sub(rotHandleCenter);
// Rotate selected element
if (inputPos.length <= handleSize) {
rotate = true;
startElementManipulation(true);
}
}
}
if (isManipulating) {
UIProperties.hwin.redraws = 2;
// Confirm
if ((transformStartedMouse && ui.inputReleased) || (!transformStartedMouse && ui.inputStarted)) {
endElementManipulation();
// Reset
} else if ((ui.isKeyPressed && ui.isEscapeDown) || ui.inputStartedR) {
endElementManipulation(true);
} else if (drag) {
var transformDelta = new Vector2(ui.inputX, ui.inputY).sub(transformInitInput);
if (!transformStartedMouse) {
if (ui.isKeyPressed && ui.key == KeyCode.X) {
elem.width = Std.int(transformInitSize.x);
elem.height = Std.int(transformInitSize.y);
dragRight = true;
dragBottom = !dragBottom;
}
if (ui.isKeyPressed && ui.key == KeyCode.Y) {
elem.width = Std.int(transformInitSize.x);
elem.height = Std.int(transformInitSize.y);
dragBottom = true;
dragRight = !dragRight;
}
}
if (dragRight) {
transformDelta.x = Math.calculateTransformDelta(ui, Editor.gridSnapPos, Editor.gridUseRelative, Editor.gridSize, transformDelta.x, transformInitPos.x + transformInitSize.x);
elem.width = Std.int(transformInitSize.x + transformDelta.x);
} else if (dragLeft) {
transformDelta.x = Math.calculateTransformDelta(ui, Editor.gridSnapPos, Editor.gridUseRelative, Editor.gridSize, transformDelta.x, transformInitPos.x);
elem.x = transformInitPos.x + transformDelta.x;
elem.width = Std.int(transformInitSize.x - transformDelta.x);
}
if (dragBottom) {
transformDelta.y = Math.calculateTransformDelta(ui, Editor.gridSnapPos, Editor.gridUseRelative, Editor.gridSize, transformDelta.y, transformInitPos.y + transformInitSize.y);
elem.height = Std.int(transformInitSize.y + transformDelta.y);
}
else if (dragTop) {
transformDelta.y = Math.calculateTransformDelta(ui, Editor.gridSnapPos, Editor.gridUseRelative, Editor.gridSize, transformDelta.y, transformInitPos.y);
elem.y = transformInitPos.y + transformDelta.y;
elem.height = Std.int(transformInitSize.y - transformDelta.y);
}
if (elem.type != ElementType.Image) {
if (elem.width < 1) elem.width = 1;
if (elem.height < 1) elem.height = 1;
}
if (!dragLeft && !dragRight && !dragBottom && !dragTop) {
grab = true;
grabX = true;
grabY = true;
drag = false;
} else {
// Ensure there the delta is 0 on unused axes
if (!dragBottom && !dragTop) transformDelta.y = 0;
else if (!dragLeft && !dragRight) transformDelta.y = 0;
Editor.currentOperation = ' x: ${elem.x} y: ${elem.y} w: ${elem.width} h: ${elem.height} (dx: ${transformDelta.x} dy: ${transformDelta.y})';
}
} else if (grab) {
var transformDelta = new Vector2(ui.inputX, ui.inputY).sub(transformInitInput);
if (ui.isKeyPressed && ui.key == KeyCode.X) {
elem.x = transformInitPos.x;
elem.y = transformInitPos.y;
grabX = true;
grabY = !grabY;
}
if (ui.isKeyPressed && ui.key == KeyCode.Y) {
elem.x = transformInitPos.x;
elem.y = transformInitPos.y;
grabY = true;
grabX = !grabX;
}
if (grabX) {
transformDelta.x = Math.calculateTransformDelta(ui, Editor.gridSnapPos, Editor.gridUseRelative, Editor.gridSize, transformDelta.x, transformInitPos.x);
elem.x = Std.int(transformInitPos.x + transformDelta.x);
}
if (grabY) {
transformDelta.y = Math.calculateTransformDelta(ui, Editor.gridSnapPos, Editor.gridUseRelative, Editor.gridSize, transformDelta.y, transformInitPos.y);
elem.y = Std.int(transformInitPos.y + transformDelta.y);
}
// Ensure there the delta is 0 on unused axes
if (!grabX) transformDelta.x = 0;
else if (!grabY) transformDelta.y = 0;
Editor.currentOperation = ' x: ${elem.x} y: ${elem.y} (dx: ${transformDelta.x} dy: ${transformDelta.y})';
} else if (rotate) {
var elemCenter = new Vector2(canvas.x + ex + ew / 2, canvas.y + ey + eh / 2);
var inputPos = new Vector2(ui.inputX, ui.inputY).sub(elemCenter);
// inputPos.x and inputPos.y are both positive when the mouse is in the lower right
// corner of the elements center, so the positive x axis used for the angle calculation
// in atan2() is equal to the global negative y axis. That's why we have to invert the
// angle and add Pi to get the correct rotation. atan2() also returns an angle in the
// intervall (-PI, PI], so we don't have to calculate the angle % PI*2 anymore.
var inputAngle = -std.Math.atan2(inputPos.x, inputPos.y) + std.Math.PI;
// Ctrl toggles rotation step mode
if ((ui.isKeyDown && ui.key == Main.prefs.keyMap.gridInvert) != Editor.useRotationSteps) {
inputAngle = std.Math.round(inputAngle / Editor.rotationSteps) * Editor.rotationSteps;
}
elem.rotation = inputAngle;
Editor.currentOperation = " Rot: " + Math.roundPrecision(Math.toDegrees(inputAngle), 2) + "deg";
}
}
if (ui.isKeyPressed && !ui.isTyping) {
if (!grab && ui.key == Main.prefs.keyMap.grabKey){startElementManipulation(); grab = true; grabX = true; grabY = true;}
if (!drag && ui.key == Main.prefs.keyMap.sizeKey) {startElementManipulation(); drag = true; dragLeft = false; dragTop = false; dragRight = true; dragBottom = true;}
if (!rotate && ui.key == Main.prefs.keyMap.rotateKey) {startElementManipulation(); rotate = true;}
if (!isManipulating) {
// Move with arrows
if (ui.key == KeyCode.Left) Editor.gridSnapPos ? elem.x -= Editor.gridSize : elem.x--;
if (ui.key == KeyCode.Right) Editor.gridSnapPos ? elem.x += Editor.gridSize : elem.x++;
if (ui.key == KeyCode.Up) Editor.gridSnapPos ? elem.y -= Editor.gridSize : elem.y--;
if (ui.key == KeyCode.Down) Editor.gridSnapPos ? elem.y += Editor.gridSize : elem.y++;
if (ui.isBackspaceDown || ui.isDeleteDown){
CanvasTools.removeElem(canvas, Editor.selectedElem);
Editor.selectedElem = null;
}
else if (ui.key == KeyCode.D) Editor.selectedElem = CanvasTools.duplicateElem(canvas, elem);
}
}
} else {
endElementManipulation();
}
}
static function startElementManipulation(?mousePressed=false) {
if (isManipulating) endElementManipulation(true);
transformInitInput = new Vector2(ui.inputX, ui.inputY);
transformInitPos = new Vector2(Editor.selectedElem.x, Editor.selectedElem.y);
transformInitSize = new Vector2(Editor.selectedElem.width, Editor.selectedElem.height);
transformInitRot = Editor.selectedElem.rotation;
transformStartedMouse = mousePressed;
isManipulating = true;
}
static function endElementManipulation(reset=false) {
if (reset) {
Editor.selectedElem.x = transformInitPos.x;
Editor.selectedElem.y = transformInitPos.y;
Editor.selectedElem.width = Std.int(transformInitSize.x);
Editor.selectedElem.height = Std.int(transformInitSize.y);
Editor.selectedElem.rotation = transformInitRot;
}
isManipulating = false;
grab = false;
drag = false;
rotate = false;
transformStartedMouse = false;
Editor.currentOperation = "";
}
static inline function scaled(f: Float): Int { return Std.int(f * cui.SCALE()); }
}

View File

@ -0,0 +1,26 @@
package lnx2d;
class Path {
public static function toRelative(path:String, cwd:String):String {
path = haxe.io.Path.normalize(path);
cwd = haxe.io.Path.normalize(cwd);
var ar:Array<String> = [];
var ar1 = path.split("/");
var ar2 = cwd.split("/");
var index = 0;
while (ar1[index] == ar2[index]) index++;
for (i in 0...ar2.length - index) ar.push("..");
for (i in index...ar1.length) ar.push(ar1[i]);
return ar.join("/");
}
public static function toAbsolute(path:String, cwd:String):String {
return haxe.io.Path.normalize(cwd + "/" + path);
}
}

View File

@ -0,0 +1,229 @@
package lnx2d.tools;
// Zui
import zui.Zui;
import leenkx.ui.Canvas;
class CanvasTools {
public static function makeElem(cui:Zui, canvas:TCanvas, type:ElementType) {
var name = "";
var height = cui.t.ELEMENT_H;
var alignment = Align.Left;
switch (type) {
case ElementType.Text:
name = unique("Text", canvas.elements, "name");
case ElementType.Button:
name = unique("Button", canvas.elements, "name");
alignment = Align.Center;
case ElementType.Image:
name = unique("Image", canvas.elements, "name");
height = 100;
case ElementType.FRectangle:
name = unique("Filled_Rectangle", canvas.elements, "name");
height = 100;
case ElementType.FCircle:
name = unique("Filled_Circle", canvas.elements, "name");
case ElementType.Rectangle:
name = unique("Rectangle", canvas.elements, "name");
height = 100;
case ElementType.FTriangle:
name = unique("Filled_Triangle", canvas.elements, "name");
case ElementType.Triangle:
name = unique("Triangle", canvas.elements, "name");
case ElementType.Circle:
name = unique("Circle", canvas.elements, "name");
case ElementType.Check:
name = unique("Check", canvas.elements, "name");
case ElementType.Radio:
name = unique("Radio", canvas.elements, "name");
case ElementType.Combo:
name = unique("Combo", canvas.elements, "name");
case ElementType.Slider:
name = unique("Slider", canvas.elements, "name");
alignment = Align.Right;
case ElementType.TextInput:
name = unique("TextInput", canvas.elements, "name");
case ElementType.KeyInput:
name = unique("KeyInput", canvas.elements, "name");
case ElementType.TextArea:
name = unique("TextArea", canvas.elements, "name");
case ElementType.ProgressBar:
name = unique("Progress_bar", canvas.elements, "name");
case ElementType.CProgressBar:
name = unique("CProgress_bar", canvas.elements, "name");
case ElementType.Empty:
name = unique("Empty", canvas.elements, "name");
height = 100;
}
var elem:TElement = {
id: Canvas.getElementId(canvas),
type: type,
name: name,
event: "",
x: 0,
y: 0,
width: 150,
height: height,
rotation: 0,
text: "My " + name,
asset: "",
progress_at: 0,
progress_total: 100,
strength: 1,
alignment: cast(alignment, Int),
anchor: 0,
parent: null,
children: [],
visible: true
};
canvas.elements.push(elem);
return elem;
}
/**
* Generates a unique string for a given array based on the string s.
*
* @param s The string that is returned in a unique form
* @param data The array where the string should be unique
* @param elemAttr The name of the attribute of the data elements to be compared with the string.
* @param counter=-1 Internal use only, do not overwrite!
* @return String A unique string in the given array
*/
//TODO: Here
public static function unique(s:String, data:Array<Dynamic>, elemAttr:String, counter=-1): String {
var originalName = s;
// Reconstruct the original name
var split = s.lastIndexOf(".");
if (split != -1) {
// .001, .002...
var suffix = s.substring(split);
if (suffix.length == 4) {
originalName = s.substring(0, split);
}
}
for (elem in data) {
if (Reflect.getProperty(elem, elemAttr) == s) {
if (counter > -1) {
counter++;
var counterLen = Std.string(counter).length;
if (counterLen > 3) counterLen = 3;
var padding = ".";
for (i in 0...3 - counterLen) {
padding += "0";
}
return unique(originalName + padding + Std.string(counter), data, elemAttr, counter);
} else {
return unique(originalName, data, elemAttr, 0);
}
}
}
return s;
}
public static function moveElem(canvas:TCanvas, elem:TElement, d:Int) {
var ar = canvas.elements;
var i = ar.indexOf(elem);
var p = elem.parent;
while (true) {
i += d;
if (i < 0 || i >= ar.length) break;
if (ar[i].parent == p) {
ar.remove(elem);
ar.insert(i, elem);
break;
}
}
}
public static function removeElem(canvas:TCanvas, elem:TElement) {
if (elem.children != null) for (id in elem.children) removeElem(canvas, CanvasTools.elemById(canvas, id));
canvas.elements.remove(elem);
if (elem.parent != null) {
CanvasTools.elemById(canvas, elem.parent).children.remove(elem.id);
elem.parent = null;
}
}
public static function elemById(canvas: TCanvas, id: Int): TElement {
for (e in canvas.elements) if (e.id == id) return e;
return null;
}
public static function unparent(canvas:TCanvas, elem:TElement) {
var parent = CanvasTools.elemById(canvas, elem.parent);
if (parent != null) {
elem.x += Math.absx(canvas, parent);
elem.y += Math.absy(canvas, parent);
elem.parent = null;
parent.children.remove(elem.id);
}
}
public static function setParent(canvas:TCanvas, elem:TElement, parent:TElement) {
var oldParent = CanvasTools.elemById(canvas, elem.parent);
if (oldParent == parent) return;
unparent(canvas, elem); //Unparent first if we already have a parent
if (parent != null) { //Parent
if (parent.children == null) elem.children = [];
parent.children.push(elem.id);
elem.parent = parent.id;
elem.x -= Math.absx(canvas, parent);
elem.y -= Math.absy(canvas, parent);
}
}
public static function duplicateElem(canvas:TCanvas, elem:TElement, parentId:Null<Int> = null):TElement {
if (elem != null) {
if (parentId == null) parentId = elem.parent;
var dupe:TElement = {
id: Canvas.getElementId(canvas),
type: elem.type,
name: elem.name,
event: elem.event,
x: elem.x + 10,
y: elem.y + 10,
width: elem.width,
height: elem.height,
rotation: elem.rotation,
text: elem.text,
asset: elem.asset,
color: elem.color,
color_text: elem.color_text,
color_hover: elem.color_hover,
color_press: elem.color_press,
color_progress: elem.color_progress,
progress_at: elem.progress_at,
progress_total: elem.progress_total,
strength: elem.strength,
anchor: elem.anchor,
parent: parentId,
children: [],
visible: elem.visible
};
canvas.elements.push(dupe);
if (parentId != null) {
var parentElem = CanvasTools.elemById(canvas, parentId);
parentElem.children.push(dupe.id);
if (elem.parent != parentId) {
dupe.x = elem.x;
dupe.y = elem.y;
}
}
for(child in elem.children) {
duplicateElem(canvas, CanvasTools.elemById(canvas, child), dupe.id);
}
return dupe;
}
return null;
}
}

View File

@ -0,0 +1,94 @@
package lnx2d.tools;
// Kha
import kha.math.Vector2;
// Zui
import zui.Zui;
import leenkx.ui.Canvas.TCanvas;
import leenkx.ui.Canvas.TElement;
class Math {
public static inline function toDegrees(radians:Float):Float { return radians * 57.29578; }
public static inline function toRadians(degrees:Float):Float { return degrees * 0.0174532924; }
/**
* Calculates if the mouse is in the given hitbox rectangle.
*
* @param ui The Zui object of which this function should take the mouse position
* @param x The x position of the hitbox
* @param y The y position of the hitbox
* @param w The width of the hitbox
* @param h The height of the hitbox
* @param rotation The rotation of the hitbox in radians, default = 0.0
* @param center (Optional) The [x, y] coordinates of the center of rotation. If not given or null, use the center of the rectangle given by (x, y, w, h)
* @return Bool
*/
public static function hitbox(ui: Zui, x: Float, y: Float, w: Float, h: Float, rotation: Float = 0.0, ?center: Array<Float>):Bool {
if (center != null && center.length != 2) {
throw "lnx2d.tools.Math.hitbox(): 'center' argument must consist of two values!";
}
if (center == null) center = [x + w / 2, y + h / 2];
var rotatedInput:Vector2 = rotatePoint(ui.inputX, ui.inputY, center[0], center[1], -rotation);
return rotatedInput.x > x && rotatedInput.x < x + w && rotatedInput.y > y && rotatedInput.y < y + h;
}
public static function absx(canvas:TCanvas, e:TElement):Float {
if (e == null) return 0;
return e.x + absx(canvas, CanvasTools.elemById(canvas, e.parent));
}
public static function absy(canvas:TCanvas, e:TElement):Float {
if (e == null) return 0;
return e.y + absy(canvas, CanvasTools.elemById(canvas, e.parent));
}
public static function roundPrecision(v:Float, ?precision=0):Float {
v *= std.Math.pow(10, precision);
v = Std.int(v) * 1.0;
v /= std.Math.pow(10, precision);
return v;
}
public static function rotatePoint(pointX: Float, pointY: Float, centerX: Float, centerY: Float, angle:Float): Vector2 {
pointX -= centerX;
pointY -= centerY;
var x = pointX * std.Math.cos(angle) - pointY * std.Math.sin(angle);
var y = pointX * std.Math.sin(angle) + pointY * std.Math.cos(angle);
return new Vector2(centerX + x, centerY + y);
}
public static function calculateTransformDelta(ui:Zui, gSP:Bool, gUR:Bool, gS:Int, value:Float, ?offset=0.0):Float {
var precisionMode = ui.isKeyDown && ui.key == Main.prefs.keyMap.slowMovement;
var enabled = gSP != (ui.isKeyDown && (ui.key == Main.prefs.keyMap.gridInvert));
var useOffset = gUR != (ui.isKeyDown && (ui.key == Main.prefs.keyMap.gridInvertRelative));
if (!enabled) return precisionMode ? value / 2 : value;
// Round the delta value to steps of gridSize
value = std.Math.round(value / gS) * gS;
if (precisionMode) value /= 2;
// Apply an offset
if (useOffset && offset != 0) {
offset = offset % gS;
// Round to nearest grid position instead of rounding off
if (offset > gS / 2) {
offset = -(gS - offset);
}
value -= offset;
}
return value;
}
}

View File

@ -0,0 +1,615 @@
package lnx2d.ui;
// Kha
import kha.input.KeyCode;
// Zui
import zui.Id;
import zui.Zui;
import leenkx.ui.Popup;
import leenkx.ui.Canvas;
using zui.Ext;
using leenkx.ui.Ext;
// Editor
import lnx2d.tools.Math;
import lnx2d.tools.CanvasTools;
@:access(zui.Zui)
class UIProperties {
public static var hwin = Id.handle();
public static function renderProperties(ui:Zui, width:Int, canvas:TCanvas) {
if (ui.window(hwin, kha.System.windowWidth() - width, 0, width, kha.System.windowHeight())) {
var htab = Id.handle();
if (ui.tab(htab, "Project")) {
var hpath = Id.handle({text:""});
ui.textInput(hpath,"Current file");
if(hpath.changed){
Main.prefs.path = hpath.text;
}
if (ui.button("Save")) {
Assets.save(canvas);
}
if(ui.button("Load")){
Assets.load(function(c:TCanvas){
Main.inst.canvas = c;
hwin.redraws = 2;
});
}
if (ui.panel(Id.handle({selected: false}), "Canvas")) {
ui.indent();
if (ui.button("New")) {
canvas.elements = [];
Editor.selectedElem = null;
}
if (ui.isHovered) ui.tooltip("Create new canvas");
var handleName = Id.handle({text: canvas.name});
handleName.text = canvas.name;
ui.textInput(handleName, "Name", Right);
if (handleName.changed) {
// Themes file is called _themes.json, so canvases should not be named like that
if (handleName.text == "_themes") {
Popup.showMessage(new Zui(ui.ops), "Sorry!", "\"_themes\" is not a valid canvas name as it is reserved!");
handleName.text = canvas.name;
} else {
canvas.name = handleName.text;
}
}
ui.row([1/2, 1/2]);
var handlecw = Id.handle({text: canvas.width + ""});
var handlech = Id.handle({text: canvas.height + ""});
handlecw.text = canvas.width + "";
handlech.text = canvas.height + "";
var strw = ui.textInput(handlecw, "Width", Right);
var strh = ui.textInput(handlech, "Height", Right);
canvas.width = Std.parseInt(strw);
canvas.height = Std.parseInt(strh);
ui.unindent();
}
if (ui.panel(Id.handle({selected: true}), "Outliner")) {
ui.indent();
function drawList(h:zui.Zui.Handle, elem:TElement) {
var b = false;
// Highlight
if (Editor.selectedElem == elem) {
ui.g.color = 0xff205d9c;
ui.g.fillRect(ui._x, ui._y, ui._w, ui.t.ELEMENT_H * ui.SCALE());
ui.g.color = 0xffffffff;
}
var started = ui.getStarted();
// Select
if (started && !ui.inputDownR) Editor.selectedElem = elem;
// Parenting
if (started && ui.inputDownR) {
if (elem == Editor.selectedElem) CanvasTools.unparent(canvas, elem);
else CanvasTools.setParent(canvas, Editor.selectedElem, elem);
}
// Draw
if (elem.children != null && elem.children.length > 0) {
ui.row([1/13, 12/13]);
b = ui.panel(h.nest(elem.id, {selected: true}), "", true, false, false);
ui.text(elem.name);
}
else {
ui._x += 18; // Sign offset
ui.text(elem.name);
ui._x -= 18;
}
// Draw children
if (b) {
var i = elem.children.length;
while(i > 0) {
i--; //Iterate backwards to avoid reparenting issues.
var id = elem.children[elem.children.length - 1 - i];
ui.indent();
drawList(h, CanvasTools.elemById(canvas, id));
ui.unindent();
}
}
}
if (canvas.elements.length > 0) {
for (i in 0...canvas.elements.length) {
var elem = canvas.elements[canvas.elements.length - 1 - i];
if (elem.parent == null) drawList(Id.handle(), elem);
}
ui.row([1/4, 1/4, 1/4, 1/4]);
if (ui.button("Up") && Editor.selectedElem != null) {
CanvasTools.moveElem(canvas, Editor.selectedElem, 1);
}
if (ui.isHovered) ui.tooltip("Move element up");
if (ui.button("Down") && Editor.selectedElem != null) {
CanvasTools.moveElem(canvas, Editor.selectedElem,-1);
}
if (ui.isHovered) ui.tooltip("Move element down");
if (ui.button("Remove") && Editor.selectedElem != null) {
CanvasTools.removeElem(canvas, Editor.selectedElem);
Editor.selectedElem = null;
}
if (ui.isHovered) ui.tooltip("Delete element");
if (ui.button("Duplicate") && Editor.selectedElem != null) {
Editor.selectedElem = CanvasTools.duplicateElem(canvas, Editor.selectedElem);
}
if (ui.isHovered) ui.tooltip("Create duplicate of element");
}
ui.unindent();
}
if (Editor.selectedElem != null) {
var elem = Editor.selectedElem;
var id = elem.id;
if (ui.panel(Id.handle({selected: true}), "Properties")) {
ui.indent();
elem.visible = ui.check(Id.handle().nest(id, {selected: elem.visible == null ? true : elem.visible}), "Visible");
elem.name = ui.textInput(Id.handle().nest(id, {text: elem.name}), "Name", Right);
elem.text = ui.textInput(Id.handle().nest(id, {text: elem.text}), "Text", Right);
ui.row([1/4, 1/4, 1/4, 1/4]);
var handlex = Id.handle().nest(id, {text: elem.x + ""});
var handley = Id.handle().nest(id, {text: elem.y + ""});
// if (drag) {
handlex.text = elem.x + "";
handley.text = elem.y + "";
// }
var strx = ui.textInput(handlex, "X", Right);
var stry = ui.textInput(handley, "Y", Right);
elem.x = Std.parseFloat(strx);
elem.y = Std.parseFloat(stry);
// ui.row([1/2, 1/2]);
var handlew = Id.handle().nest(id, {text: elem.width + ""});
var handleh = Id.handle().nest(id, {text: elem.height + ""});
// if (drag) {
handlew.text = elem.width + "";
handleh.text = elem.height + "";
// }
var strw = ui.textInput(handlew, "W", Right);
var strh = ui.textInput(handleh, "H", Right);
elem.width = Std.int(Std.parseFloat(strw));
elem.height = Std.int(Std.parseFloat(strh));
if (elem.type == ElementType.Rectangle || elem.type == ElementType.Circle || elem.type == ElementType.Triangle || elem.type == ElementType.ProgressBar || elem.type == ElementType.CProgressBar){
var handles = Id.handle().nest(id, {text: elem.strength+""});
var strs = ui.textInput(handles, "Line Strength", Right);
elem.strength = Std.int(Std.parseFloat(strs));
}
if (elem.type == ElementType.ProgressBar || elem.type == ElementType.CProgressBar){
var handlep = Id.handle().nest(id, {value: elem.progress_at});
var slp = ui.slider(handlep, "Progress", 0.0, elem.progress_total, true, 1);
var handlespt = Id.handle().nest(id, {text: elem.progress_total+""});
var strpt = ui.textInput(handlespt, "Total Progress", Right);
elem.progress_total = Std.int(Std.parseFloat(strpt));
elem.progress_at = Std.int(slp);
}
var handlerot = Id.handle().nest(id, {value: Math.roundPrecision(Math.toDegrees(elem.rotation == null ? 0 : elem.rotation), 2)});
handlerot.value = Math.roundPrecision(Math.toDegrees(elem.rotation), 2);
// Small fix for radian/degree precision errors
if (handlerot.value >= 360) handlerot.value = 0;
elem.rotation = Math.toRadians(ui.slider(handlerot, "Rotation", 0.0, 360.0, true));
var assetPos = ui.combo(Id.handle().nest(id, {position: Assets.getAssetIndex(canvas, elem.asset)}), Assets.getEnumTexts(), "Asset", true, Right);
elem.asset = Assets.getEnumTexts()[assetPos];
ui.unindent();
}
if (ui.panel(Id.handle({selected: false}), "Color")){
ui.indent();
function drawColorSelection(idMult: Int, color:Null<Int>, defaultColor:Int) {
ui.row([1/2, 1/2]);
var handleCol = Id.handle().nest(id).nest(idMult, {color: Canvas.getColor(color, defaultColor)});
ui.colorField(handleCol, true);
if (handleCol.changed) {
color = handleCol.color;
}
// Follow theme color
if (ui.button("Reset") || color == null) {
color = null;
handleCol.color = defaultColor;
handleCol.changed = false;
}
return color;
}
var canvasTheme = Canvas.getTheme(canvas.theme);
switch(elem.type) {
case Text:
ui.text("Text:");
elem.color_text = drawColorSelection(1, elem.color_text, canvasTheme.TEXT_COL);
case Button:
ui.text("Text:");
elem.color_text = drawColorSelection(1, elem.color_text, canvasTheme.BUTTON_TEXT_COL);
ui.text("Background:");
elem.color = drawColorSelection(2, elem.color, canvasTheme.BUTTON_COL);
ui.text("On Hover:");
elem.color_hover = drawColorSelection(3, elem.color_hover, canvasTheme.BUTTON_HOVER_COL);
ui.text("On Pressed:");
elem.color_press = drawColorSelection(4, elem.color_press, canvasTheme.BUTTON_PRESSED_COL);
case Rectangle, FRectangle, Circle, FCircle, Triangle, FTriangle:
ui.text("Color:");
elem.color = drawColorSelection(1, elem.color, canvasTheme.BUTTON_COL);
case ProgressBar, CProgressBar:
ui.text("Progress:");
elem.color_progress = drawColorSelection(1, elem.color_progress, canvasTheme.TEXT_COL);
ui.text("Background:");
elem.color = drawColorSelection(2, elem.color, canvasTheme.BUTTON_COL);
case Check, TextInput, KeyInput, Combo, Slider:
ui.text("Text:");
elem.color_text = drawColorSelection(1, elem.color_text, canvasTheme.TEXT_COL);
ui.text("Background:");
elem.color = drawColorSelection(2, elem.color, canvasTheme.BUTTON_COL);
ui.text("On Hover:");
elem.color_hover = drawColorSelection(3, elem.color_hover, canvasTheme.BUTTON_HOVER_COL);
default:
ui.text("This element type has no color settings!");
}
ui.unindent();
}
if (ui.panel(Id.handle({selected: false}), "Align")) {
ui.indent();
var alignmentHandle = Id.handle().nest(id, {position: elem.alignment});
ui.row([1/3, 1/3, 1/3]);
ui.radio(alignmentHandle, 0, "Left");
ui.radio(alignmentHandle, 1, "Center");
ui.radio(alignmentHandle, 2, "Right");
Editor.selectedElem.alignment = alignmentHandle.position;
ui.unindent();
}
if (ui.panel(Id.handle({selected: false}), "Anchor")) {
ui.indent();
var hanch = Id.handle().nest(id, {position: elem.anchor});
ui.row([4/11,3/11,4/11]);
ui.radio(hanch, 0, "Top-Left");
ui.radio(hanch, 1, "Top");
ui.radio(hanch, 2, "Top-Right");
ui.row([4/11,3/11,4/11]);
ui.radio(hanch, 3, "Left");
ui.radio(hanch, 4, "Center");
ui.radio(hanch, 5, "Right");
ui.row([4/11,3/11,4/11]);
ui.radio(hanch, 6, "Bot-Left");
ui.radio(hanch, 7, "Bottom");
ui.radio(hanch, 8, "Bot-Right");
elem.anchor = hanch.position;
ui.unindent();
}
if (ui.panel(Id.handle({selected: false}), "Script")) {
ui.indent();
elem.event = ui.textInput(Id.handle().nest(id, {text: elem.event}), "Event", Right);
ui.unindent();
}
if (ui.panel(Id.handle({selected: false}), "Timeline")) {
// ui.indent();
// ui.row([1/2,1/2]);
// ui.button("Insert");
// ui.button("Remove");
// ui.unindent();
}
}
}
if (ui.tab(htab, "Themes")) {
// Must be accesible all over this place because when deleting themes,
// the color handle's child handle at that theme index must be reset.
var handleThemeColor = Id.handle();
var handleThemeName = Id.handle();
var iconSize = 16;
function drawList(h:zui.Zui.Handle, theme:zui.Themes.TTheme) {
// Highlight
if (Editor.selectedTheme == theme) {
ui.g.color = 0xff205d9c;
ui.g.fillRect(0, ui._y, ui._windowW, ui.t.ELEMENT_H * ui.SCALE());
ui.g.color = 0xffffffff;
}
// Highlight default theme
if (theme == Canvas.getTheme(canvas.theme)) {
var iconMargin = (ui.t.BUTTON_H - iconSize) / 2;
ui.g.drawSubImage(kha.Assets.images.icons, ui._x + iconMargin, ui._y + iconMargin, 0, 0, 16, 16);
}
var started = ui.getStarted();
// Select
if (started && !ui.inputDownR) Editor.selectedTheme = theme;
// Draw
ui._x += iconSize; // Icon offset
ui.text(theme.NAME);
ui._x -= iconSize;
}
for (theme in Canvas.themes) drawList(Id.handle(), theme);
ui.row([1/4, 1/4, 1/4, 1/4]);
if (ui.button("Add")) {
var newTheme = Reflect.copy(leenkx.ui.Themes.light);
newTheme.NAME = CanvasTools.unique("New Theme", Canvas.themes, "NAME");
Canvas.themes.push(newTheme);
Editor.selectedTheme = newTheme;
}
if (Editor.selectedTheme == null) ui.enabled = false;
if (ui.button("Copy")) {
var newTheme = Reflect.copy(Editor.selectedTheme);
newTheme.NAME = CanvasTools.unique(newTheme.NAME, Canvas.themes, "NAME");
Canvas.themes.push(newTheme);
Editor.selectedTheme = newTheme;
}
ui.enabled = true;
if (Editor.selectedTheme == null) ui.enabled = false;
var hName = handleThemeName.nest(Canvas.themes.indexOf(Editor.selectedTheme));
if (ui.button("Rename")) {
hName.text = Editor.selectedTheme.NAME;
Popup.showCustom(
new Zui(ui.ops),
function(ui:Zui) {
ui.textInput(hName);
if (ui.button("OK")) {
Popup.show = false;
hwin.redraws = 2;
}
},
Std.int(ui.inputX), Std.int(ui.inputY), 200, 60);
}
if (Editor.selectedTheme != null) {
var name = Editor.selectedTheme.NAME;
if (hName.changed && Editor.selectedTheme.NAME != hName.text) {
name = CanvasTools.unique(hName.text, Canvas.themes, "NAME");
if (canvas.theme == Editor.selectedTheme.NAME) {
canvas.theme = name;
}
Editor.selectedTheme.NAME = name;
}
}
ui.enabled = true;
if (Canvas.themes.length == 1 || Editor.selectedTheme == null) ui.enabled = false;
if (ui.button("Delete")) {
handleThemeColor.unnest(Canvas.themes.indexOf(Editor.selectedTheme));
handleThemeName.unnest(Canvas.themes.indexOf(Editor.selectedTheme));
Canvas.themes.remove(Editor.selectedTheme);
// Canvas default theme was deleted
if (Canvas.getTheme(canvas.theme) == null) {
canvas.theme = Canvas.themes[0].NAME;
}
Editor.selectedTheme = null;
}
ui.enabled = true;
if (Editor.selectedTheme == null) ui.enabled = false;
if (ui.button("Apply to Canvas")) canvas.theme = Editor.selectedTheme.NAME;
ui.enabled = true;
if (Editor.selectedTheme == null) {
ui.text("Please select a Theme!");
} else {
// A Map would be way better, but its order is not guaranteed.
var themeColorOptions:Array<Array<String>> = [
["Text", "TEXT_COL"],
["Elements", "BUTTON_COL", "BUTTON_TEXT_COL", "BUTTON_HOVER_COL", "BUTTON_PRESSED_COL", "ACCENT_COL", "ACCENT_HOVER_COL", "ACCENT_SELECT_COL"],
["Other", "PANEL_BG_COL"],
];
for (idxCategory in 0...themeColorOptions.length) {
if (ui.panel(Id.handle().nest(idxCategory, {selected: true}), themeColorOptions[idxCategory][0])) {
ui.indent();
var attributes = themeColorOptions[idxCategory].slice(1);
for (idxElemAttribs in 0...attributes.length) {
var themeColorOption = attributes[idxElemAttribs];
// is getField() better?
ui.row([2/3, 1/3]);
ui.text(themeColorOption);
var themeColor = Reflect.getProperty(Editor.selectedTheme, themeColorOption);
var handleCol = handleThemeColor.nest(Canvas.themes.indexOf(Editor.selectedTheme)).nest(idxCategory).nest(idxElemAttribs, {color: themeColor});
var col = ui.colorField(handleCol, true);
Reflect.setProperty(Editor.selectedTheme, themeColorOption, col);
}
ui.unindent();
}
}
}
}
if (ui.tab(htab, "Assets")) {
if (ui.button("Import Asset")) {
Editor.showFiles = true;
Editor.foldersOnly = false;
Editor.filesDone = function(path:String) {
path = StringTools.rtrim(path);
path = Path.toRelative(path, Main.cwd);
Assets.importAsset(canvas, path);
}
}
if (canvas.assets.length > 0) {
ui.text("(Drag and drop assets to canvas)", zui.Zui.Align.Center);
if (ui.panel(Id.handle({selected: true}), "Imported Assets")) {
ui.indent();
var i = canvas.assets.length - 1;
while (i >= 0) {
var asset = canvas.assets[i];
if (!Assets.isPathFont(asset.name) && ui.image(Assets.getImage(asset)) == State.Started) {
Editor.dragAsset = asset;
} else if (Assets.isPathFont(asset.name)) {
var oldFont = ui.ops.font;
var oldFontSize = ui.fontSize;
ui.ops.font = Assets.getFont(asset);
ui.fontSize = Std.int(32 * ui.SCALE());
ui.text(asset.name);
ui.ops.font = oldFont;
ui.fontSize = oldFontSize;
}
ui.row([7/8, 1/8]);
asset.name = ui.textInput(Id.handle().nest(asset.id, {text: asset.name}), "", Right);
Editor.assetNames[i + 1] = asset.name; // assetNames[0] == ""
if (ui.button("X")) {
Assets.getImage(asset).unload();
canvas.assets.splice(i, 1);
Editor.assetNames.splice(i + 1, 1);
}
i--;
}
ui.unindent();
}
}
else ui.text("(Drag and drop images and fonts here)", zui.Zui.Align.Center);
}
if (ui.tab(htab, "Preferences")) {
if (ui.panel(Id.handle({selected: true}), "Application")) {
ui.indent();
var hscale = Id.handle({value: 1.0});
ui.slider(hscale, "UI Scale", 0.5, 4.0, true);
if (hscale.changed && !ui.inputDown) {
ui.setScale(hscale.value);
Editor.windowW = Std.int(Editor.defaultWindowW * hscale.value);
}
Main.prefs.window_vsync = ui.check(Id.handle({selected: true}), "VSync");
ui.unindent();
}
if (ui.panel(Id.handle({selected: true}), "Grid")) {
ui.indent();
var gsize = Id.handle({value: 20});
ui.slider(gsize, "Grid Size", 1, 128, true, 1);
if (gsize.changed) {
Editor.gridSize = Std.int(gsize.value);
Editor.redrawGrid = true;
}
Editor.gridSnapPos = ui.check(Id.handle({selected: true}), "Grid Snap Position");
if (ui.isHovered) ui.tooltip("Snap the element's position to the grid");
Editor.gridSnapBounds = ui.check(Id.handle({selected: false}), "Grid Snap Bounds");
if (ui.isHovered) ui.tooltip("Snap the element's bounds to the grid");
Editor.gridUseRelative = ui.check(Id.handle({selected: true}), "Use Relative Grid");
if (ui.isHovered) ui.tooltip("Use a grid that's relative to the selected element");
Editor.useRotationSteps = ui.check(Id.handle({selected: false}), "Use Fixed Rotation Steps");
if (ui.isHovered) ui.tooltip("Rotate elements by a fixed step size");
var rotStepHandle = Id.handle({value: 15});
if (Editor.useRotationSteps) {
ui.slider(rotStepHandle, "Rotation Step Size", 1, 180, true, 1);
if (rotStepHandle.changed) {
Editor.rotationSteps = Math.toRadians(rotStepHandle.value);
}
}
ui.unindent();
}
// if (ui.button("Save")) {
// #if kha_krom
// Krom.fileSaveBytes("config.lnx", haxe.io.Bytes.ofString(haxe.Json.stringify(leenkx.data.Config.raw)).getData());
// #end
// }
// ui.text("leenkx2d");
if (ui.panel(Id.handle({selected: true}), "Shortcuts")){
ui.indent();
ui.row([1/2, 1/2]);
ui.text("Select");
var selectMouseHandle = Id.handle({position: 0});
ui.combo(selectMouseHandle, ["Left Click", "Right Click"], "");
if (ui.isHovered) ui.tooltip("Mouse button used for element selection.");
if (selectMouseHandle.changed) {
Main.prefs.keyMap.selectMouseButton = ["Left", "Right"][selectMouseHandle.position];
}
ui.separator(8, false);
ui.row([1/2, 1/2]);
ui.text("Grab");
Main.prefs.keyMap.grabKey = ui.keyInput(Id.handle({value: KeyCode.G}), "Key");
if (ui.isHovered) ui.tooltip("Key used for grabbing elements");
ui.row([1/2, 1/2]);
ui.text("Rotate");
Main.prefs.keyMap.rotateKey = ui.keyInput(Id.handle({value: KeyCode.R}), "Key");
if (ui.isHovered) ui.tooltip("Key used for rotating elements");
ui.row([1/2, 1/2]);
ui.text("Size");
Main.prefs.keyMap.sizeKey = ui.keyInput(Id.handle({value: KeyCode.S}), "Key");
if (ui.isHovered) ui.tooltip("Key used for resizing elements");
ui.separator(8, false);
ui.row([1/2, 1/2]);
ui.text("Precision Transform");
Main.prefs.keyMap.slowMovement = ui.keyInput(Id.handle({value: KeyCode.Shift}), "Key");
if (ui.isHovered) ui.tooltip("More precise transformations");
ui.row([1/2, 1/2]);
ui.text("Invert Grid");
Main.prefs.keyMap.gridInvert = ui.keyInput(Id.handle({value: KeyCode.Control}), "Key");
if (ui.isHovered) ui.tooltip("Invert the grid setting");
ui.row([1/2, 1/2]);
ui.text("Invert Rel. Grid");
Main.prefs.keyMap.gridInvertRelative = ui.keyInput(Id.handle({value: KeyCode.Alt}), "Key");
if (ui.isHovered) ui.tooltip("Invert the relative grid setting");
ui.unindent();
}
if (ui.panel(Id.handle({selected: false}), "Console")) {
ui.indent();
//ui.text(lastTrace);
ui.text("Mouse X: "+ ui.inputX);
ui.text("Mouse Y: "+ ui.inputY);
ui.unindent();
}
}
}
}
}

View File

@ -0,0 +1,84 @@
package lnx2d.ui;
// Zui
import zui.Id;
import zui.Zui;
import leenkx.ui.Canvas.TCanvas;
import leenkx.ui.Canvas.ElementType;
// Editor
import lnx2d.tools.CanvasTools;
class UIToolBar {
public static function renderToolbar(ui:Zui, cui:Zui, canvas: TCanvas, width:Int) {
if (ui.window(Id.handle(), 0, 0, width, kha.System.windowHeight())) {
ui.text("Add Elements:");
if (ui.panel(Id.handle({selected: true}), "Basic")) {
ui.indent();
drawToolbarItem(ui, cui, canvas, "Empty", Empty, "Create an empty element");
drawToolbarItem(ui, cui, canvas, "Text", Text, "Create a text element");
drawToolbarItem(ui, cui, canvas, "Image", Image, "Create an image element");
ui.unindent();
}
// ui.button("VLayout");
// ui.button("HLayout");
if (ui.panel(Id.handle({selected: true}), "Buttons")){
ui.indent();
drawToolbarItem(ui, cui, canvas, "Button", Button, "Create a button element");
drawToolbarItem(ui, cui, canvas, "Check", Check, "Create a checkbox element");
drawToolbarItem(ui, cui, canvas, "Radio", Radio, "Create a inline-radio element");
ui.unindent();
}
if (ui.panel(Id.handle({selected: true}), "Inputs")){
ui.indent();
drawToolbarItem(ui, cui, canvas, "Text Input", TextInput, "Create a text input element");
drawToolbarItem(ui, cui, canvas, "Text Area", TextArea, "Create a text area element");
drawToolbarItem(ui, cui, canvas, "Key Input", KeyInput, "Create a key input element");
drawToolbarItem(ui, cui, canvas, "Combo Box", Combo, "Create a combo box element");
drawToolbarItem(ui, cui, canvas, "Slider", Slider, "Create a slider element");
ui.unindent();
}
if (ui.panel(Id.handle({selected: true}), "Shapes")){
ui.indent();
drawToolbarItem(ui, cui, canvas, "Rect", Rectangle, "Create a rectangle shape element");
drawToolbarItem(ui, cui, canvas, "Fill Rect", FRectangle, "Create a filled rectangle shape element");
drawToolbarItem(ui, cui, canvas, "Circle", Circle, "Create a circle shape element");
drawToolbarItem(ui, cui, canvas, "Fill Circle", FCircle, "Create a filled circle shape element");
drawToolbarItem(ui, cui, canvas, "Triangle", Triangle, "Create a triangle shape element");
drawToolbarItem(ui, cui, canvas, "Fill Triangle", FTriangle, "Create a filled triangle shape element");
ui.unindent();
}
if (ui.panel(Id.handle({selected: true}), "Progress Bars")){
ui.indent();
drawToolbarItem(ui, cui, canvas, "RectPB", ProgressBar, "Create a rectangular progress bar");
drawToolbarItem(ui, cui, canvas, "CircularPB", CProgressBar, "Create a circular progress bar");
ui.unindent();
}
}
}
static function drawToolbarItem(ui: Zui, cui: Zui, canvas: TCanvas, label: String, elemType: ElementType, tooltip: String) {
if (ui.button(label)) {
Editor.selectedElem = CanvasTools.makeElem(cui, canvas, elemType);
}
if (ui.isHovered) ui.tooltip(tooltip);
}
}