Home Reference Source

scripts/screen/ScreenArea.js

let _ScreenArea = undefined;

import * as util from '../path-controller/util/util.js';
import * as vectormath from '../path-controller/util/vectormath.js';
import * as ui_base from '../core/ui_base.js';
import * as ui from '../core/ui.js';
import * as ui_noteframe from '../widgets/ui_noteframe.js';
import {haveModal} from '../path-controller/util/simple_events.js';
import cconst from '../config/const.js';

import nstructjs from '../path-controller/util/struct.js';

let UIBase = ui_base.UIBase;

import {EnumProperty} from "../path-controller/toolsys/toolprop.js";

let Vector2 = vectormath.Vector2;
let Screen = undefined;

import {BORDER_ZINDEX_BASE, ScreenBorder, snap, snapi} from './FrameManager_mesh.js';

export const AreaFlags = {
  HIDDEN                : 1,
  FLOATING              : 2,
  INDEPENDENT           : 4, //area is indpendent of the screen mesh
  NO_SWITCHER           : 8,
  NO_HEADER_CONTEXT_MENU: 16,
  NO_COLLAPSE           : 32,
};


export * from './area_wrangler.js';
import {getAreaIntName, setAreaTypes, contextWrangler, AreaWrangler, areaclasses} from './area_wrangler.js';

export {contextWrangler};

window._contextWrangler = contextWrangler;

export const BorderMask = {
  LEFT  : 1,
  BOTTOM: 2,
  RIGHT : 4,
  TOP   : 8,
  ALL   : 1 | 2 | 4 | 8
};

export const BorderSides = {
  LEFT  : 0,
  BOTTOM: 1,
  RIGHT : 2,
  TOP   : 3
};

/**
 * Base class for all editors
 **/
export class Area extends ui_base.UIBase {
  constructor() {
    super();

    /**
     * -----b4----
     *
     * b1       b3
     *
     * -----b2----
     *
     * */

    let def = this.constructor.define();

    //set bits in mask to keep
    //borders from moving
    this.borderLock = def.borderLock || 0;
    this.flag = def.flag || 0;

    this.inactive = true;
    this.areaDragToolEnabled = true;

    this.owning_sarea = undefined;
    this._area_id = contextWrangler.idgen++;

    this.pos = undefined; //set by screenarea parent
    this.size = undefined; //set by screenarea parent
    this.minSize = [5, 5];
    this.maxSize = [undefined, undefined];

    let appendChild = this.shadow.appendChild;
    this.shadow.appendChild = (child) => {
      appendChild.call(this.shadow, child);
      if (child instanceof UIBase) {
        child.parentWidget = this;
      }
    }

    let prepend = this.shadow.prepend;
    this.shadow.prepend = (child) => {
      prepend.call(this.shadow, child);

      if (child instanceof UIBase) {
        child.parentWidget = this;
      }
    };
  }

  get floating() {
    return ~~(this.flag & AreaFlags.FLOATING);
  }

  set floating(val) {
    if (val) {
      this.flag |= AreaFlags.FLOATING;
    } else {
      this.flag &= ~AreaFlags.FLOATING;
    }
  }

  /**
   * Get active area as defined by push_ctx_active and pop_ctx_active.
   *
   * Type should be an Area subclass, if undefined the last accessed area
   * will be returned.
   * */
  static getActiveArea(type) {
    return contextWrangler.getLastArea(type);
  }

  static unregister(cls) {
    let def = cls.define();

    if (!def.areaname) {
      throw new Error("Missing areaname key in define()");
    }

    if (def.areaname in areaclasses) {
      delete areaclasses[def.areaname];
    }
  }

  /*
  addEventListener(type, cb, options) {
    let cb2 = (e) => {
      let x, y;
      let screen = this.getScreen();

      if (!screen) {
        console.warn("no screen!");
        return cb(elem);
      }

      if (type.startsWith("mouse")) {
        x = e.x; y = e.y;
      } else if (type.startsWith("touch") && e.touches && e.touches.length > 0) {
        x = e.touches[0].pageX; y = e.touches[0].pageY;
      } else if (type.startsWith("pointer")) {
        x = e.x; y = e.y;
      } else {
        if (screen) {
          x = screen.mpos[0];
          y = screen.mpos[1];
        } else {
          x = y = -100;
        }
      }

      let elem = screen.pickElement(x, y);
      console.log(elem ? elem.tagName : undefined);

      if (elem === this || elem === this.owning_sarea) {
        return cb(elem);
      }
    };

    cb.__cb2 = cb2;
    return super.addEventListener(type, cb2, options);
  }

  removeEventListener(type, cb, options) {
    super.removeEventListener(type, cb.__cb2, options);
  }
  //*/

  static register(cls) {
    let def = cls.define();

    if (!def.areaname) {
      throw new Error("Missing areaname key in define()");
    }

    areaclasses[def.areaname] = cls;

    ui_base.UIBase.internalRegister(cls);
  }

  static makeAreasEnum() {
    let areas = {};
    let icons = {};
    let i = 0;

    for (let k in areaclasses) {
      let cls = areaclasses[k];
      let def = cls.define();

      if (def.flag & AreaFlags.HIDDEN)
        continue;

      let uiname = def.uiname;

      if (uiname === undefined) {
        uiname = k.replace("_", " ").toLowerCase();
        uiname = uiname[0].toUpperCase() + uiname.slice(1, uiname.length);
      }

      areas[uiname] = k;
      icons[uiname] = def.icon !== undefined ? def.icon : -1;
    }

    let prop = new EnumProperty(undefined, areas);
    prop.addIcons(icons);

    return prop;
  }

  static define() {
    return {
      tagname : "pathux-editor-x", // tag name, e.g. editor-x
      areaname: undefined, //api name for area type
      flag    : 0, //see AreaFlags
      uiname  : undefined,
      icon    : undefined //icon representing area in MakeHeader's area switching menu. Integer.
    };
  }

  static newSTRUCT() {
    return UIBase.createElement(this.define().tagname);
  }

  init() {
    super.init();

    this.style["overflow"] = "hidden";
    this.noMarginsOrPadding();

    let onover = (e) => {
      //console.log(this._area_id, this.ctx.workspace._area_id);

      //try to trigger correct entry in context area stacks
      this.push_ctx_active();
      this.pop_ctx_active();
    };

    //*
    super.addEventListener("mouseover", onover, {passive: true});
    super.addEventListener("mousemove", onover, {passive: true});
    super.addEventListener("mousein", onover, {passive: true});
    super.addEventListener("mouseenter", onover, {passive: true});
    super.addEventListener("touchstart", onover, {passive: true});
    super.addEventListener("focusin", onover, {passive: true});
    super.addEventListener("focus", onover, {passive: true});
    //*/
  }

  _get_v_suffix() {
    if (this.flag & AreaFlags.INDEPENDENT) {
      return this._id;
    } else {
      return "";
    }
  }

  /**
   * Return a list of keymaps used by this editor
   * @returns {Array<KeyMap>}
   */
  getKeyMaps() {
    return this.keymap !== undefined ? [this.keymap] : [];
  }

  on_fileload(isActiveEditor) {
    contextWrangler.reset();
  }

  buildDataPath() {
    let p = this;

    let sarea = this.owning_sarea;

    if (sarea === undefined || sarea.screen === undefined) {
      console.warn("Area.buildDataPath(): Failed to build data path");
      return "";
    }

    let screen = sarea.screen;

    let idx1 = screen.sareas.indexOf(sarea);
    let idx2 = sarea.editors.indexOf(this);

    if (idx1 < 0 || idx2 < 0) {
      throw new Error("malformed area data");
    }

    let ret = `screen.sareas[${idx1}].editors[${idx2}]`;
    return ret;
  }

  saveData() {
    return {
      _area_id: this._area_id,
      areaName: this.areaName
    };
  }

  loadData(obj) {
    let id = obj._area_id;

    if (id !== undefined && id !== null) {
      this._area_id = id;
    }
  }

  draw() {
  }

  copy() {
    console.warn("You might want to implement this, Area.prototype.copy based method called");
    let ret = UIBase.createElement(this.constructor.define().tagname);
    return ret;
  }

  on_resize(size, oldsize) {
    super.on_resize(size, oldsize);
  }

  on_area_focus() {

  }

  on_area_blur() {

  }

  /** called when editors are swapped with another editor type*/
  on_area_active() {
  }

  /** called when editors are swapped with another editor type*/
  on_area_inactive() {
  }

  /*
  * This is needed so UI controls can know what their parent area is.
  * For example, a slider with data path "view2d.zoomfac" needs to know where
  * to find view2d.
  *
  * Typically this works by adding a field to a ContextOverlay:
  *
  * class ContextOverlay {
  *   get view3d() {
  *     return Area.getActiveArea(View3D);
  *   }
  * }
  *
  * Make sure to wrap event callbacks in push_ctx_active and pop_ctx_active.
  * */
  push_ctx_active(dontSetLastRef = false) {
    contextWrangler.push(this.constructor, this, !dontSetLastRef);
  }

  /**
   * see push_ctx_active
   * */
  pop_ctx_active(dontSetLastRef = false) {
    contextWrangler.pop(this.constructor, this, !dontSetLastRef);
  }

  getScreen() {
    //XXX
    //return _appstate.screen;
    throw new Error("replace me in Area.prototype");
  }

  toJSON() {
    return Object.assign(super.toJSON(), {
      areaname: this.constructor.define().areaname,
      _area_id: this._area_id
    });
  }

  loadJSON(obj) {
    super.loadJSON(obj);
    this._area_id = obj._area_id;

    return this;
  }

  getBarHeight() {
    return this.header.getClientRects()[0].height;
  }

  makeAreaSwitcher(container) {
    if (cconst.useAreaTabSwitcher) {
      let ret = UIBase.createElement("area-docker-x");
      container.add(ret);
      return ret;
    }

    let prop = Area.makeAreasEnum();

    let dropbox = container.listenum(undefined, {
      name    : this.constructor.define().uiname,
      enumDef : prop,
      callback: (id) => {
        let cls = areaclasses[id];
        this.owning_sarea.switch_editor(cls);
      }
    });

    dropbox.update.after(() => {
      let name = this.constructor.define().uiname;
      let val = prop.values[name];

      if (dropbox.value !== val && val in prop.keys) {
        val = prop.keys[val];
      }

      if (dropbox.value !== val) {
        dropbox.setValue(prop.values[name], true);
      }
    });

    return dropbox;
  }

  makeHeader(container, add_note_area = true, make_draggable = true) {
    let switcherRow;
    let row;
    let helpRow;

    if (!(this.flag & AreaFlags.NO_SWITCHER) && cconst.useAreaTabSwitcher) {
      let col = this.header = container.col();

      switcherRow = helpRow = col.row();
      row = col.row();
    } else {
      row = helpRow = this.header = container.row();
    }

    if (!(this.flag & AreaFlags.NO_HEADER_CONTEXT_MENU)) {
      let callmenu = ScreenBorder.bindBorderMenu(this.header, true);

      this.addEventListener("mousedown", e => {
        if (e.button !== 2 || this.header.pickElement(e.x, e.y) !== this.header) {
          return;
        }

        callmenu(e);
      });
    }

    this.header.remove();
    container._prepend(this.header);

    row.setCSS.after(() => row.background = this.getDefault("AreaHeaderBG"));

    let rh = ~~(16*this.getDPI());

    //container.setSize(undefined, rh);
    //row.setSize(undefined, rh);
    //row.setSize(undefined, rh);

    container.noMarginsOrPadding();
    row.noMarginsOrPadding();

    row.style["width"] = "100%";
    row.style["margin"] = "0px";
    row.style["padding"] = "0px";

    if (!(this.flag & AreaFlags.NO_SWITCHER)) {
      if (this.switcher) {
        //add back same switcher
        switcherRow.add(this.switcher);
      } else {
        this.switcher = this.makeAreaSwitcher(cconst.useAreaTabSwitcher ? switcherRow : row);
      }
    }

    if (util.isMobile() || cconst.addHelpPickers) {
      if (this.helppicker) {
        this.helppicker.remove();
      }

      this.helppicker = helpRow.helppicker();
      this.helppicker.iconsheet = 0;
    }

    if (add_note_area) {
      let notef = UIBase.createElement("noteframe-x");
      notef.ctx = this.ctx;
      row._add(notef);
    }

    /* don't do normal dragging for tab switchers */
    if (cconst.useAreaTabSwitcher) {
      return row;
    }

    let eventdom = this.header;

    let mdown = false;
    let mpos = new Vector2();

    let mpre = (e, pageX, pageY) => {
      if (haveModal()) {
        return;
      }

      pageX = pageX === undefined ? e.x : pageX;
      pageY = pageY === undefined ? e.y : pageY;

      let node = this.getScreen().pickElement(pageX, pageY);

      /*
      while (node) {
        if (node === row) {
          break;
        }
        node = node.parentWidget;
      }//*/

      //console.log(node === row, node ? node._id : undefined, row._id)

      if (node !== row) {
        return false;
      }

      return true;
    }

    eventdom.addEventListener("pointerout", (e) => {
      //console.log("pointerout", e);
      mdown = false;
    });
    eventdom.addEventListener("pointerleave", (e) => {
      mdown = false;
      //console.log("pointerleave", e);
    });

    eventdom.addEventListener("pointerdown", (e) => {
      //console.log("pointerdown", e, mpre(e));

      if (!mpre(e)) return;

      mpos[0] = e.pageX;
      mpos[1] = e.pageY;
      mdown = true;
    });

    let last_time = util.time_ms();

    let do_mousemove = (e, pageX, pageY) => {
      if (haveModal() || !make_draggable) {
        return;
      }

      let mdown2 = e.buttons !== 0 || (e.touches && e.touches.length > 0);
      mdown2 = mdown2 && mdown;

      //console.log("area drag?", e, mdown2, e.pageX, e.pageY, mpre(e, pageX, pageY), e.was_touch);

      //calls to pickElement in mpre are expensive
      if (util.time_ms() - last_time < 250) {
        return;
      }

      last_time = util.time_ms;

      if (!mdown2 || !mpre(e, pageX, pageY)) return;


      if (e.type === "mousemove" && e.was_touch) {
        //okay how are patched events getting here?
        //avoid double call. . .
        return;
      }

      //console.log(mdown);
      let dx = pageX - mpos[0];
      let dy = pageY - mpos[1];

      let dis = dx*dx + dy*dy;
      let limit = 7;

      if (dis > limit*limit) {
        let sarea = this.owning_sarea;
        if (sarea === undefined) {
          console.warn("Error: missing sarea ref");
          return;
        }

        let screen = sarea.screen;
        if (screen === undefined) {
          console.log("Error: missing screen ref");
          return;
        }

        if (!this.areaDragToolEnabled) {
          return;
        }
        mdown = false;
        console.log("area drag tool!", e.type, e);
        screen.areaDragTool(this.owning_sarea);
      }
    };

    //not working on mobile
    //row.setAttribute("draggable", true);
    //row.draggable = true;
    /*
    eventdom.addEventListener("dragstart", (e) => {
      return;
      console.log("drag start!", e);
      e.dataTransfer.setData("text/json", "SplitAreaDrag");

      let canvas = document.createElement("canvas");
      let g = canvas.g;

      canvas.width = 32;
      canvas.height = 32;

      e.dataTransfer.setDragImage(canvas, 0, 0);

      mdown = false;
      console.log("area drag tool!");
      this.getScreen().areaDragTool(this.owning_sarea);
    });

    eventdom.addEventListener("drag", (e) => {
      console.log("drag!", e);
    });*/

    eventdom.addEventListener("pointermove", (e) => {
      return do_mousemove(e, e.pageX, e.pageY);
    }, false);
    eventdom.addEventListener("pointerup", (e) => {
      console.log("pointerup", e);
      mdown = false;
    }, false);
    eventdom.addEventListener("pointercancel", (e) => {
      console.log("pointercancel", e);
      mdown = false;
    }, false);

    return row;
  }

  setCSS() {
    if (this.size !== undefined) {
      this.style["position"] = UIBase.PositionKey;
      //this.style["left"] = this.pos[0] + "px";
      //this.style["top"] = this.pos[1] + "px";
      this.style["width"] = this.size[0] + "px";
      this.style["height"] = this.size[1] + "px";
    }
  }

  update() {
    //don't update non-active editors
    if (this.owning_sarea === undefined || this !== this.owning_sarea.area) {
      return;
    }

    super.update();

    //see FrameManager.js, we use a single update
    //function for everything now
    //this._forEachChildWidget((n) => {
    //  n.update();
    //});
  }

  loadSTRUCT(reader) {
    reader(this);
  }

  _isDead() {
    if (this.dead) {
      return true;
    }

    let screen = this.getScreen();

    if (screen === undefined)
      return true;

    if (screen.parentNode === undefined)
      return true;
  }

  //called by owning ScreenArea on file load
  afterSTRUCT() {
    let f = () => {
      if (this._isDead()) {
        return;
      }
      if (!this.ctx) {
        this.doOnce(f);
        return;
      }

      try {
        ui_base.loadUIData(this, this.saved_uidata);
        this.saved_uidata = undefined;
      } catch (error) {
        console.log("failed to load ui data");
        util.print_stack(error);
      }
    };

    this.doOnce(f);
  }

  loadSTRUCT(reader) {
    reader(this);
  }

  _getSavedUIData() {
    return ui_base.saveUIData(this, "area");
  }
}

Area.STRUCT = `
pathux.Area { 
  flag : int;
  saved_uidata : string | obj._getSavedUIData();
}
`

nstructjs.register(Area, "pathux.Area");
ui_base.UIBase.internalRegister(Area);

export class ScreenArea extends ui_base.UIBase {
  constructor() {
    super();

    this._flag = undefined;

    this.flag = 0; /** holds AreaFlags.FLOATING and AreaFlags.INDEPENDENT */

    this._borders = [];
    this._verts = [];
    this.dead = false;

    this._sarea_id = contextWrangler.idgen++;

    this._pos = new Vector2();
    this._size = new Vector2([512, 512]);

    if (cconst.DEBUG.screenAreaPosSizeAccesses) {
      let wrapVector = (name, axis) => {
        Object.defineProperty(this[name], axis, {
          get: function () {
            return this["_" + axis];
          },

          set: function (val) {
            console.warn(`ScreenArea.${name}[${axis}] set:`, val);
            this["_" + axis] = val;
          }
        });
      };

      wrapVector("size", 0);
      wrapVector("size", 1);
      wrapVector("pos", 0);
      wrapVector("pos", 1);
    }

    this.area = undefined;
    this.editors = [];
    this.editormap = {};

    this.addEventListener("mouseover", (e) => {
      if (haveModal()) {
        return;
      }

      //console.log("screen area mouseover");
      let screen = this.getScreen();
      if (screen.sareas.active !== this && screen.sareas.active && screen.sareas.active.area) {
        screen.sareas.active.area.on_area_blur();
      }

      if (this.area && screen.sareas.active !== this) {
        this.area.on_area_focus();
      }

      screen.sareas.active = this;
    });

    //this.addEventListener("mouseleave", (e) => {
    //console.log("screen area mouseleave");
    //});
  }

  /*
  saveData() {
    return {
      _sarea_id : this._sarea_id,
      pos       : this.pos,
      size      : this.size,
    };
  }
  loadData(obj) {
    super.loadData(obj);

    let id = obj._sarea_id;
    
    let type = obj.areatype;
    
    if (id !== undefined && id !== null) {
      this._sarea_id = id;
    }
    
    for (let area of this.editors) {
      if (area.areaType == type) {
        console.log("             found saved area type");
        
        this.switch_editor(area.constructor);
      }
    }
    
    this.pos.load(obj.pos);
    this.size.load(obj.size);
  }//*/

  get floating() {
    return this.flag & AreaFlags.FLOATING;
  }

  set floating(val) {
    if (val) {
      this.flag |= AreaFlags.FLOATING;
    } else {
      this.flag &= ~AreaFlags.FLOATING;
    }
  }

  get flag() {
    let flag = this._flag & (AreaFlags.FLOATING | AreaFlags.INDEPENDENT);

    if (this.area) {
      flag |= this.area.flag;
    }

    return flag;
  }

  set flag(v) {
    this._flag &= ~(AreaFlags.FLOATING | AreaFlags.INDEPENDENT);
    this._flag |= v & (AreaFlags.FLOATING | AreaFlags.INDEPENDENT);

    if (this.area) {
      this.area.flag |= v & ~(AreaFlags.FLOATING | AreaFlags.INDEPENDENT);
    }
  }

  get borderLock() {
    return this.area !== undefined ? this.area.borderLock : 0;
  }

  get minSize() {
    return this.area !== undefined ? this.area.minSize : this.size;
  }

  get maxSize() {
    return this.area !== undefined ? this.area.maxSize : this.size;
  }

  get pos() {
    return this._pos;
  }

  set pos(val) {
    if (cconst.DEBUG.screenAreaPosSizeAccesses) {
      console.log("ScreenArea set pos", val);
    }
    this._pos.load(val);
  }

  get size() {
    return this._size;
  }

  set size(val) {
    if (cconst.DEBUG.screenAreaPosSizeAccesses) {
      console.log("ScreenArea set size", val);
    }
    this._size.load(val);
  }

  static newSTRUCT() {
    return UIBase.createElement("screenarea-x");
  }

  static define() {
    return {
      tagname: "screenarea-x"
    };
  }

  _get_v_suffix() {
    return this.area ? this.area._get_v_suffix() : "";
  }

  bringToFront() {
    let screen = this.getScreen();

    HTMLElement.prototype.remove.call(this);
    screen.sareas.remove(this);

    screen.appendChild(this);

    let zindex = BORDER_ZINDEX_BASE + 1;

    if (screen.style["z-index"]) {
      zindex = parseInt(screen.style["z-index"]) + 1;
    }

    for (let sarea of screen.sareas) {
      let zindex = sarea.style["z-index"];
      if (sarea.style["z-index"]) {
        zindex = Math.max(zindex, parseInt(sarea.style["z-index"]) + 1);
      }
    }

    this.style["z-index"] = zindex;
  }

  _side(border) {
    let ret = this._borders.indexOf(border);
    if (ret < 0) {
      throw new Error("border not in screen area");
    }

    return ret;
  }

  init() {
    super.init();

    this.noMarginsOrPadding();
  }

  draw() {
    if (this.area && this.area.draw) {
      this.area.push_ctx_active();
      this.area.draw();
      this.area.pop_ctx_active();
    }
  }

  _isDead() {
    if (this.dead) {
      return true;
    }

    let screen = this.getScreen();

    if (screen === undefined)
      return true;

    if (screen.parentNode === undefined)
      return true;
  }

  toJSON() {
    let ret = {
      editors  : this.editors,
      _sarea_id: this._sarea_id,
      area     : this.area.constructor.define().areaname,
      pos      : this.pos,
      size     : this.size
    };

    return Object.assign(super.toJSON(), ret);
  }

  on_keydown(e) {
    if (this.area.on_keydown) {
      this.area.push_ctx_active();
      this.area.on_keydown(e);
      this.area.pop_ctx_active();
    }
  }

  loadJSON(obj) {
    if (obj === undefined) {
      console.warn("undefined in loadJSON");
      return;
    }

    super.loadJSON(obj);

    this.pos.load(obj.pos);
    this.size.load(obj.size);

    for (let editor of obj.editors) {
      let areaname = editor.areaname;

      //console.log(editor);

      let tagname = areaclasses[areaname].define().tagname;
      let area = UIBase.createElement(tagname);

      area.owning_sarea = this;
      this.editormap[areaname] = area;
      this.editors.push(this.editormap[areaname]);

      area.pos = new Vector2(obj.pos);
      area.size = new Vector2(obj.size);
      area.ctx = this.ctx;

      area.inactive = true;
      area.loadJSON(editor);
      area.owning_sarea = undefined;

      if (areaname === obj.area) {
        this.area = area;
      }
    }

    if (this.area !== undefined) {
      this.area.ctx = this.ctx;
      this.area.style["width"] = "100%";
      this.area.style["height"] = "100%";
      this.area.owning_sarea = this;
      this.area.parentWidget = this;

      this.area.pos = this.pos;
      this.area.size = this.size;

      this.area.inactive = false;
      this.shadow.appendChild(this.area);
      this.area.on_area_active();
      this.area.onadd();
    }

    this.setCSS();
  }

  _ondestroy() {
    super._ondestroy();

    this.dead = true;

    for (let editor of this.editors) {
      if (editor === this.area) continue;

      editor._ondestroy();
    }
  }

  getScreen() {
    if (this.screen !== undefined) {
      return this.screen;
    }

    //try to walk up graph, if possible
    let p = this.parentNode;
    let _i = 0;

    while (p && !(p instanceof Screen) && p !== p.parentNode) {
      p = this.parentNode;

      if (_i++ > 1000) {
        console.warn("infinite loop detected in ScreenArea.prototype.getScreen()");
        return undefined;
      }
    }

    return p && p instanceof Screen ? p : undefined;
  }

  copy(screen) {
    let ret = UIBase.createElement("screenarea-x");

    ret.screen = screen;
    ret.ctx = this.ctx;

    ret.pos[0] = this.pos[0];
    ret.pos[1] = this.pos[1];

    ret.size[0] = this.size[0];
    ret.size[1] = this.size[1];

    for (let area of this.editors) {
      let cpy = area.copy();

      cpy.ctx = this.ctx;

      cpy.parentWidget = ret;
      ret.editors.push(cpy);
      ret.editormap[cpy.constructor.define().areaname] = cpy;

      if (area === this.area) {
        ret.area = cpy;
      }
    }

    //console.trace("RET.AREA", this.area, ret.area);

    ret.ctx = this.ctx;

    if (ret.area !== undefined) {
      ret.area.ctx = this.ctx;

      ret.area.pos = ret.pos;
      ret.area.size = ret.size;
      ret.area.owning_sarea = ret;
      ret.area.parentWidget = ret;

      ret.shadow.appendChild(ret.area);
      //ret.area.onadd();

      if (ret.area._init_done) {
        ret.area.push_ctx_active();
        ret.area.on_area_active();
        ret.area.pop_ctx_active();
      } else {
        ret.doOnce(() => {
          if (this.dead) {
            return;
          }
          ret._init();
          ret.area._init();
          ret.area.push_ctx_active();
          ret.area.on_area_active();
          ret.area.pop_ctx_active();
        });
      }
    }

    return ret;
  }

  snapToScreenSize() {
    let screen = this.getScreen();
    let co = new Vector2();
    let changed = 0;

    for (let v of this._verts) {
      co.load(v);

      v[0] = Math.min(Math.max(v[0], 0), screen.size[0]);
      v[1] = Math.min(Math.max(v[1], 0), screen.size[1]);

      if (co.vectorDistance(v) > 0.1) {
        changed = 1;
      }
    }

    if (changed) {
      this.loadFromVerts();
    }
  }

  /**
   *
   * Sets screen verts from pos/size
   * */
  loadFromPosSize() {
    if (this.floating && this._verts.length > 0) {
      let p = this.pos, s = this.size;

      this._verts[0].loadXY(p[0], p[1]);
      this._verts[1].loadXY(p[0], p[1] + s[1]);
      this._verts[2].loadXY(p[0] + s[0], p[1] + s[1]);
      this._verts[3].loadXY(p[0] + s[0], p[1]);

      for (let border of this._borders) {
        border.setCSS();
      }

      this.setCSS();
      return;
    }

    let screen = this.getScreen();
    if (!screen) return;

    for (let b of this._borders) {
      screen.freeBorder(b);
    }

    this.makeBorders(screen);
    this.setCSS();

    return this;
  }

  /**
   *
   * Sets pos/size from screen verts
   * */
  loadFromVerts() {
    if (this._verts.length == 0) {
      return;
    }

    let min = new Vector2([1e17, 1e17]);
    let max = new Vector2([-1e17, -1e17]);

    for (let v of this._verts) {
      min.min(v);
      max.max(v);
    }

    this.pos[0] = min[0];
    this.pos[1] = min[1];

    this.size[0] = max[0] - min[0];
    this.size[1] = max[1] - min[1];

    this.setCSS();
    return this;
  }

  on_resize(size, oldsize) {
    super.on_resize(size, oldsize);

    if (this.area !== undefined) {
      this.area.on_resize(size, oldsize);
    }
  }

  makeBorders(screen) {
    this._borders.length = 0;
    this._verts.length = 0;

    let p = this.pos, s = this.size;

    //s = snapi(new Vector2(s));

    let vs = [
      new Vector2([p[0], p[1]]),
      new Vector2([p[0], p[1] + s[1]]),
      new Vector2([p[0] + s[0], p[1] + s[1]]),
      new Vector2([p[0] + s[0], p[1]])
    ];

    let floating = this.floating;

    for (let i = 0; i < vs.length; i++) {
      vs[i] = snap(vs[i]);
      vs[i] = screen.getScreenVert(vs[i], i, floating);
      this._verts.push(vs[i]);
    }

    for (let i = 0; i < vs.length; i++) {
      let v1 = vs[i], v2 = vs[(i + 1)%vs.length];

      let b = screen.getScreenBorder(this, v1, v2, i);


      for (let j = 0; j < 2; j++) {
        let v = j ? b.v2 : b.v1;

        if (v.sareas.indexOf(this) < 0) {
          v.sareas.push(this);
        }
      }

      if (b.sareas.indexOf(this) < 0) {
        b.sareas.push(this);
      }

      this._borders.push(b);

      b.movable = screen.isBorderMovable(b);
    }

    return this;
  }

  setCSS() {
    this.style["position"] = UIBase.PositionKey;

    this.style["left"] = this.pos[0] + "px";
    this.style["top"] = this.pos[1] + "px";

    this.style["width"] = this.size[0] + "px";
    this.style["height"] = this.size[1] + "px";
    
    this.style["overflow"] = "hidden";
    this.style["contain"] = "layout"; //ensure we have a new positioning stack

    if (this.area !== undefined) {
      this.area.setCSS();
    }
  }

  appendChild(child) {
    if (child instanceof Area) {
      let def = child.constructor.define();
      let existing = this.editormap[def.areaname];

      if (existing && existing !== child) {
        console.warn("Warning, replacing an exising editor instance", child, existing);

        if (this.area === existing) {
          this.area = child;
        }

        existing.remove();
        this.editormap[def.areaname] = child;
      }

      child.ctx = this.ctx;
      child.pos = this.pos;
      child.size = this.size;

      if (this.editors.indexOf(child) < 0) {
        this.editors.push(child);
      }

      child.owning_sarea = undefined;
      if (this.area === undefined) {
        this.area = child;
      }
    }

    super.appendChild(child);

    if (child instanceof ui_base.UIBase) {
      child.parentWidget = this;
      child.onadd();
    }
  }

  switch_editor(cls) {
    return this.switchEditor(cls);
  }

  switchEditor(cls) {
    let def = cls.define();
    let name = def.areaname;

    //areaclasses[name]
    if (!(name in this.editormap)) {
      this.editormap[name] = UIBase.createElement(def.tagname);
      this.editormap[name].ctx = this.ctx;
      this.editormap[name].parentWidget = this;
      this.editormap[name].owning_sarea = this;
      this.editormap[name].inactive = false;

      this.editors.push(this.editormap[name]);
    }

    //var finish = () => {
    if (this.area) {
      //break direct pos/size references for old active area
      this.area.pos = new Vector2(this.area.pos);
      this.area.size = new Vector2(this.area.size);

      this.area.owning_sarea = undefined;
      this.area.inactive = true;
      this.area.push_ctx_active();
      this.area._init(); //check that init was called
      this.area.on_area_inactive();
      this.area.pop_ctx_active();

      this.area.remove();
    } else {
      this.area = undefined;
    }

    this.area = this.editormap[name];

    this.area.inactive = false;
    this.area.parentWidget = this;

    //. . .and set references to pos/size
    this.area.pos = this.pos;
    this.area.size = this.size;
    this.area.owning_sarea = this;
    this.area.ctx = this.ctx;

    this.area.packflag |= this.packflag;

    this.shadow.appendChild(this.area);

    this.area.style["width"] = "100%";
    this.area.style["height"] = "100%";

    //propegate new size
    this.area.push_ctx_active();
    this.area._init(); //check that init was called
    this.area.on_resize(this.size, this.size);
    this.area.pop_ctx_active();

    this.area.push_ctx_active();
    this.area.on_area_active();
    this.area.pop_ctx_active();

    this.regenTabOrder();
    //}
  }

  _checkWrangler() {
    if (this.ctx)
      contextWrangler._checkWrangler(this.ctx);
  }

  update() {
    this._checkWrangler();

    super.update();

    //flag client controller implementation that
    //this area is active for its type
    if (this.area !== undefined) {
      this.area.owning_sarea = this;
      this.area.parentWidget = this;
      this.area.size = this.size;
      this.area.pos = this.pos;

      let screen = this.getScreen();
      let oldsize = [this.size[0], this.size[1]];

      let moved = screen ? screen.checkAreaConstraint(this, true) : 0;
      //*
      if (moved) {
        if (cconst.DEBUG.areaConstraintSolver) {
          console.log("screen constraint solve", moved, this.area.minSize, this.area.maxSize, this.area.tagName, this.size);
        }

        screen.solveAreaConstraints();
        screen.regenBorders();
        this.on_resize(oldsize);
      }//*/

      this.area.push_ctx_active(true);
    }

    this._forEachChildWidget((n) => {
      n.update();
    });

    if (this.area !== undefined) {
      this.area.pop_ctx_active(true);
    }
  }

  appendChild(ch) {
    if (ch instanceof Area) {
      this.editors.push(ch);
      this.editormap[ch.constructor.define().areaname] = ch;
    } else {
      super.appendChild(ch);
    }
  }

  removeChild(ch) {
    if (ch instanceof Area) {
      ch.owining_sarea = undefined;
      ch.pos = undefined;
      ch.size = undefined;

      if (this.area === ch && this.editors.length > 1) {
        let i = (this.editors.indexOf(ch) + 1)%this.editors.length;
        this.switchEditor(this.editors[i].constructor);
      } else if (this.area === ch) {
        this.editors = [];
        this.editormap = {};
        this.area = undefined;

        ch.remove();
        return;
      }

      let areaname = ch.constructor.define().areaname;

      this.editors.remove(ch);
      delete this.editormap[areaname];

      ch.parentWidget = undefined;
    } else {
      return super.removeChild(ch);
    }
  }

  afterSTRUCT() {
    for (let area of this.editors) {
      area.pos = this.pos;
      area.size = this.size;
      area.owning_sarea = this;

      area.push_ctx_active();
      area._ctx = this.ctx;
      area.afterSTRUCT();
      area.pop_ctx_active();
    }
  }

  loadSTRUCT(reader) {
    reader(this);

    this.pos = new Vector2(this.pos);
    this.size = new Vector2(this.size);

    //find active editor

    let editors = [];

    for (let area of this.editors) {
      if (!area.constructor || !area.constructor.define || area.constructor === Area) {
        //failed to load this area
        continue;
      }

      /*
      if (area.constructor === undefined || area.constructor.define === undefined) {
        console.warn("Missing class for area", area, "maybe buggy loadSTRUCT()?");
        continue;
      }
      //*/

      let areaname = area.constructor.define().areaname;

      area.inactive = true;
      area.owning_sarea = undefined;
      this.editormap[areaname] = area;

      if (areaname === this.area) {
        this.area = area;
      }

      /*
      * originally inactive areas weren't supposed to have
      * a reference to their owning ScreenAreas.
      *
      * Unfortunately this will cause isDead() to return true,
      * which might lead to nasty problems.
      * */
      area.parentWidget = this;

      editors.push(area);
    }
    this.editors = editors;

    if (typeof this.area !== "object") {
      let area = this.editors[0];

      console.warn("Failed to find active area!", this.area);

      if (typeof area !== "object") {
        for (let k in areaclasses) {
          area = areaclasses[k].define().tagname;
          area = UIBase.createElement(area);
          let areaname = area.constructor.define().areaname;

          this.editors.push(area);
          this.editormap[areaname] = area;

          break;
        }
      }

      if (area) {
        this.area = area;
      }
    }

    if (this.area !== undefined) {
      this.area.style["width"] = "100%";
      this.area.style["height"] = "100%";
      this.area.owning_sarea = this;
      this.area.parentWidget = this;

      this.area.pos = this.pos;
      this.area.size = this.size;

      this.area.inactive = false;
      this.shadow.appendChild(this.area);

      let f = () => {
        if (this._isDead()) {
          return;
        }

        if (!this.ctx && this.parentNode) {
          console.log("waiting to start. . .");
          this.doOnce(f);
          return;
        }

        this.area.ctx = this.ctx;
        this.area._init(); //ensure init has been called already
        this.area.on_area_active();
        this.area.onadd();
      };

      this.doOnce(f);
    }

  }
}

ScreenArea.STRUCT = `
pathux.ScreenArea { 
  pos      : vec2;
  size     : vec2;
  type     : string;
  hidden   : bool;
  editors  : array(abstract(pathux.Area));
  area     : string | this.area ? this.area.constructor.define().areaname : "";
}
`;

nstructjs.register(ScreenArea, "pathux.ScreenArea");
ui_base.UIBase.internalRegister(ScreenArea);

ui_base._setAreaClass(Area);

export function setScreenClass(cls) {
  Screen = cls;
}