pxt-ev3/sim/visuals/view.ts

330 lines
9.3 KiB
TypeScript

namespace pxsim.visuals {
export abstract class View {
protected element: SVGGElement;
protected rendered = false;
protected visible = false;
protected selected: boolean;
protected width: number = 0;
protected height: number = 0;
protected left: number = 0;
protected top: number = 0;
protected scaleFactor: number = 1;
private changed: boolean;
private hover: boolean = false;
protected theme: IBoardTheme;
protected abstract buildDom(): SVGElement;
public abstract getInnerWidth(): number;
public abstract getInnerHeight(): number;
public inject(parent: SVGElement, theme: IBoardTheme, width?: number, visible = true) {
this.width = width;
this.theme = theme;
parent.appendChild(this.getView());
if (visible) {
this.visible = true;
this.onComponentInjected();
}
}
public getWidth() {
return this.scaleFactor == undefined ? this.getInnerWidth() : this.getInnerWidth() * this.scaleFactor;
}
public getHeight() {
return this.scaleFactor == undefined ? this.getInnerHeight() : this.getInnerHeight() * this.scaleFactor;
}
public onComponentInjected() {
// To be overridden by sub class
}
public onComponentVisible() {
// To be overridden by sub class
}
public onComponentHidden() {
// To be overridden by sub class
}
public translate(x: number, y: number, applyImmediately = true) {
this.left = x;
this.top = y;
if (applyImmediately) {
this.updateTransform();
}
}
public scale(scaleFactor: number, applyImmediately = true) {
this.scaleFactor = scaleFactor;
if (applyImmediately) {
this.updateTransform();
}
}
public updateState() {
}
public updateTheme(theme: IBoardTheme) {
this.theme = theme;
this.updateThemeCore();
}
public updateThemeCore() {
}
public setVisible(visible: boolean) {
if (this.rendered) {
this.getView().style.display = visible ? 'block' : 'none';
}
}
public hasClick() {
return true;
}
private onClickHandler: (ev: any) => void;
public registerClick(handler: (ev: any) => void, zoom?: boolean) {
this.onClickHandler = handler;
if (zoom) {
this.getView().addEventListener(pointerEvents.up, (ev: any) => {
if (!this.getSelected()) {
this.onClickHandler(ev);
this.setHover(false);
}
});
this.getView().addEventListener(pointerEvents.move, () => {
if (!this.getSelected()) {
this.setHover(true);
}
});
this.getView().addEventListener(pointerEvents.leave, () => {
this.setHover(false);
});
} else {
this.getView().addEventListener(pointerEvents.up, this.onClickHandler);
}
}
public dispose() {
if (this.onClickHandler) this.getView().removeEventListener(pointerEvents.up, this.onClickHandler)
View.dispose(this);
}
protected getView() {
if (!this.rendered) {
this.element = svg.elt("g") as SVGGElement;
View.track(this);
const content = this.buildDom();
if (content) {
this.element.appendChild(content);
}
this.updateTransform();
this.rendered = true;
}
return this.element;
}
public resize(width: number, height: number, strict?: boolean) {
this.width = width;
this.height = height;
}
public getActualHeight() {
return this.height;
}
public getActualWidth() {
return this.width;
}
private updateTransform() {
if (this.rendered) {
let left = this.left;
let top = this.top;
let scaleFactor = this.scaleFactor;
if (this.hover) {
const hoverScaleFactor = scaleFactor + 0.05;
// Scale around center of module
const centerX = this.getWidth() / 2;
const centerY = this.getHeight() / 2;
left = left - centerX * (hoverScaleFactor - 1);
top = top - centerY * (hoverScaleFactor - 1);
scaleFactor = hoverScaleFactor;
}
let transform = `translate(${left} ${top})`;
if (scaleFactor !== 1) {
transform += ` scale(${scaleFactor})`;
}
this.element.setAttribute("transform", transform);
}
}
private static currentId = 0;
private static allViews: Map<View> = {};
protected static getInstance(element: Element) {
if (element.hasAttribute("ref-id")) {
return View.allViews[element.getAttribute("ref-id")];
}
return undefined;
}
private static track(view: View) {
const myId = "id-" + (View.currentId++);
view.element.setAttribute("ref-id", myId);
View.allViews[myId] = view;
}
private static dispose(view: View) {
if (view.element) {
const id = view.element.getAttribute("ref-id");
// TODO: Remove from DOM
view.element.parentNode.removeChild(view.element);
delete View.allViews[id];
}
}
///////// HOVERED STATE /////////////
public getHover() {
return this.hover;
}
public setHover(hover: boolean) {
if (this.hover != hover) {
this.hover = hover;
this.updateTransform();
}
}
///////// SELECTED STATE /////////////
public getSelected() {
return this.selected;
}
public setSelected(selected: boolean) {
if (this.selected != selected) {
this.selected = selected;
this.setChangedState();
}
}
protected setChangedState() {
this.changed = true;
}
public didChange() {
const res = this.changed;
this.changed = false;
return res;
}
public hasBackground() {
return false;
}
}
export abstract class SimView<T extends BaseNode> extends View implements LayoutElement {
constructor(protected state: T) {
super();
}
public getId() {
return this.state.id;
}
public getPort() {
return this.state.port;
}
public getPaddingRatio() {
return 0;
}
public getWiringRatio() {
return 0.5;
}
public setSelected(selected: boolean) { }
protected getView() {
return super.getView();
}
public kill() {
}
}
export class ViewContainer extends View {
public getInnerWidth() {
return 0;
}
public getInnerHeight() {
return 0;
}
public addView(view: View) {
view.inject(this.element, this.theme);
}
public clear() {
const markForRemoval: Element[] = [];
forEachElement(this.element.childNodes, e => {
markForRemoval.push(e);
});
markForRemoval.forEach(e => {
this.element.removeChild(e);
})
}
public onComponentInjected() {
const observer = new MutationObserver(records => {
records.forEach(r => {
forEachElement(r.addedNodes, node => {
const instance = View.getInstance(node);
if (instance) {
instance.onComponentVisible();
}
});
forEachElement(r.removedNodes, node => {
const instance = View.getInstance(node);
if (instance) {
instance.onComponentHidden();
}
});
})
});
observer.observe(this.element, {
childList: true,
subtree: true
});
}
protected buildDom(): SVGElement {
return undefined;
}
}
function forEachElement(nodes: NodeList, cb: (e: Element) => void) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.nodeType === Node.ELEMENT_NODE) {
cb(node as Element);
}
}
}
}