UNPKG

@dcl/react-ecs

Version:
272 lines (271 loc) • 12.2 kB
import * as components from '@dcl/ecs/dist/components'; import Reconciler from 'react-reconciler'; import { isListener } from '../components'; import { CANVAS_ROOT_ENTITY } from '../components/uiTransform'; import { componentKeys, isNotUndefined, noopConfig, propsChanged } from './utils'; function getPointerEnum(pointerKey) { const pointers = { onMouseDown: 1 /* PointerEventType.PET_DOWN */, onMouseUp: 0 /* PointerEventType.PET_UP */, onMouseEnter: 2 /* PointerEventType.PET_HOVER_ENTER */, onMouseLeave: 3 /* PointerEventType.PET_HOVER_LEAVE */ }; return pointers[pointerKey]; } export function createReconciler(engine, pointerEvents) { // Store all the entities so when we destroy the UI we can also destroy them const entities = new Set(); // Store the onChange callbacks to be runned every time a Result has changed const changeEvents = new Map(); const clickEvents = new Map(); // Initialize components const UiTransform = components.UiTransform(engine); const UiText = components.UiText(engine); const UiBackground = components.UiBackground(engine); const UiInput = components.UiInput(engine); const UiInputResult = components.UiInputResult(engine); const UiDropdown = components.UiDropdown(engine); const UiDropdownResult = components.UiDropdownResult(engine); // Component ID Helper const getComponentId = { uiTransform: UiTransform.componentId, uiText: UiText.componentId, uiBackground: UiBackground.componentId, uiInput: UiInput.componentId, uiDropdown: UiDropdown.componentId }; function pointerEventCallback(entity, pointerEvent) { const callback = clickEvents.get(entity)?.get(pointerEvent); if (callback) callback(); return; } function updateTree(instance, props) { upsertComponent(instance, props, 'uiTransform'); } function upsertListener(instance, update) { if (update.type === 'delete' || !update.props) { clickEvents.get(instance.entity)?.delete(getPointerEnum(update.component)); if (update.component === 'onMouseDown') { pointerEvents.removeOnPointerDown(instance.entity); } else if (update.component === 'onMouseUp') { pointerEvents.removeOnPointerUp(instance.entity); } else if (update.component === 'onMouseEnter') { pointerEvents.removeOnPointerHoverEnter(instance.entity); } else if (update.component === 'onMouseLeave') { pointerEvents.removeOnPointerHoverLeave(instance.entity); } return; } if (update.props) { const pointerEvent = getPointerEnum(update.component); const entityEvent = clickEvents.get(instance.entity) || clickEvents.set(instance.entity, new Map()).get(instance.entity); const alreadyHasPointerEvent = entityEvent.get(pointerEvent); entityEvent.set(pointerEvent, update.props); if (alreadyHasPointerEvent) return; const pointerEventSystem = update.component === 'onMouseDown' ? pointerEvents.onPointerDown : update.component === 'onMouseUp' ? pointerEvents.onPointerUp : update.component === 'onMouseEnter' ? pointerEvents.onPointerHoverEnter : update.component === 'onMouseLeave' && pointerEvents.onPointerHoverLeave; if (pointerEventSystem) { pointerEventSystem({ entity: instance.entity, opts: { button: 0 /* InputAction.IA_POINTER */, // We add this showFeedBack so the pointerEventSystem creates a PointerEvent component with our entity // This is needed for the renderer to know which entities are clickeables showFeedback: true } }, () => pointerEventCallback(instance.entity, pointerEvent)); } } } function removeComponent(instance, component) { const componentId = getComponentId[component]; const Component = engine.getComponent(componentId); Component.deleteFrom(instance.entity); } function upsertComponent(instance, props = {}, componentName) { const componentId = getComponentId[componentName]; const onChangeExists = 'onChange' in props; const onSubmitExists = 'onSubmit' in props; const entityState = changeEvents.get(instance.entity)?.get(componentId); const onChange = onChangeExists ? props['onChange'] : entityState?.onChangeCallback; const onSubmit = onSubmitExists ? props['onSubmit'] : entityState?.onSubmitCallback; if (onChangeExists || onSubmitExists) { updateOnChange(instance.entity, componentId, { onChangeCallback: onChange, onSubmitCallback: onSubmit }); delete props.onChange; delete props.onSubmit; } // We check if there is any key pending to be changed to avoid updating the existing component if (!Object.keys(props).length) { return; } const ComponentDef = engine.getComponent(componentId); const component = ComponentDef.getMutableOrNull(instance.entity) || ComponentDef.create(instance.entity); for (const key in props) { const keyProp = key; component[keyProp] = props[keyProp]; } } function removeChildEntity(instance) { changeEvents.delete(instance.entity); clickEvents.delete(instance.entity); engine.removeEntity(instance.entity); for (const child of instance._child) { removeChildEntity(child); } } function appendChild(parent, child) { if (!child || !Object.keys(parent).length) return; const isReorder = parent._child.find((c) => c.entity === child.entity); // If its a reorder its seems that its a mutation of an array with key prop // We need to move the child to the end of the array // And update the order of the parent_.child array // child.rightOf => Latest entity of the array // childThatWasAtRightOfEntity = childEntity.rightOf if (isReorder) { const rightOfChild = parent._child.find((c) => c.rightOf === child.entity); if (rightOfChild) { rightOfChild.rightOf = child.rightOf; // Re-order parent._child array parent._child = parent._child.filter((c) => c.entity !== child.entity); parent._child.push(child); updateTree(rightOfChild, { rightOf: rightOfChild.rightOf }); } // Its a re-order. We are the last element, so we need to fetch the element before us. child.rightOf = parent._child[parent._child.length - 2]?.entity; } else { // Its an append. Put it at the end child.rightOf = parent._child[parent._child.length - 1]?.entity; parent._child.push(child); } child.parent = parent.entity; updateTree(child, { rightOf: child.rightOf, parent: parent.entity }); } function removeChild(parentInstance, child) { const childIndex = parentInstance._child.findIndex((c) => c.entity === child.entity); const childToModify = parentInstance._child[childIndex + 1]; if (childToModify) { childToModify.rightOf = child.rightOf; updateTree(childToModify, { rightOf: child.rightOf }); } // Mutate 💀 parentInstance._child.splice(childIndex, 1); removeChildEntity(child); } function updateOnChange(entity, componentId, state) { const hasEvent = changeEvents.has(entity); const event = changeEvents.get(entity) || changeEvents.set(entity, new Map()).get(entity); const onChangeCallback = state?.onChangeCallback; const onSubmitCallback = state?.onSubmitCallback; event.set(componentId, { onChangeCallback, onSubmitCallback }); // Create onChange callback if its the first callback event for this entity if (!hasEvent) { const resultComponentId = componentId === UiDropdown.componentId ? UiDropdownResult.componentId : UiInputResult.componentId; engine.getComponent(resultComponentId).onChange(entity, (value) => { if (value?.isSubmit) { const onSubmit = changeEvents.get(entity)?.get(componentId)?.onSubmitCallback; onSubmit && onSubmit(value?.value); } const onChange = changeEvents.get(entity)?.get(componentId)?.onChangeCallback; onChange && onChange(value?.value); }); } } const hostConfig = { ...noopConfig, createInstance(type, props) { const entity = engine.addEntity(); entities.add(entity); const instance = { entity, _child: [], parent: CANVAS_ROOT_ENTITY, rightOf: undefined }; for (const key in props) { const keyTyped = key; if (keyTyped === 'children' || keyTyped === 'key') { continue; } if (isListener(keyTyped)) { upsertListener(instance, { type: 'add', props: props[keyTyped], component: keyTyped }); } else { upsertComponent(instance, props[keyTyped], keyTyped); } } return instance; }, appendChild, appendChildToContainer: appendChild, appendInitialChild: appendChild, removeChild: removeChild, prepareUpdate(_instance, _type, oldProps, newProps) { return componentKeys .map((component) => propsChanged(component, oldProps[component], newProps[component])) .filter(isNotUndefined); }, commitUpdate(instance, updatePayload, _type, _prevProps, _nextProps, _internalHandle) { for (const update of updatePayload) { if (isListener(update.component)) { upsertListener(instance, update); continue; } if (update.type === 'delete') { removeComponent(instance, update.component); } else if (update.props) { upsertComponent(instance, update.props, update.component); } } }, insertBefore(parentInstance, child, beforeChild) { const beforeChildIndex = parentInstance._child.findIndex((c) => c.entity === beforeChild.entity); parentInstance._child = [ ...parentInstance._child.slice(0, beforeChildIndex), child, ...parentInstance._child.slice(beforeChildIndex) ]; child.rightOf = beforeChild.rightOf; beforeChild.rightOf = child.entity; child.parent = parentInstance.entity; updateTree(child, { rightOf: child.rightOf, parent: child.parent }); updateTree(beforeChild, { rightOf: beforeChild.rightOf }); }, removeChildFromContainer(parenInstance, child) { removeChildEntity(child); } }; const reconciler = Reconciler(hostConfig); const root = reconciler.createContainer({}, 0, null, false, null, '', /* istanbul ignore next */ function () { }, null); return { update: function (component) { return reconciler.updateContainer(component, root, null); }, getEntities: () => Array.from(entities) }; }