2025-01-22 16:18:30 +01:00

471 lines
14 KiB
Haxe

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