import { DoricContext } from "../DoricContext"; import { acquireViewNode } from "../DoricRegistry"; export enum LayoutSpec { EXACTLY = 0, WRAP_CONTENT = 1, AT_MOST = 2, } const SPECIFIED = 1 const START = 1 << 1 const END = 1 << 2 const SHIFT_X = 0 const SHIFT_Y = 4 export const LEFT = (START | SPECIFIED) << SHIFT_X export const RIGHT = (END | SPECIFIED) << SHIFT_X export const TOP = (START | SPECIFIED) << SHIFT_Y export const BOTTOM = (END | SPECIFIED) << SHIFT_Y export const CENTER_X = SPECIFIED << SHIFT_X export const CENTER_Y = SPECIFIED << SHIFT_Y export const CENTER = CENTER_X | CENTER_Y export type FrameSize = { width: number, height: number, } export function toPixelString(v: number) { return `${v}px` } export function pixelString2Number(v: string) { return parseFloat(v.substring(0, v.indexOf("px"))) } export function toRGBAString(color: number) { let strs = [] for (let i = 0; i < 32; i += 8) { strs.push(((color >> i) & 0xff)) } strs = strs.reverse() /// RGBAd return `rgba(${strs[1]},${strs[2]},${strs[3]},${strs[0] / 255})` } export type DoricViewNodeClass = { new(...args: any[]): {} } export interface DVModel { id: string, type: string, props: { [index: string]: any }, } export abstract class DoricViewNode { viewId = "" viewType = "View" context: DoricContext superNode?: DoricSuperNode layoutConfig = { widthSpec: LayoutSpec.EXACTLY, heightSpec: LayoutSpec.EXACTLY, alignment: 0, weight: 0, margin: { left: 0, right: 0, top: 0, bottom: 0 } } padding = { left: 0, right: 0, top: 0, bottom: 0, } border?: { width: number, color: number, } frameWidth = 0 frameHeight = 0 offsetX = 0 offsetY = 0 view!: HTMLElement _originDisplay: string = "" transform: { translateX?: number, translateY?: number, scaleX?: number, scaleY?: number, rotation?: number, rotationX?: number, rotationY?: number } = {} transformOrigin: { x: number, y: number } | undefined constructor(context: DoricContext) { this.context = context } init(superNode?: DoricSuperNode) { if (superNode) { this.superNode = superNode if (this instanceof DoricSuperNode) { this.reusable = superNode.reusable } } this.view = this.build() this._originDisplay = this.view.style.display } abstract build(): HTMLElement get paddingLeft() { return this.padding.left || 0 } get paddingRight() { return this.padding.right || 0 } get paddingTop() { return this.padding.top || 0 } get paddingBottom() { return this.padding.bottom || 0 } get borderWidth() { return this.border?.width || 0 } blend(props: { [index: string]: any }) { this.view.id = `${this.viewId}` for (let key in props) { this.blendProps(this.view, key, props[key]) } this.onBlending() this.layout() } onBlending() { this.updateTransform() } onBlended() { } configBorder() { if (this.border) { this.applyCSSStyle({ borderStyle: "solid", borderWidth: toPixelString(this.border.width), borderColor: toRGBAString(this.border.color), }) } } configWidth() { let width: string switch (this.layoutConfig.widthSpec) { case LayoutSpec.WRAP_CONTENT: width = "max-content" break case LayoutSpec.AT_MOST: width = "100%" break case LayoutSpec.EXACTLY: default: width = toPixelString(this.frameWidth - this.paddingLeft - this.paddingRight - this.borderWidth * 2) break } this.applyCSSStyle({ width }) } configHeight() { let height switch (this.layoutConfig.heightSpec) { case LayoutSpec.WRAP_CONTENT: height = "max-content" break case LayoutSpec.AT_MOST: height = "100%" break case LayoutSpec.EXACTLY: default: height = toPixelString(this.frameHeight - this.paddingTop - this.paddingBottom - this.borderWidth * 2) break } this.applyCSSStyle({ height }) } configMargin() { if (this.layoutConfig.margin) { this.applyCSSStyle({ marginLeft: toPixelString(this.layoutConfig.margin.left || 0), marginRight: toPixelString(this.layoutConfig.margin.right || 0), marginTop: toPixelString(this.layoutConfig.margin.top || 0), marginBottom: toPixelString(this.layoutConfig.margin.bottom || 0), }) } } configPadding() { if (this.padding) { this.applyCSSStyle({ paddingLeft: toPixelString(this.paddingLeft), paddingRight: toPixelString(this.paddingRight), paddingTop: toPixelString(this.paddingTop), paddingBottom: toPixelString(this.paddingBottom), }) } } layout() { this.configMargin() this.configBorder() this.configPadding() this.configWidth() this.configHeight() } blendProps(v: HTMLElement, propName: string, prop: any) { switch (propName) { case "border": this.border = prop break case "padding": this.padding = prop break case 'width': this.frameWidth = prop as number break case 'height': this.frameHeight = prop as number break case 'backgroundColor': this.backgroundColor = prop as number break case 'layoutConfig': const layoutConfig = prop for (let key in layoutConfig) { Reflect.set(this.layoutConfig, key, Reflect.get(layoutConfig, key, layoutConfig)) } break case 'x': this.offsetX = prop as number break case 'y': this.offsetY = prop as number break case 'onClick': this.view.onclick = (event: Event) => { this.callJSResponse(prop as string) event.stopPropagation() } break case 'corners': if (typeof prop === 'object') { this.applyCSSStyle({ borderTopLeftRadius: toPixelString(prop.leftTop), borderTopRightRadius: toPixelString(prop.rightTop), borderBottomRightRadius: toPixelString(prop.rightBottom), borderBottomLeftRadius: toPixelString(prop.leftBottom), }) } else { this.applyCSSStyle({ borderRadius: toPixelString(prop) }) } break case 'shadow': const opacity = prop.opacity || 0 let boxShadow if (opacity > 0) { const offsetX = prop.offsetX || 0 const offsetY = prop.offsetY || 0 const shadowColor = prop.color || 0xff000000 const shadowRadius = prop.radius const alpha = opacity * 255 boxShadow = `${toPixelString(offsetX)} ${toPixelString(offsetY)} ${toPixelString(shadowRadius)} ${toRGBAString((shadowColor & 0xffffff) | ((alpha & 0xff) << 24))} ` } else { boxShadow = "" } this.applyCSSStyle({ boxShadow, }) break case 'alpha': this.applyCSSStyle({ opacity: `${prop}`, }) break case 'rotation': this.transform.rotation = prop break case 'rotationX': this.transform.rotationX = prop break case 'rotationY': this.transform.rotationY = prop break case 'scaleX': this.transform.scaleX = prop break case 'scaleY': this.transform.scaleY = prop break case 'translationX': this.transform.translateX = prop break case 'translationY': this.transform.translateY = prop break case 'pivotX': if (this.transformOrigin) { this.transformOrigin.x = prop } else { this.transformOrigin = { x: prop, y: 0.5, } } break case 'pivotY': if (this.transformOrigin) { this.transformOrigin.y = prop } else { this.transformOrigin = { x: 0.5, y: prop, } } break case 'hidden': this.applyCSSStyle({ display: prop === true ? "none" : this._originDisplay }) break default: console.error(`Cannot blend prop for ${propName}`) break } } set backgroundColor(v: number) { this.applyCSSStyle({ backgroundColor: toRGBAString(v) }) } static create(context: DoricContext, type: string) { const viewNodeClass = acquireViewNode(type) if (viewNodeClass === undefined) { console.error(`Cannot find ViewNode for ${type}`) return undefined } const ret = new viewNodeClass(context) as DoricViewNode ret.viewType = type return ret } getIdList() { const ids: string[] = [] let viewNode: DoricViewNode | undefined = this do { ids.push(viewNode.viewId) viewNode = viewNode.superNode } while (viewNode) return ids.reverse() } callJSResponse(funcId: string, ...args: any) { const argumentsList: any = ['__response__', this.getIdList(), funcId] for (let i = 1; i < arguments.length; i++) { argumentsList.push(arguments[i]) } return Reflect.apply(this.context.invokeEntityMethod, this.context, argumentsList) } pureCallJSResponse(funcId: string, ...args: any) { const argumentsList: any = ['__response__', this.getIdList(), funcId] for (let i = 1; i < arguments.length; i++) { argumentsList.push(arguments[i]) } return Reflect.apply(this.context.pureInvokeEntityMethod, this.context, argumentsList) } updateTransform() { this.applyCSSStyle({ transform: Object.entries(this.transform).filter((e: [string, number?]) => !!e[1]).map((e: [string, number?]) => { const v = e[1] || 0 switch (e[0]) { case "translateX": return `translateX(${v}px)` case "scaleX": return `scaleX(${v})` case "scaleY": return `scaleY(${v})` case "rotation": return `rotate(${v / 2}turn)` case "rotationX": return `rotateX(${v / 2}turn)` case "rotationY": return `rotateY(${v / 2}turn)` default: console.error(`Do not support transform ${e[0]}`) return "" } }).join(" ") }) } updateTransformOrigin() { if (this.transformOrigin) { this.applyCSSStyle({ transformOrigin: `${Math.round(this.transformOrigin.x * 100)}% ${Math.round(this.transformOrigin.y * 100)}%` }) } } applyCSSStyle(cssStyle: Partial) { if (this.context.inAnimation()) { this.context.addAnimation(this, cssStyle) } else { for (let v in cssStyle) { Reflect.set(this.view.style, v, cssStyle[v]) } } } /** ++++++++++call from doric ++++++++++*/ getWidth() { return this.view.offsetWidth } getHeight() { return this.view.offsetHeight } setWidth(v: number) { this.view.style.width = toPixelString(v) } setHeight(v: number) { this.view.style.height = toPixelString(v) } getX() { return this.view.offsetLeft } getY() { return this.view.offsetTop } setX(v: number) { this.view.style.left = toPixelString(v) } setY(v: number) { this.view.style.top = toPixelString(v) } getBackgroundColor() { return this.view.style.backgroundColor } setBackgroundColor(v: number) { this.backgroundColor = v } getAlpha() { return parseFloat(this.view.style.opacity) } setAlpha(v: number) { this.view.style.opacity = `${v}` } getCorners() { return parseFloat(this.view.style.borderRadius) } setCorners(v: number) { this.view.style.borderRadius = toPixelString(v) } getLocationOnScreen() { const rect = this.view.getClientRects()[0] return { x: rect.left, y: rect.top, } } getRotation() { return this.transform.rotation } setRotation(v: number) { this.transform.rotation = v this.updateTransform() } getRotationX() { return this.transform.rotationX } setRotationX(v: number) { this.transform.rotationX = v this.updateTransform() } getRotationY() { return this.transform.rotationY } setRotationY(v: number) { this.transform.rotationY = v this.updateTransform() } getTranslationX() { return this.transform.translateX } setTranslationX(v: number) { this.transform.translateX = v this.updateTransform() } getTranslationY() { return this.transform.translateY } setTranslationY(v: number) { this.transform.translateY = v this.updateTransform() } getScaleX() { return this.transform.scaleX } setScaleX(v: number) { this.transform.scaleX = v this.updateTransform() } getScaleY() { return this.transform.scaleY } setScaleY(v: number) { this.transform.scaleY = v this.updateTransform() } getPivotX() { return this.transformOrigin?.x || 0.5 } setPivotX(v: number) { if (this.transformOrigin) { this.transformOrigin.x = v } else { this.transformOrigin = { x: v, y: 0.5, } } this.updateTransform() } getPivotY() { return this.transformOrigin?.y || 0.5 } setPivotY(v: number) { if (this.transformOrigin) { this.transformOrigin.y = v } else { this.transformOrigin = { x: 0.5, y: v, } } this.updateTransform() } /** ----------call from doric ----------*/ } export abstract class DoricSuperNode extends DoricViewNode { reusable = false subModels: Map = new Map blendProps(v: HTMLElement, propName: string, prop: any) { if (propName === 'subviews') { if (prop instanceof Array) { prop.forEach((e: DVModel) => { this.mixinSubModel(e) this.blendSubNode(e) }) } } else { super.blendProps(v, propName, prop) } } mixinSubModel(subNode: DVModel) { const oldValue = this.getSubModel(subNode.id) if (oldValue) { this.mixin(subNode, oldValue) } else { this.subModels.set(subNode.id, subNode) } } getSubModel(id: string) { return this.subModels.get(id) } mixin(src: DVModel, target: DVModel) { for (let key in src.props) { if (key === "subviews") { continue } Reflect.set(target.props, key, Reflect.get(src.props, key)) } } clearSubModels() { this.subModels.clear() } removeSubModel(id: string) { this.subModels.delete(id) } abstract blendSubNode(model: DVModel): void abstract getSubNodeById(viewId: string): DoricViewNode | undefined } export abstract class DoricGroupViewNode extends DoricSuperNode { childNodes: DoricViewNode[] = [] childViewIds: string[] = [] init(superNode?: DoricSuperNode) { super.init(superNode) this.view.style.overflow = "hidden" } blendProps(v: HTMLElement, propName: string, prop: any) { if (propName === 'children') { if (prop instanceof Array) { this.childViewIds = prop } } else { super.blendProps(v, propName, prop) } } blend(props: { [index: string]: any }) { super.blend(props) } onBlending() { super.onBlending() this.configChildNode() } onBlended() { super.onBlended() this.childNodes.forEach(e => e.onBlended()) } configChildNode() { this.childViewIds.forEach((childViewId, index) => { const model = this.getSubModel(childViewId) if (model === undefined) { return } if (index < this.childNodes.length) { const oldNode = this.childNodes[index] if (oldNode.viewId === childViewId) { //The same,skip } else { if (this.reusable) { if (oldNode.viewType === model.type) { //Same type,can be reused oldNode.viewId = childViewId oldNode.blend(model.props) } else { //Replace this view this.view.removeChild(oldNode.view) const newNode = DoricViewNode.create(this.context, model.type) if (newNode === undefined) { return } newNode.viewId = childViewId newNode.init(this) newNode.blend(model.props) this.childNodes[index] = newNode this.view.replaceChild(newNode.view, oldNode.view) } } else { //Find in remain nodes let position = -1 for (let start = index + 1; start < this.childNodes.length; start++) { if (childViewId === this.childNodes[start].viewId) { //Found position = start break } } if (position >= 0) { //Found swap idx,position const reused = this.childNodes[position] const abandoned = this.childNodes[index] this.childNodes[index] = reused this.childNodes[position] = abandoned this.view.removeChild(reused.view) this.view.insertBefore(reused.view, abandoned.view) this.view.removeChild(abandoned.view) if (position === this.view.childElementCount - 1) { this.view.appendChild(abandoned.view) } else { this.view.insertBefore(abandoned.view, this.view.children[position]) } } else { //Not found,insert const newNode = DoricViewNode.create(this.context, model.type) if (newNode === undefined) { return } newNode.viewId = childViewId newNode.init(this) newNode.blend(model.props) this.childNodes[index] = newNode this.view.insertBefore(newNode.view, this.view.children[index]) } } } } else { //Insert const newNode = DoricViewNode.create(this.context, model.type) if (newNode === undefined) { return } newNode.viewId = childViewId newNode.init(this) newNode.blend(model.props) this.childNodes.push(newNode) this.view.appendChild(newNode.view) } }) let size = this.childNodes.length for (let idx = this.childViewIds.length; idx < size; idx++) { this.view.removeChild(this.childNodes[idx].view) } this.childNodes = this.childNodes.slice(0, this.childViewIds.length) } blendSubNode(model: DVModel) { this.getSubNodeById(model.id)?.blend(model.props) } getSubNodeById(viewId: string) { return this.childNodes.filter(e => e.viewId === viewId)[0] } }