UNPKG

@wonderlandengine/react-ui

Version:
959 lines (958 loc) 36.9 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Alignment, Collider, CollisionComponent, Component, TextComponent, TextEffect, VerticalAlignment, } from '@wonderlandengine/api'; import { property } from '@wonderlandengine/api/decorators.js'; import { mat4, vec3 } from 'gl-matrix'; import Reconciler from 'react-reconciler'; /* These are used by props */ export { Align, Display, FlexDirection, Justify, Overflow, PositionType, Wrap, } from 'yoga-layout/load'; /* These are used by the renderer only */ import { Align, Display, Edge, Direction, FlexDirection, Gutter, Justify, Overflow, PositionType, Wrap, loadYoga, } from 'yoga-layout/load'; import { roundedRectangle, roundedRectangleOutline } from './rounded-rectangle-mesh.js'; import { CursorTarget } from '@wonderlandengine/components'; import { nineSlice } from './nine-slice.js'; const Z_INC = 0.001; const TEXT_BASE_SIZE = 12; const DEFAULT_FONT_SIZE = 50; let Y = null; function destroyTreeForNode(child, ctx) { const childCount = child.children.length ?? 0; for (let c = childCount - 1; c >= 0; --c) { destroyTreeForNode(child.children[c], ctx); } if (child.parent) { const parent = child.parent; parent.node.removeChild(child.node); parent.children.splice(parent.children.indexOf(child), 1); child.parent = null; } child.node.free(); if (child.object && !child.object.isDestroyed) { child.object.destroy(); } child.object = null; child.ctx?.removeNodeWrapper(child); } function propsEqual(oldProps, newProps) { const oldKeys = Object.keys(oldProps); const newKeys = Object.keys(newProps); if (oldKeys.length !== newKeys.length) return false; for (const k of oldKeys) { if (oldProps[k] != newProps[k]) { return false; } } return true; } class NodeWrapper { node; tag; /* Applied properties cache */ props = {}; object = null; parent = null; children = []; ctx; hovering = [false, false]; dirty = true; constructor(ctx, node, tag) { this.ctx = ctx; this.tag = tag; this.node = node; } } class Context { root; config; comp; wrappers = []; constructor(c) { this.root = null; this.comp = c; this.config = Y.Config.create(); this.config.setUseWebDefaults(false); this.config.setPointScaleFactor(1); } addNodeWrapper(w) { this.wrappers.push(w); w.ctx = this; } removeNodeWrapper(w) { const i = this.wrappers.indexOf(w); this.wrappers.splice(i, 1); } printTree(node, prefix) { node = node ?? this.root; prefix = prefix ?? ''; const yn = node.node; debug(prefix + node.tag, `{${yn.getComputedLeft()}, ${yn.getComputedTop()}, ${yn.getComputedWidth()}, ${yn.getComputedHeight()}}`, node.props); if (!node.children) return; for (let n of node.children) { this.printTree(n, prefix + '--'); } } } function setPositionCenter(o, n, s) { o.setPositionLocal([ (n.node.getComputedLeft() + 0.5 * n.node.getComputedWidth()) * s[0], -n.node.getComputedTop() * s[1], Z_INC + (n.props.z ?? 0), ]); } function setPositionLeft(o, n, s) { o.setPositionLocal([ n.node.getComputedLeft() * s[0], -n.node.getComputedTop() * s[1], Z_INC + (n.props.z ?? 0), ]); } function setPositionRight(o, n, s) { // n.node.getComputedRight() was giving 0; o.setPositionLocal([ (n.node.getComputedLeft() + n.node.getComputedWidth()) * s[0], -n.node.getComputedTop() * s[1], Z_INC + (n.props.z ?? 0), ]); } function applyLayoutToSceneGraph(n, context, force) { debug('applyLayoutToSceneGraph'); if (!force && !n.dirty && !n.node.hasNewLayout()) return; n.node.markLayoutSeen(); if (n.object?.isDestroyed) n.object = null; const o = n.object ?? (() => { if (n.parent?.object?.isDestroyed) n.parent.object = null; const o = context.comp.engine.scene.addObject(n.parent?.object ?? context.comp.object); return o; })(); n.object = o; o.parent = n.parent?.object ?? context?.comp?.object; o.resetPositionRotation(); o.resetScaling(); if (n.tag === 'text3d') { const align = n.props.textAlign; let alignment = Alignment.Left; if (align === 'center') { setPositionCenter(o, n, context.comp.scaling); alignment = Alignment.Center; } else if (align === 'right') { setPositionRight(o, n, context.comp.scaling); alignment = Alignment.Right; } else { setPositionLeft(o, n, context.comp.scaling); } const s = TEXT_BASE_SIZE * (n.props.fontSize ?? DEFAULT_FONT_SIZE) * context.comp.scaling[1]; o.setScalingLocal([s, s, s]); const t = o.getComponent('text') ?? o.addComponent('text', { alignment, effect: TextEffect.Outline, verticalAlignment: VerticalAlignment.Top, }); t.material = n.props.material ?? context.comp.textMaterial; if (t.text !== n.props.text) t.text = n.props.text; } else { /* "mesh" and everything else */ if (context && context.comp && context.comp.scaling) { setPositionLeft(o, n, context.comp.scaling); } } if (n.tag === 'mesh' || n.tag === 'roundedRectangle' || n.tag === 'nineSlice') { /* To offset the mesh, but avoid offsetting the children, * we need to add a child object */ const child = o.findByNameDirect('mesh')[0] ?? (() => { const child = context.comp.engine.scene.addObject(o); child.name = 'mesh'; return child; })(); let sw = n.node.getComputedWidth() * context.comp.scaling[0]; let sh = n.node.getComputedHeight() * context.comp.scaling[1]; // there is a change that on the first time the code is executed getComputedWidth // and getComputedHeight return NaN. In that case we set the values to 0 to prevent // any more issues. if (isNaN(sw) || isNaN(sh)) { sw = 0; sh = 0; } const centerX = 0.5 * sw; const centerY = -0.5 * sh; const m = child.getComponent('mesh') ?? child.addComponent('mesh', {}); m.material = n.props.material; let mesh = m.mesh; if (n.tag === 'roundedRectangle') { const p = { sw, sh, rounding: (n.props.rounding ?? 30) * context.comp.scaling[0], resolution: n.props.resolution ?? 4, tl: n.props.roundTopLeft ?? true, tr: n.props.roundTopRight ?? true, bl: n.props.roundBottomLeft ?? true, br: n.props.roundBottomRight ?? true, }; const props = m.roundedRectangleProps ?? {}; const needsUpdate = !propsEqual(props, p); const borderSize = (n.props.borderSize ?? 0) * context.comp.scaling[0]; if (needsUpdate) { mesh = roundedRectangle(context.comp.engine, p.sw, p.sh, p.rounding, p.resolution, { tl: p.tl, tr: p.tr, bl: p.bl, br: p.br, }, mesh); m.roundedRectangleProps = p; } const oldBorderSize = m.borderSize ?? 0; const needsBorderMeshUpdate = oldBorderSize != borderSize; const bm = child.getComponent('mesh', 1) ?? child.addComponent('mesh', {}); bm.active = borderSize != 0; bm.material = n.props.borderMaterial; if (needsBorderMeshUpdate) { if (borderSize != 0) { bm.mesh = roundedRectangleOutline(context.comp.engine, p.sw, p.sh, p.rounding, p.resolution, borderSize, { tl: p.tl, tr: p.tr, bl: p.bl, br: p.br, }, bm.mesh); m.borderSize = borderSize; } } child.setPositionLocal([centerX, centerY, Z_INC + (n.props.z ?? 0)]); child.resetScaling(); } else if (n.tag === 'nineSlice') { const p = { sw, sh, borderTextureSize: n.props.borderTextureSize ?? 0, borderSize: (n.props.borderSize ?? 0) * context.comp.scaling[0], }; const props = m.nineSliceProps ?? {}; const needsUpdate = !propsEqual(props, p); if (needsUpdate) { mesh = nineSlice(context.comp.engine, p.sw, p.sh, p.borderSize, p.borderTextureSize, mesh); m.nineSliceProps = p; } child.setPositionLocal([centerX, centerY, Z_INC + (n.props.z ?? 0)]); child.resetScaling(); } else { /* Planes are diameter of 2 */ child.setPositionLocal([centerX, centerY, Z_INC + (n.props.z ?? 0)]); child.setScalingLocal([ 0.5 * sw, 0.5 * sh, // if there's a z value set we're not scaling the z value. n.props.z === undefined ? 0.5 * sw : 1, ]); } m.mesh = n.props.mesh ?? mesh; } /* For children created earlier */ n.children?.forEach((c) => { applyLayoutToSceneGraph(c, context, force); if (c.object && !c.object.isDestroyed) { c.object.parent = n.object; } }); } const tempVec4 = new Float32Array(4); function applyToYogaNode(tag, node, props, wrapper, ctx) { if (tag === 'text3d') { const p = props; const s = TEXT_BASE_SIZE * (p.fontSize ?? DEFAULT_FONT_SIZE); let t = wrapper.object?.getComponent(TextComponent); if (!t) { /* Apply properties relevant to text component here */ wrapper.props.text = p.text; wrapper.props.textAlign = p.textAlign; applyLayoutToSceneGraph(wrapper, ctx, true); t = wrapper.object?.getComponent(TextComponent); } // TODO: Avoid all the computation when width and height is set let h = 0; const b = t.getBoundingBoxForText(p.text?.toString() ?? '', tempVec4); if (props.height) { h = props.height; } else { const font = t.material.getFont(); if (font) { h = font.capHeight * s; } else { h = s * (b[3] - b[1]); } } // when alighment is left or right, the width is the width of the text // when alignment is center, the width is the width of the container let w; if (p.textAlign === 'left' || p.textAlign === 'right') { w = props.width ?? s * (b[2] - b[0]); } else { w = props.width; } node.setHeight(h); node.setWidth(w); } else { if (ctx) { applyLayoutToSceneGraph(wrapper, ctx, true); } else { debug('Context is undefined, skipping applyLayoutToSceneGraph'); } node.setWidth(props.width); node.setHeight(props.height); } /* Properties that allow undefined should be assigned to `undefined`, * * Properties that have a default value and do not allow `undefined` should be * assigned the default value if props.value is `undefined`. */ if (wrapper.props.alignContent !== props.alignContent) node.setAlignContent(props.alignContent ?? Align.FlexStart); if (wrapper.props.alignItems !== props.alignItems) node.setAlignItems(props.alignItems ?? Align.Stretch); if (wrapper.props.alignSelf !== props.alignSelf) // TODO This default was not documented! node.setAlignSelf(props.alignSelf ?? Align.FlexStart); if (wrapper.props.aspectRatio !== props.aspectRatio) node.setAspectRatio(props.aspectRatio); if (wrapper.props.display !== props.display) node.setDisplay(props.display ?? Display.Flex); if (wrapper.props.flex !== props.flex) node.setFlex(props.flex); if (wrapper.props.flexDirection !== props.flexDirection) node.setFlexDirection(props.flexDirection ?? FlexDirection.Column); if (wrapper.props.flexBasis !== props.flexBasis) node.setFlexBasis(props.flexBasis); if (wrapper.props.flexGrow !== props.flexGrow) node.setFlexGrow(props.flexGrow); if (wrapper.props.flexShrink !== props.flexShrink) node.setFlexShrink(props.flexShrink); if (wrapper.props.flexWrap !== props.flexWrap) node.setFlexWrap(props.flexWrap ?? Wrap.NoWrap); if (wrapper.props.isReferenceBaseline !== props.isReferenceBaseline) node.setIsReferenceBaseline(props.isReferenceBaseline ?? false); if (wrapper.props.gap !== props.gap) node.setGap(Gutter.All, props.gap); if (wrapper.props.rowGap !== props.rowGap) node.setGap(Gutter.Row, props.rowGap); if (wrapper.props.columnGap !== props.columnGap) node.setGap(Gutter.Column, props.columnGap); if (wrapper.props.justifyContent !== props.justifyContent) node.setJustifyContent(props.justifyContent ?? Justify.FlexStart); if (wrapper.props.border !== props.border) node.setBorder(Edge.All, props.border); if (wrapper.props.borderTop !== props.borderTop) node.setBorder(Edge.Top, props.borderTop); if (wrapper.props.borderBottom !== props.borderBottom) node.setBorder(Edge.Bottom, props.borderBottom); if (wrapper.props.borderLeft !== props.borderLeft) node.setBorder(Edge.Left, props.borderLeft); if (wrapper.props.borderRight !== props.borderRight) node.setBorder(Edge.Right, props.borderRight); if (wrapper.props.margin !== props.margin) node.setMargin(Edge.All, props.margin); if (wrapper.props.marginTop !== props.marginTop) node.setMargin(Edge.Top, props.marginTop); if (wrapper.props.marginBottom !== props.marginBottom) node.setMargin(Edge.Bottom, props.marginBottom); if (wrapper.props.marginLeft !== props.marginLeft) node.setMargin(Edge.Left, props.marginLeft); if (wrapper.props.marginRight !== props.marginRight) node.setMargin(Edge.Right, props.marginRight); if (wrapper.props.maxHeight !== props.maxHeight) node.setMaxHeight(props.maxHeight); if (wrapper.props.maxWidth !== props.maxWidth) node.setMaxWidth(props.maxWidth); if (wrapper.props.minHeight !== props.minHeight) node.setMinHeight(props.minHeight); if (wrapper.props.minWidth !== props.minWidth) node.setMinWidth(props.minWidth); if (wrapper.props.overflow !== props.overflow) node.setOverflow(props.overflow ?? Overflow.Hidden); if (wrapper.props.padding !== props.padding) node.setPadding(Edge.All, props.padding); if (wrapper.props.paddingTop !== props.paddingTop) node.setPadding(Edge.Top, props.paddingTop); if (wrapper.props.paddingBottom !== props.paddingBottom) node.setPadding(Edge.Bottom, props.paddingBottom); if (wrapper.props.paddingLeft !== props.paddingLeft) node.setPadding(Edge.Left, props.paddingLeft); if (wrapper.props.paddingRight !== props.paddingRight) node.setPadding(Edge.Right, props.paddingRight); if (wrapper.props.position !== props.position) node.setPositionType(props.position ?? PositionType.Relative); if (wrapper.props.top !== props.top) node.setPosition(Edge.Top, props.top); if (wrapper.props.bottom !== props.bottom) node.setPosition(Edge.Bottom, props.bottom); if (wrapper.props.left !== props.left) node.setPosition(Edge.Left, props.left); if (wrapper.props.right !== props.right) node.setPosition(Edge.Right, props.right); if (wrapper) wrapper.props = props; } const DEBUG_RENDERER = false; const DEBUG_EVENTS = false; const debug = DEBUG_RENDERER ? console.log : () => { }; const HostConfig = { getRootHostContext(context) { return context; }, getChildHostContext(parentHostContext) { return parentHostContext; }, shouldSetTextContent(tag) { return false; }, createTextInstance(text, ctx, hostContext, node) { debug('createTextInstance', text, ctx, hostContext, node); }, createInstance(tag, props, ctx) { debug('createInstance', tag, props, ctx); const node = Y.Node.create(ctx.config); const w = new NodeWrapper(ctx, node, tag); ctx.addNodeWrapper(w); applyToYogaNode(tag, node, props, w, ctx); return w; }, appendInitialChild(parent, child) { debug('appendInitialChild', child, parent); if (!child) return; applyToYogaNode(child.tag, child.node, child.props, child); parent.node.insertChild(child.node, parent.node.getChildCount()); child.parent = parent; parent.children.push(child); parent.ctx.comp.needsUpdate = true; }, appendChild(parent, child) { debug('appendChild', parent, child); if (!child) return; applyToYogaNode(child.tag, child.node, child.props, child); parent.node.insertChild(child.node, parent.node.getChildCount()); child.parent = parent; parent.children.push(child); parent.ctx.comp.needsUpdate = true; }, appendChildToContainer(ctx, child) { debug('appendChildToContainer', ctx, child); if (!child) return; ctx.root = child; ctx.comp.needsUpdate = true; }, insertInContainerBefore(container, child, beforeChild) { debug('insertContainerBefore', parent, child, beforeChild); if (child === undefined) return; }, insertBefore(parent, child, before) { debug('insertBefore', parent, child, before); if (!child) return; applyToYogaNode(child.tag, child.node, child.props, child); // Find the index of the 'before' node to determine the position at which to insert the new node const beforeIndex = parent.children.findIndex((childNode) => childNode === before); if (beforeIndex !== -1) { parent.node.insertChild(child.node, beforeIndex); } child.parent = parent; // We also need to insert the child in the correct position in the parent's children array if (beforeIndex !== -1) { parent.children.splice(beforeIndex, 0, child); } else { // If the 'before' node is not found, we will append the child by default. parent.children.push(child); } parent.ctx.comp.needsUpdate = true; }, removeChild(parent, child) { debug('removeChild', parent, child); if (!child) return; destroyTreeForNode(child, parent.ctx); parent.ctx.comp.needsUpdate = true; }, removeChildFromContainer(ctx, child) { debug('removeChildFromContainer', ctx, child); if (!child) return; destroyTreeForNode(child, ctx); }, finalizeInitialChildren(instance, tag, props, ctx, hostContext) { debug('finalizeInitialChildren', instance, tag); return false; }, prepareForCommit(ctx) { debug('prepareForCommit'); return null; }, resetAfterCommit(containerInfo) { debug('resetAfterCommit', containerInfo); }, commitUpdate(instance, updatePayload, type, prevProps, nextProps, internalHandle) { debug('commitUpdate'); instance.props = nextProps; instance.dirty = true; applyToYogaNode(instance.tag, instance.node, instance.props, instance); instance.ctx.comp.needsUpdate = true; }, prepareUpdate(instance, tag, oldProps, newProps, rootContainer, hostContext) { debug('prepareUpdate', oldProps, newProps); if (propsEqual(oldProps, newProps)) return null; return {}; }, getPublicInstance(instance) { return instance.object; }, afterActiveInstanceBlur() { }, beforeActiveInstanceBlur() { }, detachDeletedInstance(node) { }, getCurrentEventPriority() { return 0; }, getInstanceFromNode(node) { return null; }, clearContainer(ctx) { debug('clearContainer', ctx); if (!ctx.root) return; destroyTreeForNode(ctx.root, ctx); ctx.root = null; }, getInstanceFromScope(scopeInstance) { debug('getInstanceFromScope', scopeInstance); return null; }, scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, supportsMutation: true, supportsHydration: false, supportsPersistence: false, isPrimaryRenderer: false, noTimeout: -1, preparePortalMount(containerInfo) { }, prepareScopeUpdate(scopeInstance, instance) { }, }; const reconcilerInstance = Reconciler(HostConfig); export var UISpace; (function (UISpace) { UISpace[UISpace["World"] = 0] = "World"; UISpace[UISpace["Screen"] = 1] = "Screen"; })(UISpace || (UISpace = {})); export var ScalingType; (function (ScalingType) { /** * The sizes absolute to the screen size. A pixel in the UI is a pixel on screen. * This is the default. */ ScalingType[ScalingType["Absolute"] = 0] = "Absolute"; /** * The height of the UI will be fixed to the value set in the `manualHeight` property. */ ScalingType[ScalingType["FixedHeight"] = 1] = "FixedHeight"; /** * The height of the UI will be fixed to the value set in the `manualHeight` property. * But if the width is below a certain threshold, the height will be scaled down anyway */ ScalingType[ScalingType["FixedHeightLimitedWidth"] = 2] = "FixedHeightLimitedWidth"; })(ScalingType || (ScalingType = {})); const tempPos = [0, 0, 0]; const tempScale = [0, 0, 0]; export class ReactUiBase extends Component { static TypeName = 'react-ui-base'; space = 0; /* Material from which all text materials will be cloned */ textMaterial; /* Material from which all panel materials will be cloned */ panelMaterial; /* Textured material from which all panel materials will be cloned, * e.g. for Image */ panelMaterialTextured; width = 100; height = 100; scalingMode = ScalingType.Absolute; manualHeight = 1080; manualWidth = 1080; /** * Device pixel ratio, defaults to 1. Used on mobile/tablet devices to scale. */ dpr = 1; get pixelSizeAdjustment() { switch (this.scalingMode) { case ScalingType.FixedHeight: return this.manualHeight / this.engine.canvas.height; case ScalingType.FixedHeightLimitedWidth: const factor = this.engine.canvas.height / this.manualHeight; if (this.engine.canvas.width / factor < this.manualWidth) { return this.manualWidth / this.engine.canvas.width; } else { return this.manualHeight / this.engine.canvas.height; } default: return 1; } } static onRegister(engine) { engine.registerComponent(CursorTarget); } _onViewportResize = () => { /* This callback is only added if space is "screen" */ const activeView = this.engine.scene.activeViews[0]; if (!activeView) return; /* Projection matrix will change if the viewport is resized, which will affect the * projection matrix because of the aspect ratio. */ const invProj = new Float32Array(16); mat4.invert(invProj, activeView.projectionMatrix); const topLeft = vec3.create(); const bottomRight = vec3.create(); vec3.transformMat4(topLeft, [-1, 1, 0], invProj); vec3.transformMat4(bottomRight, [1, -1, 0], invProj); const s = bottomRight[0] - topLeft[0]; this.object.setScalingLocal([s, s, s]); /* Convert from yoga units to 0-1 */ this.dpr = window.devicePixelRatio; this.width = this._dpiAdjust(this.engine.canvas.clientWidth); this.height = this._dpiAdjust(this.engine.canvas.clientHeight); this.scaling = [1 / this.width, 1 / this.width]; this.object.setPositionLocal(topLeft); this.needsUpdate = true; this.viewportChanged = true; }; scaling = [0.01, 0.01]; renderer; ctx = null; _viewComponent; needsUpdate = true; viewportChanged = true; setContext(c) { this.ctx = c; } updateLayout() { if (!this.ctx?.root) return; debug('updateLayout', this.width, this.height); this.ctx.wrappers.forEach((w) => applyToYogaNode(w.tag, w.node, w.props, w)); this.ctx.root.node.calculateLayout(this.width ?? 100, this.height ?? 100, Direction.LTR); applyLayoutToSceneGraph(this.ctx.root, this.ctx, this.viewportChanged); this.needsUpdate = false; } init() { /* We need to ensure React defers re-renders to after the callbacks were called */ const onMove = this.onMove; this.onMove = (e) => reconcilerInstance.batchedUpdates(onMove, e); const onClick = this.onClick; this.onClick = (e) => reconcilerInstance.batchedUpdates(onClick, e); const onUp = this.onUp; this.onUp = (e) => reconcilerInstance.batchedUpdates(onUp, e); const onDown = this.onDown; this.onDown = (e) => reconcilerInstance.batchedUpdates(onDown, e); this.callbacks = { click: this.onPointerClick.bind(this), pointermove: this.onPointerMove.bind(this), pointerdown: this.onPointerDown.bind(this), pointerup: this.onPointerUp.bind(this), }; } async start() { if (this.space == UISpace.Screen) { this._viewComponent = this.engine.scene.activeViews[0]; /* Reparent to main view */ this.object.parent = this._viewComponent.object; this.object.setPositionLocal([0, 0, -2 * this._viewComponent.near]); this.object.resetRotation(); /* Calculate size of the UI */ this._onViewportResize(); this.engine.onResize.add(this._onViewportResize); } this.renderer = await initializeRenderer(); this.renderer.render(this.render(), this); } update(dt = undefined) { if (this.needsUpdate) this.updateLayout(); } callbacks = {}; getCursorPosition(c) { this.object.transformPointInverseWorld(tempPos, c.cursorPos); this.object.getScalingWorld(tempScale); return [ tempPos[0] / this.scaling[0] / tempScale[0], -tempPos[1] / this.scaling[1] / tempScale[1], ]; } onActivate() { if (this.space == UISpace.World) { const colliderObject = this.object.findByNameDirect('UIColliderObject')[0] ?? (() => { const o = this.engine.scene.addObject(this.object); o.name = 'UIColliderObject'; o.addComponent(CursorTarget); o.addComponent(CollisionComponent, { collider: Collider.Box, group: 0xff, }); return o; })(); const target = colliderObject.getComponent(CursorTarget); const collision = colliderObject.getComponent(CollisionComponent); target.onClick.add((_, c, e) => { const [x, y] = this.getCursorPosition(c); this.onClick({ x, y, e: e }); }, { id: 'onClick' }); target.onMove.add((_, c, e) => { const [x, y] = this.getCursorPosition(c); this.onMove({ x, y, e }); }, { id: 'onMove' }); target.onUp.add((_, c, e) => { const [x, y] = this.getCursorPosition(c); this.onUp({ x, y, e: e }); }, { id: 'onUp' }); target.onDown.add((_, c, e) => { const [x, y] = this.getCursorPosition(c); this.onDown({ x, y, e: e }); }, { id: 'onDown' }); const extents = this.object.getScalingWorld(new Float32Array(3)); extents[0] *= 0.5 * this.width * this.scaling[0]; extents[1] *= 0.5 * this.height * this.scaling[1]; extents[2] = 0.05; collision.extents.set(extents); colliderObject.setPositionLocal([ this.width * 0.5 * this.scaling[0], -this.height * 0.5 * this.scaling[1], 0.025, ]); } else { for (const [k, v] of Object.entries(this.callbacks)) { this.engine.canvas.addEventListener(k, v); } } } onDeactivate() { if (this.space == UISpace.World) { const colliderObject = this.object.findByNameDirect('UIColliderObject')[0]; if (!colliderObject) return; const target = colliderObject.getComponent(CursorTarget); if (!target) return; // FIXME: We might be able to just deactivate the target here instead? target.onClick.remove('onClick'); target.onMove.remove('onMove'); target.onUp.remove('onUp'); target.onDown.remove('onDown'); } else { if (!this._viewComponent) return; const canvas = this.engine.canvas; for (const [k, v] of Object.entries(this.callbacks)) { canvas.removeEventListener(k, v); } } } onDestroy() { this.renderer?.unmountRoot(); } forEachElementUnderneath(node, x, y, callback) { if (node === null) return null; const t = node.node.getComputedTop(); const l = node.node.getComputedLeft(); const w = node.node.getComputedWidth(); const h = node.node.getComputedHeight(); const inside = !(x > l + w || x < l || y > t + h || y < t); let target = null; if (inside) { node.hovering[this.curGen] = true; if (callback(node)) target = node; } for (let n of node.children) { target = this.forEachElementUnderneath(n, x - l, y - t, callback) ?? target; } return target; } emitEvent(eventName, x, y, e) { if (!this.ctx?.root) return null; const target = this.forEachElementUnderneath(this.ctx.root, x, y, (w) => { const event = w?.props[eventName]; if (event !== undefined) { event({ x, y, e }); return true; } return false; }); if (DEBUG_EVENTS) { debug(eventName, `{${x}, ${y}}`, target); this.ctx.printTree(); } return target; } /** 'pointermove' event listener */ curGen = 0; onPointerMove(e) { /* Don't care about secondary pointers */ if (!e.isPrimary) return null; const x = this._dpiAdjust(e.clientX); const y = this._dpiAdjust(e.clientY); this.onMove({ x, y, e }); } onMove = ({ x, y, e }) => { if (!this.ctx) return null; const cur = (this.curGen = this.curGen ^ 0x1); /* Clear hovering flag */ this.ctx.wrappers.forEach((w) => (w.hovering[cur] = false)); const target = this.emitEvent('onMove', x, y, e); this.updateHoverState(x, y, e, target); }; updateHoverState(x, y, e, node) { const cur = this.curGen; const other = cur ^ 0x1; while (node) { node.hovering[cur] = true; node = node.parent; } this.ctx.wrappers.forEach((w) => { const hovering = w.hovering[cur]; if (hovering != w.hovering[other]) { const event = hovering ? w?.props.onHover : w?.props.onUnhover; if (event !== undefined) { event({ x, y, e }); return true; } } }); } /** 'click' event listener */ onClick = (e) => { const t = this.emitEvent('onClick', e.x, e.y, e.e); return t; }; onDown = (e) => { const t = this.emitEvent('onDown', e.x, e.y, e.e); return t; }; onUp = (e) => { const t = this.emitEvent('onUp', e.x, e.y, e.e); return t; }; onPointerClick(e) { const x = this._dpiAdjust(e.clientX); const y = this._dpiAdjust(e.clientY); this.onClick({ x, y, e }); } /** 'pointerdown' event listener */ onPointerDown(e) { /* Don't care about secondary pointers or non-left clicks */ if (!e.isPrimary || e.button !== 0) return null; const x = this._dpiAdjust(e.clientX); const y = this._dpiAdjust(e.clientY); return this.onDown({ x, y, e }); } /** 'pointerup' event listener */ onPointerUp(e) { /* Don't care about secondary pointers or non-left clicks */ if (!e.isPrimary || e.button !== 0) return null; const x = this._dpiAdjust(e.clientX); const y = this._dpiAdjust(e.clientY); return this.onUp({ x, y, e }); } renderCallback() { // FIXME: Never called this.needsUpdate = true; } _dpiAdjust(value) { return value * this.pixelSizeAdjustment * this.dpr; } } __decorate([ property.enum(['world', 'screen']) ], ReactUiBase.prototype, "space", void 0); __decorate([ property.material({ required: true }) ], ReactUiBase.prototype, "textMaterial", void 0); __decorate([ property.material({ required: true }) ], ReactUiBase.prototype, "panelMaterial", void 0); __decorate([ property.material({ required: true }) ], ReactUiBase.prototype, "panelMaterialTextured", void 0); __decorate([ property.int(100) ], ReactUiBase.prototype, "width", void 0); __decorate([ property.int(100) ], ReactUiBase.prototype, "height", void 0); __decorate([ property.enum(Object.keys(ScalingType).filter((e) => isNaN(Number(e))), ScalingType.Absolute) ], ReactUiBase.prototype, "scalingMode", void 0); __decorate([ property.float(1080) ], ReactUiBase.prototype, "manualHeight", void 0); __decorate([ property.float(1080) ], ReactUiBase.prototype, "manualWidth", void 0); let yogaInitializationPromise = null; export async function initializeRenderer() { if (!Y) { if (!yogaInitializationPromise) { yogaInitializationPromise = loadYoga().then((loadedYoga) => { Y = loadedYoga; }); } await yogaInitializationPromise; } return { rootContainer: null, unmountRoot() { reconcilerInstance.updateContainer(null, this.rootContainer); }, render(element, reactComp, callback) { const container = reconcilerInstance.createContainer(new Context(reactComp), 0, null, false, null, 'root', (e) => console.error(e), null); this.rootContainer = container; reactComp.setContext(container.containerInfo); const parentComponent = null; reconcilerInstance.updateContainer(element, container, parentComponent, reactComp.renderCallback.bind(reactComp)); reconcilerInstance.injectIntoDevTools({ bundleType: 0, version: '0.2.1', rendererPackageName: 'wonderlandengine/react-ui', }); }, }; }