package zui;

// Immediate Mode UI Library
// https://github.com/leenkx3d/zui

import kha.input.Mouse;
import kha.input.Pen;
import kha.input.Surface;
import kha.input.Keyboard;
import kha.input.KeyCode;
import kha.graphics2.Graphics;

@:structInit
typedef ZuiOptions = {
	font: kha.Font,
	?theme: zui.Themes.TTheme,
	?khaWindowId: Int,
	?scaleFactor: Float,
	?autoNotifyInput: Bool,
	?color_wheel: kha.Image,
	?black_white_gradient: kha.Image,
}

class Zui {
	public var isScrolling = false; // Use to limit other activities
	public var isTyping = false;
	public var enabled = true; // Current element state
	public var isStarted = false;
	public var isPushed = false;
	public var isHovered = false;
	public var isReleased = false;
	public var changed = false; // Global elements change check
	public var imageInvertY = false;
	public var scrollEnabled = true;
	public var alwaysRedraw = false; // Hurts performance
	public var highlightOnSelect = true; // Highlight text edit contents on selection
	public var tabSwitchEnabled = true; // Allow switching focus to the next element by pressing tab
	public var textColoring: TTextColoring = null; // Set coloring scheme for drawString() calls
	public var windowBorderTop = 0;
	public var windowBorderBottom = 0;
	public var windowBorderLeft = 0;
	public var windowBorderRight = 0;
	var highlightFullRow = false;
	public static var current: Zui = null;
	public static var onBorderHover: Handle->Int->Void = null; // Mouse over window border, use for resizing
	public static var onTextHover: Void->Void = null; // Mouse over text input, use to set I-cursor
	public static var onDeselectText: Void->Void = null; // Text editing finished
	public static var alwaysRedrawWindow = true; // Redraw cached window texture each frame or on changes only
	public static var keyRepeat = true; // Emulate key repeat for non-character keys
	public static var dynamicGlyphLoad = true; // Allow text input fields to push new glyphs into the font atlas
	public static var touchScroll = false; // Pan with finger to scroll
	public static var touchHold = false; // Touch and hold finger for right click
	public static var touchTooltip = false; // Show extra tooltips above finger / on-screen keyboard
	var touchHoldActivated = false;
	var sliderTooltip = false;
	var sliderTooltipX = 0.0;
	var sliderTooltipY = 0.0;
	var sliderTooltipW = 0.0;
	static var keyRepeatTime = 0.0;

	public var inputRegistered = false;
	public var inputEnabled = true;
	public var inputX: Float; // Input position
	public var inputY: Float;
	public var inputStartedX: Float;
	public var inputStartedY: Float;
	public var inputDX: Float; // Delta
	public var inputDY: Float;
	public var inputWheelDelta = 0;
	public var inputStarted: Bool; // Buttons
	public var inputStartedR: Bool;
	public var inputReleased: Bool;
	public var inputReleasedR: Bool;
	public var inputDown: Bool;
	public var inputDownR: Bool;
	public var penInUse: Bool;
	public var isKeyPressed = false; // Keys
	public var isKeyDown = false;
	public var isShiftDown = false;
	public var isCtrlDown = false;
	public var isAltDown = false;
	public var isADown = false;
	public var isBackspaceDown = false;
	public var isDeleteDown = false;
	public var isEscapeDown = false;
	public var isReturnDown = false;
	public var isTabDown = false;
	public var key: Null<KeyCode> = null;
	public var char: String;
	static var textToPaste = "";
	static var textToCopy = "";
	static var isCut = false;
	static var isCopy = false;
	static var isPaste = false;
	static var copyReceiver: Zui = null;
	static var copyFrame = 0;

	var inputStartedTime = 0.0;
	var cursorX = 0; // Text input
	var highlightAnchor = 0;
	var ratios: Array<Float>; // Splitting rows
	var curRatio = -1;
	var xBeforeSplit: Float;
	var wBeforeSplit: Int;

	public var g: Graphics; // Drawing
	public var t: zui.Themes.TTheme;
	public var ops: ZuiOptions;
	var globalG: Graphics;
	var rtTextPipeline: kha.graphics4.PipelineState; // Rendering text into rendertargets

	var fontSize: Int;
	var fontOffsetY: Float; // Precalculated offsets
	var arrowOffsetX: Float;
	var arrowOffsetY: Float;
	var titleOffsetX: Float;
	var buttonOffsetY: Float;
	var checkOffsetX: Float;
	var checkOffsetY: Float;
	var checkSelectOffsetX: Float;
	var checkSelectOffsetY: Float;
	var radioOffsetX: Float;
	var radioOffsetY: Float;
	var radioSelectOffsetX: Float;
	var radioSelectOffsetY: Float;
	var scrollAlign = 0.0;
	var imageScrollAlign = true;

	var _x: Float; // Cursor(stack) position
	var _y: Float;
	var _w: Int;
	var _h: Int;

	var _windowX = 0.0; // Window state
	var _windowY = 0.0;
	var _windowW: Float;
	var _windowH: Float;
	var currentWindow: Handle;
	var windowEnded = true;
	var scrollHandle: Handle = null; // Window or slider being scrolled
	var dragHandle: Handle = null; // Window being dragged
	var windowHeaderW = 0.0;
	var windowHeaderH = 0.0;
	var restoreX = -1.0;
	var restoreY = -1.0;

	var textSelectedHandle: Handle = null;
	var textSelected: String;
	var submitTextHandle: Handle = null;
	var textToSubmit = "";
	var tabPressed = false;
	var tabPressedHandle: Handle = null;
	var comboSelectedHandle: Handle = null;
	var comboSelectedWindow: Handle = null;
	var comboSelectedAlign: Align;
	var comboSelectedTexts: Array<String>;
	var comboSelectedLabel: String;
	var comboSelectedX: Int;
	var comboSelectedY: Int;
	var comboSelectedW: Int;
	var comboSearchBar = false;
	var submitComboHandle: Handle = null;
	var comboToSubmit = 0;
	var comboInitialValue = 0;
	var tooltipText = "";
	var tooltipImg: kha.Image = null;
	var tooltipImgMaxWidth: Null<Int> = null;
	var tooltipInvertY = false;
	var tooltipX = 0.0;
	var tooltipY = 0.0;
	var tooltipShown = false;
	var tooltipWait = false;
	var tooltipTime = 0.0;
	var tabNames: Array<String> = null; // Number of tab calls since window begin
	var tabColors: Array<Int> = null;
	var tabHandle: Handle = null;
	var tabScroll = 0.0;
	var tabVertical = false;
	var sticky = false;
	var scissor = false;

	var elementsBaked = false;
	var checkSelectImage: kha.Image = null;

	public function new(ops: ZuiOptions) {
		if (ops.theme == null) ops.theme = Themes.dark;
		t = ops.theme;
		if (ops.khaWindowId == null) ops.khaWindowId = 0;
		if (ops.scaleFactor == null) ops.scaleFactor = 1.0;
		if (ops.autoNotifyInput == null) ops.autoNotifyInput = true;
		this.ops = ops;
		setScale(ops.scaleFactor);
		if (ops.autoNotifyInput) registerInput();
		if (copyReceiver == null) {
			copyReceiver = this;
			kha.System.notifyOnCutCopyPaste(onCut, onCopy, onPaste);
			kha.System.notifyOnFrames(function(frames: Array<kha.Framebuffer>) {
				// Set isCopy to false on next frame
				if ((isCopy || isPaste) && ++copyFrame > 1) {
					isCopy = isCut = isPaste = false;
				}
				// Clear unpasted text on next frame
				else if (copyFrame > 1 && ++copyFrame > 2) {
					copyFrame = 0;
					textToPaste = "";
				}
			});
		}
		var rtTextVS = kha.graphics4.Graphics2.createTextVertexStructure();
		rtTextPipeline = kha.graphics4.Graphics2.createTextPipeline(rtTextVS);
		rtTextPipeline.alphaBlendSource = BlendOne;
		rtTextPipeline.compile();
	}

	public function setScale(factor: Float) {
		ops.scaleFactor = factor;
		fontSize = FONT_SIZE();
		var fontHeight = ops.font.height(fontSize);
		fontOffsetY = (ELEMENT_H() - fontHeight) / 2; // Precalculate offsets
		arrowOffsetY = (ELEMENT_H() - ARROW_SIZE()) / 2;
		arrowOffsetX = arrowOffsetY;
		titleOffsetX = (arrowOffsetX * 2 + ARROW_SIZE()) / SCALE();
		buttonOffsetY = (ELEMENT_H() - BUTTON_H()) / 2;
		checkOffsetY = (ELEMENT_H() - CHECK_SIZE()) / 2;
		checkOffsetX = checkOffsetY;
		checkSelectOffsetY = (CHECK_SIZE() - CHECK_SELECT_SIZE()) / 2;
		checkSelectOffsetX = checkSelectOffsetY;
		radioOffsetY = (ELEMENT_H() - CHECK_SIZE()) / 2;
		radioOffsetX = radioOffsetY;
		radioSelectOffsetY = (CHECK_SIZE() - CHECK_SELECT_SIZE()) / 2;
		radioSelectOffsetX = radioSelectOffsetY;
		elementsBaked = false;
	}

	function bakeElements() {
		if (checkSelectImage != null) {
			checkSelectImage.unload();
		}
		checkSelectImage = kha.Image.createRenderTarget(Std.int(CHECK_SELECT_SIZE()), Std.int(CHECK_SELECT_SIZE()), null, NoDepthAndStencil, 1);
		var g = checkSelectImage.g2;
		g.begin(true, 0x00000000);
		g.color = t.ACCENT_SELECT_COL;
		g.drawLine(0, 0, checkSelectImage.width, checkSelectImage.height, 2 * SCALE());
		g.drawLine(checkSelectImage.width, 0, 0, checkSelectImage.height, 2 * SCALE());
		g.end();
		elementsBaked = true;
	}

	public function remove() { // Clean up
		if (ops.autoNotifyInput) unregisterInput();
	}

	public function registerInput() {
		if (inputRegistered) return;
		Mouse.get().notifyWindowed(ops.khaWindowId, onMouseDown, onMouseUp, onMouseMove, onMouseWheel);
		if (Pen.get() != null) Pen.get().notify(onPenDown, onPenUp, onPenMove);
		Keyboard.get().notify(onKeyDown, onKeyUp, onKeyPress);
		#if (kha_android || kha_ios)
		if (Surface.get() != null) Surface.get().notify(onTouchDown, onTouchUp, onTouchMove);
		#end
		// Reset mouse delta on foreground
		kha.System.notifyOnApplicationState(function() { inputDX = inputDY = 0; }, null, null, null, null);
		inputRegistered = true;
	}

	public function unregisterInput() {
		if (!inputRegistered) return;
		Mouse.get().removeWindowed(ops.khaWindowId, onMouseDown, onMouseUp, onMouseMove, onMouseWheel);
		if (Pen.get() != null) Pen.get().remove(onPenDown, onPenUp, onPenMove);
		Keyboard.get().remove(onKeyDown, onKeyUp, onKeyPress);
		#if (kha_android || kha_ios)
		if (Surface.get() != null) Surface.get().remove(onTouchDown, onTouchUp, onTouchMove);
		#end
		endInput();
		isShiftDown = isCtrlDown = isAltDown = false;
		inputX = inputY = 0;
		inputRegistered = false;
	}

	public function begin(g: Graphics) { // Begin UI drawing
		if (!elementsBaked) bakeElements();
		changed = false;
		globalG = g;
		current = this;
		_x = 0; // Reset cursor
		_y = 0;
		_w = 0;
		_h = 0;
	}

	public function end(last = true) { // End drawing
		if (!windowEnded) endWindow();
		drawCombo(); // Handle active combo
		drawTooltip(true);
		tabPressedHandle = null;
		if (last) endInput();
	}

	public function beginRegion(g: Graphics, x: Int, y: Int, w: Int) {
		if (!elementsBaked) {
			g.end();
			bakeElements();
			g.begin(false);
		}
		changed = false;
		globalG = g;
		this.g = g;
		currentWindow = null;
		tooltipText = "";
		tooltipImg = null;
		_windowX = 0;
		_windowY = 0;
		_windowW = w;
		_x = x;
		_y = y;
		_w = w;
	}

	public function endRegion(last = true) {
		drawTooltip(false);
		tabPressedHandle = null;
		if (last) endInput();
	}

	// Sticky region ignores window scrolling
	public function beginSticky() {
		sticky = true;
		_y -= currentWindow.scrollOffset;
	}

	public function endSticky() {
		sticky = false;
		scissor = true;
		g.scissor(0, Std.int(_y), Std.int(_windowW), Std.int(_windowH - _y));
		windowHeaderH += _y - windowHeaderH;
		_y += currentWindow.scrollOffset;
		isHovered = false;
	}

	function endInput() {
		isKeyPressed = false;
		inputStarted = false;
		inputStartedR = false;
		inputReleased = false;
		inputReleasedR = false;
		inputDX = 0;
		inputDY = 0;
		inputWheelDelta = 0;
		penInUse = false;
		if (keyRepeat && isKeyDown && kha.Scheduler.time() - keyRepeatTime > 0.05) {
			if (key == KeyCode.Backspace || key == KeyCode.Delete || key == KeyCode.Left || key == KeyCode.Right || key == KeyCode.Up || key == KeyCode.Down) {
				keyRepeatTime = kha.Scheduler.time();
				isKeyPressed = true;
			}
		}
		if (touchHold && inputDown && inputX == inputStartedX && inputY == inputStartedY && inputStartedTime > 0 && kha.Scheduler.time() - inputStartedTime > 0.7) {
			touchHoldActivated = true;
			inputReleasedR = true;
			inputStartedTime = 0;
		}
	}

	function inputChanged(): Bool {
		return inputDX != 0 || inputDY != 0 || inputWheelDelta != 0 || inputStarted || inputStartedR || inputReleased || inputReleasedR || inputDown || inputDownR || isKeyPressed;
	}

	public function windowDirty(handle: Handle, x: Int, y: Int, w: Int, h: Int): Bool {
		var wx = x + handle.dragX;
		var wy = y + handle.dragY;
		var inputChanged = getInputInRect(wx, wy, w, h) && inputChanged();
		return alwaysRedraw || isScrolling || inputChanged;
	}

	// Returns true if redraw is needed
	public function window(handle: Handle, x: Int, y: Int, w: Int, h: Int, drag = false): Bool {
		if (handle.texture == null || w != handle.texture.width || h != handle.texture.height) {
			resize(handle, w, h);
		}

		if (!windowEnded) endWindow(); // End previous window if necessary
		windowEnded = false;

		g = handle.texture.g2; // Set g
		currentWindow = handle;
		_windowX = x + handle.dragX;
		_windowY = y + handle.dragY;
		_windowW = w;
		_windowH = h;
		windowHeaderW = 0;
		windowHeaderH = 0;

		if (windowDirty(handle, x, y, w, h)) {
			handle.redraws = 2;
		}

		if (onBorderHover != null) {
			if (getInputInRect(_windowX - 4, _windowY, 8, _windowH)) {
				onBorderHover(handle, 0);
			}
			else if (getInputInRect(_windowX + _windowW - 4, _windowY, 8, _windowH)) {
				onBorderHover(handle, 1);
			}
			else if (getInputInRect(_windowX, _windowY - 4, _windowW, 8)) {
				onBorderHover(handle, 2);
			}
			else if (getInputInRect(_windowX, _windowY + _windowH - 4, _windowW, 8)) {
				onBorderHover(handle, 3);
			}
		}

		if (handle.redraws <= 0) {
			return false;
		}

		_x = 0;
		_y = handle.scrollOffset;
		if (handle.layout == Horizontal) w = Std.int(ELEMENT_W());
		_w = !handle.scrollEnabled ? w : w - SCROLL_W(); // Exclude scrollbar if present
		_h = h;
		tooltipText = "";
		tooltipImg = null;
		tabNames = null;

		if (t.FILL_WINDOW_BG) {
			g.begin(true, t.WINDOW_BG_COL);
		}
		else {
			g.begin(true, 0x00000000);
			g.color = t.WINDOW_BG_COL;
			g.fillRect(_x, _y - handle.scrollOffset, handle.lastMaxX, handle.lastMaxY);
		}

		handle.dragEnabled = drag;
		if (drag) {
			if (inputStarted && getInputInRect(_windowX, _windowY, _windowW, HEADER_DRAG_H())) {
				dragHandle = handle;
			}
			else if (inputReleased) {
				dragHandle = null;
			}
			if (handle == dragHandle) {
				handle.redraws = 2;
				handle.dragX += Std.int(inputDX);
				handle.dragY += Std.int(inputDY);
			}
			_y += HEADER_DRAG_H(); // Header offset
			windowHeaderH += HEADER_DRAG_H();
		}

		return true;
	}

	public function endWindow(bindGlobalG = true) {
		var handle = currentWindow;
		if (handle == null) return;
		if (handle.redraws > 0 || isScrolling) {
			if (scissor) {
				scissor = false;
				g.disableScissor();
			}

			if (tabNames != null) drawTabs();

			if (handle.dragEnabled) { // Draggable header
				g.color = t.SEPARATOR_COL;
				g.fillRect(0, 0, _windowW, HEADER_DRAG_H());
			}

			var wh = _windowH - windowHeaderH; // Exclude header
			var fullHeight = _y - handle.scrollOffset - windowHeaderH;
			if (fullHeight < wh || handle.layout == Horizontal || !scrollEnabled) { // Disable scrollbar
				handle.scrollEnabled = false;
				handle.scrollOffset = 0;
			}
			else { // Draw window scrollbar if necessary
				handle.scrollEnabled = true;
				if (tabScroll < 0) { // Restore tab
					handle.scrollOffset = tabScroll;
					tabScroll = 0;
				}
				var wy = _windowY + windowHeaderH;
				var amountToScroll = fullHeight - wh;
				var amountScrolled = -handle.scrollOffset;
				var ratio = amountScrolled / amountToScroll;
				var barH = wh * Math.abs(wh / fullHeight);
				barH = Math.max(barH, ELEMENT_H());

				var totalScrollableArea = wh - barH;
				var e = amountToScroll / totalScrollableArea;
				var barY = totalScrollableArea * ratio + windowHeaderH;
				var barFocus = getInputInRect(_windowX + _windowW - SCROLL_W(), barY + _windowY, SCROLL_W(), barH);

				if (inputStarted && barFocus) { // Start scrolling
					scrollHandle = handle;
					isScrolling = true;
				}

				var scrollDelta: Float = inputWheelDelta;
				if (touchScroll && inputDown && inputDY != 0 && inputX > _windowX + windowHeaderW&& inputY > _windowY + windowHeaderH) {
					isScrolling = true;
					scrollDelta = -inputDY / 20;
				}
				if (handle == scrollHandle) { // Scroll
					scroll(inputDY * e, fullHeight);
				}
				else if (scrollDelta != 0 && comboSelectedHandle == null &&
						 getInputInRect(_windowX, wy, _windowW, wh)) { // Wheel
					scroll(scrollDelta * ELEMENT_H(), fullHeight);
				}

				// Stay in bounds
				if (handle.scrollOffset > 0) {
					handle.scrollOffset = 0;
				}
				else if (fullHeight + handle.scrollOffset < wh) {
					handle.scrollOffset = wh - fullHeight;
				}

				g.color = t.ACCENT_COL; // Bar
				var scrollbarFocus = getInputInRect(_windowX + _windowW - SCROLL_W(), wy, SCROLL_W(), wh);
				var barW = (scrollbarFocus || handle == scrollHandle) ? SCROLL_W() : SCROLL_W() / 3;
				g.fillRect(_windowW - barW - scrollAlign, barY, barW, barH);
			}

			handle.lastMaxX = _x;
			handle.lastMaxY = _y;
			if (handle.layout == Vertical) handle.lastMaxX += _windowW;
			else handle.lastMaxY += _windowH;
			handle.redraws--;

			g.end();
		}

		windowEnded = true;

		// Draw window texture
		if (alwaysRedrawWindow || handle.redraws > -4) {
			if (bindGlobalG) globalG.begin(false);
			globalG.color = t.WINDOW_TINT_COL;
			globalG.drawImage(handle.texture, _windowX, _windowY);
			if (bindGlobalG) globalG.end();
			if (handle.redraws <= 0) handle.redraws--;
		}
	}

	function scroll(delta: Float, fullHeight: Float) {
		currentWindow.scrollOffset -= delta;
	}

	public function tab(handle: Handle, text: String, vertical = false, color: Int = -1): Bool {
		if (tabNames == null) { // First tab
			tabNames = [];
			tabColors = [];
			tabHandle = handle;
			tabVertical = vertical;
			_w -= tabVertical ? Std.int(ELEMENT_OFFSET() + ELEMENT_W() - 1 * SCALE()) : 0; // Shrink window area by width of vertical tabs
			vertical ?
				windowHeaderW += ELEMENT_W() :
				windowHeaderH += BUTTON_H() + buttonOffsetY + ELEMENT_OFFSET();
			restoreX = inputX; // Mouse in tab header, disable clicks for tab content
			restoreY = inputY;
			if (!vertical && getInputInRect(_windowX, _windowY, _windowW, windowHeaderH)) {
				inputX = inputY = -1;
			}
			vertical ? { _x += windowHeaderW + 6; _w -= 6; } : _y += windowHeaderH + 3;
		}
		tabNames.push(text);
		tabColors.push(color);
		return handle.position == tabNames.length - 1;
	}

	function drawTabs() {
		inputX = restoreX;
		inputY = restoreY;
		if (currentWindow == null) return;
		var tabX = 0.0;
		var tabY = 0.0;
		var tabHMin = Std.int(BUTTON_H() * 1.1);
		var headerH = currentWindow.dragEnabled ? HEADER_DRAG_H() : 0;
		var tabH = t.FULL_TABS && tabVertical ? Std.int((_windowH - headerH) / tabNames.length) : tabHMin;
		var origy = _y;
		_y = headerH;
		tabHandle.changed = false;

		if (isCtrlDown && isTabDown) { // Next tab
			tabHandle.position++;
			if (tabHandle.position >= tabNames.length) tabHandle.position = 0;
			tabHandle.changed = true;
			isTabDown = false;
		}

		if (tabHandle.position >= tabNames.length) tabHandle.position = tabNames.length - 1;

		g.color = t.SEPARATOR_COL; // Tab background
		tabVertical ?
			g.fillRect(0, _y, ELEMENT_W(), _windowH) :
			g.fillRect(0, _y, _windowW, buttonOffsetY + tabH + 2);

		g.color = t.ACCENT_COL; // Underline tab buttons
		tabVertical ?
			g.fillRect(ELEMENT_W(), _y, 1, _windowH) :
			g.fillRect(buttonOffsetY, _y + buttonOffsetY + tabH + 2, _windowW - buttonOffsetY * 2, 1);

		var basey = tabVertical ? _y : _y + 2;

		for (i in 0...tabNames.length) {
			_x = tabX;
			_y = basey + tabY;
			_w = tabVertical ? Std.int(ELEMENT_W() - 1 * SCALE()) :
				 t.FULL_TABS ? Std.int(_windowW / tabNames.length) :
							   Std.int(ops.font.width(fontSize, tabNames[i]) + buttonOffsetY * 2 + 18 * SCALE());
			var released = getReleased(tabH);
			var pushed = getPushed(tabH);
			var hover = getHover(tabH);
			if (released) {
				var h = tabHandle.nest(tabHandle.position); // Restore tab scroll
				h.scrollOffset = currentWindow.scrollOffset;
				h = tabHandle.nest(i);
				tabScroll = h.scrollOffset;
				tabHandle.position = i; // Set new tab
				currentWindow.redraws = 3;
				tabHandle.changed = true;
			}
			var selected = tabHandle.position == i;

			g.color = (pushed || hover) ? t.BUTTON_HOVER_COL :
					  tabColors[i] != -1 ? tabColors[i] :
					  selected ? t.WINDOW_BG_COL :
					  t.SEPARATOR_COL;
			tabVertical ?
				tabY += tabH + 1 :
				tabX += _w + 1;
			drawRect(g, true, _x + buttonOffsetY, _y + buttonOffsetY, _w, tabH);
			g.color = selected ? t.BUTTON_TEXT_COL : t.LABEL_COL;
			drawString(g, tabNames[i], null, (tabH - tabHMin) / 2, t.FULL_TABS ? Align.Center : Align.Left);

			if (selected) { // Hide underline for active tab
				if (tabVertical) {
					// g.color = t.WINDOW_BG_COL;
					// g.fillRect(_x + buttonOffsetY + _w - 1, _y + buttonOffsetY - 1, 2, tabH + buttonOffsetY);
					g.color = t.HIGHLIGHT_COL;
					g.fillRect(_x + buttonOffsetY, _y + buttonOffsetY - 1, 2, tabH + buttonOffsetY);
				}
				else {
					g.color = t.WINDOW_BG_COL;
					g.fillRect(_x + buttonOffsetY + 1, _y + buttonOffsetY + tabH, _w - 1, 1);
					g.color = t.HIGHLIGHT_COL;
					g.fillRect(_x + buttonOffsetY, _y + buttonOffsetY, _w, 2);
				}
			}
		}

		_x = 0; // Restore positions
		_y = origy;
		_w = Std.int(!currentWindow.scrollEnabled ? _windowW : _windowW - SCROLL_W());
	}

	public function panel(handle: Handle, text: String, isTree = false, filled = true, pack = true): Bool {
		if (!isVisible(ELEMENT_H())) {
			endElement();
			return handle.selected;
		}
		if (getReleased()) {
			handle.selected = !handle.selected;
			handle.changed = changed = true;
		}
		if (filled) {
			g.color = t.PANEL_BG_COL;
			drawRect(g, true, _x, _y, _w, ELEMENT_H());
		}

		isTree ? drawTree(handle.selected) : drawArrow(handle.selected);

		g.color = t.LABEL_COL; // Title
		drawString(g, text, titleOffsetX, 0);

		endElement();
		if (pack && !handle.selected) _y -= ELEMENT_OFFSET();

		return handle.selected;
	}

	public function image(image: kha.Image, tint = 0xffffffff, h: Null<Float> = null, sx = 0, sy = 0, sw = 0, sh = 0): State {
		var iw = (sw > 0 ? sw : image.width) * SCALE();
		var ih = (sh > 0 ? sh : image.height) * SCALE();
		var w = Math.min(iw, _w);
		var x = _x;
		var scroll = currentWindow != null ? currentWindow.scrollEnabled : false;
		var r = curRatio == -1 ? 1.0 : getRatio(ratios[curRatio], 1);
		if (imageScrollAlign) { // Account for scrollbar size
			w = Math.min(iw, _w - buttonOffsetY * 2);
			x += buttonOffsetY;
			if (!scroll) {
				w -= SCROLL_W() * r;
				x += SCROLL_W() * r / 2;
			}
		}
		else if (scroll) w += SCROLL_W() * r;

		// Image size
		var ratio = h == null ?
			w / iw :
			h / ih;
		h == null ?
			h = ih * ratio :
			w = iw * ratio;

		if (!isVisible(h)) {
			endElement(h);
			return State.Idle;
		}
		var started = getStarted(h);
		var down = getPushed(h);
		var released = getReleased(h);
		var hover = getHover(h);
		if (curRatio == -1 && (started || down || released || hover)) {
			if (inputX < _windowX + _x || inputX > _windowX + _x + w) {
				started = down = released = hover = false;
			}
		}
		g.color = tint;
		if (!enabled) fadeColor();
		var h_float: Float = h; // TODO: hashlink fix
		if (sw > 0) { // Source rect specified
			imageInvertY ?
				g.drawScaledSubImage(image, sx, sy, sw, sh, x, _y + h_float, w, -h_float) :
				g.drawScaledSubImage(image, sx, sy, sw, sh, x, _y, w, h_float);
		}
		else {
			imageInvertY ?
				g.drawScaledImage(image, x, _y + h_float, w, -h_float) :
				g.drawScaledImage(image, x, _y, w, h_float);
		}

		endElement(h);
		return started ? State.Started : released ? State.Released : down ? State.Down : hover ? State.Hovered : State.Idle;
	}

	public function text(text: String, align = Align.Left, bg = 0x00000000): State {
		if (text.indexOf("\n") >= 0) {
			splitText(text, align, bg);
			return State.Idle;
		}
		var h = Math.max(ELEMENT_H(), ops.font.height(fontSize));
		if (!isVisible(h)) {
			endElement(h + ELEMENT_OFFSET());
			return State.Idle;
		}
		var started = getStarted(h);
		var down = getPushed(h);
		var released = getReleased(h);
		var hover = getHover(h);
		if (bg != 0x0000000) {
			g.color = bg;
			g.fillRect(_x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H());
		}
		g.color = t.TEXT_COL;
		drawString(g, text, null, 0, align);

		endElement(h + ELEMENT_OFFSET());
		return started ? State.Started : released ? State.Released : down ? State.Down : State.Idle;
	}

	inline function splitText(lines: String, align = Align.Left, bg = 0x00000000) {
		for (line in lines.split("\n")) text(line, align, bg);
	}

	function startTextEdit(handle: Handle, align = Align.Left) {
		isTyping = true;
		submitTextHandle = textSelectedHandle;
		textToSubmit = textSelected;
		textSelectedHandle = handle;
		textSelected = handle.text;
		cursorX = handle.text.length;
		if (tabPressed) {
			tabPressed = false;
			isKeyPressed = false; // Prevent text deselect after tab press
		}
		else if (!highlightOnSelect) { // Set cursor to click location
			setCursorToInput(align);
		}
		tabPressedHandle = handle;
		highlightAnchor = highlightOnSelect ? 0 : cursorX;
		if (Keyboard.get() != null) Keyboard.get().show();
	}

	function submitTextEdit() {
		submitTextHandle.changed = submitTextHandle.text != textToSubmit;
		submitTextHandle.text = textToSubmit;
		submitTextHandle = null;
		textToSubmit = "";
		textSelected = "";
	}

	function updateTextEdit(align = Align.Left, editable = true, liveUpdate = false) {
		var text = textSelected;
		if (isKeyPressed) { // Process input
			if (key == KeyCode.Left) { // Move cursor
				if (cursorX > 0) cursorX--;
			}
			else if (key == KeyCode.Right) {
				if (cursorX < text.length) cursorX++;
			}
			else if (editable && key == KeyCode.Backspace) { // Remove char
				if (cursorX > 0 && highlightAnchor == cursorX) {
					text = text.substr(0, cursorX - 1) + text.substr(cursorX, text.length);
					cursorX--;
				}
				else if (highlightAnchor < cursorX) {
					text = text.substr(0, highlightAnchor) + text.substr(cursorX, text.length);
					cursorX = highlightAnchor;
				}
				else {
					text = text.substr(0, cursorX) + text.substr(highlightAnchor, text.length);
				}
			}
			else if (editable && key == KeyCode.Delete) {
				if (highlightAnchor == cursorX) {
					text = text.substr(0, cursorX) + text.substr(cursorX + 1);
				}
				else if (highlightAnchor < cursorX) {
					text = text.substr(0, highlightAnchor) + text.substr(cursorX, text.length);
					cursorX = highlightAnchor;
				}
				else {
					text = text.substr(0, cursorX) + text.substr(highlightAnchor, text.length);
				}
			}
			else if (key == KeyCode.Return) { // Deselect
				deselectText();
			}
			else if (key == KeyCode.Escape) { // Cancel
				textSelected = textSelectedHandle.text;
				deselectText();
			}
			else if (key == KeyCode.Tab && tabSwitchEnabled && !isCtrlDown) { // Next field
				tabPressed = true;
				deselectText();
				key = null;
			}
			else if (key == KeyCode.Home) {
				cursorX = 0;
			}
			else if (key == KeyCode.End) {
				cursorX = text.length;
			}
			else if (isCtrlDown && isADown) { // Select all
				cursorX = text.length;
				highlightAnchor = 0;
			}
			else if (editable && // Write
					 key != KeyCode.Shift &&
					 key != KeyCode.CapsLock &&
					 key != KeyCode.Control &&
					 key != KeyCode.Meta &&
					 key != KeyCode.Alt &&
					 key != KeyCode.Up &&
					 key != KeyCode.Down &&
					 char != null &&
					 char != "" &&
					 char.charCodeAt(0) >= 32) {
				text = text.substr(0, highlightAnchor) + char + text.substr(cursorX);
				cursorX = cursorX + 1 > text.length ? text.length : cursorX + 1;
			}
			var selecting = isShiftDown && (key == KeyCode.Left || key == KeyCode.Right || key == KeyCode.Shift);
			// isCtrlDown && isAltDown is the condition for AltGr was pressed
			// AltGr is part of the German keyboard layout and part of key combinations like AltGr + e -> €
			if (!selecting && (!isCtrlDown || (isCtrlDown && isAltDown))) highlightAnchor = cursorX;
		}

		if (editable && textToPaste != "") { // Process cut copy paste
			text = text.substr(0, highlightAnchor) + textToPaste + text.substr(cursorX);
			cursorX += textToPaste.length;
			highlightAnchor = cursorX;
			textToPaste = "";
			isPaste = false;
		}
		if (highlightAnchor == cursorX) textToCopy = text; // Copy
		else if (highlightAnchor < cursorX) textToCopy = text.substring(highlightAnchor, cursorX);
		else textToCopy = text.substring(cursorX, highlightAnchor);
		if (editable && isCut) { // Cut
			if (highlightAnchor == cursorX) text = "";
			else if (highlightAnchor < cursorX) {
				text = text.substr(0, highlightAnchor) + text.substr(cursorX, text.length);
				cursorX = highlightAnchor;
			}
			else {
				text = text.substr(0, cursorX) + text.substr(highlightAnchor, text.length);
			}
		}

		var off = TEXT_OFFSET();
		var lineHeight = ELEMENT_H();
		var cursorHeight = lineHeight - buttonOffsetY * 3.0;
		// Draw highlight
		if (highlightAnchor != cursorX) {
			var istart = cursorX;
			var iend = highlightAnchor;
			if (highlightAnchor < cursorX) {
				istart = highlightAnchor;
				iend = cursorX;
			}
			var hlstr = text.substr(istart, iend - istart);
			var hlstrw = ops.font.width(fontSize, hlstr);
			var startoff = ops.font.width(fontSize, text.substr(0, istart));
			var hlStart = align == Align.Left ? _x + startoff + off : _x + _w - hlstrw - off;
			if (align == Align.Right) {
				hlStart -= ops.font.width(fontSize, text.substr(iend, text.length));
			}
			g.color = t.ACCENT_SELECT_COL;
			g.fillRect(hlStart, _y + buttonOffsetY * 1.5, hlstrw, cursorHeight);
		}

		// Draw cursor
		var str = align == Align.Left ? text.substr(0, cursorX) : text.substring(cursorX, text.length);
		var strw = ops.font.width(fontSize, str);
		var cursorX = align == Align.Left ? _x + strw + off : _x + _w - strw - off;
		g.color = t.TEXT_COL; // Cursor
		g.fillRect(cursorX, _y + buttonOffsetY * 1.5, 2 * SCALE(), cursorHeight);

		textSelected = text;
		if (liveUpdate && textSelectedHandle != null) {
			textSelectedHandle.changed = textSelectedHandle.text != textSelected;
			textSelectedHandle.text = textSelected;
		}
	}

	public function textInput(handle: Handle, label = "", align = Align.Left, editable = true, liveUpdate = false): String {
		if (!isVisible(ELEMENT_H())) {
			endElement();
			return handle.text;
		}

		var hover = getHover();
		if (hover && onTextHover != null) onTextHover();
		g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL; // Text bg
		drawRect(g, t.FILL_ACCENT_BG, _x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H());

		var released = getReleased();
		if (submitTextHandle == handle && released) { // Keep editing selected text
			isTyping = true;
			textSelectedHandle = submitTextHandle;
			submitTextHandle = null;
			setCursorToInput(align);
		}
		var startEdit = released || tabPressed;
		handle.changed = false;

		if (textSelectedHandle != handle && startEdit) startTextEdit(handle, align);
		if (textSelectedHandle == handle) updateTextEdit(align, editable, liveUpdate);
		if (submitTextHandle == handle) submitTextEdit();

		if (label != "") {
			g.color = t.LABEL_COL; // Label
			var labelAlign = align == Align.Right ? Align.Left : Align.Right;
			drawString(g, label, labelAlign == Align.Left ? null : 0, 0, labelAlign);
		}

		g.color = t.TEXT_COL; // Text
		textSelectedHandle != handle ? drawString(g, handle.text, null, 0, align) : drawString(g, textSelected, null, 0, align, false);

		endElement();
		return handle.text;
	}

	function setCursorToInput(align: Align) {
		var off = align == Align.Left ? TEXT_OFFSET() : _w - ops.font.width(fontSize, textSelected);
		var x = inputX - (_windowX + _x + off);
		cursorX = 0;
		while (cursorX < textSelected.length && ops.font.width(fontSize, textSelected.substr(0, cursorX)) < x) {
			cursorX++;
		}
		highlightAnchor = cursorX;
	}

	function deselectText() {
		if (textSelectedHandle == null) return;
		submitTextHandle = textSelectedHandle;
		textToSubmit = textSelected;
		textSelectedHandle = null;
		isTyping = false;
		if (currentWindow != null) currentWindow.redraws = 2;
		if (Keyboard.get() != null) Keyboard.get().hide();
		highlightAnchor = cursorX;
		if (onDeselectText != null) onDeselectText();
	}

	public function button(text: String, align = Align.Center, label = ""): Bool {
		if (!isVisible(ELEMENT_H())) {
			endElement();
			return false;
		}
		var released = getReleased();
		var pushed = getPushed();
		var hover = getHover();
		if (released) changed = true;

		g.color = pushed ? t.BUTTON_PRESSED_COL :
				  hover ? t.BUTTON_HOVER_COL :
				  t.BUTTON_COL;

		drawRect(g, t.FILL_BUTTON_BG, _x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H());

		g.color = t.BUTTON_TEXT_COL;
		drawString(g, text, null, 0, align);
		if (label != "") {
			g.color = t.LABEL_COL;
			drawString(g, label, null, 0, align == Align.Right ? Align.Left : Align.Right);
		}

		endElement();

		return released;
	}

	public function check(handle: Handle, text: String, label: String = ""): Bool {
		if (!isVisible(ELEMENT_H())) {
			endElement();
			return handle.selected;
		}
		if (getReleased()) {
			handle.selected = !handle.selected;
			handle.changed = changed = true;
		}
		else handle.changed = false;

		var hover = getHover();
		drawCheck(handle.selected, hover); // Check

		g.color = t.TEXT_COL; // Text
		drawString(g, text, titleOffsetX, 0, Align.Left);

		if (label != "") {
			g.color = t.LABEL_COL;
			drawString(g, label, null, 0, Align.Right);
		}

		endElement();

		return handle.selected;
	}

	public function radio(handle: Handle, position: Int, text: String, label: String = ""): Bool {
		if (!isVisible(ELEMENT_H())) {
			endElement();
			return handle.position == position;
		}
		if (position == 0) {
			handle.changed = false;
		}
		if (getReleased()) {
			handle.position = position;
			handle.changed = changed = true;
		}

		var hover = getHover();
		drawRadio(handle.position == position, hover); // Radio

		g.color = t.TEXT_COL; // Text
		drawString(g, text, titleOffsetX, 0);

		if (label != "") {
			g.color = t.LABEL_COL;
			drawString(g, label, null, 0, Align.Right);
		}

		endElement();

		return handle.position == position;
	}

	public function combo(handle: Handle, texts: Array<String>, label = "", showLabel = false, align = Align.Left, searchBar = true): Int {
		if (!isVisible(ELEMENT_H())) {
			endElement();
			return handle.position;
		}
		if (getReleased()) {
			if (comboSelectedHandle == null) {
				inputEnabled = false;
				comboSelectedHandle = handle;
				comboSelectedWindow = currentWindow;
				comboSelectedAlign = align;
				comboSelectedTexts = texts;
				comboSelectedLabel = label;
				comboSelectedX = Std.int(_x + _windowX);
				comboSelectedY = Std.int(_y + _windowY + ELEMENT_H());
				comboSelectedW = Std.int(_w);
				comboSearchBar = searchBar;
				for (t in texts) { // Adapt combo list width to combo item width
					var w = Std.int(ops.font.width(fontSize, t)) + 10;
					if (comboSelectedW < w) comboSelectedW = w;
				}
				if (comboSelectedW > _w * 2) comboSelectedW = Std.int(_w * 2);
				if (comboSelectedW > _w) comboSelectedW += Std.int(TEXT_OFFSET());
				comboToSubmit = handle.position;
				comboInitialValue = handle.position;
			}
		}
		if (handle == comboSelectedHandle && (isEscapeDown || inputReleasedR)) {
			handle.position = comboInitialValue;
			handle.changed = changed = true;
			submitComboHandle = null;
		}
		else if (handle == submitComboHandle) {
			handle.position = comboToSubmit;
			submitComboHandle = null;
			handle.changed = changed = true;
		}
		else handle.changed = false;

		var hover = getHover();
		if (hover) { // Bg
			g.color = t.ACCENT_HOVER_COL;
			drawRect(g, t.FILL_ACCENT_BG, _x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H());
		}
		else {
			g.color = t.ACCENT_COL;
			drawRect(g, t.FILL_ACCENT_BG, _x + buttonOffsetY, _y + buttonOffsetY, _w - buttonOffsetY * 2, BUTTON_H());
		}

		var x = _x + _w - arrowOffsetX - 8;
		var y = _y + arrowOffsetY + 3;
		g.fillTriangle(x, y, x + ARROW_SIZE(), y, x + ARROW_SIZE() / 2, y + ARROW_SIZE() / 2);

		if (showLabel && label != "") {
			if (align == Align.Left) _x -= 15;
			g.color = t.LABEL_COL;
			drawString(g, label, null, 0, align == Align.Left ? Align.Right : Align.Left);
			if (align == Align.Left) _x += 15;
		}

		if (align == Align.Right) _x -= 15;
		g.color = t.TEXT_COL; // Value
		if (handle.position < texts.length) {
			drawString(g, texts[handle.position], null, 0, align);
		}
		if (align == Align.Right) _x += 15;

		endElement();
		return handle.position;
	}

	public function slider(handle: Handle, text: String, from = 0.0, to = 1.0, filled = false, precision = 100.0, displayValue = true, align = Align.Right, textEdit = true): Float {
		if (!isVisible(ELEMENT_H())) {
			endElement();
			return handle.value;
		}
		if (getStarted()) {
			scrollHandle = handle;
			isScrolling = true;
			changed = handle.changed = true;
			if (touchTooltip) {
				sliderTooltip = true;
				sliderTooltipX = _x + _windowX;
				sliderTooltipY = _y + _windowY;
				sliderTooltipW = _w;
			}
		}
		else handle.changed = false;
		
		#if (!kha_android && !kha_ios)
		if (handle == scrollHandle && inputDX != 0) { // Scroll
		#else
		if (handle == scrollHandle) { // Scroll
		#end
			var range = to - from;
			var sliderX = _x + _windowX + buttonOffsetY;
			var sliderW = _w - buttonOffsetY * 2;
			var step = range / sliderW;
			var value = from + (inputX - sliderX) * step;
			handle.value = Math.round(value * precision) / precision;
			if (handle.value < from) handle.value = from; // Stay in bounds
			else if (handle.value > to) handle.value = to;
			handle.changed = changed = true;
		}

		var hover = getHover();
		drawSlider(handle.value, from, to, filled, hover); // Slider

		// Text edit
		var startEdit = (getReleased() || tabPressed) && textEdit;
		if (startEdit) { // Mouse did not move
			handle.text = handle.value + "";
			startTextEdit(handle);
			handle.changed = changed = true;
		}
		var lalign = align == Align.Left ? Align.Right : Align.Left;
		if (textSelectedHandle == handle) {
			updateTextEdit(lalign);
		}
		if (submitTextHandle == handle) {
			submitTextEdit();
			#if js
			try {
				handle.value = js.Lib.eval(handle.text);
			}
			catch(_) {}
			#else
			handle.value = Std.parseFloat(handle.text);
			#end
			handle.changed = changed = true;
		}

		g.color = t.LABEL_COL; // Text
		drawString(g, text, null, 0, align);

		if (displayValue) {
			g.color = t.TEXT_COL; // Value
			textSelectedHandle != handle ?
				drawString(g, (Math.round(handle.value * precision) / precision) + "", null, 0, lalign) :
				drawString(g, textSelected, null, 0, lalign);
		}

		endElement();
		return handle.value;
	}

	public function separator(h = 4, fill = true) {
		if (!isVisible(ELEMENT_H())) {
			_y += h * SCALE();
			return;
		}
		if (fill) {
			g.color = t.SEPARATOR_COL;
			g.fillRect(_x, _y, _w, h * SCALE());
		}
		_y += h * SCALE();
	}

	public function tooltip(text: String) {
		tooltipText = text;
		tooltipY = _y + _windowY;
	}

	public function tooltipImage(image: kha.Image, maxWidth: Null<Int> = null) {
		tooltipImg = image;
		tooltipImgMaxWidth = maxWidth;
		tooltipInvertY = imageInvertY;
		tooltipY = _y + _windowY;
	}

	function drawArrow(selected: Bool) {
		var x = _x + arrowOffsetX;
		var y = _y + arrowOffsetY;
		g.color = t.TEXT_COL;
		if (selected) {
			g.fillTriangle(x, y,
						   x + ARROW_SIZE(), y,
						   x + ARROW_SIZE() / 2, y + ARROW_SIZE());
		}
		else {
			g.fillTriangle(x, y,
						   x, y + ARROW_SIZE(),
						   x + ARROW_SIZE(), y + ARROW_SIZE() / 2);
		}
	}

	function drawTree(selected: Bool) {
		var SIGN_W = 7 * SCALE();
		var x = _x + arrowOffsetX + 1;
		var y = _y + arrowOffsetY + 1;
		g.color = t.TEXT_COL;
		if (selected) {
			g.fillRect(x, y + SIGN_W / 2 - 1, SIGN_W, SIGN_W / 8);
		}
		else {
			g.fillRect(x, y + SIGN_W / 2 - 1, SIGN_W, SIGN_W / 8);
			g.fillRect(x + SIGN_W / 2 - 1, y, SIGN_W / 8, SIGN_W);
		}
	}

	function drawCheck(selected: Bool, hover: Bool) {
		var x = _x + checkOffsetX;
		var y = _y + checkOffsetY;

		g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL;
		drawRect(g, t.FILL_ACCENT_BG, x, y, CHECK_SIZE(), CHECK_SIZE()); // Bg

		if (selected) { // Check
			g.color = kha.Color.White;
			if (!enabled) fadeColor();
			var size = Std.int(CHECK_SELECT_SIZE());
			g.drawScaledImage(checkSelectImage, x + checkSelectOffsetX, y + checkSelectOffsetY, size, size);
		}
	}

	function drawRadio(selected: Bool, hover: Bool) {
		var x = _x + radioOffsetX;
		var y = _y + radioOffsetY;
		g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL;
		drawRect(g, t.FILL_ACCENT_BG, x, y, CHECK_SIZE(), CHECK_SIZE()); // Bg

		if (selected) { // Check
			g.color = t.ACCENT_SELECT_COL;
			if (!enabled) fadeColor();
			g.fillRect(x + radioSelectOffsetX, y + radioSelectOffsetY, CHECK_SELECT_SIZE(), CHECK_SELECT_SIZE());
		}
	}

	function drawSlider(value: Float, from: Float, to: Float, filled: Bool, hover: Bool) {
		var x = _x + buttonOffsetY;
		var y = _y + buttonOffsetY;
		var w = _w - buttonOffsetY * 2;

		g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL;
		drawRect(g, t.FILL_ACCENT_BG, x, y, w, BUTTON_H()); // Bg

		g.color = hover ? t.ACCENT_HOVER_COL : t.ACCENT_COL;
		var offset = (value - from) / (to - from);
		var barW = 8 * SCALE(); // Unfilled bar
		var sliderX = filled ? x : x + (w - barW) * offset;
		sliderX = Math.max(Math.min(sliderX, x + (w - barW)), x);
		var sliderW = filled ? w * offset : barW;
		sliderW = Math.max(Math.min(sliderW, w), 0);
		drawRect(g, true, sliderX, y, sliderW, BUTTON_H());
	}

	static var comboFirst = true;
	function drawCombo() {
		if (comboSelectedHandle == null) return;
		var _g = g;
		globalG.color = t.SEPARATOR_COL;
		globalG.begin(false);

		var comboH = (comboSelectedTexts.length + (comboSelectedLabel != "" ? 1 : 0) + (comboSearchBar ? 1 : 0)) * Std.int(ELEMENT_H());
		var distTop = comboSelectedY - comboH - Std.int(ELEMENT_H()) - windowBorderTop;
		var distBottom = kha.System.windowHeight() - windowBorderBottom - (comboSelectedY + comboH );
		var unrollUp = distBottom < 0 && distBottom < distTop;
		beginRegion(globalG, comboSelectedX, comboSelectedY, comboSelectedW);
		if (isKeyPressed || inputWheelDelta != 0) {
			var arrowUp = isKeyPressed && key == (unrollUp ? KeyCode.Down : KeyCode.Up);
			var arrowDown = isKeyPressed && key == (unrollUp ? KeyCode.Up : KeyCode.Down);
			var wheelUp = (unrollUp && inputWheelDelta > 0) || (!unrollUp && inputWheelDelta < 0);
			var wheelDown = (unrollUp && inputWheelDelta < 0) || (!unrollUp && inputWheelDelta > 0);
			if ((arrowUp || wheelUp) && comboToSubmit > 0) {
				var step = 1;
				if (comboSearchBar && textSelected.length > 0) {
					var search = textSelected.toLowerCase();
					while (comboSelectedTexts[comboToSubmit - step].toLowerCase().indexOf(search) < 0 && comboToSubmit - step > 0)
						++step;
					
					// Corner case: Current position is the top one according to the search pattern.
					if (comboSelectedTexts[comboToSubmit - step].toLowerCase().indexOf(search) < 0) step = 0;
				}
				comboToSubmit -= step;
				submitComboHandle = comboSelectedHandle;
			}
			else if ((arrowDown || wheelDown) && comboToSubmit < comboSelectedTexts.length - 1) {
				var step = 1;
				if (comboSearchBar && textSelected.length > 0) {
					var search = textSelected.toLowerCase();
					while (comboSelectedTexts[comboToSubmit + step].toLowerCase().indexOf(search) < 0 && comboToSubmit + step < comboSelectedTexts.length - 1)
						++step;

					// Corner case: Current position is the lowest one according to the search pattern.
					if (comboSelectedTexts[comboToSubmit + step].toLowerCase().indexOf(search) < 0) step = 0;
				}
				
				comboToSubmit += step;
				submitComboHandle = comboSelectedHandle;
			}
			if (comboSelectedWindow != null) comboSelectedWindow.redraws = 2;
		}

		inputEnabled = true;
		var _BUTTON_COL = t.BUTTON_COL;
		var _ELEMENT_OFFSET = t.ELEMENT_OFFSET;
		t.ELEMENT_OFFSET = 0;
		var unrollRight = _x + comboSelectedW * 2 < kha.System.windowWidth() - windowBorderRight ? 1 : -1;
		var resetPosition = false;
		var search = "";
		if (comboSearchBar) {
			if (unrollUp) _y -= ELEMENT_H() * 2;
			var comboSearchHandle = Id.handle();
			if (comboFirst) comboSearchHandle.text = "";
			fill(0, 0, _w / SCALE(), ELEMENT_H() / SCALE(), t.SEPARATOR_COL);
			search = textInput(comboSearchHandle, "", Align.Left, true, true).toLowerCase();
			if (isReleased) comboFirst = true; // Keep combo open
			if (comboFirst) {
				#if (!kha_android && !kha_ios)
				startTextEdit(comboSearchHandle); // Focus search bar
				#end
			}
			resetPosition = comboSearchHandle.changed;
		}

		for (i in 0...comboSelectedTexts.length) {
			if (search.length > 0 && comboSelectedTexts[i].toLowerCase().indexOf(search) < 0)
				continue; // Don't show items that don't fit the current search pattern

			if (resetPosition) { // The search has changed, select first entry that matches
				comboToSubmit = comboSelectedHandle.position = i;
				submitComboHandle = comboSelectedHandle;
				resetPosition = false;
			}
			if (unrollUp) _y -= ELEMENT_H() * 2;
			t.BUTTON_COL = i == comboSelectedHandle.position ? t.ACCENT_SELECT_COL : t.SEPARATOR_COL;
			fill(0, 0, _w / SCALE(), ELEMENT_H() / SCALE(), t.SEPARATOR_COL);
			if (button(comboSelectedTexts[i], comboSelectedAlign)) {
				comboToSubmit = i;
				submitComboHandle = comboSelectedHandle;
				if (comboSelectedWindow != null) comboSelectedWindow.redraws = 2;
				break;
			}
			if (_y + ELEMENT_H() > kha.System.windowHeight() - windowBorderBottom || _y - ELEMENT_H() * 2 < windowBorderTop) {
				_x += comboSelectedW * unrollRight; // Next column
				_y = comboSelectedY;
			}
		}
		t.BUTTON_COL = _BUTTON_COL;
		t.ELEMENT_OFFSET = _ELEMENT_OFFSET;

		if (comboSelectedLabel != "") { // Unroll down
			if (unrollUp) {
				_y -= ELEMENT_H() * 2;
				fill(0, 0, _w / SCALE(), ELEMENT_H() / SCALE(), t.SEPARATOR_COL);
				g.color = t.LABEL_COL;
				drawString(g, comboSelectedLabel, null, 0, Align.Right);
				_y += ELEMENT_H();
				fill(0, 0, _w / SCALE(), 1 * SCALE(), t.ACCENT_SELECT_COL); // Separator
			}
			else {
				fill(0, 0, _w / SCALE(), ELEMENT_H() / SCALE(), t.SEPARATOR_COL);
				fill(0, 0, _w / SCALE(), 1 * SCALE(), t.ACCENT_SELECT_COL); // Separator
				g.color = t.LABEL_COL;
				drawString(g, comboSelectedLabel, null, 0, Align.Right);
			}
		}

		if ((inputReleased || inputReleasedR || isEscapeDown || isReturnDown) && !comboFirst) {
			comboSelectedHandle = null;
			comboFirst = true;
		}
		else comboFirst = false;
		inputEnabled = comboSelectedHandle == null;
		endRegion(false);
		globalG.end();
		g = _g; // Restore
	}

	function drawTooltip(bindGlobalG: Bool) {
		if (sliderTooltip) {
			if (bindGlobalG) globalG.begin(false);
			globalG.font = ops.font;
			globalG.fontSize = fontSize * 2;
			var text = (Math.round(scrollHandle.value * 100) / 100) + "";
			var xoff = ops.font.width(globalG.fontSize, text) / 2;
			var yoff = ops.font.height(globalG.fontSize);
			var x = Math.min(Math.max(sliderTooltipX, inputX), sliderTooltipX + sliderTooltipW);
			globalG.color = t.ACCENT_COL;
			globalG.fillRect(x - xoff, sliderTooltipY - yoff, xoff * 2, yoff);
			globalG.color = t.TEXT_COL;
			globalG.drawString(text, x - xoff, sliderTooltipY - yoff);
			if (bindGlobalG) globalG.end();
		}
		if (touchTooltip && textSelectedHandle != null) {
			if (bindGlobalG) globalG.begin(false);
			globalG.font = ops.font;
			globalG.fontSize = fontSize * 2;
			var xoff = ops.font.width(globalG.fontSize, textSelected) / 2;
			var yoff = ops.font.height(globalG.fontSize) / 2;
			var x = kha.System.windowWidth() / 2;
			var y = kha.System.windowHeight() / 3;
			globalG.color = t.ACCENT_COL;
			globalG.fillRect(x - xoff, y - yoff, xoff * 2, yoff * 2);
			globalG.color = t.TEXT_COL;
			globalG.drawString(textSelected, x - xoff, y - yoff);
			if (bindGlobalG) globalG.end();
		}

		if (tooltipText != "" || tooltipImg != null) {
			if (inputChanged()) {
				tooltipShown = false;
				tooltipWait = inputDX == 0 && inputDY == 0; // Wait for movement before showing up again
			}
			if (!tooltipShown) {
				tooltipShown = true;
				tooltipX = inputX;
				tooltipTime = kha.Scheduler.time();
			}
			if (!tooltipWait && kha.Scheduler.time() - tooltipTime > TOOLTIP_DELAY()) {
				if (tooltipImg != null) drawTooltipImage(bindGlobalG);
				if (tooltipText != "") drawTooltipText(bindGlobalG);
			}
		}
		else tooltipShown = false;
	}

	function drawTooltipText(bindGlobalG: Bool) {
		globalG.color = t.TEXT_COL;
		var lines = tooltipText.split("\n");
		var tooltipW = 0.0;
		for (line in lines) {
			var lineTooltipW = ops.font.width(fontSize, line);
			if (lineTooltipW > tooltipW) tooltipW = lineTooltipW;
		}
		tooltipX = Math.min(tooltipX, kha.System.windowWidth() - tooltipW - 20);
		if (bindGlobalG) globalG.begin(false);
		var fontHeight = ops.font.height(fontSize);
		var off = 0;
		if (tooltipImg != null) {
			var w = tooltipImg.width;
			if (tooltipImgMaxWidth != null && w > tooltipImgMaxWidth) w = tooltipImgMaxWidth;
			off = Std.int(tooltipImg.height * (w / tooltipImg.width));
		}
		globalG.fillRect(tooltipX, tooltipY + off, tooltipW + 20, fontHeight * lines.length);
		globalG.font = ops.font;
		globalG.fontSize = fontSize;
		globalG.color = t.ACCENT_COL;
		for (i in 0...lines.length) {
			globalG.drawString(lines[i], tooltipX + 5, tooltipY + off + i * fontSize);
		}
		if (bindGlobalG) globalG.end();
	}

	function drawTooltipImage(bindGlobalG: Bool) {
		var w = tooltipImg.width;
		if (tooltipImgMaxWidth != null && w > tooltipImgMaxWidth) w = tooltipImgMaxWidth;
		var h = tooltipImg.height * (w / tooltipImg.width);
		tooltipX = Math.min(tooltipX, kha.System.windowWidth() - w - 20);
		tooltipY = Math.min(tooltipY, kha.System.windowHeight() - h - 20);
		if (bindGlobalG) globalG.begin(false);
		globalG.color = 0xff000000;
		globalG.fillRect(tooltipX, tooltipY, w, h);
		globalG.color = 0xffffffff;
		tooltipInvertY ?
			globalG.drawScaledImage(tooltipImg, tooltipX, tooltipY + h, w, -h) :
			globalG.drawScaledImage(tooltipImg, tooltipX, tooltipY, w, h);
		if (bindGlobalG) globalG.end();
	}

	function drawString(g: Graphics, text: String, xOffset: Null<Float> = null, yOffset: Float = 0, align = Align.Left, truncation = true) {
		var fullText = text;
		if (truncation) {
			while (text.length > 0 && ops.font.width(fontSize, text) > _w - 6 * SCALE()) {
				text = text.substr(0, text.length - 1);
			}
			if (text.length < fullText.length) {
				text += "..";
				// Strip more to fit ".."
				while (text.length > 2 && ops.font.width(fontSize, text) > _w - 10 * SCALE()) {
					text = text.substr(0, text.length - 3) + "..";
				}
				if (isHovered) tooltip(fullText);
			}
		}

		if (dynamicGlyphLoad) {
			for (i in 0...text.length) {
				if (text.charCodeAt(i) > 126 && Graphics.fontGlyphs.indexOf(text.charCodeAt(i)) == -1) {
					Graphics.fontGlyphs.push(text.charCodeAt(i));
					Graphics.fontGlyphs = Graphics.fontGlyphs.copy(); // Trigger atlas update
				}
			}
		}

		if (xOffset == null) xOffset = t.TEXT_OFFSET;
		xOffset *= SCALE();
		g.font = ops.font;
		g.fontSize = fontSize;
		if (align == Align.Center) xOffset = _w / 2 - ops.font.width(fontSize, text) / 2;
		else if (align == Align.Right) xOffset = _w - ops.font.width(fontSize, text) - TEXT_OFFSET();

		if (!enabled) fadeColor();
		g.pipeline = rtTextPipeline;

		if (textColoring == null) {
			g.drawString(text, _x + xOffset, _y + fontOffsetY + yOffset);
		}
		else {
			// Monospace fonts only for now
			for (coloring in textColoring.colorings) {
				var result = extractColoring(text, coloring);
				if (result.colored != "") {
					g.color = coloring.color;
					g.drawString(result.colored, _x + xOffset, _y + fontOffsetY + yOffset);
				}
				text = result.uncolored;
			}
			g.color = textColoring.default_color;
			g.drawString(text, _x + xOffset, _y + fontOffsetY + yOffset);
		}

		g.pipeline = null;
	}

	static function extractColoring(text: String, col: TColoring) {
		var res = { colored: "", uncolored: "" };
		var coloring = false;
		var startFrom = 0;
		var startLength = 0;
		for (i in 0...text.length) {
			var skipFirst = false;
			// Check if upcoming text should be colored
			var length = checkStart(i, text, col.start);
			// Not touching another character
			var separatedLeft = i == 0 || !isChar(text.charCodeAt(i - 1));
			var separatedRight = i + length >= text.length || !isChar(text.charCodeAt(i + length));
			var isSeparated = separatedLeft && separatedRight;
			// Start coloring
			if (length > 0 && (!coloring || col.end == "") && (!col.separated || isSeparated)) {
				coloring = true;
				startFrom = i;
				startLength = length;
				if (col.end != "" && col.end != "\n") skipFirst = true;
			}
			// End coloring
			else if (col.end == "") {
				if (i == startFrom + startLength) coloring = false;
			}
			else if (text.substr(i, col.end.length) == col.end) {
				coloring = false;
			}
			// If true, add current character to colored string
			var b = coloring && !skipFirst;
			res.colored += b ? text.charAt(i) : " ";
			res.uncolored += b ? " " : text.charAt(i);
		}
		return res;
	}

	static inline function isChar(code: Int): Bool {
		return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
	}

	static function checkStart(i:Int, text: String, start: Array<String>): Int {
		for (s in start) if (text.substr(i, s.length) == s) return s.length;
		return 0;
	}

	function endElement(elementSize: Null<Float> = null) {
		if (elementSize == null) elementSize = ELEMENT_H() + ELEMENT_OFFSET();
		if (currentWindow == null || currentWindow.layout == Vertical) {
			if (curRatio == -1 || (ratios != null && curRatio == ratios.length - 1)) { // New line
				_y += elementSize;

				if ((ratios != null && curRatio == ratios.length - 1)) { // Last row element
					curRatio = -1;
					ratios = null;
					_x = xBeforeSplit;
					_w = wBeforeSplit;
					highlightFullRow = false;
				}
			}
			else { // Row
				curRatio++;
				_x += _w; // More row elements to place
				_w = Std.int(getRatio(ratios[curRatio], wBeforeSplit));
			}
		}
		else { // Horizontal
			_x += _w + ELEMENT_OFFSET();
		}
	}

	/**
		Highlight all upcoming elements in the next row on a `mouse-over` event.
	**/
	public inline function highlightNextRow() {
		highlightFullRow = true;
	}

	inline function getRatio(ratio: Float, dyn: Float): Float {
		return ratio < 0 ? -ratio : ratio * dyn;
	}

	/**
		Draw the upcoming elements in the same row.
		Negative values will be treated as absolute, positive values as ratio to `window width`.
	**/
	public function row(ratios: Array<Float>) {
		this.ratios = ratios;
		curRatio = 0;
		xBeforeSplit = _x;
		wBeforeSplit = _w;
		_w = Std.int(getRatio(ratios[curRatio], _w));
	}

	public function indent(bothSides = true) {
		_x += TAB_W();
		_w -= TAB_W();
		if (bothSides) _w -= TAB_W();
	}

	public function unindent(bothSides = true) {
		_x -= TAB_W();
		_w += TAB_W();
		if (bothSides) _w += TAB_W();
	}

	function fadeColor() {
		g.color = kha.Color.fromFloats(g.color.R, g.color.G, g.color.B, 0.25);
	}

	public function fill(x: Float, y: Float, w: Float, h: Float, color: kha.Color) {
		g.color = color;
		if (!enabled) fadeColor();
		g.fillRect(_x + x * SCALE(), _y + y * SCALE() - 1, w * SCALE(), h * SCALE());
		g.color = 0xffffffff;
	}

	public function rect(x: Float, y: Float, w: Float, h: Float, color: kha.Color, strength = 1.0) {
		g.color = color;
		if (!enabled) fadeColor();
		g.drawRect(_x + x * SCALE(), _y + y * SCALE(), w * SCALE(), h * SCALE(), strength);
		g.color = 0xffffffff;
	}

	inline function drawRect(g: Graphics, fill: Bool, x: Float, y: Float, w: Float, h: Float, strength = 0.0) {
		if (strength == 0.0) strength = 1;
		if (!enabled) fadeColor();
		fill ? g.fillRect(x, y - 1, w, h + 1) : g.drawRect(x, y, w, h, strength);
	}

	function isVisible(elemH: Float): Bool {
		if (currentWindow == null) return true;
		return (_y + elemH > windowHeaderH && _y < currentWindow.texture.height);
	}

	function getReleased(elemH = -1.0): Bool { // Input selection
		isReleased = enabled && inputEnabled && inputReleased && getHover(elemH) && getInitialHover(elemH);
		return isReleased;
	}

	function getPushed(elemH = -1.0): Bool {
		isPushed = enabled && inputEnabled && inputDown && getHover(elemH) && getInitialHover(elemH);
		return isPushed;
	}

	function getStarted(elemH = -1.0): Bool {
		isStarted = enabled && inputEnabled && inputStarted && getHover(elemH);
		return isStarted;
	}

	function getInitialHover(elemH = -1.0): Bool {
		if (scissor && inputY < _windowY + windowHeaderH) return false;
		if (elemH == -1.0) elemH = ELEMENT_H();
		return enabled && inputEnabled &&
			inputStartedX >= _windowX + _x && inputStartedX < (_windowX + _x + _w) &&
			inputStartedY >= _windowY + _y && inputStartedY < (_windowY + _y + elemH);
	}

	function getHover(elemH = -1.0): Bool {
		if (scissor && inputY < _windowY + windowHeaderH) return false;
		if (elemH == -1.0) elemH = ELEMENT_H();
		isHovered = enabled && inputEnabled &&
			inputX >= _windowX + (highlightFullRow ? 0 : _x) && inputX < (_windowX + _x + (highlightFullRow ? _windowW : _w)) &&
			inputY >= _windowY + _y && inputY < (_windowY + _y + elemH);
		return isHovered;
	}

	function getInputInRect(x: Float, y: Float, w: Float, h: Float, scale = 1.0): Bool {
		return enabled && inputEnabled &&
			inputX >= x * scale && inputX < (x + w) * scale &&
			inputY >= y * scale && inputY < (y + h) * scale;
	}

	public function onMouseDown(button: Int, x: Int, y: Int) { // Input events
		if (penInUse) return;
		button == 0 ? inputStarted = true : inputStartedR = true;
		button == 0 ? inputDown = true : inputDownR = true;
		inputStartedTime = kha.Scheduler.time();
		#if (kha_android || kha_ios)
		setInputPosition(x, y);
		#end
		inputStartedX = x;
		inputStartedY = y;
	}

	public function onMouseUp(button: Int, x: Int, y: Int) {
		if (penInUse) return;

		if (touchHoldActivated) {
			touchHoldActivated = false;
			return;
		}

		if (isScrolling) { // Prevent action when scrolling is active
			isScrolling = false;
			scrollHandle = null;
			sliderTooltip = false;
			if (x == inputStartedX && y == inputStartedY) { // Mouse not moved
				button == 0 ? inputReleased = true : inputReleasedR = true;
			}
		}
		else {
			button == 0 ? inputReleased = true : inputReleasedR = true;
		}
		button == 0 ? inputDown = false : inputDownR = false;
		#if (kha_android || kha_ios)
		setInputPosition(x, y);
		#end
		deselectText();
	}

	public function onMouseMove(x: Int, y: Int, movementX: Int, movementY: Int) {
		#if (!kha_android && !kha_ios)
		setInputPosition(x, y);
		#end
	}

	public function onMouseWheel(delta: Int) {
		inputWheelDelta = delta;
	}

	function setInputPosition(x: Int, y: Int) {
		inputDX += x - inputX;
		inputDY += y - inputY;
		inputX = x;
		inputY = y;
	}

	public function onPenDown(x: Int, y: Int, pressure: Float) {
		#if (kha_android || kha_ios)
		return;
		#end

		onMouseDown(0, x, y);
	}

	public function onPenUp(x: Int, y: Int, pressure: Float) {
		#if (kha_android || kha_ios)
		return;
		#end

		if (inputStarted) { inputStarted = false; penInUse = true; return; }
		onMouseUp(0, x, y);
		penInUse = true; // On pen release, additional mouse down & up events are fired at once - filter those out
	}

	public function onPenMove(x: Int, y: Int, pressure: Float) {
		#if (kha_android || kha_ios)
		return;
		#end

		onMouseMove(x, y, 0, 0);
	}

	public function onKeyDown(code: KeyCode) {
		this.key = code;
		isKeyPressed = true;
		isKeyDown = true;
		keyRepeatTime = kha.Scheduler.time() + 0.4;
		switch code {
			case KeyCode.Shift: isShiftDown = true;
			case KeyCode.Control: isCtrlDown = true;
			#if kha_darwin
			case KeyCode.Meta: isCtrlDown = true;
			#end
			case KeyCode.Alt: isAltDown = true;
			case KeyCode.Backspace: isBackspaceDown = true;
			case KeyCode.Delete: isDeleteDown = true;
			case KeyCode.Escape: isEscapeDown = true;
			case KeyCode.Return: isReturnDown = true;
			case KeyCode.Tab: isTabDown = true;
			case KeyCode.A: isADown = true;
			case KeyCode.Space: char = " ";
			#if kha_android_rmb // Detect right mouse button on Android..
			case KeyCode.Back: if (!inputDownR) onMouseDown(1, Std.int(inputX), Std.int(inputY));
			#end
			default:
		}
	}

	public function onKeyUp(code: KeyCode) {
		isKeyDown = false;
		switch code {
			case KeyCode.Shift: isShiftDown = false;
			case KeyCode.Control: isCtrlDown = false;
			#if kha_darwin
			case KeyCode.Meta: isCtrlDown = false;
			#end
			case KeyCode.Alt: isAltDown = false;
			case KeyCode.Backspace: isBackspaceDown = false;
			case KeyCode.Delete: isDeleteDown = false;
			case KeyCode.Escape: isEscapeDown = false;
			case KeyCode.Return: isReturnDown = false;
			case KeyCode.Tab: isTabDown = false;
			case KeyCode.A: isADown = false;
			#if kha_android_rmb
			case KeyCode.Back: onMouseUp(1, Std.int(inputX), Std.int(inputY));
			#end
			default:
		}
	}

	public function onKeyPress(char: String) {
		this.char = char;
		isKeyPressed = true;
	}

	#if (kha_android || kha_ios)
	public function onTouchDown(index: Int, x: Int, y: Int) {
		// Reset movement delta on touch start
		if (index == 0) {
			inputDX = 0;
			inputDY = 0;
			inputX = x;
			inputY = y;
		}
		// Two fingers down - right mouse button
		else if (index == 1) {
			inputDown = false;
			onMouseDown(1, Std.int(inputX), Std.int(inputY));
			pinchStarted = true;
			pinchTotal = 0.0;
			pinchDistance = 0.0;
		}
		// Three fingers down - middle mouse button
		else if (index == 2) {
			inputDownR = false;
			onMouseDown(2, Std.int(inputX), Std.int(inputY));
		}
	}

	public function onTouchUp(index: Int, x: Int, y: Int) {
		if (index == 1) onMouseUp(1, Std.int(inputX), Std.int(inputY));
	}

	var pinchDistance = 0.0;
	var pinchTotal = 0.0;
	var pinchStarted = false;

	public function onTouchMove(index: Int, x: Int, y: Int) {
		if (index == 0) setInputPosition(x, y);

		// Pinch to zoom - mouse wheel
		if (index == 1) {
			var lastDistance = pinchDistance;
			var dx = inputX - x;
			var dy = inputY - y;
			pinchDistance = Math.sqrt(dx * dx + dy * dy);
			pinchTotal += lastDistance != 0 ? lastDistance - pinchDistance : 0;
			if (!pinchStarted) {
				inputWheelDelta = Std.int(pinchTotal / 50);
				if (inputWheelDelta != 0) {
					pinchTotal = 0.0;
				}
			}
			pinchStarted = false;
		}
	}
	#end

	public function onCut(): String {
		isCut = true;
		return onCopy();
	}
	public function onCopy(): String {
		isCopy = true;
		return textToCopy;
	}
	public function onPaste(s: String) {
		isPaste = true;
		textToPaste = s;
	}

	public inline function ELEMENT_W(): Float {
		return t.ELEMENT_W * SCALE();
	}
	public inline function ELEMENT_H(): Float {
		return t.ELEMENT_H * SCALE();
	}
	public inline function ELEMENT_OFFSET(): Float {
		return t.ELEMENT_OFFSET * SCALE();
	}
	public inline function ARROW_SIZE(): Float {
		return t.ARROW_SIZE * SCALE();
	}
	public inline function BUTTON_H(): Float {
		return t.BUTTON_H * SCALE();
	}
	public inline function CHECK_SIZE(): Float {
		return t.CHECK_SIZE * SCALE();
	}
	public inline function CHECK_SELECT_SIZE(): Float {
		return t.CHECK_SELECT_SIZE * SCALE();
	}
	public inline function FONT_SIZE(): Int {
		return Std.int(t.FONT_SIZE * SCALE());
	}
	public inline function SCROLL_W(): Int {
		return Std.int(t.SCROLL_W * SCALE());
	}
	public inline function TEXT_OFFSET(): Float {
		return t.TEXT_OFFSET * SCALE();
	}
	public inline function TAB_W(): Int {
		return Std.int(t.TAB_W * SCALE());
	}
	public inline function HEADER_DRAG_H(): Int {
		return Std.int(15 * SCALE());
	}
	public inline function SCALE(): Float {
		return ops.scaleFactor;
	}
	inline function TOOLTIP_DELAY(): Float {
		return 1.0;
	}

	public function resize(handle: Handle, w: Int, h: Int) {
		handle.redraws = 2;
		if (handle.texture != null) handle.texture.unload();
		if (w < 1) w = 1;
		if (h < 1) h = 1;
		handle.texture = kha.Image.createRenderTarget(w, h, kha.graphics4.TextureFormat.RGBA32, kha.graphics4.DepthStencilFormat.NoDepthAndStencil, 1);
		handle.texture.g2.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;
	}
}

typedef HandleOptions = {
	?selected: Bool,
	?position: Int,
	?value: Float,
	?text: String,
	?color: kha.Color,
	?layout: Layout
}

class Handle {
	public var selected = false;
	public var position = 0;
	public var color = kha.Color.White;
	public var value = 0.0;
	public var text = "";
	public var texture: kha.Image = null;
	public var redraws = 2;
	public var scrollOffset = 0.0;
	public var scrollEnabled = false;
	public var layout: Layout = 0;
	public var lastMaxX = 0.0;
	public var lastMaxY = 0.0;
	public var dragEnabled = false;
	public var dragX = 0;
	public var dragY = 0;
	public var changed = false;
	var children: Map<Int, Handle>;

	public function new(ops: HandleOptions = null) {
		if (ops != null) {
			if (ops.selected != null) selected = ops.selected;
			if (ops.position != null) position = ops.position;
			if (ops.value != null) value = ops.value;
			if (ops.text != null) text = ops.text;
			if (ops.color != null) color = ops.color;
			if (ops.layout != null) layout = ops.layout;
		}
	}

	public function nest(i: Int, ops: HandleOptions = null): Handle {
		if (children == null) children = [];
		var c = children.get(i);
		if (c == null) {
			c = new Handle(ops);
			children.set(i, c);
		}
		return c;
	}

	public function unnest(i: Int) {
		if (children != null) {
			children.remove(i);
		}
	}

	public static var global = new Handle();
}

@:enum abstract Layout(Int) from Int {
	var Vertical = 0;
	var Horizontal = 1;
}

@:enum abstract Align(Int) from Int {
	var Left = 0;
	var Center = 1;
	var Right = 2;
}

@:enum abstract State(Int) from Int {
	var Idle = 0;
	var Started = 1;
	var Down = 2;
	var Released = 3;
	var Hovered = 4;
}

typedef TColoring = {
	var color: Int;
	var start: Array<String>;
	var end: String;
	@:optional var separated: Null<Bool>;
}

typedef TTextColoring = {
	var colorings: Array<TColoring>;
	var default_color: Int;
}