@dcl/react-ecs
Version:
Decentraland ECS
272 lines (271 loc) • 12.2 kB
JavaScript
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)
};
}