scripts/core/ui_base.js
import {contextWrangler} from '../screen/area_wrangler.js';
let _ui_base = undefined;
//avoid circular module references
let TextBox = undefined;
export function _setTextboxClass(cls) {
TextBox = cls;
}
//import * as ui_save from './ui_save.js';
/*
if (window.document && document.body) {
console.log("ensuring body.style.margin/padding are zero");
document.body.style["margin"] = "0px";
document.body.style["padding"] = "0px";
}
*/
import * as cssutils from '../path-controller/util/cssutils.js';
import {Animator} from "./anim.js";
import './units.js';
import * as util from '../path-controller/util/util.js';
import * as vectormath from '../path-controller/util/vectormath.js';
import * as math from '../path-controller/util/math.js';
import * as toolprop from '../path-controller/toolsys/toolprop.js';
import * as controller from '../path-controller/controller/controller.js';
import {
pushModalLight, popModalLight, copyEvent, pathDebugEvent,
haveModal, keymap, reverse_keymap, pushPointerModal
} from '../path-controller/util/simple_events.js';
import {getDataPathToolOp} from '../path-controller/controller/controller.js';
import * as units from './units.js';
import {rgb_to_hsv, hsv_to_rgb} from "../util/colorutils.js";
export * from './ui_theme.js';
import {CSSFont, theme, parsepx, compatMap} from "./ui_theme.js";
import {DefaultTheme} from './theme.js';
//global list of elements to, hopefully, prevent minification tree shaking
//of live elements
export let ElementClasses = [];
export {theme} from "./ui_theme.js";
import cconst from '../config/const.js';
window.__cconst = cconst;
let Vector4 = vectormath.Vector4;
export {Icons} from '../icon_enum.js';
import {Icons} from '../icon_enum.js';
export {setIconMap} from '../icon_enum.js';
import {setIconMap} from '../icon_enum.js';
import {AfterAspect, initAspectClass} from "./aspect.js";
import * as aspect from './aspect.js';
const EnumProperty = toolprop.EnumProperty;
let Area;
export let _setAreaClass = (cls) => {
Area = cls;
}
export const ErrorColors = {
WARNING: "yellow",
ERROR : "red",
OK : "green"
};
window.__theme = theme;
let registered_has_happened = false;
let tagPrefix = "";
const EventCBSymbol = Symbol("wrapped event callback");
function calcElemCBKey(elem, type, options) {
return elem._id + ":" + type + ":" + JSON.stringify(options || {});
}
/**
* Sets tag prefix for pathux html elements.
* Must be called prior to loading other modules.
* Since this is tricky, you can alternatively
* add a script tag with the prefix with the id "pathux-tag-prefix",
* e.g.<pre> <script type="text/plain" id="pathux-tag-prefix">prefix</script> </pre>
* */
export function setTagPrefix(prefix) {
if (registered_has_happened) {
throw new Error("have to call ui_base.setTagPrefix before loading any other path.ux modules");
}
tagPrefix = "" + prefix;
}
export function getTagPrefix(prefix) {
return tagPrefix;
}
let prefix = document.getElementById("pathux-tag-prefix");
if (prefix) {
console.log("Found pathux-tag-prefix element");
prefix = prefix.innerText.trim();
setTagPrefix(prefix);
}
export const ClassIdSymbol = Symbol("pathux-class-id");
let class_idgen = 1;
export function setTheme(theme2) {
//merge theme
for (let k in theme2) {
let v = theme2[k];
if (typeof v !== "object") {
theme[k] = v;
continue;
}
let v0 = theme[k];
if (!(k in theme)) {
theme[k] = {};
}
for (let k2 in v) {
//if (v0 && !(k2 in v0)) {
// continue;
//}
if (k2 in compatMap) {
let k3 = compatMap[k2];
if (v[k3] === undefined) {
v[k3] = v[k2];
}
delete v[k2];
k2 = k3;
}
theme[k][k2] = v[k2];
}
}
}
setTheme(DefaultTheme);
let _last_report = util.time_ms();
export function report() {
if (util.time_ms() - _last_report > 350) {
console.warn(...arguments);
_last_report = util.time_ms();
}
}
//this function is deprecated
export function getDefault(key, elem) {
console.warn("Deprecated call to ui_base.js:getDefault");
if (key in theme.base) {
return theme.base[key];
} else {
throw new Error("Unknown default " + key);
}
}
//XXX implement me!
export function IsMobile() {
console.warn("ui_base.IsMobile is deprecated; use util.isMobile instead")
return util.isMobile();
};
let keys = ["margin", "padding", "margin-block-start", "margin-block-end"];
keys = keys.concat(["padding-block-start", "padding-block-end"]);
keys = keys.concat(["margin-left", "margin-top", "margin-bottom", "margin-right"]);
keys = keys.concat(["padding-left", "padding-top", "padding-bottom", "padding-right"]);
export const marginPaddingCSSKeys = keys;
class _IconManager {
constructor(image, tilesize, number_of_horizontal_tiles, drawsize) {
this.tilex = number_of_horizontal_tiles;
this.tilesize = tilesize;
this.drawsize = drawsize;
this.customIcons = new Map();
this.image = image;
this.promise = undefined;
this._accept = undefined;
this._reject = undefined;
}
get ready() {
return this.image && this.image.width;
}
onReady() {
if (this.ready) {
return new Promise((accept, reject) => {
accept(this);
});
}
if (this.promise) {
return this.promise;
}
let onload = this.image.onload;
this.image.onload = (e) => {
if (onload) {
onload.call(this.image, e);
}
if (!this._accept) {
return;
}
let accept = this._accept;
this._accept = this._reject = this.promise = undefined;
if (this.image.width) {
accept(this);
}
}
this.promise = new util.TimeoutPromise((accept, reject) => {
this._accept = accept;
this._reject = reject;
}, 15000, true); /* silently rejects on timeout */
this.promise.catch(error => {
util.print_stack(error);
this.promise = this._accept = this._reject = undefined;
});
return this.promise;
}
canvasDraw(elem, canvas, g, icon, x = 0, y = 0) {
let customIcon = this.customIcons.get(icon);
if (customIcon) {
g.drawImage(customIcon.canvas, x, y);
return;
}
let tx = icon%this.tilex;
let ty = ~~(icon/this.tilex);
let dpi = elem.getDPI();
let ts = this.tilesize;
let ds = this.drawsize;
if (!this.image) {
//console.warn("Failed to render an iconsheet");
return;
}
try {
g.drawImage(this.image, tx*ts, ty*ts, ts, ts, x, y, ds*dpi, ds*dpi);
} catch (error) {
console.log("failed to draw an icon");
}
}
setCSS(icon, dom, fitsize = undefined) {
if (!fitsize) {
fitsize = this.drawsize;
}
if (typeof fitsize === "object") {
fitsize = Math.max(fitsize[0], fitsize[1]);
}
dom.style["background"] = this.getCSS(icon, fitsize);
if (this.customIcons.has(icon)) {
dom.style["background-size"] = (fitsize) + "px";
} else {
dom.style["background-size"] = (fitsize*this.tilex) + "px";
}
dom.style["background-clip"] = "content-box";
if (!dom.style["width"]) {
dom.style["width"] = this.drawsize + "px";
}
if (!dom.style["height"]) {
dom.style["height"] = this.drawsize + "px";
}
}
//icon is an integer
getCSS(icon, fitsize = this.drawsize) {
if (icon === -1) { //-1 means no icon
return '';
}
if (typeof fitsize === "object") {
fitsize = Math.max(fitsize[0], fitsize[1]);
}
let ratio = fitsize/this.tilesize;
let customIcon = this.customIcons.get(icon);
if (customIcon !== undefined) {
//ratio = fitsize / this.drawsize;
//let d = this.drawsize*0.25;
let d = 0.0;
let css = `url("${customIcon.blobUrl}")`;
return css;
}
let x = (-(icon%this.tilex)*this.tilesize)*ratio;
let y = (-(~~(icon/this.tilex))*this.tilesize)*ratio;
//x = ~~x;
//y = ~~y;
//console.log(this.tilesize, this.drawsize, x, y);
//let ts = this.tilesize;
//return `image('${this.image.src}#xywh=${x},${y},${ts},${ts}')`;
return `url("${this.image.src}") ${x}px ${y}px`;
}
}
export class CustomIcon {
constructor(manager, key, id, baseImage) {
this.key = key;
this.baseImage = baseImage;
this.images = []
this.id = id;
this.manager = manager;
}
regenIcons() {
let manager = this.manager;
let doSheet = (sheet) => {
let size = sheet.drawsize;
let canvas = document.createElement("canvas");
let g = canvas.getContext("2d");
canvas.width = canvas.height = size;
g.drawImage(this.baseImage, 0, 0, size, size);
canvas.toBlob((blob) => {
let blobUrl = URL.createObjectURL(blob);
sheet.customIcons.set(this.id, {
blobUrl,
canvas
});
})
}
for (let sheet of manager.iconsheets) {
doSheet(sheet);
}
}
}
export class IconManager {
/**
images is a list of dom ids of img tags
sizes is a list of tile sizes, one per image.
you can control the final *draw* size by passing an array
of [tilesize, drawsize] instead of just a number.
*/
constructor(images, sizes, horizontal_tile_count) {
this.iconsheets = [];
this.tilex = horizontal_tile_count;
this.customIcons = new Map();
this.customIconIDMap = new Map();
for (let i = 0; i < images.length; i++) {
let size, drawsize;
if (typeof sizes[i] == "object") {
size = sizes[i][0], drawsize = sizes[i][1];
} else {
size = drawsize = sizes[i];
}
if (util.isMobile()) {
drawsize = ~~(drawsize*theme.base.mobileSizeMultiplier);
}
this.iconsheets.push(new _IconManager(images[i], size, horizontal_tile_count, drawsize));
}
}
isReady(sheet = 0) {
return this.iconsheets[sheet].ready;
}
addCustomIcon(key, image) {
let icon = this.customIcons.get(key);
if (!icon) {
let maxid = 0;
for (let k in Icons) {
maxid = Math.max(maxid, Icons[k] + 1);
}
for (let icon of this.customIcons.values()) {
maxid = Math.max(maxid, icon.id + 1);
}
maxid = Math.max(maxid, 1000); //just to be on the safe side
let id = maxid;
icon = new CustomIcon(this, key, id, image);
this.customIcons.set(key, icon);
this.customIconIDMap.set(id, icon);
}
icon.baseImage = image;
icon.regenIcons();
return icon.id;
}
load(manager2) {
this.iconsheets = manager2.iconsheets;
this.tilex = manager2.tilex;
return this;
}
reset(horizontal_tile_count) {
this.iconsheets.length = 0;
this.tilex = horizontal_tile_count;
}
add(image, size, drawsize = size) {
this.iconsheets.push(new _IconManager(image, size, this.tilex, drawsize));
return this;
}
canvasDraw(elem, canvas, g, icon, x = 0, y = 0, sheet = 0) {
let base = this.iconsheets[sheet];
sheet = this.findSheet(sheet);
let ds = sheet.drawsize;
sheet.drawsize = base.drawsize;
sheet.canvasDraw(elem, canvas, g, icon, x, y);
sheet.drawsize = ds;
}
findClosestSheet(size) {
let sheets = this.iconsheets.concat([]);
sheets.sort((a, b) => a.drawsize - b.drawsize);
let sheet;
for (let i = 0; i < sheets.length; i++) {
if (sheets[i].drawsize <= size) {
sheet = sheets[i];
break;
}
}
if (!sheet)
sheet = sheets[sheets.length - 1];
return this.iconsheets.indexOf(sheet);
}
findSheet(sheet) {
if (sheet === undefined) {
console.warn("sheet was undefined");
sheet = 0;
}
let base = this.iconsheets[sheet];
/**sigh**/
let dpi = UIBase.getDPI();
let minsheet = undefined;
let goal = dpi*base.drawsize;
for (let sheet of this.iconsheets) {
minsheet = sheet;
if (sheet.drawsize >= goal) {
break;
}
}
return minsheet === undefined ? base : minsheet;
}
getTileSize(sheet = 0) {
return this.iconsheets[sheet].drawsize;
return this.findSheet(sheet).drawsize;
}
getRealSize(sheet = 0) {
return this.iconsheets[sheet].tilesize;
return this.findSheet(sheet).tilesize;
//return this.iconsheets[sheet].tilesize;
}
//icon is an integer
getCSS(icon, sheet = 0) {
//return this.iconsheets[sheet].getCSS(icon);
//return this.findSheet(sheet).getCSS(icon);
let base = this.iconsheets[sheet];
sheet = this.findSheet(sheet);
let ds = sheet.drawsize;
sheet.drawsize = base.drawsize;
let ret = sheet.getCSS(icon);
sheet.drawsize = ds;
return ret;
}
setCSS(icon, dom, sheet = 0, fitsize = undefined) {
//return this.iconsheets[sheet].setCSS(icon, dom);
let base = this.iconsheets[sheet];
sheet = this.findSheet(sheet);
let ds = sheet.drawsize;
sheet.drawsize = base.drawsize;
let ret = sheet.setCSS(icon, dom, fitsize);
sheet.drawsize = ds;
return ret;
}
}
export let iconmanager = new IconManager([
document.getElementById("iconsheet16"),
document.getElementById("iconsheet32"),
document.getElementById("iconsheet48")
], [16, 32, 64], 16);
window._iconmanager = iconmanager; //debug global
//if client code overrides iconsheets, they must follow logical convention
//that the first one is "small" and the second is "large"
export let IconSheets = {
SMALL : 0,
LARGE : 1,
XLARGE: 2
};
export function iconSheetFromPackFlag(flag) {
if (flag & PackFlags.CUSTOM_ICON_SHEET) {
//console.log("Custom Icon Sheet:", flag>>PackFlags.CUSTOM_ICON_SHEET_START);
return flag>>PackFlags.CUSTOM_ICON_SHEET_START;
}
if ((flag & PackFlags.SMALL_ICON) && !(PackFlags.LARGE_ICON)) {
return 0//IconSheets.SMALL; //0
} else {
return 1//IconSheets.LARGE; //1
}
}
export function getIconManager() {
return iconmanager;
}
export function setIconManager(manager, IconSheetsOverride) {
iconmanager.load(manager);
if (IconSheetsOverride !== undefined) {
for (let k in IconSheetsOverride) {
IconSheets[k] = IconSheetsOverride[k];
}
}
}
export function makeIconDiv(icon, sheet = 0) {
let size = iconmanager.getRealSize(sheet);
let drawsize = iconmanager.getTileSize(sheet);
let icontest = document.createElement("div");
icontest.style["width"] = icontest.style["min-width"] = drawsize + "px";
icontest.style["height"] = icontest.style["min-height"] = drawsize + "px";
//icontest.style["background-color"] = "orange";
icontest.style["margin"] = "0px";
icontest.style["padding"] = "0px";
iconmanager.setCSS(icon, icontest, sheet);
return icontest;
}
let Vector2 = vectormath.Vector2;
let Matrix4 = vectormath.Matrix4;
export let dpistack = [];
export const UIFlags = {};
const internalElementNames = {};
const externalElementNames = {};
export const PackFlags = {
INHERIT_WIDTH : 1,
INHERIT_HEIGHT: 2,
VERTICAL : 4,
USE_ICONS : 8,
SMALL_ICON : 16,
LARGE_ICON : 32,
FORCE_PROP_LABELS : 64, //force propeties (Container.prototype.prop()) to always have labels
PUT_FLAG_CHECKS_IN_COLUMNS: 128, //group flag property checkmarks in columns (doesn't apply to icons)
WRAP_CHECKBOXES: 256,
//internal flags
STRIP_HORIZ : 512,
STRIP_VERT : 1024,
STRIP : 512 | 1024,
SIMPLE_NUMSLIDERS : 2048,
FORCE_ROLLER_SLIDER : 4096,
HIDE_CHECK_MARKS : (1<<13),
NO_NUMSLIDER_TEXTBOX : (1<<14),
CUSTOM_ICON_SHEET : 1<<15,
CUSTOM_ICON_SHEET_START: 20, //custom icon sheet bits are shifted to here
NO_UPDATE : 1<<16
};
let first = (iter) => {
if (iter === undefined) {
return undefined;
}
if (!(Symbol.iterator in iter)) {
for (let item in iter) {
return item;
}
return undefined;
}
for (let item of iter) {
return item;
}
}
import {DataPathError} from '../path-controller/controller/controller.js';
import {TimeoutPromise} from '../path-controller/util/util.js';
let _mobile_theme_patterns = [
/.*width.*/,
/.*height.*/,
/.*size.*/,
/.*margin.*/,
/.*pad/,
/.*radius.*/
];
let _idgen = 0;
window._testSetScrollbars = function (color = "grey", contrast = 0.5, width = 15, border = "solid") {
let buf = styleScrollBars(color, undefined, contrast, width, border, "*");
CTX.screen.mergeGlobalCSS(buf);
//document.body.style["overflow"] = "scroll";
/*
if (!window._tsttag) {
window._tsttag = document.createElement("style");
document.body.prepend(_tsttag);
}
_tsttag.textContent = buf;
//*/
return buf;
};
export function styleScrollBars(color = "grey", color2 = undefined, contrast = 0.5, width = 15,
border = "1px groove black", selector = "*") {
if (!color2) {
let c = css2color(color);
let a = c.length > 3 ? c[3] : 1.0;
c = rgb_to_hsv(c[0], c[1], c[2]);
let inv = c.slice(0, c.length);
inv[2] = 1.0 - inv[2];
inv[2] += (c[2] - inv[2])*(1.0 - contrast);
inv = hsv_to_rgb(inv[0], inv[1], inv[2]);
inv.length = 4;
inv[3] = a;
inv = color2css(inv);
color2 = inv;
}
let buf = `
${selector} {
scrollbar-width : ${width <= 16 ? 'thin' : 'auto'};
scrollbar-color : ${color2} ${color};
}
${selector}::-webkit-scrollbar {
width : ${width}px;
background-color : ${color};
}
${selector}::-webkit-scrollbar-track {
background-color : ${color};
border : ${border};
}
${selector}::-webkit-scrollbar-thumb {
background-color : ${color2};
border : ${border};
}
`;
//console.log(buf);
return buf;
}
window.styleScrollBars = styleScrollBars;
let _digest = new util.HashDigest();
export function calcThemeKey(digest = _digest.reset()) {
for (let k in theme) {
let obj = theme[k];
if (typeof obj !== "object") {
continue;
}
for (let k2 in obj) {
let v2 = obj[k2];
if (typeof v2 === "number" || typeof v2 === "boolean" || typeof v2 === "string") {
digest.add(v2);
} else if (typeof v2 === "object" && v2 instanceof CSSFont) {
v2.calcHashUpdate(digest);
}
}
}
return digest.get();
}
export var _themeUpdateKey = calcThemeKey();
export function flagThemeUpdate() {
_themeUpdateKey = calcThemeKey();
}
export class UIBase extends HTMLElement {
constructor() {
super();
this._modalstack = [];
this._tool_tip_abort_delay = undefined;
this._tooltip_ref = undefined;
this._textBoxEvents = false;
this._themeOverride = undefined;
this._checkTheme = true;
this._last_theme_update_key = _themeUpdateKey;
this._client_disabled_set = undefined;
//this._parent_disabled_set = 0;
this._useNativeToolTips = cconst.useNativeToolTips;
this._useNativeToolTips_set = false;
this._has_own_tooltips = undefined;
this._tooltip_timer = util.time_ms();
this.pathUndoGen = 0;
this._lastPathUndoGen = 0;
this._useDataPathUndo = undefined;
this._active_animations = [];
//ref to Link element referencing Screen style node
//Screen.update_intern sets the contents of this
this._screenStyleTag = document.createElement("style");
this._screenStyleUpdateHash = 0;
initAspectClass(this, new Set(["appendChild", "animate", "shadow", "removeNode", "prepend", "add", "init"]));
this.shadow = this.attachShadow({mode: 'open'});
if (cconst.DEBUG.paranoidEvents) {
this.__cbs = [];
}
this.shadow.appendChild(this._screenStyleTag);
this.shadow._appendChild = this.shadow.appendChild;
///*
let appendChild = this.shadow.appendChild;
this.shadow.appendChild = (child) => {
if (child && typeof child === "object" && child instanceof UIBase) {
child.parentWidget = this;
}
return this.shadow._appendChild(child, ...arguments);
};
//*/
this._wasAddedToNodeAtSomeTime = false;
this.visibleToPick = true;
this._override_class = undefined;
this.parentWidget = undefined;
/*
this.shadow._appendChild = this.shadow.appendChild;
this.shadow.appendChild = (child) => {
if (child instanceof UIBase) {
child.ctx = this.ctx;
child.parentWidget = this;
if (child._useDataPathUndo === undefined) {
child.useDataPathUndo = this.useDataPathUndo;
}
}
return this.shadow._appendChild(child);
};
//*/
let tagname = this.constructor.define().tagname;
this._id = tagname.replace(/\-/g, "_") + (_idgen++);
this.default_overrides = {}; //inherited by child widgets
this.my_default_overrides = {}; //not inherited to child widgets
this.class_default_overrides = {};
this._last_description = undefined;
this._description_final = undefined;
//getting css to flow down properly can be a pain, so
//some packing settings are set as bitflags here,
//see PackFlags
/*
setInterval(() => {
this.update();
}, 200);
//*/
this._modaldata = undefined;
this.packflag = this.getDefault("BasePackFlag");
this._internalDisabled = false;
this.__disabledState = false;
this._disdata = undefined;
this._ctx = undefined;
this._description = undefined;
let style = document.createElement("style");
style.textContent = `
.DefaultText {
font: ` + _getFont(this) + `;
}
`;
this.shadow.appendChild(style);
this._init_done = false;
//make default touch handlers that send mouse events
let do_touch = (e, type, button) => {
if (haveModal()) {
return;
}
button = button === undefined ? 0 : button;
let e2 = copyEvent(e);
if (e.touches.length === 0) {
//hrm, what to do, what to do. . .
} else {
let t = e.touches[0];
e2.pageX = t.pageX;
e2.pageY = t.pageY;
e2.screenX = t.screenX;
e2.screenY = t.screenY;
e2.clientX = t.clientX;
e2.clientY = t.clientY;
e2.x = t.x;
e2.y = t.y;
}
e2.button = button;
e2 = new MouseEvent(type, e2);
e2.was_touch = true;
e2.stopPropagation = e.stopPropagation.bind(e);
e2.preventDefault = e.preventDefault.bind(e);
e2.touches = e.touches;
this.dispatchEvent(e2);
};
this.addEventListener("touchstart", (e) => {
do_touch(e, "mousedown", 0);
}, {passive: false});
this.addEventListener("touchmove", (e) => {
do_touch(e, "mousemove");
}, {passive: false});
this.addEventListener("touchcancel", (e) => {
do_touch(e, "mouseup", 2);
}, {passive: false});
this.addEventListener("touchend", (e) => {
do_touch(e, "mouseup", 0);
}, {passive: false});
}
/*
set default_overrides(v) {
console.error("default_overrides was set", v);
this._default_overrides = v;
}
get default_overrides() {
return this._default_overrides;
}//*/
get useNativeToolTips() {
return this._useNativeToolTips;
}
set useNativeToolTips(val) {
this._useNativeToolTips = val;
this._useNativeToolTips_set = true;
}
get parentWidget() {
return this._parentWidget;
}
set parentWidget(val) {
if (val) {
this._wasAddedToNodeAtSomeTime = true;
}
this._parentWidget = val;
}
get useDataPathUndo() {
let p = this;
while (p) {
if (p._useDataPathUndo !== undefined) {
return p._useDataPathUndo;
}
p = p.parentWidget;
}
return false;
}
/**
causes calls to setPathValue to go through
toolpath app.datapath_set(path="" newValueJSON="")
every child will inherit
*/
set useDataPathUndo(val) {
this._useDataPathUndo = val;
}
get description() {
return this._description;
}
set description(val) {
if (val === null) {
this._description = undefined;
return;
}
this._description = val;
if (val === undefined || val === null) {
return;
}
if (cconst.showPathsInToolTips && this.hasAttribute("datapath")) {
let s = "" + this._description;
let path = this.getAttribute("datapath");
s += "\n path: " + path;
if (this.hasAttribute("mass_set_path")) {
let m = this.getAttribute("mass_set_path");
s += "\n massSetPath: " + m;
}
this._description_final = s;
}
if (cconst.useNativeToolTips) {
this.title = "" + this._description_final;
}
}
get background() {
return this.__background;
}
set background(bg) {
this.__background = bg;
this.overrideDefault("background-color", bg, true);
this.style["background-color"] = bg;
}
get disabled() {
//hrm, I could just propegate checks upward. . .
if (this.parentWidget && this.parentWidget.disabled) {
return true;
}
return !!this._client_disabled_set || !!this._internalDisabled;// || !!this._parent_disabled_set;
}
set disabled(v) {
this._client_disabled_set = v;
this.__updateDisable(this.disabled);
}
get internalDisabled() {
return this._internalDisabled;
}
set internalDisabled(val) {
this._internalDisabled = !!val;
this.__updateDisable(this.disabled);
}
get ctx() {
return this._ctx;
}
set ctx(c) {
this._ctx = c;
this._forEachChildWidget((n) => {
n.ctx = c;
});
}
get _reportCtxName() {
return "" + this._id;
}
get modalRunning() {
return this._modaldata !== undefined;
}
static getIconEnum() {
return Icons;
}
static setDefault(element) {
return element;
}
/**DEPRECATED
scaling ratio (e.g. for high-resolution displays)
*/
static getDPI() {
//if (dpistack.length > 0) {
// return dpistack[this.dpistack.length-1];
//} else {
//if (util.isMobile()) {
return window.devicePixelRatio; // * visualViewport.scale;
//}
return window.devicePixelRatio;
//}
}
static prefix(name) {
return tagPrefix + name;
}
static internalRegister(cls) {
cls[ClassIdSymbol] = class_idgen++;
registered_has_happened = true;
internalElementNames[cls.define().tagname] = this.prefix(cls.define().tagname);
customElements.define(this.prefix(cls.define().tagname), cls);
}
static getInternalName(name) {
return internalElementNames[name];
}
static createElement(name, internal = false) {
if (!internal && name in externalElementNames) {
return document.createElement(name);
} else if (name in internalElementNames) {
return document.createElement(internalElementNames[name]);
} else {
return document.createElement(name)
}
}
static register(cls) {
registered_has_happened = true;
cls[ClassIdSymbol] = class_idgen++;
ElementClasses.push(cls);
externalElementNames[cls.define().tagname] = cls.define().tagname;
customElements.define(cls.define().tagname, cls);
}
/**
* Defines core attributes of the class
*
* @example
*
* static define() {return {
* tagname : "custom-element-x",
* style : "[style class in theme]"
* subclassChecksTheme : boolean //set to true to disable base class invokation of checkTheme()
* havePickClipboard : boolean //whether element supports mouse hover copy/paste
* pasteForAllChildren : boolean //mouse hover paste happens even over child widgets
* copyForAllChildren : boolean //mouse hover copy happens even over child widgets
* }}
*/
static define() {
throw new Error("Missing define() for ux element");
}
setUndo(val) {
this.useDataPathUndo = val;
return this;
}
hide(sethide = true) {
this.hidden = sethide;
for (let n of this.shadow.childNodes) {
n.hidden = sethide;
}
this._forEachChildWidget((n) => {
n.hide(sethide);
})
}
getElementById(id) {
let ret;
let rec = (n) => {
if (ret) {
return;
}
if (n.getAttribute("id") === id || n.id === id) {
ret = n;
}
if (n instanceof UIBase && n.constructor.define().tagname === "panelframe-x") {
rec(n.contents);
} else if (n instanceof UIBase && n.constructor.define().tagname === "tabcontainer-x") {
for (let k in n.tabs) {
let tab = n.tabs[k];
if (tab) {
rec(tab);
}
}
}
for (let n2 of n.childNodes) {
if (n2 instanceof HTMLElement) {
rec(n2);
if (ret) {
break;
}
}
}
if (n.shadow) {
for (let n2 of n.shadow.childNodes) {
if (n2 instanceof HTMLElement) {
rec(n2);
if (ret) {
break;
}
}
}
}
}
rec(this);
return ret;
}
unhide() {
this.hide(false);
}
findArea() {
let p = this;
while (p) {
if (p instanceof Area) {
return p;
}
p = p.parentWidget;
}
return p;
}
addEventListener(type, cb, options) {
if (cconst.DEBUG.domEventAddRemove) {
console.log("addEventListener", type, this._id, options);
}
let cb2 = (e) => {
if (cconst.DEBUG.paranoidEvents) {
if (this.isDead()) {
this.removeEventListener(type, cb, options);
return;
}
}
if (cconst.DEBUG.domEvents) {
pathDebugEvent(e);
}
let area = this.findArea();
if (area) {
area.push_ctx_active();
try {
let ret = cb(e);
area.pop_ctx_active();
return ret;
} catch (error) {
area.pop_ctx_active();
throw error;
}
} else {
if (cconst.DEBUG.areaContextPushes) {
console.warn("Element is not part of an area?", element);
}
return cb(e);
}
};
if (!cb[EventCBSymbol]) {
cb[EventCBSymbol] = new Map();
}
let key = calcElemCBKey(this, type, options);
cb[EventCBSymbol].set(key, cb2);
if (cconst.DEBUG.paranoidEvents) {
this.__cbs.push([type, cb2, options]);
}
return super.addEventListener(type, cb2, options);
}
removeEventListener(type, cb, options) {
if (cconst.DEBUG.paranoidEvents) {
for (let item of this.__cbs) {
if (item[0] == type && item[1] === cb._cb2 && ("" + item[2]) === ("" + options)) {
this.__cbs.remove(item);
break;
}
}
}
if (cconst.DEBUG.domEventAddRemove) {
console.log("removeEventListener", type, this._id, options);
}
let key = calcElemCBKey(this, type, options);
if (!cb[EventCBSymbol] || !cb[EventCBSymbol].has(key)) {
return super.removeEventListener(type, cb, options);
} else {
let cb2 = cb[EventCBSymbol].get(key);
let ret = super.removeEventListener(type, cb2, options);
cb[EventCBSymbol].delete(key);
return ret;
}
}
connectedCallback() {
}
noMarginsOrPadding() {
return;
let keys = ["margin", "padding", "margin-block-start", "margin-block-end"];
keys = keys.concat(["padding-block-start", "padding-block-end"]);
keys = keys.concat(["margin-left", "margin-top", "margin-bottom", "margin-right"]);
keys = keys.concat(["padding-left", "padding-top", "padding-bottom", "padding-right"]);
for (let k of keys) {
this.style[k] = "0px";
}
return this;
}
/**
* find owning screen and tell it to update
* the global tab order
* */
regenTabOrder() {
let screen = this.getScreen();
if (screen !== undefined) {
screen.needsTabRecalc = true;
}
return this;
}
noMargins() {
this.style["margin"] = this.style["margin-left"] = this.style["margin-right"] = "0px";
this.style["margin-top"] = this.style["margin-bottom"] = "0px";
return this;
}
noPadding() {
this.style["padding"] = this.style["padding-left"] = this.style["padding-right"] = "0px";
this.style["padding-top"] = this.style["padding-bottom"] = "0px";
return this;
}
getTotalRect() {
let found = false;
let min = new Vector2([1e17, 1e17]);
let max = new Vector2([-1e17, -1e17]);
let doaabb = (n) => {
let rs = n.getClientRects();
for (let r of rs) {
min[0] = Math.min(min[0], r.x);
min[1] = Math.min(min[1], r.y);
max[0] = Math.max(max[0], r.x + r.width);
max[1] = Math.max(max[1], r.y + r.height);
found = true;
}
};
doaabb(this);
this._forEachChildWidget((n) => {
doaabb(n);
});
if (found) {
return {
width : max[0] - min[0],
height: max[1] - min[1],
x : min[0],
y : min[1],
left : min[0],
top : min[1],
right : max[0],
bottom: max[1]
};
} else {
return undefined;
}
}
parseNumber(value, args = {}) {
value = ("" + value).trim().toLowerCase();
let baseUnit = args.baseUnit || this.baseUnit;
let isInt = args.isInt || this.isInt;
let sign = 1.0;
if (value.startsWith("-")) {
value = value.slice(1, value.length).trim();
sign = -1;
}
let hexre = /-?[0-9a-f]+h$/;
if (value.startsWith("0b")) {
value = value.slice(2, value.length).trim();
value = parseInt(value, 2);
} else if (value.startsWith("0x")) {
value = value.slice(2, value.length).trim();
value = parseInt(value, 16);
} else if (value.search(hexre) === 0) {
value = value.slice(0, value.length - 1).trim();
value = parseInt(value, 16);
} else {
value = units.parseValue(value, baseUnit);
}
if (isInt) {
value = ~~value;
}
return value*sign;
}
formatNumber(value, args = {}) {
let baseUnit = args.baseUnit || this.baseUnit;
let displayUnit = args.displayUnit || this.displayUnit;
let isInt = args.isInt || this.isInt;
let radix = args.radix || this.radix || 10;
let decimalPlaces = args.decimalPlaces || this.decimalPlaces;
//console.log(this.baseUnit, this.displayUnit);
if (isInt && radix !== 10) {
let ret = Math.floor(value).toString(radix);
if (radix === 2)
return "0b" + ret;
else if (radix === 16)
return ret + "h";
}
return units.buildString(value, baseUnit, decimalPlaces, displayUnit);
}
setBoxCSS(subkey) {
let boxcode = '';
//debugger;
let keys = ["left", "right", "top", "bottom"];
let sub;
if (subkey) {
sub = this.getAttribute(subkey) || {};
}
let def = (key) => {
if (sub) {
return this.getSubDefault(subkey, key);
}
return this.getDefault(key);
}
for (let i = 0; i < 2; i++) {
let key = i ? "padding" : "margin";
this.style[key] = "unset";
let val = def(key);
if (val !== undefined) { //handle default first
for (let j = 0; j < 4; j++) {
this.style[key + "-" + keys[j]] = val + "px";
}
}
for (let j = 0; j < 4; j++) { //now do box sides
let key2 = `${key}-${keys[j]}`;
let val2 = def(key2);
if (val2 !== undefined) {
this.style[key2] = val2 + "px";
}
}
}
this.style["border-radius"] = def("border-radius") + "px";
this.style["border"] = `${def("border-width")}px ${def("border-style")} ${def("border-color")}`;
}
genBoxCSS(subkey) {
let boxcode = '';
let keys = ["left", "right", "top", "bottom"];
let sub;
if (subkey) {
sub = this.getAttribute(subkey) || {};
}
let def = (key) => {
if (sub) {
return this.getSubDefault(subkey, key);
}
return this.getDefault(key);
}
for (let i = 0; i < 2; i++) {
let key = i ? "padding" : "margin";
let val = def(key);
if (val !== undefined) {
boxcode += `${key}: ${val} px;\n`;
}
for (let j = 0; j < 4; j++) {
let key2 = `${key}-${keys[j]}`;
let val2 = def(key2);
if (val2 !== undefined) {
boxcode += `${key2}: ${val}px;\n`;
}
}
}
boxcode += `border-radius: ${def("border-radius")}px;\n`;
boxcode += `border: ${def("border-width")}px ${def("border-style")} ${def("border-color")};\n`;
return boxcode;
}
setCSS(setBG = true) {
if (setBG) {
let bg = this.getDefault("background-color");
if (bg) {
this.style["background-color"] = bg;
}
}
let zoom = this.getZoom();
if (zoom === 1.0) {
return;
}
let transform = "" + this.style["transform"];
//try to preserve user set transform by selectively deleting scale
//kind of hackish. . .
//normalize whitespace
transform = transform.replace(/[ \t\n\r]+/g, ' ');
transform = transform.replace(/, /g, ',');
//cut out scale
let transform2 = transform.replace(/scale\([^)]+\)/, '').trim();
this.style["transform"] = transform2 + ` scale(${zoom},${zoom})`;
}
flushSetCSS() {
//check init
this._init();
this.setCSS();
this._forEachChildWidget((c) => {
if (!(c.packflag & PackFlags.NO_UPDATE)) {
c.flushSetCSS();
}
});
}
/* Why is the DOM API argument order swapped here?*/
replaceChild(newnode, node) {
for (let i = 0; i < this.childNodes.length; i++) {
if (this.childNodes[i] === node) {
super.replaceChild(newnode, node);
return true;
}
}
for (let i = 0; i < this.shadow.childNodes.length; i++) {
if (this.shadow.childNodes[i] === node) {
this.shadow.replaceChild(newnode, node);
return true;
}
}
console.error("Unknown child node", node);
return false;
}
swapWith(b) {
let p1 = this.parentNode;
let p2 = b.parentNode;
if (this.parentWidget && (p1 === this.parentWidget.shadow) || p1 === null) {
p1 = this.parentWidget;
}
if (b.parentWidget && (p2 === b.parentWidget.shadow) || p2 === null) {
p2 = b.parentWidget;
}
if (!p1 || !p2) {
console.error("Invalid call to UIBase.prototype.swapWith", this, b, p1, p2);
return false;
}
let getPos = (n, p) => {
let i = Array.prototype.indexOf.call(p.childNodes, n);
if (i < 0 && p.shadow) {
p = p.shadow;
i = Array.prototype.indexOf.call(p.childNodes, n);
}
return [i, p];
}
let [i1, n1] = getPos(this, p1);
let [i2, n2] = getPos(b, p2);
console.log("i1, i2, n1, n2", i1, i2, n1, n2);
let tmp1 = document.createElement("div");
let tmp2 = document.createElement("div");
n1.insertBefore(tmp1, this);
n2.insertBefore(tmp2, b);
//HTMLElement.prototype.remove.call(this);
//HTMLElement.prototype.remove.call(b);
n1.replaceChild(b, tmp1);
n2.replaceChild(this, tmp2);
let ptmp = this.parentWidget;
this.parentWidget = b.parentWidget;
b.parentWidget = ptmp;
tmp1.remove();
tmp2.remove();
return true;
}
traverse(type_or_set) {
let this2 = this;
let classes = type_or_set;
let is_set = type_or_set instanceof Set;
is_set = is_set || type_or_set instanceof util.set;
is_set = is_set || Array.isArray(type_or_set);
if (!is_set) {
classes = [type_or_set];
}
let visit = new Set();
return (function* () {
let stack = [this2];
while (stack.length > 0) {
let n = stack.pop();
visit.add(n);
if (!n || !n.childNodes) {
continue;
}
for (let cls of classes) {
if (n instanceof cls) {
yield n;
}
}
for (let c of n.childNodes) {
if (!visit.has(c)) {
stack.push(c);
}
}
if (n.shadow) {
for (let c of n.shadow.childNodes) {
if (!visit.has(c)) {
stack.push(c);
}
}
}
}
})();
}
appendChild(child) {
if (child instanceof UIBase) {
child.ctx = this.ctx;
child.parentWidget = this;
child.useDataPathUndo = this.useDataPathUndo;
}
return super.appendChild(child);
}
_clipboardHotkeyInit() {
this._clipboard_over = false;
this._last_clipboard_keyevt = undefined;
this._clipboard_keystart = () => {
if (this._clipboard_events) {
return;
}
this._clipboard_events = true;
window.addEventListener("keydown", this._clipboard_keydown, {capture: true, passive: false});
}
this._clipboard_keyend = () => {
if (!this._clipboard_events) {
return;
}
this._clipboard_events = false;
window.removeEventListener("keydown", this._clipboard_keydown, {capture: true, passive: false});
}
this._clipboard_keydown = (e, internal_mode) => {
if (!this.isConnected || !cconst.getClipboardData) {
this._clipboard_keyend();
return;
}
if (e === this._last_clipboard_keyevt || !this._clipboard_over) {
return;
}
/* the user's mouse cursor might not be over the element
* if they've tabbed to it */
let is_copy = e.keyCode === keymap["C"] && (e.ctrlKey || e.commandKey) && !e.shiftKey && !e.altKey;
let is_paste = e.keyCode === keymap["V"] && (e.ctrlKey || e.commandKey) && !e.shiftKey && !e.altKey;
if (!is_copy && !is_paste) {
//early out, remember that pickElement is highly expensive to run
return;
}
//pasteForAllChildren
if (!internal_mode) {
let screen = this.ctx.screen;
let elem = screen.pickElement(screen.mpos[0], screen.mpos[1]);
let checkTree = is_paste && this.constructor.define().pasteForAllChildren;
checkTree = checkTree || (is_copy && this.constructor.define().copyForAllChildren);
while (checkTree && !(elem instanceof TextBox) && elem !== this && elem.parentWidget) {
console.log(" " + elem._id);
elem = elem.parentWidget;
}
console.warn("COLOR", this._id, elem._id);
if (elem !== this) {
//remove global keyhandler
this._clipboard_keyend();
return;
}
} else {
console.warn("COLOR", this._id);
}
this._last_clipboard_keyevt = e;
if (is_copy) {
this.clipboardCopy();
e.preventDefault();
e.stopPropagation();
}
if (is_paste) {
this.clipboardPaste();
e.preventDefault();
e.stopPropagation();
}
}
let start = (e) => {
this._clipboard_over = true;
this._clipboard_keystart();
}
let stop = (e) => {
this._clipboard_over = false;
this._clipboard_keyend();
}
this.tabIndex = 0; //enable self key events when element has focus
this.addEventListener("keydown", (e) => {
return this._clipboard_keydown(e, true);
});
this.addEventListener("pointerover", start, {capture: true, passive: true});
this.addEventListener("pointerout", stop, {capture: true, passive: true});
this.addEventListener("focus", stop, {capture: true, passive: true});
}
/** set havePickClipboard to true in define() to
* enable mouseover pick clipboarding */
clipboardCopy() {
throw new Error("implement me!");
}
clipboardPaste() {
throw new Error("implement me!");
}
//delayed init
init() {
this._init_done = true;
if (!this.hasAttribute("id") && this._id) {
this.setAttribute("id", this._id);
}
if (this.constructor.define().havePickClipboard) {
this._clipboardHotkeyInit();
}
}
_ondestroy() {
if (this.tabIndex >= 0) {
this.regenTabOrder();
}
if (cconst.DEBUG.paranoidEvents) {
for (let item of this.__cbs) {
this.removeEventListener(item[0], item[1], item[2]);
}
this.__cbs = [];
}
if (this.ondestroy !== undefined) {
this.ondestroy();
}
}
remove(trigger_on_destroy = true) {
if (this.tabIndex >= 0) {
this.regenTabOrder();
}
super.remove();
if (trigger_on_destroy) {
this._ondestroy();
}
if (this.on_remove) {
this.on_remove();
}
this.parentWidget = undefined;
}
/*
*
* called when elements are removed.
* you should assume the element will be reused later;
* on_destroy is the callback for when elements are permanently destroyed
* */
on_remove() {
}
removeChild(child, trigger_on_destroy = true) {
super.removeChild(child);
if (trigger_on_destroy) {
child._ondestroy();
}
}
flushUpdate(force = false) {
//check init
this._init();
this.update();
this._forEachChildWidget((c) => {
if (force || !(c.packflag & PackFlags.NO_UPDATE)) {
if (!c.ctx) {
c.ctx = this.ctx;
}
c.flushUpdate(force);
}
});
}
//used by container nodes
/**
* Iterates over all child widgets,
* including ones that might be inside
* of normal DOM nodes.
*
* This is done by recursing into the dom
* tree and stopping at any node that's
* descended from ui_base.UIBase
**/
_forEachChildWidget(cb, thisvar) {
let rec = (n) => {
if (n instanceof UIBase) {
if (thisvar !== undefined) {
cb.call(thisvar, n);
} else {
cb(n);
}
} else {
for (let n2 of n.childNodes) {
rec(n2);
}
if (n.shadow !== undefined) {
for (let n2 of n.shadow.childNodes) {
rec(n2);
}
}
}
};
for (let n of this.childNodes) {
rec(n);
}
if (this.shadow) {
for (let n of this.shadow.childNodes) {
rec(n);
}
}
}
checkInit() {
return this._init();
}
_init() {
if (this._init_done) {
return false;
}
this._init_done = true;
this.init();
return true;
}
getWinWidth() {
return window.innerWidth;
}
getWinHeight() {
return window.innerHeight;
}
calcZ() {
let p = this;
let n = this;
while (n) {
if (n.style && n.style["z-index"]) {
let z = parseFloat(n.style["z-index"]);
return z;
}
n = n.parentNode;
if (!n) {
n = p = p.parentWidget;
}
}
return 0;
}
pickElement(x, y, args = {}, marginy = 0, nodeclass = UIBase, excluded_classes = undefined) {
let marginx;
let clip;
let mouseEvent;
let isMouseMove, isMouseDown;
if (typeof args === "object") {
marginx = args.sx || 0;
marginy = args.sy || 0;
nodeclass = args.nodeclass || UIBase;
excluded_classes = args.excluded_classes;
clip = args.clip;
mouseEvent = args.mouseEvent;
} else {
marginx = args;
args = {
marginx : marginx || 0,
marginy : marginy || 0,
nodeclass : nodeclass || UIBase,
excluded_classes: excluded_classes,
clip : clip
}
}
if (mouseEvent) {
isMouseMove = mouseEvent.type === "mousemove" || mouseEvent.type === "touchmove" || mouseEvent.type === "pointermove";
isMouseDown = mouseEvent.buttons || (mouseEvent.touches && mouseEvent.touches.length > 0);
}
x -= window.scrollX;
y -= window.scrollY;
let elem = document.elementFromPoint(x, y);
if (!elem) {
return;
}
let path = [elem];
let lastelem = elem;
let i = 0;
while (elem.shadow) {
if (i++ > 1000) {
console.error("Infinite loop error");
break;
}
elem = elem.shadow.elementFromPoint(x, y);
if (elem === lastelem) {
break;
}
if (elem) {
path.push(elem);
}
lastelem = elem;
}
path.reverse();
//console.warn(path);
for (let i = 0; i < path.length; i++) {
let node = path[i];
let ok = node instanceof nodeclass;
if (excluded_classes) {
for (let cls of excluded_classes) {
ok = ok && !(node instanceof cls);
}
}
if (clip) {
let rect = node.getBoundingClientRect();
let clip2 = math.aabb_intersect_2d(clip.pos, clip.size, [rect.x, rect.y], [rect.width, rect.height]);
ok = ok && clip2;
}
if (ok) {
window.elem = node;
//console.log(node._id);
return node;
}
}
}
__updateDisable(val) {
if (!!val === !!this.__disabledState) {
return;
}
this.__disabledState = !!val;
if (val && !this._disdata) {
let style = this.getDefault("disabled") || this.getDefault("internalDisabled") || {
"background-color": this.getDefault("DisabledBG")
};
this._disdata = {
style : {},
defaults: {}
};
for (let k in style) {
//save old style information
this._disdata.style[k] = this.style[k];
this._disdata.defaults[k] = this.default_overrides[k];
let v = style[k];
if (typeof v === "object" && v instanceof CSSFont) {
this.style[k] = style[k].genCSS();
} else {
this.style[k] = style[k];
}
this.default_overrides[k] = style[k];
}
this.__disabledState = !!val;
this.on_disabled();
} else if (!val && this._disdata) {
//load old style information
for (let k in this._disdata.style) {
this.style[k] = this._disdata.style[k];
}
for (let k in this._disdata.defaults) {
let v = this._disdata.defaults[k];
if (v === undefined) {
delete this.default_overrides[k];
} else {
this.default_overrides[k] = v;
}
}
//this.background = this.style["background-color"];
this._disdata = undefined;
this.__disabledState = !!val;
this.on_enabled();
}
this.__disabledState = !!val;
let visit = (n) => {
if (n instanceof UIBase) {
let changed = !!n.__disabledState;
/*
if (val) {
n._parent_disabled_set = Math.max(n._parent_disabled_set + 1, 0);
} else {
n._parent_disabled_set = Math.max(n._parent_disabled_set - 1, 0);
}//*/
n.__updateDisable(n.disabled);
changed = changed !== !!n.__disabledState;
if (changed) {
n.update();
n.setCSS();
}
}
};
this._forEachChildWidget(visit);
}
on_disabled() {
}
on_enabled() {
}
pushModal(handlers = this, autoStopPropagation = true, pointerId = undefined, pointerElem = this) {
if (this._modaldata !== undefined) {
console.warn("UIBase.prototype.pushModal called when already in modal mode");
this.popModal();
}
let _areaWrangler = contextWrangler.copy();
contextWrangler.copy(this.ctx);
function bindFunc(func) {
return function () {
_areaWrangler.copyTo(contextWrangler);
return func.apply(handlers, arguments);
}
}
let handlers2 = {};
for (let k in handlers) {
let func = handlers[k];
if (typeof func !== "function") {
continue;
}
handlers2[k] = bindFunc(func);
}
//this._modalstack.push(this.ctx);
//this.ctx = this.ctx.toLocked();
if (pointerId !== undefined && pointerElem) {
this._modaldata = pushPointerModal(handlers2, autoStopPropagation);
} else {
this._modaldata = pushModalLight(handlers2, autoStopPropagation);
}
return this._modaldata;
}
popModal() {
if (this._modaldata === undefined) {
console.warn("Invalid call to UIBase.prototype.popModal");
return;
}
this.ctx = this._modalstack.pop();
popModalLight(this._modaldata);
this._modaldata = undefined;
}
/** child classes can override this to prevent focus on flash*/
_flash_focus() {
this.focus();
}
flash(color, rect_element = this, timems = 355, autoFocus = true) {
if (typeof color != "object") {
color = css2color(color);
}
color = new Vector4(color);
let csscolor = color2css(color);
if (this._flashtimer !== undefined && this._flashcolor !== csscolor) {
window.setTimeout(() => {
this.flash(color, rect_element, timems, autoFocus);
}, 100);
return;
} else if (this._flashtimer !== undefined) {
return;
}
//let rect = rect_element.getClientRects()[0];
let rect = rect_element.getBoundingClientRect();
if (rect === undefined) {
return;
}
//okay, dom apparently calls onchange() on .remove, so we have
//to put the timer code first to avoid loops
let timer;
let tick = 0;
let max = ~~(timems/20);
let x = rect.x, y = rect.y;
let cb = (e) => {
if (timer === undefined) {
return
}
let a = 1.0 - tick/max;
div.style["background-color"] = color2css(color, a*a*0.5);
if (tick > max) {
window.clearInterval(timer);
this._flashtimer = undefined;
this._flashcolor = undefined;
timer = undefined;
div.remove();
if (autoFocus) {
this._flash_focus();
}
}
tick++;
};
setTimeout(cb, 5);
this._flashtimer = timer = window.setInterval(cb, 20);
let div = document.createElement("div");
div.style["pointer-events"] = "none";
div.tabIndex = undefined;
div.style["z-index"] = "900";
div.style["display"] = "float";
div.style["position"] = UIBase.PositionKey;
div.style["margin"] = "0px";
div.style["left"] = x + "px";
div.style["top"] = y + "px";
div.style["background-color"] = color2css(color, 0.5);
div.style["width"] = rect.width + "px";
div.style["height"] = rect.height + "px";
div.setAttribute("class", "UIBaseFlash");
let screen = this.getScreen();
if (screen !== undefined) {
screen._enterPopupSafe();
}
document.body.appendChild(div);
if (autoFocus) {
this._flash_focus();
}
this._flashcolor = csscolor;
if (screen !== undefined) {
screen._exitPopupSafe();
}
}
destroy() {
}
on_resize(newsize) {
}
toJSON() {
let ret = {};
if (this.hasAttribute("datapath")) {
ret.datapath = this.getAttribute("datapath");
}
return ret;
}
loadJSON(obj) {
if (!this._init_done) {
this._init();
}
}
getPathValue(ctx, path) {
try {
return ctx.api.getValue(ctx, path);
} catch (error) {
//report("data path error in ui for" + path);
return undefined;
}
}
undoBreakPoint() {
this.pathUndoGen++;
}
setPathValueUndo(ctx, path, val) {
let mass_set_path = this.getAttribute("mass_set_path");
let rdef = ctx.api.resolvePath(ctx, path);
let prop = rdef.prop;
if (ctx.api.getValue(ctx, path) === val) {
return;
}
let toolstack = this.ctx.toolstack;
let head = toolstack.head;
let bad = head === undefined || !(head instanceof getDataPathToolOp());
bad = bad || head.hashThis() !== head.hash(mass_set_path, path, prop.type, this._id);
bad = bad || this.pathUndoGen !== this._lastPathUndoGen;
//if (head !== undefined && head instanceof getDataPathToolOp()) {
//console.log("===>", bad, head.hashThis());
//console.log(" ->", head.hash(mass_set_path, path, prop.type, this._id));
//}
if (!bad) {
toolstack.undo();
head.setValue(ctx, val, rdef.obj);
toolstack.redo();
} else {
this._lastPathUndoGen = this.pathUndoGen;
let toolop = getDataPathToolOp().create(ctx, path, val, this._id, mass_set_path);
ctx.toolstack.execTool(this.ctx, toolop);
head = toolstack.head;
}
if (!head || head.hadError === true) {
throw new Error("toolpath error");
}
}
/*
adds a method call to the event queue,
but only if that method (for this instance, as differentiated
by ._id) isn't there already.
also, method won't be ran until this.ctx exists
*/
pushReportContext(key) {
if (this.ctx.api.pushReportContext) {
this.ctx.api.pushReportContext(key);
}
}
popReportContext() {
if (this.ctx.api.popReportContext)
this.ctx.api.popReportContext();
}
setPathValue(ctx, path, val) {
if (this.useDataPathUndo) {
this.pushReportContext(this._reportCtxName);
try {
this.setPathValueUndo(ctx, path, val);
} catch (error) {
this.popReportContext();
if (!(error instanceof DataPathError)) {
throw error;
} else {
return;
}
}
this.popReportContext();
return;
}
this.pushReportContext(this._reportCtxName);
try {
if (this.hasAttribute("mass_set_path")) {
ctx.api.massSetProp(ctx, this.getAttribute("mass_set_path"), val);
ctx.api.setValue(ctx, path, val);
} else {
ctx.api.setValue(ctx, path, val);
}
} catch (error) {
this.popReportContext();
if (!(error instanceof DataPathError)) {
throw error;
}
return;
}
this.popReportContext();
}
getPathMeta(ctx, path) {
this.pushReportContext(this._reportCtxName);
let ret = ctx.api.resolvePath(ctx, path);
this.popReportContext();
return ret !== undefined ? ret.prop : undefined;
}
getPathDescription(ctx, path) {
let ret;
this.pushReportContext(this._reportCtxName);
try {
ret = ctx.api.getDescription(ctx, path);
} catch (error) {
this.popReportContext();
if (error instanceof DataPathError) {
//console.warn("Invalid data path '" + path + "'");
return undefined;
} else {
throw error;
}
}
this.popReportContext();
return ret;
}
getScreen() {
if (this.ctx !== undefined)
return this.ctx.screen;
}
isDead() {
return !this.isConnected;
let p = this, lastp = this;
function find(c, n) {
for (let n2 of c) {
if (n2 === n) {
return true;
}
}
}
while (p) {
lastp = p;
let parent = p.parentWidget;
if (!parent) {
parent = p.parentElement ? p.parentElement : p.parentNode;
}
if (parent && p && !find(parent.childNodes, p)) {
if (parent.shadow !== undefined && !find(parent.shadow.childNodes)) {
return true;
}
}
p = parent;
if (p === document.body) {
return false;
}
}
return true;
}
doOnce(func, timeout = undefined) {
if (func._doOnce === undefined) {
func._doOnce_reqs = new Set();
func._doOnce = function (thisvar, trace) {
if (func._doOnce_reqs.has(thisvar._id)) {
return;
}
func._doOnce_reqs.add(thisvar._id);
function f() {
if (thisvar.isDead()) {
func._doOnce_reqs.delete(thisvar._id);
if (func === thisvar._init || !cconst.DEBUG.doOnce) {
return;
}
console.warn("Ignoring doOnce call for dead element", thisvar._id, func, trace);
return;
}
if (!thisvar.ctx) {
if (cconst.DEBUG.doOnce) {
console.warn("doOnce call is waiting for context...", thisvar._id, func);
}
window.setTimeout(f, 0);
return;
}
func._doOnce_reqs.delete(thisvar._id);
func.call(thisvar);
};
window.setTimeout(f, timeout);
}
}
let trace = new Error().stack;
func._doOnce(this, trace);
}
float(x = 0, y = 0, zindex = undefined, positionKey = UIBase.PositionKey) {
this.style.position = positionKey;
this.style.left = x + "px";
this.style.top = y + "px";
if (zindex !== undefined) {
this.style["z-index"] = zindex
}
return this;
}
_ensureChildrenCtx() {
let ctx = this.ctx;
if (ctx === undefined) {
return;
}
this._forEachChildWidget((n) => {
n.parentWidget = this;
if (n.ctx === undefined) {
n.ctx = ctx;
}
n._ensureChildrenCtx(ctx);
});
}
checkThemeUpdate() {
if (!cconst.enableThemeAutoUpdate) {
return false;
}
if (_themeUpdateKey !== this._last_theme_update_key) {
this._last_theme_update_key = _themeUpdateKey;
return true;
}
return false;
}
abortToolTips(delayMs = 500) {
if (this._has_own_tooltips) {
this._has_own_tooltips.stop_timer();
}
if (this._tooltip_ref) {
this._tooltip_ref.remove();
this._tooltip_ref = undefined;
}
this._tool_tip_abort_delay = util.time_ms() + delayMs;
return this;
}
updateToolTipHandlers() {
if (!this._useNativeToolTips_set && !cconst.useNativeToolTips !== !this._useNativeToolTips) {
this._useNativeToolTips = cconst.useNativeToolTips;
}
if (!!this.useNativeToolTips === !this._has_own_tooltips) {
return;
}
if (!this.useNativeToolTips) {
let state = this._has_own_tooltips = {
start_timer : (e) => {
this._tooltip_timer = util.time_ms();
//console.warn(this._id, "tooltip timer start", e.type);
},
stop_timer : (e) => {
//console.warn(this._id, "tooltip timer end", util.time_ms()-this._tooltip_timer, e.type);
this._tooltip_timer = undefined;
},
reset_timer : (e) => {
//console.warn(this._id, "tooltip timer reset", util.time_ms()-this._tooltip_timer, e.type);
if (this._tooltip_timer !== undefined) {
this._tooltip_timer = util.time_ms();
}
},
start_events: [
"mouseover"
],
reset_events: [
"mousemove", "mousedown", "mouseup",
"touchstart", "touchend", "keydown", "focus"
],
stop_events : [
"mouseleave", "blur", "mouseout"
],
handlers : {}
}
let bind_handler = (type, etype) => {
let handler = (e) => {
if (this._tool_tip_abort_delay !== undefined && util.time_ms() < this._tool_tip_abort_delay) {
this._tooltip_timer = undefined;
return;
}
state[type](e);
};
if (etype in state.handlers) {
console.error(type, "is in handlers already");
return;
}
state.handlers[etype] = handler;
return handler;
}
let i = 0;
let lists = [state.start_events, state.stop_events, state.reset_events];
for (let type of ["start_timer", "stop_timer", "reset_timer"]) {
for (let etype of lists[i]) {
this.addEventListener(etype, bind_handler(type, etype), {passive: true});
}
i++;
}
} else {
console.warn(this.id, "removing tooltip handlers");
let state = this._has_own_tooltips;
for (let k in this.state.handlers) {
let handler = this.state.handlers[k];
this.removeEventListener(k, handler);
}
this._has_own_tooltips = undefined;
this._tooltip_timer = undefined;
}
}
updateToolTips() {
if (this._description_final === undefined || this._description_final === null ||
this._description_final.trim().length === 0) {
return;
}
if (!this.ctx || !this.ctx.screen) {
return;
}
this.updateToolTipHandlers();
if (this.useNativeToolTips || this._tooltip_timer === undefined) {
return;
}
if (this._tool_tip_abort_delay !== undefined && util.time_ms() < this._tool_tip_abort_delay) {
return;
}
this._tool_tip_abort_delay = undefined;
let screen = this.ctx.screen;
const timelimit = 500;
let ok = util.time_ms() - this._tooltip_timer > timelimit;
let x = screen.mpos[0], y = screen.mpos[1];
let r = this.getClientRects();
r = r ? r[0] : r;
if (!r) {
ok = false;
} else {
ok = ok && x >= r.x && x < r.x + r.width;
ok = ok && y >= r.y && y < r.y + r.height;
}
//console.log(r);
if (r) {
//console.warn(this._id, "possible tooltip", x, y, r.x-3, r.y-3, r.width, r.height);
}
ok = ok && !haveModal();
ok = ok && screen.pickElement(x, y) === this;
ok = ok && this._description_final;
if (ok) {
//console.warn("Showing tooltop", this.id);
this._tooltip_ref = _ToolTip.show(this._description_final, this.ctx.screen, x, y);
} else {
if (this._tooltip_ref) {
this._tooltip_ref.remove();
}
this._tooltip_ref = undefined;
}
//console.warn(this._id, "tooltip timer end");
if (util.time_ms() - this._tooltip_timer > timelimit) {
this._tooltip_timer = undefined;
}
}
//called regularly
update() {
this.updateToolTips();
if (this.ctx && this._description === undefined && this.getAttribute("datapath")) {
let d = this.getPathDescription(this.ctx, this.getAttribute("datapath"));
this.description = d;
}
if (!this._init_done) {
this._init();
}
if (this._init_done && !this.constructor.define().subclassChecksTheme) {
if (this.checkThemeUpdate()) {
console.log("theme update!");
this.setCSS();
}
}
}
onadd() {
//if (this.parentWidget !== undefined) {
// this._useDataPathUndo = this.parentWidget._useDataPathUndo;
//}
if (!this._init_done) {
this.doOnce(this._init);
}
if (this.tabIndex >= 0) {
this.regenTabOrder();
}
}
getZoom() {
if (this.parentWidget !== undefined) {
return this.parentWidget.getZoom();
}
return 1.0;
}
/**try to use this method
scaling ratio (e.g. for high-resolution displays)
for zoom ratio use getZoom()
*/
getDPI() {
if (this.parentWidget !== undefined) {
return this.parentWidget.getDPI();
}
return UIBase.getDPI();
}
/**
* for saving ui state.
* see saveUIData() export
*
* should fail gracefully.
*/
saveData() {
return {};
}
/**
* for saving ui state.
* see saveUIData() export
*
* should fail gracefully.
*
* also, it doesn't rebuild the object graph,
* it patches it; for true serialization use
* the toJSON/loadJSON or STRUCT interfaces.
*/
loadData(obj) {
return this;
}
overrideDefault(key, val, localOnly = false) {
this.my_default_overrides[key] = val;
if (!localOnly) {
this.default_overrides[key] = val;
}
return this;
}
overrideClass(style) {
this._override_class = style;
}
overrideClassDefault(style, key, val) {
if (!(style in this.class_default_overrides)) {
this.class_default_overrides[style] = {};
}
this.class_default_overrides[style][key] = val;
}
_doMobileDefault(key, val) {
if (!util.isMobile())
return val;
key = key.toLowerCase();
let ok = false;
for (let re of _mobile_theme_patterns) {
if (key.search(re) >= 0) {
ok = true;
break;
}
}
if (ok) {
val *= theme.base.mobileSizeMultiplier;
}
return val;
}
hasDefault(key) {
let p = this;
while (p) {
if (key in p.default_overrides) {
return true;
}
p = p.parentWidget;
}
return this.hasClassDefault(key);
}
/** get a sub style from a theme style class.
* note that if key is falsy then it just forwards to this.getDefault directly*/
getSubDefault(key, subkey, backupkey = subkey, defaultval = undefined) {
if (!key) {
return this.getDefault(subkey, undefined, defaultval);
}
let style = this.getDefault(key);
if (!style || typeof style !== "object" || !(subkey in style)) {
if (defaultval !== undefined) {
return defaultval;
} else if (backupkey !== undefined) {
return this.getDefault(backupkey);
}
} else {
return style[subkey];
}
}
getDefault(key, checkForMobile = true, defaultval = undefined) {
let ret = this.getDefault_intern(key, checkForMobile, defaultval);
//convert pixel units straight to numbers
if (typeof ret === "string" && ret.trim().toLowerCase().endsWith("px")) {
let s = ret.trim().toLowerCase();
s = s.slice(0, s.length - 2).trim();
let f = parseFloat(s);
if (!isNaN(f) && isFinite(f)) {
return f;
}
}
return ret;
}
getDefault_intern(key, checkForMobile = true, defaultval = undefined) {
if (this.my_default_overrides[key] !== undefined) {
let v = this.my_default_overrides[key];
return checkForMobile ? this._doMobileDefault(key, v) : v;
}
let p = this;
while (p) {
if (p.default_overrides[key] !== undefined) {
let v = p.default_overrides[key];
checkForMobile ? this._doMobileDefault(key, v) : v;
}
p = p.parentWidget;
}
return this.getClassDefault(key, checkForMobile, defaultval);
}
getStyleClass() {
if (this._override_class !== undefined) {
return this._override_class;
}
let p = this.constructor, lastp = undefined;
while (p && p !== lastp && p !== UIBase && p !== Object) {
let def = p.define();
if (def.style) {
return def.style;
}
if (!p.prototype || !p.prototype.__proto__)
break;
p = p.prototype.__proto__.constructor;
}
return "base";
}
hasClassDefault(key) {
let style = this.getStyleClass();
let p = this;
while (p) {
let def = p.class_default_overrides[style];
if (def && (key in def)) {
return true;
}
p = p.parentWidget;
}
let th = this._themeOverride;
if (th && style in th && key in th[style]) {
return true;
}
if (style in theme && key in theme[style]) {
return true;
}
return key in theme.base;
}
getClassDefault(key, checkForMobile = true, defaultval = undefined) {
let style = this.getStyleClass();
if (style === "none") {
return undefined;
}
let val = undefined;
let p = this;
while (p) {
let def = p.class_default_overrides[style];
if (def && (key in def)) {
val = def[key];
break;
}
p = p.parentWidget;
}
if (val === undefined && style in theme && !(key in theme[style]) && !(key in theme.base)) {
if (window.DEBUG.theme) {
report("Missing theme key ", key, "for", style);
}
}
for (let i = 0; i < 2; i++) {
let th = !i ? this._themeOverride : theme;
if (!th) {
continue;
}
if (val === undefined && style in th && key in th[style]) {
val = th[style][key];
} else if (defaultval !== undefined) {
val = defaultval;
} else if (val === undefined) {
let def = this.constructor.define();
if (def.parentStyle && key in th[def.parentStyle]) {
val = th[def.parentStyle][key];
} else {
val = th.base[key];
}
}
}
return checkForMobile ? this._doMobileDefault(key, val) : val;
}
overrideTheme(theme) {
this._themeOverride = theme;
this._forEachChildWidget((child) => {
child.overrideTheme(theme);
});
if (this.ctx) {
this.flushSetCSS();
this.flushUpdate();
}
return this;
}
getStyle() {
console.warn("deprecated call to UIBase.getStyle");
return this.getStyleClass();
}
/** returns a new Animator instance
*
* example:
*
* container.animate().goto("style.width", 500, 100, "ease");
* */
animate(_extra_handlers = {}) {
let transform = new DOMMatrix(this.style["transform"]);
let update_trans = () => {
let t = transform;
let css = "matrix(" + t.a + "," + t.b + "," + t.c + "," + t.d + "," + t.e + "," + t.f + ")";
this.style["transform"] = css;
}
let handlers = {
background_get() {
return css2color(this.background);
},
background_set(c) {
if (typeof c !== "string") {
c = color2css(c);
}
this.background = c;
},
dx_get() {
return transform.m41;
},
dx_set(x) {
transform.m41 = x;
update_trans();
},
dy_get() {
return transform.m42;
},
dy_set(x) {
transform.m42 = x;
update_trans();
}
}
let pixkeys = ["width", "height", "left", "top", "right", "bottom", "border-radius",
"border-width", "margin", "padding", "margin-left", "margin-right",
"margin-top", "margin-bottom", "padding-left", "padding-right", "padding-bottom",
"padding-top"];
handlers = Object.assign(handlers, _extra_handlers);
let makePixHandler = (k, k2) => {
handlers[k2 + "_get"] = () => {
let s = this.style[k];
if (s.endsWith("px")) {
return parsepx(s);
} else {
return 0.0;
}
}
handlers[k2 + "_set"] = (val) => {
this.style[k] = val + "px";
}
}
for (let k of pixkeys) {
if (!(k in handlers)) {
makePixHandler(k, `style.${k}`);
makePixHandler(k, `style["${k}"]`);
makePixHandler(k, `style['${k}']`);
}
}
let handler = {
get: (target, key, receiver) => {
console.log(key, handlers[key + "_get"], handlers);
if ((key + "_get") in handlers) {
return handlers[key + "_get"].call(target);
} else {
return target[key];
}
},
set: (target, key, val, receiver) => {
console.log(key);
if ((key + "_set") in handlers) {
handlers[key + "_set"].call(target, val);
} else {
target[key] = val;
}
return true;
}
}
let proxy = new Proxy(this, handler);
let anim = new Animator(proxy);
anim.onend = () => {
this._active_animations.remove(anim);
}
this._active_animations.push(anim);
return anim;
}
abortAnimations() {
for (let anim of util.list(this._active_animations)) {
anim.end();
}
this._active_animations = [];
}
}
export function drawRoundBox2(elem, options = {}) {
drawRoundBox(elem, options.canvas, options.g, options.width, options.height, options.r, options.op, options.color, options.margin, options.no_clear);
}
/**okay, I need to refactor this function,
it needs to take x, y as well as width, height,
and be usable for more use cases.*/
export function drawRoundBox(elem, canvas, g, width, height, r = undefined,
op = "fill", color = undefined, margin = undefined,
no_clear = false) {
width = width === undefined ? canvas.width : width;
height = height === undefined ? canvas.height : height;
g.save();
let dpi = elem.getDPI();
r = r === undefined ? elem.getDefault("border-radius") : r;
if (margin === undefined) {
margin = 1;
}
r *= dpi;
let r1 = r, r2 = r;
if (r > (height - margin*2)*0.5) {
r1 = (height - margin*2)*0.5;
}
if (r > (width - margin*2)*0.5) {
r2 = (width - margin*2)*0.5;
}
let bg = color;
if (bg === undefined && canvas._background !== undefined) {
bg = canvas._background;
} else if (bg === undefined) {
bg = elem.getDefault("background-color");
}
if (op === "fill" && !no_clear) {
g.clearRect(0, 0, width, height);
}
g.fillStyle = bg;
//hackish!
g.strokeStyle = color === undefined ? elem.getDefault("border-color") : color;
let w = width, h = height;
let th = Math.PI/4;
let th2 = Math.PI*0.75;
g.beginPath();
g.moveTo(margin, margin + r1);
g.lineTo(margin, h - r1 - margin);
g.quadraticCurveTo(margin, h - margin, margin + r2, h - margin);
g.lineTo(w - margin - r2, h - margin);
g.quadraticCurveTo(w - margin, h - margin, w - margin, h - margin - r1);
g.lineTo(w - margin, margin + r1);
g.quadraticCurveTo(w - margin, margin, w - margin - r2, margin);
g.lineTo(margin + r2, margin);
g.quadraticCurveTo(margin, margin, margin, margin + r1);
g.closePath();
if (op === "clip") {
g.clip();
} else if (op === "fill") {
g.fill();
} else {
g.stroke();
}
g.restore();
};
export function _getFont_new(elem, size, font = "DefaultText", do_dpi = true) {
font = elem.getDefault(font);
return font.genCSS(size);
}
export function getFont(elem, size, font = "DefaultText", do_dpi = true) {
return _getFont_new(elem, size, font = "DefaultText", do_dpi = true);
}
//size is optional, defaults to font's default size
export function _getFont(elem, size, font = "DefaultText", do_dpi = true) {
let dpi = elem.getDPI();
let font2 = elem.getDefault(font);
if (font2 !== undefined) {
//console.warn("New style font detected", font2, font2.genCSS(size));
return _getFont_new(elem, size, font, do_dpi);
}
throw new Error("unknown font " + font);
}
export function _ensureFont(elem, canvas, g, size) {
if (canvas.font) {
g.font = canvas.font;
} else {
let font = elem.getDefault("DefaultText");
g.font = font.genCSS(size);
}
}
let _mc;
function get_measure_canvas() {
if (_mc !== undefined) {
return _mc;
}
_mc = document.createElement("canvas");
_mc.width = 256;
_mc.height = 256;
_mc.g = _mc.getContext("2d");
return _mc;
}
export function measureTextBlock(elem, text, canvas = undefined,
g = undefined, size = undefined, font = undefined) {
let lines = text.split("\n");
let ret = {
width : 0,
height: 0
};
if (size === undefined) {
if (font !== undefined && typeof font === "object") {
size = font.size;
}
if (size === undefined) {
size = elem.getDefault("DefaultText").size;
}
}
for (let line of lines) {
let m = measureText(elem, line, canvas, g, size, font);
ret.width = Math.max(ret.width, m.width);
let h = m.height !== undefined ? m.height : size*1.25;
ret.height += h;
}
return ret;
}
export function measureText(elem, text, canvas = undefined,
g = undefined, size = undefined, font = undefined) {
if (typeof canvas === "object" && canvas !== null && !(canvas instanceof HTMLCanvasElement) && canvas.tagName !== "CANVAS") {
let args = canvas;
canvas = args.canvas;
g = args.g;
size = args.size;
font = args.font;
}
if (g === undefined) {
canvas = get_measure_canvas();
g = canvas.g;
}
if (font !== undefined) {
if (typeof font === "object" && font instanceof CSSFont) {
font = font.genCSS(size);
}
g.font = font;
} else {
_ensureFont(elem, canvas, g, size);
}
let ret = g.measureText(text);
if (ret && util.isMobile()) {
let ret2 = {};
let dpi = UIBase.getDPI();
for (let k in ret) {
let v = ret[k];
if (typeof v === "number") {
v *= dpi;
//v *= window.devicePixelRatio;
}
ret2[k] = v;
}
ret = ret2;
}
if (size !== undefined) {
//clear custom font for next time
g.font = undefined;
}
return ret;
}
//export function drawText(elem, x, y, text, canvas, g, color=undefined, size=undefined, font=undefined) {
export function drawText(elem, x, y, text, args = {}) {
let canvas = args.canvas, g = args.g, color = args.color, font = args.font;
let size = args.size;
if (size === undefined) {
if (font !== undefined && font instanceof CSSFont) {
size = font.size;
} else {
size = elem.getDefault("DefaultText").size;
}
}
size *= UIBase.getDPI();
if (color === undefined) {
if (font && font.color) {
color = font.color;
} else {
color = elem.getDefault("DefaultText").color;
}
}
if (font === undefined) {
_ensureFont(elem, canvas, g, size);
} else if (typeof font === "object" && font instanceof CSSFont) {
g.font = font = font.genCSS(size);
} else if (font) {
g.font = font;
}
if (typeof color === "object") {
color = color2css(color);
}
g.fillStyle = color;
g.fillText(text, x + 0.5, y + 0.5);
if (size !== undefined) {
//clear custom font for next time
g.font = undefined;
}
}
let PIDX = 0, PSHADOW = 1, PTOT = 2;
/**
Saves UI layout data, like panel layouts, active tabs, etc.
Uses the UIBase.prototype.[save/load]Data interface.
Note that this is error-tolerant.
*/
export function saveUIData(node, key) {
if (key === undefined) {
throw new Error("ui_base.saveUIData(): key cannot be undefined");
}
let paths = [];
let rec = (n, path, ni, is_shadow) => {
path = path.slice(0, path.length); //copy path
let pi = path.length;
for (let i = 0; i < PTOT; i++) {
path.push(undefined);
}
path[pi] = ni;
path[pi + 1] = is_shadow ? 1 : 0;
if (n instanceof UIBase) {
let path2 = path.slice(0, path.length);
let data = n.saveData();
let bad = !data;
bad = bad || (typeof data === "object" && Object.keys(data).length === 0);
if (!bad) {
path2.push(data);
if (path2[pi + 2]) {
paths.push(path2);
}
}
}
for (let i = 0; i < n.childNodes.length; i++) {
let n2 = n.childNodes[i];
rec(n2, path, i, false);
}
let shadow = n.shadow;
if (!shadow)
return;
for (let i = 0; i < shadow.childNodes.length; i++) {
let n2 = shadow.childNodes[i];
rec(n2, path, i, true);
}
}
rec(node, [], 0, false);
return JSON.stringify({
key : key,
paths : paths,
_ui_version: 1
});
}
window._saveUIData = saveUIData;
export function loadUIData(node, buf) {
if (buf === undefined || buf === null) {
return;
}
let obj = JSON.parse(buf);
let key = buf.key;
for (let path of obj.paths) {
let n = node;
let data = path[path.length - 1];
path = path.slice(2, path.length - 1); //in case some api doesn't want me calling .pop()
for (let pi = 0; pi < path.length; pi += PTOT) {
let ni = path[pi], shadow = path[pi + 1];
let list;
if (shadow) {
list = n.shadow;
if (list) {
list = list.childNodes;
}
} else {
list = n.childNodes;
}
if (list === undefined || list[ni] === undefined) {
//console.log("failed to load a ui data block", path);
n = undefined;
break;
}
n = list[ni];
}
if (n !== undefined && n instanceof UIBase) {
n._init(); //ensure init's been called, _init will check if it has
n.loadData(data);
//console.log(n, path, data);
}
}
}
UIBase.PositionKey = "fixed";
window._loadUIData = loadUIData;
//avoid explicit circular references
aspect._setUIBase(UIBase);
//ui_save.setUIBase(UIBase);