Home Reference Source

scripts/widgets/ui_tabs.js

"use strict";

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 events from '../path-controller/util/events.js';
import * as ui from '../core/ui.js';
import {loadUIData, saveUIData} from '../core/ui_base.js';

let UIBase      = ui_base.UIBase,
    PackFlags   = ui_base.PackFlags,
    IconSheets  = ui_base.IconSheets,
    iconmanager = ui_base.iconmanager;

export let tab_idgen = 1;
let debug = false;
let Vector2 = vectormath.Vector2;

function getpx(css) {
  return parseFloat(css.trim().replace("px", ""))
}

let FAKE_TAB_ID = Symbol("fake_tab_id");


class TabDragEvent extends PointerEvent {
}


/* subclass HTMLElement so tabs can be used as the .target
*  member of events*/
export class TabItem extends UIBase {
  constructor() {
    super();

    this.name = name;
    this.icon = undefined;
    this.tooltip = "";
    this.movable = true;
    this.tbar = undefined;

    this.ontabclick = null;
    this.ontabdragstart = null;
    this.ontabdragmove = null;
    this.ontabdragend = null;

    let helper = (key) => {
      let key2 = "on" + key;

      this.addEventListener(key, (e) => {
        if (this[key2]) {
          return this[key2](e);
        }
      });
    }

    helper("tabclick");
    helper("tabdragstart");
    helper("tabdragmove");
    helper("tabdragend");

    this.dom = undefined;
    this.extra = undefined;
    this.extraSize = undefined;

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

    this.abssize = new Vector2();
    this.abspos = new Vector2();

    this.addEventListener("pointerdown", (e) => {
      this.parentWidget.on_pointerdown(e);
    });

    this.addEventListener("pointermove", (e) => {
      this.parentWidget.on_pointermove(e);
    });

    this.addEventListener("pointerup", (e) => {
      this.parentWidget.on_pointerup(e);
    });
  }

  static define() {
    return {
      tagname: "tab-item-x"
    }
  }

  sendEvent(type, forwardEvent) {
    let cls;

    if (type === "tabdragstart" || type === "tabdragend") {
      cls = TabDragEvent;
    } else if (forwardEvent && forwardEvent instanceof Event) {
      cls = forwardEvent.constructor;
    } else {
      cls = PointerEvent;
    }

    let e2 = {};

    if (forwardEvent) {
      for (let k in forwardEvent) {
        if (k === "defaultPrevented" || k === "cancelBubble") {
          continue;
        }

        e2[k] = forwardEvent[k];
      }
    }

    e2.target = this;

    e2 = new cls(type, e2);

    this.dispatchEvent(e2);
    return e2;
  }

  getClientRects() {
    let r = this.tbar.getClientRects()[0];

    let s = this.abssize, p = this.abspos;

    s.load(this.size);
    p.load(this.pos);

    if (r) {
      p[0] += r.x;
      p[1] += r.y;
    }

    return [{
      x  : p[0], y: p[1], width: s[0], height: s[1], left: p[0],
      top: p[1], right: p[0] + s[0], bottom: p[1] + s[1]
    }];
  }

  setCSS() {
    let dpi = UIBase.getDPI();
    let x = this.pos[0]/dpi;
    let y = this.pos[1]/dpi;
    let w = this.size[0]/dpi;
    let h = this.size[1]/dpi;

    this.style["background-color"] = "transparent";

    this.style["margin"] = this.style["padding"] = "0px";
    this.style["position"] = "absolute";
    this.style["pointer-events"] = "auto";

    this.style["left"] = x + "px";
    this.style["top"] = y + "px";
    this.style["width"] = w + "px";
    this.style["height"] = h + "px";
  }
}
UIBase.internalRegister(TabItem);

export class ModalTabMove extends events.EventHandler {
  constructor(tab, tbar, dom) {
    super();

    this.dom = dom;
    this.tab = tab;
    this.tbar = tbar;
    this.first = true;

    this.droptarget = undefined;
    this.start_mpos = new Vector2();
    this.mpos = undefined;

    this.dragtab = undefined;
    this.dragstate = false;
    this.finished = false;
  }

  finish() {
    if (debug) if (debug) console.log("finish");

    if (this.finished) {
      return;
    }

    this.finished = true;

    if (this.tbar.tool === this) {
      this.tbar.tool = undefined;
    }

    this.popModal(this.dom);
    this.tbar.update(true);
  }

  popModal() {
    if (this.dragcanvas !== undefined) {
      this.dragcanvas.remove();
    }
    let ret = super.popModal(...arguments);

    this.tab.sendEvent("tabdragend");

    return ret;
  }

  on_pointerleave(e) {
  }
  on_pointerenter(e) {
  }
  on_pointerenter(e) {
  }
  on_pointerstart(e) {
  }
  on_pointerend(e) {
  }

  on_pointerdown(e) {
    this.finish();
  }

  on_pointercancel(e) {
    this.finish();
  }

  on_pointerup(e) {
    this.finish();
  }

  on_pointermove(e) {
    return this._on_move(e, e.x, e.y);
  }

  _dragstate(e, x, y) {
    this.dragcanvas.style["left"] = x + "px";
    this.dragcanvas.style["top"] = y + "px";

    let ctx = this.tbar.ctx;
    let screen = ctx.screen;
    let elem = screen.pickElement(x, y);

    let e2 = new DragEvent("dragenter", this.dragevent);
    if (elem !== this.droptarget) {
      let e2 = new DragEvent("dragexit", this.dragevent);
      if (this.droptarget) {
        this.droptarget.dispatchEvent(e2);
      }

      e2 = new DragEvent("dragover", this.dragevent);
      this.droptarget = elem;
      if (elem) {
        elem.dispatchEvent(e2);
      }
    }
    //console.log(elem);
  }

  _on_move(e, x, y) {
    let r = this.tbar.getClientRects()[0];
    let dpi = UIBase.getDPI();

    if (r === undefined) {
      //element was removed during/before move
      this.finish();
      return;
    }

    if (this.dragstate) {
      this._dragstate(e, x, y);
      return;
    }

    x -= r.x;
    y -= r.y;

    let dx, dy;

    x *= dpi;
    y *= dpi;

    if (this.first) {
      this.first = false;
      this.start_mpos[0] = x;
      this.start_mpos[1] = y;
    }
    if (this.mpos === undefined) {
      this.mpos = [0, 0];
      dx = dy = 0;
    } else {
      dx = x - this.mpos[0];
      dy = y - this.mpos[1];
    }

    if (debug) console.log(x, y, dx, dy);

    let tab = this.tab, tbar = this.tbar;
    let axis = tbar.horiz ? 0 : 1;
    let distx, disty;

    if (tbar.horiz) {
      tab.pos[0] += dx;
      disty = Math.abs(y - this.start_mpos[1]);
    } else {
      tab.pos[1] += dy;
      disty = Math.abs(x - this.start_mpos[0]);
    }

    let limit = 50;
    let csize = tbar.horiz ? this.tbar.canvas.width : this.tbar.canvas.height;

    let dragok = tab.pos[axis] + tab.size[axis] < -limit || tab.pos[axis] >= csize + limit;
    dragok = dragok || disty > limit*1.5;
    dragok = dragok && (this.tbar.draggable || this.tbar.getAttribute("draggable"));

    //console.log(dragok, disty, this.tbar.draggable);

    if (dragok) {
      this.dragstate = true;
      this.dragevent = new DragEvent("dragstart", {
        dataTransfer: new DataTransfer()
      });

      this.dragtab = tab;
      let g = this.tbar.g;
      this.dragimg = g.getImageData(~~tab.pos[0], ~~tab.pos[1], ~~tab.size[0], ~~tab.size[1]);
      this.dragcanvas = document.createElement("canvas");
      let g2 = this.drag_g = this.dragcanvas.getContext("2d");

      this.dragcanvas.visibleToPick = false;
      this.dragcanvas.width = ~~tab.size[0];
      this.dragcanvas.height = ~~tab.size[1];
      this.dragcanvas.style["width"] = (tab.size[0]/dpi) + "px";
      this.dragcanvas.style["height"] = (tab.size[1]/dpi) + "px";
      this.dragcanvas.style["position"] = UIBase.PositionKey;
      this.dragcanvas.style["left"] = e.x + "px";
      this.dragcanvas.style["top"] = e.y + "px";
      this.dragcanvas.style["z-index"] = "500";
      document.body.appendChild(this.dragcanvas);
      g2.putImageData(this.dragimg, 0, 0);


      this.tbar.dispatchEvent(this.dragevent);

      return;
    }

    let ti = tbar.tabs.indexOf(tab);
    let next = ti < tbar.tabs.length - 1 ? tbar.tabs[ti + 1] : undefined;
    let prev = ti > 0 ? tbar.tabs[ti - 1] : undefined;

    if (next !== undefined && next.movable && tab.pos[axis] > next.pos[axis]) {
      tbar.swapTabs(tab, next);
    } else if (prev !== undefined && prev.movable && tab.pos[axis] < prev.pos[axis] + prev.size[axis]*0.5) {
      tbar.swapTabs(tab, prev);
    }

    tbar.update(true);

    this.mpos[0] = x;
    this.mpos[1] = y;

    let e2 = tab.sendEvent("tabdragmove", e);

    if (e2.defaultPrevented) {
      this.finish();
    }
  }

  on_keydown(e) {
    if (debug) console.log(e.keyCode);

    switch (e.keyCode) {
      case 27: //escape
      case 32: //space
      case 13: //enter
      case 9: //tab
        this.finish();
        break;
    }
  }
}

export class TabBar extends UIBase {
  constructor() {
    super();

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

    this.iconsheet = 0;
    this.movableTabs = true;

    this.tabFontScale = 1.0;

    this.tabs = [];
    this.tabs.active = undefined;
    this.tabs.highlight = undefined;

    this._last_style_key = undefined;

    canvas.style["width"] = "145px";
    canvas.style["height"] = "45px";

    this.r = 8;

    this.canvas = canvas;
    this.g = canvas.getContext("2d");

    this.canvas.style["touch-action"] = "none";

    style.textContent = `
    `;

    this.shadow.appendChild(style);
    this.shadow.appendChild(canvas);

    this._last_dpi = undefined;
    this._last_pos = undefined;

    this.horiz = true;
    this.onchange = null;
    this.onselect = null; //onselect is like onchange, but fires even if tab hasn't changed

    let mx, my;

    this.canvas.addEventListener("pointermove", (e) => {
      this.on_pointermove(e);
    }, false);


    this.canvas.addEventListener("pointerdown", (e) => {
      this.on_pointerdown(e);
    });
  }

  _doelement(e, mx, my){
    for (let tab of this.tabs) {
      let ok;

      if (this.horiz) {
        ok = mx >= tab.pos[0] && mx <= tab.pos[0] + tab.size[0];
      } else {
        ok = my >= tab.pos[1] && my <= tab.pos[1] + tab.size[1];
      }

      if (ok && this.tabs.highlight !== tab) {
        this.tabs.highlight = tab;
        this.update(true);
      }
    }
  }

  _domouse (e) {
    let r = this.canvas.getClientRects()[0];

    let mx = e.x - r.x;
    let my = e.y - r.y;

    let dpi = this.getDPI();

    mx *= dpi;
    my *= dpi;

    this._doelement(e, mx, my);

    const is_mdown = e.type === "mousedown";
    if (is_mdown && this.onselect && this._fireOnSelect().defaultPrevented) {
      e.preventDefault();
    }
  }

  _doclick (e) {
    this._domouse(e);

    if (e.defaultPrevented) {
      return;
    }

    if (debug) console.log("mdown");

    if (e.button !== 0) {
      return;
    }

    let ht = this.tabs.highlight;

    let acte = {};
    for (let k in e) {
      if (k === "defaultPrevented" || k === "cancelBubble") {
        continue;
      }

      acte[k] = e[k];
    }

    acte.target = ht;
    acte.pointerId = e.pointerId;

    acte = new PointerEvent("tabactive", acte);

    let e2 = ht.sendEvent("tabclick", e);

    if (e2.defaultPrevented) {
      acte.preventDefault();
    }

    if (ht !== undefined && this.tool === undefined) {
      this.setActive(ht, acte);

      if (this.movableTabs && !acte.defaultPrevented) {
        this._startMove(ht, e);
      }

      e.preventDefault();
      e.stopPropagation();
    }
  }

  on_pointerdown(e) {
    this._doclick(e);
  }

  on_pointermove(e) {
    let r = this.canvas.getClientRects()[0];
    this._domouse(e);

    e.preventDefault();
    e.stopPropagation();
  }

  on_pointerup(e) {

  }


  static setDefault(e) {
    e.setAttribute("bar_pos", "top");
    e.updatePos(true);

    return e;
  }

  static define() {
    return {
      tagname: "tabbar-x",
      style  : "tabs"
    };
  }

  _ensureNoModal() {
    if (this.tool) {
      this.tool.finish();
      this.tool = undefined;
    }
  }

  get tool() {
    return this._tool;
  }

  set tool(v) {
    //console.warn("SET TOOL", v, this._id);
    this._tool = v;
  }

  _startMove(tab=this.tabs.active, event, pointerId=event ? event.pointerId : undefined, pointerElem=tab) {
    if (this.movableTabs) {
      let e2 = tab.sendEvent("tabdragstart", event);

      if (e2.defaultPrevented) {
        return;
      }

      if (this.tool) {
        this.tool.finish();
      }

      let edom = this.getScreen();
      let tool = this.tool = new ModalTabMove(tab, this, edom);

      if (event && pointerElem && pointerId !== undefined) {
        tool.pushPointerModal(pointerElem, pointerId);
      } else {
        tool.pushModal(edom, false);
      }
    }
  }

  _fireOnSelect() {
    let e = this._makeOnSelectEvt();

    if (this.onselect) {
      this.onselect(e);
    }

    return e;
  }

  _makeOnSelectEvt() {
    return {
      tab             : this.tabs.highlight,
      defaultPrevented: false,
      preventDefault() {
        this.defaultPrevented = true;
      }
    }
    /*
      return new MouseEvent("mousedown", {
        target : [this.tabs.highlight],
        tab : "sdfdsf"
      });

   */
  }

  getTab(name_or_id) {
    for (let tab of this.tabs) {
      if (tab.id === name_or_id || tab.name === name_or_id)
        return tab;
    }

    return undefined;
  }

  clear() {
    for (let t of this.tabs) {
      if (t.dom) {
        t.dom.remove();
        t.dom = undefined;
      }
    }

    this.tabs = [];
    this.setCSS();
    this._redraw();
  }

  saveData() {
    let taborder = [];

    for (let tab of this.tabs) {
      taborder.push(tab.name);
    }

    let act = this.tabs.active !== undefined ? this.tabs.active.name : "null";

    return {
      taborder: taborder,
      active  : act
    };
  }

  loadData(obj) {
    if (!obj.taborder) {
      return;
    }

    let tabs = this.tabs;
    let active = undefined;
    let ntabs = [];

    ntabs.active = undefined;
    ntabs.highlight = undefined;

    for (let tname of obj.taborder) {
      let tab = this.getTab(tname);

      if (tab === undefined) {
        continue;
      }

      if (tab.name === obj.active) {
        active = tab;
      }

      ntabs.push(tab);
    }

    for (let tab of tabs) {
      if (ntabs.indexOf(tab) < 0) {
        ntabs.push(tab);
      }
    }

    this.tabs = ntabs;

    try {
      if (active !== undefined) {
        this.setActive(active);
      } else if (this.tabs.length > 0) {
        this.setActive(this.tabs[0]);
      }
    } catch (error) {
      util.print_stack(error);
    }

    this.update(true);

    return this;
  }

  swapTabs(a, b) {
    let tabs = this.tabs;

    let ai = tabs.indexOf(a);
    let bi = tabs.indexOf(b);

    tabs[ai] = b;
    tabs[bi] = a;

    this.update(true);
  }

  addIconTab(icon, id, tooltip, movable = true) {
    let tab = this.addTab("", id, tooltip, movable);
    tab.icon = icon;

    return tab;
  }

  addTab(name, id, tooltip = "", movable) {
    let tab = UIBase.createElement("tab-item-x", true);

    this.shadow.appendChild(tab);
    tab.parentWidget = this;


    tab.name = name;
    tab.id = id;
    tab.tooltip = tooltip;
    tab.movable = movable;
    tab.tbar = this;

    this.tabs.push(tab);
    this.update(true);

    if (this.tabs.length === 1) {
      this.setActive(this.tabs[0]);
    }

    return tab;
  }

  updatePos(force_update = false) {
    let pos = this.getAttribute("bar_pos");

    if (pos !== this._last_pos || force_update) {
      this._last_pos = pos;

      this.horiz = pos === "top" || pos === "bottom";
      if (debug) console.log("tab bar position update", this.horiz);

      if (this.horiz) {
        this.style["width"] = "100%";
        delete this.style["height"];
      } else {
        this.style["height"] = "100%";
        delete this.style["width"];
      }

      this._redraw();
    }
  }

  updateDPI(force_update = false) {
    let dpi = this.getDPI();

    if (dpi !== this._last_dpi) {
      if (debug) console.log("DPI update!");
      this._last_dpi = dpi;

      this.updateCanvas(true);
    }
  }

  updateCanvas(force_update = false) {
    let canvas = this.canvas;

    let dpi = this.getDPI();

    let rwidth = getpx(this.canvas.style["width"]);
    let rheight = getpx(this.canvas.style["height"]);

    let width = ~~(rwidth*dpi);
    let height = ~~(rheight*dpi);

    let update = force_update;
    update = update || canvas.width !== width || canvas.height !== height;

    if (update) {
      canvas.width = width;
      canvas.height = height;

      this._redraw();
    }
  }

  _getFont(tsize) {
    let font = this.getDefault("TabText");

    if (this.tabFontScale !== 1.0) {
      font = font.copy();
      font.size *= this.tabFontScale;
    }

    return font;
  }

  _layout() {
    if ((!this.ctx || !this.ctx.screen) && !this.isDead()) {
      this.doOnce(this._layout);
    }

    let g = this.g;

    if (debug) console.log("tab layout");

    let dpi = this.getDPI();

    let font = this._getFont();
    let tsize = (font.size*dpi);

    g.font = font.genCSS(tsize);

    let axis = this.horiz ? 0 : 1;

    let pad = 4*dpi + Math.ceil(tsize*0.25);
    let x = pad;
    let y = 0;

    let h = tsize + Math.ceil(tsize*0.5);
    let iconsize = iconmanager.getTileSize(this.iconsheet);
    let have_icons = false;

    for (let tab of this.tabs) {
      if (tab.icon !== undefined) {
        have_icons = true;
        h = Math.max(h, iconsize + 4);
        break;
      }
    }

    let r1 = this.parentWidget ? this.parentWidget.getClientRects()[0] : undefined;
    let r2 = this.canvas.getClientRects()[0];

    let rx = 0, ry = 0;
    if (r1 && r2) {
      rx = r2.x;//r2.x - r1.x;
      ry = r2.y; //r2.y - r1.y;
    }

    let ti = -1;

    let makeTabWatcher = (tab) => {
      if (tab.watcher) {
        clearInterval(tab.watcher.timer);
      }

      let watcher = () => {
        let dead = this.tabs.indexOf(tab) < 0;
        dead = dead || this.isDead();

        if (dead) {
          if (tab.dom)
            tab.dom.remove();
          tab.dom = undefined;

          if (tab.watcher.timer)
            clearInterval(tab.watcher.timer);
        }
      };

      tab.watcher = watcher;
      tab.watcher.timer = window.setInterval(watcher, 750);

      return tab.watcher.timer;
    };

    let haveTabDom = false;
    for (let tab of this.tabs) {
      if (tab.extra) {
        haveTabDom = true;
      }
    }

    if (haveTabDom && this.ctx && this.ctx.screen && !this._size_cb) {
      this._size_cb = () => {
        if (this.isDead()) {
          this.ctx.screen.removeEventListener("resize", this._size_cb);
          this._size_cb = undefined;
          return;
        }
        if (!this.ctx) return;

        this._layout();
        this._redraw();
      };

      this.ctx.screen.addEventListener("resize", this._size_cb);
    }

    for (let tab of this.tabs) {
      ti++;

      if (tab.extra && !tab.dom) {

        tab.dom = document.createElement("div");
        tab.dom.style["margin"] = tab.dom.style["padding"] = "0px";

        let z = this.calcZ();
        tab.dom.style["z-index"] = z + 1 + ti;

        document.body.appendChild(tab.dom);
        tab.dom.style["position"] = UIBase.PositionKey;
        tab.dom.style["display"] = "flex";
        tab.dom.style["flex-direction"] = this.horiz ? "row" : "column";

        tab.dom.style["pointer-events"] = "none";

        if (!this.horiz) {
          tab.dom.style["width"] = (tab.size[0]/dpi) + "px";
          tab.dom.style["height"] = (tab.size[1]/dpi) + "px";
          tab.dom.style["left"] = (rx + tab.pos[0]/dpi) + "px";
          tab.dom.style["top"] = (ry + tab.pos[1]/dpi) + "px";
        } else {
          tab.dom.style["width"] = (tab.size[0]/dpi) + "px";
          tab.dom.style["height"] = (tab.size[1]/dpi) + "px";
          tab.dom.style["left"] = (rx + tab.pos[0]/dpi) + "px";
          tab.dom.style["top"] = (ry + tab.pos[1]/dpi) + "px";
        }

        let font = this._getFont();
        tab.dom.style["font"] = font.genCSS();
        tab.dom.style["color"] = font.color;

        tab.dom.appendChild(tab.extra);

        //tab.dom.style["background-color"] = "red";

        makeTabWatcher(tab);
      }

      let w = g.measureText(tab.name).width;

      if (tab.extra) {
        w += tab.extraSize || tab.extra.getClientRects()[0].width;

      }

      if (tab.icon !== undefined) {
        w += iconsize;
      }

      //don't interfere with tab dragging
      let bad = this.tool !== undefined && tab === this.tabs.active;

      if (!bad) {
        tab.pos[axis] = x;
        tab.pos[axis ^ 1] = y;
      }

      //tab.size = [0, 0];
      tab.size[axis] = w + pad*2;
      tab.size[axis ^ 1] = h;

      x += w + pad*2;
    }

    x = (~~(x + pad))/dpi;
    h = (~~h)/dpi;


    if (this.horiz) {
      this.canvas.style["width"] = x + "px";
      this.canvas.style["height"] = h + "px";
    } else {
      this.canvas.style["height"] = x + "px";
      this.canvas.style["width"] = h + "px";
    }

    for (let tab of this.tabs) {
      tab.setCSS();
    }

    //this.canvas.width = x;
  }

  /** tab is a TabItem instance */
  setActive(tab, event) {
    if (tab.noSwitch) {
      return;
    }

    let update = tab !== this.tabs.active;
    this.tabs.active = tab;

    if (update) {
      if (this.onchange)
        this.onchange(tab, event);

      this.update(true);
    }
  }

  _redraw() {
    let g = this.g;

    let activecolor = this.getDefault("TabActive") || "rgba(0,0,0,0)";

    if (debug) console.log("tab draw");

    g.clearRect(0, 0, this.canvas.width, this.canvas.height);

    let dpi = this.getDPI();
    let font = this._getFont();

    let tsize = font.size;
    let iconsize = iconmanager.getTileSize(this.iconsheet);

    tsize = (tsize*dpi);
    g.font = font.genCSS(tsize);

    g.lineWidth = 2;
    g.strokeStyle = this.getDefault("TabStrokeStyle1");

    let r = this.r*dpi;
    this._layout();
    let tab;

    let ti = -1;
    for (tab of this.tabs) {
      ti++;

      if (tab === this.tabs.active)
        continue;

      let x = tab.pos[0], y = tab.pos[1];
      let w = tab.size[0], h = tab.size[1];
      //let tw = g.measureText(tab.name).width;
      let tw = ui_base.measureText(this, tab.name, this.canvas, g, tsize, font).width;

      let x2 = x + (tab.size[this.horiz ^ 1] - tw)*0.5;
      let y2 = y + tsize;

      if (tab === this.tabs.highlight) {
        let p = 2;

        g.beginPath();
        g.rect(x + p, y + p, w - p*2, h - p*2);
        g.fillStyle = this.getDefault("TabHighlight");
        g.fill();
      }

      g.fillStyle = this.getDefault("TabText").color;

      if (!this.horiz) {
        let x3 = 0, y3 = y2;

        g.save();
        g.translate(x3, y3);
        g.rotate(Math.PI/2);
        g.translate(x3 - tsize, -y3 - tsize*0.5);
      }

      if (tab.icon !== undefined) {
        iconmanager.canvasDraw(this, this.canvas, g, tab.icon, x, y, this.iconsheet);
        x2 += iconsize + 4;
      }

      g.fillText(tab.name, x2, y2);

      if (!this.horiz) {
        g.restore();
      }

      let prev = this.tabs[Math.max(ti - 1 + this.tabs.length, 0)];
      let next = this.tabs[Math.min(ti + 1, this.tabs.length - 1)];

      if (tab !== this.tabs[this.tabs.length - 1] && prev !== this.tabs.active && next !== this.tabs.active) {
        g.beginPath();
        if (this.horiz) {
          g.moveTo(x + w, h - 5);
          g.lineTo(x + w, 5);
        } else {
          g.moveTo(w - 5, y + h);
          g.lineTo(5, y + h);
        }
        g.strokeStyle = this.getDefault("TabStrokeStyle1");
        g.stroke();
      }
    }

    let th = tsize;

    //draw active tab
    tab = this.tabs.active;
    if (tab) {
      let x = tab.pos[0], y = tab.pos[1];
      let w = tab.size[0], h = tab.size[1];
      //let tw = g.measureText(tab.name).width;
      let tw = ui_base.measureText(this, tab.name, this.canvas, g, tsize, font).width;

      if (this.horiz) {
        h += 2;
      } else {
        w += 2;
      }

      let x2 = x + (tab.size[this.horiz ^ 1] - tw)*0.5;
      let y2 = y + tsize;

      if (tab === this.tabs.active) {
        /*
        let x = !this.horiz ? tab.y : tab.x;
        let y = !this.horiz ? tab.x : tab.y;
        let w = !this.horiz ? tab.size[1] : tab.size[0];
        let h = !this.horiz ? tab.size[0] : tab.size[1];

        if (!this.horiz) {
          //g.save();
          //g.translate(0, y);
          //g.rotate(Math.PI/16);
          //g.translate(0, -y);
        }//*/

        g.beginPath();
        //g.lineWidth *= 5;
        let ypad = 2;

        g.strokeStyle = this.getDefault("TabStrokeStyle2");
        g.fillStyle = activecolor;
        let r2 = r*1.5;

        if (this.horiz) {
          g.moveTo(x - r, h);
          g.quadraticCurveTo(x, h, x, h - r);
          g.lineTo(x, r2);
          g.quadraticCurveTo(x, ypad, x + r2, ypad)
          g.lineTo(x + w - r2, ypad);
          g.quadraticCurveTo(x + w, 0, x + w, r2);
          g.lineTo(x + w, h - r2);
          g.quadraticCurveTo(x + w, h, x + w + r, h);

          g.stroke()
          //
          g.closePath()
        } else {
          g.moveTo(w, y - r);
          g.quadraticCurveTo(w, y, w - r, y);
          ///*
          g.lineTo(r2, y);
          g.quadraticCurveTo(ypad, y, ypad, y + r2);
          g.lineTo(ypad, y + h - r2);
          g.quadraticCurveTo(0, y + h, r2, y + h);
          g.lineTo(w - r2, y + h);
          g.quadraticCurveTo(w, y + h, w, y + h + r);
          //*/
          g.stroke()
          //
          g.closePath()
        }

        let cw = this.horiz ? this.canvas.width : this.canvas.height;

        let worig = g.lineWidth;

        g.lineWidth *= 0.5;

        g.fill();
        //g.stroke();

        g.lineWidth = worig;

        if (!this.horiz) {
          let x3 = 0, y3 = y2;

          g.save();
          g.translate(x3, y3);
          g.rotate(Math.PI/2);
          g.translate(-x3 - tsize, -y3 - tsize*0.5);
        }

        g.fillStyle = this.getDefault("TabText").color;

        //y2 += tsize*0.3;
        g.fillText(tab.name, x2, y2);

        if (!this.horiz) {
          g.restore();
        }

        if (!this.horiz) {
          //g.restore();
        }
      }
    }
  }

  removeTab(tab) {
    this.tabs.remove(tab);
    if (tab === this.tabs.active) {
      this.tabs.active = this.tabs[0];
    }

    this._layout();
    this._redraw();
    this.setCSS();
  }

  setCSS() {
    super.setCSS(false);

    /* create a no stacking context */
    this.style["contain"] = "layout";
    
    let r = this.getDefault("TabBarRadius");
    r = r !== undefined ? r : 3;

    this.style["touch-action"] = "none";


    this.canvas.style["background-color"] = this.getDefault("TabInactive");
    this.canvas.style["border-radius"] = r + "px";
    //this.style["background-color"] = this.getDefault("TabInactive");
  }

  updateStyle() {
    let key = "" + this.getDefault("background-color");
    key += this.getDefault("TabActive");
    key += this.getDefault("TabInactive");
    key += this.getDefault("TabBarRadius");
    key += this.getDefault("TabStrokeStyle1");
    key += this.getDefault("TabStrokeStyle2");
    key += this.getDefault("TabHighlight");
    key += JSON.stringify(this.getDefault("TabText"));
    key += this.tabFontScale;

    if (key !== this._last_style_key) {
      this._last_style_key = key;

      this._layout();
      this.setCSS();
      this._redraw();
    }
  }

  update(force_update = false) {
    let rect = this.getClientRects()[0];
    if (rect) {
      let key = Math.floor(rect.x*4.0) + ":" + Math.floor(rect.y*4.0);
      if (key !== this._last_p_key) {
        this._last_p_key = key;

        //console.log("tab bar autobuild");
        this._layout();
      }
    }
    super.update();

    this.updateStyle();
    this.updatePos(force_update);
    this.updateDPI(force_update);
    this.updateCanvas(force_update);
  }
}

UIBase.internalRegister(TabBar);

export class TabContainer extends UIBase {
  constructor() {
    super();

    this._last_style_key = "";

    this.dataPrefix = "";

    this.inherit_packflag = 0;
    this.packflag = 0;

    this.tabFontScale = 1.0;

    this.tbar = UIBase.createElement("tabbar-x");
    this.tbar.parentWidget = this;
    this.tbar.setAttribute("class", "_tbar_" + this._id);
    this.tbar.constructor.setDefault(this.tbar);
    this.tbar.tabFontScale = this.tabFontScale;

    this._remakeStyle();

    this.tabs = {};

    this._last_horiz = undefined;
    this._last_bar_pos = undefined;
    this._tab = undefined;

    let div = document.createElement("div");
    div.setAttribute("class", `_tab_${this._id}`);
    div.appendChild(this.tbar);
    this.shadow.appendChild(div);

    this.tbar.parentWidget = this;

    this.tbar.onselect = (e) => {
      if (this.onselect) {
        this.onselect(e);
      }
    };

    this.tbar.onchange = (tab, event) => {
      if (this._tab) {
        HTMLElement.prototype.remove.call(this._tab);
      }

      this._tab = this.tabs[tab.id];
      //this._tab = document.createElement("div");
      //this._tab.innerText = "SDfdsfsdyay";

      this._tab.parentWidget = this;

      //ensure we get full update convergence when switching
      //tabs
      for (let i = 0; i < 2; i++) {
        this._tab.flushUpdate();
      }

      let div = document.createElement("div");

      this.tbar.setCSS.once(() => div.style["background-color"] = this.getDefault("background-color"), div);

      div.setAttribute("class", `_tab_${this._id}`);
      div.appendChild(this._tab);

      //XXX why is this necassary?
      //this._tab.style["margin-left"] = "40px";
      this.shadow.appendChild(div);

      if (this.onchange) {
        this.onchange(tab, event);
      }
    }
  }

  get movableTabs() {
    let attr;

    if (!this.hasAttribute("movable-tabs")) {
      attr = this.getDefault("movable-tabs");

      if (attr === undefined || attr === null) {
        attr = "true";
      }

      if (typeof attr === "boolean" || typeof attr === "number") {
        attr = attr ? "true" : "false";
      }
    } else {
      attr = "" + this.getAttribute("movable-tabs");
    }

    attr = attr.toLowerCase();

    return attr === "true";
  }

  set movableTabs(val) {
    val = !!val;

    this.setAttribute("movable-tabs", val ? "true" : "false");
    this.tbar.movableTabs = this.movableTabs;
  }

  get hideScrollBars() {
    let attr = ("" + this.getAttribute("hide-scrollbars")).toLowerCase();
    return attr === "true" || attr === "yes";
  }

  set hideScrollBars(val) {
    val = !!val;

    this.setAttribute("hide-scrollbars", "" + val);
  }

  static setDefault(e) {
    e.setAttribute("bar_pos", "top");

    return e;
  }

  static define() {
    return {
      tagname: "tabcontainer-x",
      style  : "tabs"
    };
  }

  _startMove(tab=this.tbar.tabs.active, event) {
    return this.tbar._startMove(tab, event);
  }

  _ensureNoModal() {
    return this.tbar._ensureNoModal();
  }

  saveData() {
    let json = super.saveData() || {};
    json.tabs = {};

    for (let k in this.tabs) {
      let tab = this.tabs[k];

      if (k === this.tbar.tabs.active.id) {
        //no need to save active tab here
        continue;
      }

      try {
        json.tabs[tab.id] = JSON.parse(saveUIData(tab, "tab"));
      } catch (error) {
        console.error("Failed to save tab UI layout", tab.id);
      }
    }

    return json;
  }

  loadData(json) {
    if (!json.tabs) {
      return;
    }

    for (let k in json.tabs) {
      if (!(k in this.tabs)) {
        continue;
      }

      let uidata = JSON.stringify(json.tabs[k]);
      loadUIData(this.tabs[k], uidata);
    }
  }

  enableDrag() {
    this.tbar.draggable = this.draggable = true;
    this.tbar.addEventListener("dragstart", (e) => {
      this.dispatchEvent(new DragEvent("dragstart", e));
    });
    this.tbar.addEventListener("dragover", (e) => {
      this.dispatchEvent(new DragEvent("dragover", e));
    });
    this.tbar.addEventListener("dragexit", (e) => {
      this.dispatchEvent(new DragEvent("dragexit", e));
    });
    /*
    let doms = [this, this.tbar, this.tbar.canvas];
    for (let dom of doms) {
      dom.setAttribute("draggable", "true");
      dom.draggable = true;

      dom.addEventListener("dragstart", (e) => {
        console.log("drag start", e);
      });

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

  clear() {
    this.tbar.clear();
    if (this._tab !== undefined) {
      HTMLElement.prototype.remove.call(this._tab);
      this._tab = undefined;
    }

    this.tabs = {};
  }

  init() {
    super.init();

    this.background = this.getDefault("background-color");
  }

  setCSS() {
    super.setCSS();

    this.background = this.getDefault("background-color");
    this._remakeStyle();
  }

  _remakeStyle() {
    let horiz = this.tbar.horiz;
    let display = "flex";
    let flexDir = !horiz ? "row" : "column";
    let bgcolor = this.__background; //this.getDefault("background-color");

    //display = "inline" //XXX
    let style = document.createElement("style");
    style.textContent = `
      ._tab_${this._id} {
        display : ${display};
        flex-direction : ${flexDir};
        margin : 0px;
        padding : 0px;
        align-self : flex-start;
        ${!horiz ? "vertical-align : top;" : ""}
      }
      
      ._tbar_${this._id} {
        list-style-type : none;
        align-self : flex-start;
        background-color : ${bgcolor};
        flex-direction : ${flexDir};
        ${!horiz ? "vertical-align : top;" : ""}
      }
    `;

    if (this._style)
      this._style.remove();
    this._style = style;

    this.shadow.prepend(style);
  }

  icontab(icon, id, tooltip) {
    let t = this.tab("", id, tooltip);
    t._tab.icon = icon;

    return t;
  }

  removeTab(tab) {
    let tab2 = tab._tab;
    this.tbar.removeTab(tab2);
    tab.remove();
  }

  tab(name, id = undefined, tooltip = undefined, movable = true) {
    if (id === undefined) {
      id = tab_idgen++;
    }

    let col = UIBase.createElement("colframe-x");

    this.tabs[id] = col;

    col.dataPrefix = this.dataPrefix;

    col.ctx = this.ctx;
    col._tab = this.tbar.addTab(name, id, tooltip, movable);

    col.inherit_packflag |= this.inherit_packflag;
    col.packflag |= this.packflag;

    //let cls = this.tbar.horiz ? ui.ColumnFrame : ui.RowFrame;

    col.parentWidget = this;

    if (col.ctx) {
      col._init();
    }

    col.setCSS();

    if (this._tab === undefined) {
      this.setActive(col);
    }

    col.noSwitch = function () {
      this._tab.noSwitch = true;
      return this;
    }

    function defineTabEvent(key) {
      key = "on" + key;

      Object.defineProperty(col, key, {
        get() {
          return this._tab[key];
        },
        set(v) {
          this._tab[key] = v;
        }
      });
    }

    defineTabEvent("tabclick");
    defineTabEvent("tabdragmove");
    defineTabEvent("tabdragstart");
    defineTabEvent("tabdragend");

    return col;
  }

  setActive(tab) {
    if (typeof tab === "string") {
      tab = this.getTab(tab);
    }

    if (!tab) {
      return;
    }

    if (tab._tab !== this.tbar.tabs.active) {
      this.tbar.setActive(tab._tab);
    }
  }

  getTabCount() {
    return this.tbar.tabs.length;
  }

  moveTab(tab, i) {
    tab = tab._tab;

    let tab2 = this.tbar.tabs[i];

    if (tab !== tab2) {
      this.tbar.swapTabs(tab, tab2);
    }

    this.tbar.setCSS();
    this.tbar._layout();
    this.tbar._redraw();
  }

  getTab(name_or_id) {
    if (name_or_id in this.tabs) {
      return this.tabs[name_or_id];
    }

    for (let k in this.tabs) {
      let t = this.tabs[k];

      if (t.name === name_or_id) {
        return t;
      }
    }

    throw new Error("Unknown tab " + name_or_id);
  }

  updateBarPos() {
    let barpos = this.getAttribute("bar_pos");

    if (barpos !== this._last_bar_pos) {
      this.horiz = barpos === "top" || barpos === "bottom";
      this._last_bar_pos = barpos;

      this.tbar.setAttribute("bar_pos", barpos);
      this.tbar.update(true);
      this.update();
    }
  }

  updateHoriz() {
    let horiz = this.tbar.horiz;

    if (this._last_horiz !== horiz) {
      this._last_horiz = horiz;
      this._remakeStyle();
    }
  }

  updateStyle() {
    let key = "" + this.getDefault("background-color");

    if (key !== this._last_style_key) {
      this._last_style_key = key;
      this.setCSS();
    }
  }

  getActive() {
    return this.tbar.tabs.active;
  }

  update() {
    super.update();

    this.tbar.movableTabs = this.movableTabs;

    if (this._tab !== undefined) {
      this._tab.update();
    }

    this.style["display"] = "flex";
    this.style["flex-direction"] = !this.horiz ? "row" : "column";

    this.tbar.tabFontScale = this.tabFontScale;

    this.updateStyle();
    this.updateHoriz();
    this.updateBarPos();
    this.tbar.update();

    let act = this.tbar.tabs.active;

    if (act && !this.hideScrollBars) {
      let container = this.tabs[act.id];

      //propegate overflow-y to tab container as a whole

      if (container.hasAttribute("overflow-y") && this.style["overflow-y"] !== container.getAttribute("overflow-y")) {
        this.style["overflow-y"] = container.getAttribute("overflow-y");
        //container.style["overflow-y"] = "unset";
      } else if (!container.hasAttribute("overflow-y")) {
        this.style["overflow-y"] = this.getDefault("overflow-y") || "unset";
      }

      if (container.hasAttribute("overflow") && this.style["overflow"] !== container.getAttribute("overflow")) {
        this.style["overflow"] = container.getAttribute("overflow");
        //container.style["overflow-y"] = "unset";
      } else if (!container.hasAttribute("overflow")) {
        this.style["overflow"] = this.getDefault("overflow") || "unset";
      }
    } else if (this.hideScrollBars) {
      this.style["overflow"] = this.style["overflow-y"] = "unset";
    }
  }
}

UIBase.internalRegister(TabContainer);