Home Reference Source

scripts/widgets/ui_button.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 simple_toolsys from '../path-controller/toolsys/toolsys.js';
import * as toolprop from '../path-controller/toolsys/toolprop.js';
import {DataPathError} from '../path-controller/controller/controller.js';
import {Vector3, Vector4, Quat, Matrix4} from '../path-controller/util/vectormath.js';
import cconst from '../config/const.js';
import {_themeUpdateKey, CSSFont} from "../core/ui_base.js";

let keymap = events.keymap;

let EnumProperty = toolprop.EnumProperty,
    PropTypes = toolprop.PropTypes;

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

let parsepx = ui_base.parsepx;

cconst.DEBUG.buttonEvents = true;

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

    this._auto_depress = true;
    this._highlight = false;
    this._pressed = false;
  }

  bindEvents() {
    let press_gen = 0;
    let depress;

    let press = (e) => {
      e.stopPropagation();

      if (!this.modalRunning) {
        let this2 = this;

        this.pushModal({
          on_pointerdown(e) {
            this.end(e);
          },

          on_pointerup(e) {
            this.end(e);
          },

          on_pointercancel(e) {
            console.warn("Pointer cancel in button");
            this2.popModal();
          },

          on_keydown(e) {
            switch (e.keyCode) {
              case keymap["Enter"]:
              case keymap["Escape"]:
              case keymap["Space"]:
                this.end();
                break;
            }
          },

          end(e) {
            if (!this2.modalRunning) {
              return;
            }

            this2.popModal();
            depress(e);
          }
        }, undefined, e.pointerId);
      }

      if (cconst.DEBUG.buttonEvents) {
        console.log("button press", this._pressed, this.disabled, e.button);
      }

      if (this.disabled) return;

      this._pressed = true;

      if (this._onpress) {
        this._onpress(this);
      }

      this._redraw();

      e.preventDefault();
    };

    depress = (e) => {
      if (cconst.DEBUG.buttonEvents)
        console.log("button depress", e.button, e.was_touch);

      if (this._auto_depress) {
        this._pressed = false;

        if (this.disabled) return;

        this._redraw();
      }

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

      if (util.isMobile() || e.type === "pointerup" && e.button) {
        return;
      }

      this._redraw();

      if (cconst.DEBUG.buttonEvents)
        console.log("button click callback:", this.onclick, this._onpress, this.onpress);

      if (this.onclick && e.pointerType !== "mouse") {
        this.onclick(this);
      }

      this.undoBreakPoint();
    }

    this.addEventListener("click", () => {
      this._pressed = false;
      this._highlight = false;
      this._redraw();
    });

    this.addEventListener("pointerdown", press, {captured : true, passive : false});
    this.addEventListener("pointerup", depress, {captured : true, passive : false});
    this.addEventListener("pointerover", (e) => {
      if (this.disabled)
        return;

      this._highlight = true;
      this._redraw();
    })

    this.addEventListener("pointerout", (e) => {
      if (this.disabled)
        return;

      this._highlight = false;
      this._redraw();
    })

    this.addEventListener("keydown", (e) => {
      if (this.disabled) return;

      if (cconst.DEBUG.buttonEvents)
        console.log(e.keyCode);

      switch (e.keyCode) {
        case 27: //escape
          this.blur();
          e.preventDefault();
          e.stopPropagation();
          break;
        case 32: //spacebar
        case 13: //enter
          this.click();
          e.preventDefault();
          e.stopPropagation();
          break;
      }
    });

    this.addEventListener("focusin", () => {
      if (this.disabled) return;

      this._focus = 1;
      this._redraw();
      this.focus();
    });

    this.addEventListener("blur", () => {
      if (this.disabled) return;

      this._focus = 0;
      this._redraw();
    });
  }

  _redraw() {

  }
}

export class Button extends ButtonEventBase {
  constructor() {
    super();

    this.label = document.createElement("span");
    this.label.innerText = "button";
    this.shadow.appendChild(this.label);

    this.label.style["pointer-events"] = "none";

    this._pressed = false;
    this._highlight = false;

    this._auto_depress = true;

    this._last_name = undefined;
    this._last_disabled = undefined;
  }

  init() {
    super.init();
    this.tabIndex = 0;

    this.bindEvents();

    this.setCSS();
  }

  get name() {
    return "" + this.getAttribute("name");
  }

  set name(val) {
    this.setAttribute("name", val);
  }

  setCSS() {
    super.setCSS();

    let subkey = undefined;

    if (this.disabled) {
      subkey = "disabled";
    } else if (this._pressed && this._highlight) {
      subkey = "highlight-pressed";
    } else if (this._pressed) {
      subkey = "pressed";
    } else if (this._highlight) {
      subkey = "highlight";
    }

    let h = this.getDefault("height");

    this.setBoxCSS(subkey);

    this.label.style["padding"] = this.label.style["margin"] = "0px";
    this.style["background-color"] = this.getSubDefault(subkey, "background-color");

    let font = this.getSubDefault(subkey, "DefaultText");
    this.label.style["font"] = font.genCSS();
    this.label.style["color"] = font.color;

    this.style["display"] = "flex";
    this.style["align-items"] = "center";
    this.style["width"] = "max-content";
    this.style["height"] = h + "px";

    this.style["user-select"] = "none";
    this.label.style["user-select"] = "none";
  }

  click() {
    if (this._onpress) {
      let rect = this.getClientRects();
      let x = rect.x + rect.width*0.5;
      let y = rect.y + rect.height*0.5;

      let e = {x : x, y : y, stopPropagation : () => {}, preventDefault : () => {}};

      this._onpress(e);
    }

    super.click();
  }


  _redraw() {
    this.setCSS();
  }

  updateDisabled() {
    if (this._last_disabled !== this.disabled) {
      this._last_disabled = this.disabled;

      //setTimeout(() => {

      this._redraw();

      if (cconst.DEBUG.buttonEvents)
        console.log("disabled update!", this.disabled, this.style["background-color"]);
      //}, 100);
    }
  }

  update() {
    if (this._last_name !== this.name) {
      this.label.innerHTML = this.name;
      this._last_name = this.name;
    }
  }

  static define() {
    return {
      tagname: "button-x",
      style  : "button"
    };
  }
}
UIBase.register(Button);

//use .setAttribute("linear") to disable nonlinear sliding
export class OldButton extends ButtonEventBase {
  constructor() {
    super();

    let dpi = this.getDPI();

    this._last_but_update_key = "";

    this._name = "";
    this._namePad = undefined;
    this._leftPad = 5; //extra pad before text
    this._rightPad = 5; //extra pad after text

    this._last_w = 0;
    this._last_h = 0;

    this._last_dpi = dpi;

    this._lastw = undefined;
    this._lasth = undefined;

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

    this.dom.setAttribute("class", "canvas1");
    this.dom.tabIndex = 0;

    this._last_bg = undefined;

    this.addEventListener("keydown", (e) => {
      if (this.disabled) return;

      if (cconst.DEBUG.buttonEvents)
        console.log(e.keyCode);

      switch (e.keyCode) {
        case 27: //escape
          this.blur();
          e.preventDefault();
          e.stopPropagation();
          break;
        case 32: //spacebar
        case 13: //enter
          this.click();
          e.preventDefault();
          e.stopPropagation();
          break;
      }
    });

    this.addEventListener("focusin", () => {
      if (this.disabled) return;

      this._focus = 1;
      this._redraw();
      this.focus();
    });

    this.addEventListener("blur", () => {
      if (this.disabled) return;

      this._focus = 0;
      this._redraw();
    });

    this._last_disabled = false;
    this._auto_depress = true;

    this.shadow.appendChild(this.dom);
  }

  click() {
    if (this._onpress) {
      let rect = this.getClientRects();
      let x = rect.x + rect.width*0.5;
      let y = rect.y + rect.height*0.5;

      let e = {x : x, y : y, stopPropagation : () => {}, preventDefault : () => {}};

      this._onpress(e);
    }

    super.click();
  }

  init() {
    let dpi = this.getDPI();

    //set default dimensions
    let width = ~~(this.getDefault("width"));
    let height = ~~(this.getDefault("height"));

    this.dom.style["width"] = width + "px";
    this.dom.style["height"] = height + "px";
    this.dom.style["padding"] = this.dom.style["margin"] = "0px";

    this.dom.width = Math.ceil(width*dpi); //parsepx(this.dom.style["width"])*dpi;
    this.dom.height = Math.ceil(parsepx(this.dom.style["height"])*dpi);

    this._name = undefined;
    this.updateName();

    this.bindEvents();
    this._redraw();
  }

  setAttribute(key, val) {
    super.setAttribute(key, val);

    if (key === "name") {
      this.updateName();
      this.updateWidth();
    }
  }

  get r() {
    return this.getDefault("border-radius");
  }

  set r(val) {
    this.overrideDefault("border-radius", val);
  }

  old_bindEvents() {
    let press_gen = 0;

    let press = (e) => {
      e.stopPropagation();

      if (cconst.DEBUG.buttonEvents) {
        console.log("button press", this._pressed, this.disabled, e.button);
      }

      if (this.disabled) return;

      this._pressed = true;

      if (util.isMobile() && this.onclick && e.button === 0) {
        this.onclick();
      }

      if (this._onpress) {
        this._onpress(this);
      }

      this._redraw();

      e.preventDefault();
    };

    let depress = (e) => {
      if (cconst.DEBUG.buttonEvents)
        console.log("button depress", e.button, e.was_touch);

      if (this._auto_depress) {
        this._pressed = false;

        if (this.disabled) return;

        this._redraw();
      }

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

      if (util.isMobile() || e.type === "pointerup" && e.button) {
        return;
      }

      this._redraw();

      if (cconst.DEBUG.buttonEvents) {
        console.log("button click callback:", this.onclick, this._onpress, this.onpress);
      }

      if (this.onclick && e.pointerType !== "mouse") {
        this.onclick(this);
      }

      this.undoBreakPoint();
    }

    this.addEventListener("mousedown", press, {captured : true, passive : false});
    this.addEventListener("mouseup", depress, {captured : true, passive : false});
    this.addEventListener("mouseover", (e) => {
      if (this.disabled)
        return;

      this._highlight = true;
      this._repos_canvas();
      this._redraw();
    })

    this.addEventListener("mouseout", (e) => {
      if (this.disabled)
        return;

      this._highlight = false;
      this._repos_canvas();
      this._redraw();
    })
  }

  updateDisabled() {
    if (this._last_disabled !== this.disabled) {
      this._last_disabled = this.disabled;

      //setTimeout(() => {
      this.dom._background = this.getDefault("background-color");

      this._repos_canvas();
      this._redraw();

      if (cconst.DEBUG.buttonEvents)
        console.log("disabled update!", this.disabled, this.style["background-color"]);
      //}, 100);
    }
  }

  updateDefaultSize() {
    let height = ~~(this.getDefault("height")) + this.getDefault("padding");
    let size = this.getDefault("DefaultText").size * 1.33;

    if (height === undefined || size === undefined || isNaN(height) || isNaN(size)) {
      return;
    }

    height = ~~Math.max(height, size);
    height = height + "px";

    if (height !== this.style["height"]) {
      this.style["height"] = height;
      this.dom.style["height"] = height;

      this._repos_canvas();
      this._redraw();
    }
  }

  _calcUpdateKey() {
    return _themeUpdateKey;
  }

  update() {
    super.update();

    this.style["user-select"] = "none";
    this.dom.style["user-select"] = "none";

    this.updateDefaultSize();
    this.updateWidth();
    this.updateDPI();
    this.updateName();
    this.updateDisabled();

    if (this.background !== this._last_bg) {
      this._last_bg = this.background;
      this._repos_canvas();
      this._redraw();
    }

    let key = this._calcUpdateKey();
    if (key !== this._last_but_update_key) {
      this._last_but_update_key = key;

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

  setCSS() {
    super.setCSS();

    this.dom.style["margin"] = this.getDefault("margin", undefined, 0) + "px";
    this.dom.style["margin-left"] = this.getDefault("margin-left", undefined, 0) + "px";
    this.dom.style["margin-right"] = this.getDefault("margin-right", undefined, 0) + "px";
    this.dom.style["margin-top"] = this.getDefault("margin-top", undefined, 0) + "px";
    this.dom.style["margin-bottom"] = this.getDefault("margin-bottom", undefined, 0) + "px";

    let name = this._name;
    if (name === undefined) {
      return;
    }

    let dpi = this.getDPI();

    let pad = this.getDefault("padding");
    let ts = this.getDefault("DefaultText").size;

    let tw = ui_base.measureText(this, this._genLabel(),{
      size : ts,
      font : this.getDefault("DefaultText")
    }).width + 2.0*pad + this._leftPad + this._rightPad;

    if (this._namePad !== undefined) {
      tw += this._namePad;
    }

    let w = this.getDefault("width");

    w = Math.max(w, tw);
    w = ~~w;
    this.dom.style["width"] = w+"px";
    this.updateBorders();
  }

  updateBorders() {
    let lwid = this.getDefault("border-width");

    if (lwid) {
      this.dom.style["border-color"] = this.getDefault("border-color");
      this.dom.style["border-width"] = lwid + "px";
      this.dom.style["border-style"] = "solid";
      this.dom.style["border-radius"] = this.getDefault("border-radius") + "px";
    } else {
      this.dom.style["border-color"] = "none";
      this.dom.style["border-width"] = "0px";
      this.dom.style["border-radius"] = this.getDefault("border-radius") + "px";
    }

  }

  updateName() {
    if (!this.hasAttribute("name")) {
      return;
    }

    let name = this.getAttribute("name");

    if (name !== this._name) {
      this._name = name;

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

  updateWidth(w_add=0) {
  }

  _repos_canvas() {
    let dpi = this.getDPI();

    let w = parsepx(this.dom.style["width"]);
    let h = parsepx(this.dom.style["height"]);

    let w2 = ~~(w*dpi);
    let h2 = ~~(h*dpi);

    w = w2/dpi;
    h = h2/dpi;

    this.dom.width = w2;
    this.dom.style["width"] = w + "px";

    this.dom.height = h2;
    this.dom.style["height"] = h + "px";
  }

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

    if (this._last_dpi !== dpi) {
      //console.log("update dpi", dpi);

      this._last_dpi = dpi;

      this.g.font = undefined; //reset font

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

    if (this.style["background-color"]) {
      this.dom._background = this.style["background-color"];
      this.style["background-color"] = "";
    }

    //console.log(">", this.dom.style["background-color"], "-----");
    //console.log("width:", this.clientWidth)
  }

  _genLabel() {
    return "" + this._name;
  }

  _getSubKey() {
    if (this._pressed) {
      return 'depressed';
    } else if (this._highlight) {
      return 'highlight';
    } else {
      return undefined; //make getSubDefault forward to getDefault
    }
  }

  _redraw(draw_text=true) {
    //console.log("button draw");

    let dpi = this.getDPI();

    let subkey = this._getSubKey();

    if (this._pressed && this._highlight) {
      this.dom._background = this.getSubDefault(subkey, "highlight-pressed", "BoxHighlight");
    } else if (this._pressed) {
      this.dom._background = this.getSubDefault(subkey, "pressed","BoxDepressed");
    } else if (this._highlight) {
      this.dom._background = this.getSubDefault(subkey, "highlight", "BoxHighlight");
    } else {
      this.dom._background = this.getSubDefault(subkey, "background-color", "background-color");
    }

    ui_base.drawRoundBox(this, this.dom, this.g);

    //drawn with CSS now
    /*
    let lwid = this.getDefault("border-width");
    if (lwid) {
      this.g.lineWidth = lwid*dpi;
      ui_base.drawRoundBox(this, this.dom, this.g, undefined, undefined, undefined, "stroke");
    }
     */

    this.updateBorders();

    if (this._focus) {
      let w = this.dom.width, h = this.dom.height;
      let p = 1/dpi;

      //XXX remove this.g.translate lines after refactoring drawRoundBox, see comment in ui_base.js
      this.g.translate(p, p);
      let lw = this.g.lineWidth;


      this.g.lineWidth = this.getDefault("focus-border-width", undefined, 1.0)*dpi;

      ui_base.drawRoundBox(this, this.dom, this.g, w-p*2, h-p*2, this.r, "stroke", this.getDefault("BoxHighlight"));

      this.g.lineWidth = lw;
      this.g.translate(-p, -p);
    }

    if (draw_text) {
      this._draw_text();
    }
  }

  _draw_text() {
    let dpi = this.getDPI();

    let subkey = this._getSubKey();

    //if (util.isMobile()) {
    //dpi = dpi; //visualViewport.scale;
    //}

    let font = this.getSubDefault(subkey, "DefaultText");

    let pad = this.getDefault("padding") * dpi;
    let ts = font.size * dpi;

    let text = this._genLabel();

    //console.log(text, "text", this._name);

    let w = this.dom.width, h = this.dom.height;

    let tw = ui_base.measureText(this, text, undefined, undefined, ts, font).width;

    let cx = pad*0.5 + this._leftPad*dpi;
    let cy = ts + (h-ts)/3.0;

    let g = this.g;

    ui_base.drawText(this, ~~cx, ~~cy, text, {
      canvas : this.dom,
      g : this.g,
      size : ts / dpi,
      font : font
    });
  }

  static define() {
    return {
      tagname: "old-button-x",
      style  : "button"
    };
  }
}
UIBase.internalRegister(OldButton);