@dcl/react-ecs
Version:
Decentraland ECS
117 lines (116 loc) • 5.07 kB
JavaScript
import { EntityState } from '@dcl/ecs';
import * as ecsComponents from '@dcl/ecs/dist/components';
import React from 'react';
import { createReconciler } from './reconciler';
import { getUiScaleFactor, resetInteractableArea, resetScreenInsetArea, resetUiScaleFactor, setInteractableArea, setScreenInsetArea, setUiScaleFactor } from './components/utils';
/**
* @public
*/
export function createReactBasedUiSystem(engine, pointerSystem) {
const renderer = createReconciler(engine, pointerSystem);
let uiComponent = undefined;
let virtualSize = undefined;
const additionalRenderers = new Map();
const UiCanvasInformation = ecsComponents.UiCanvasInformation(engine);
// Unique owner to prevent other UI systems resetting this scale factor.
const uiScaleFactorOwner = Symbol('react-ecs-ui-scale');
// Unique owner for the screen inset module variable.
const screenInsetAreaOwner = Symbol('react-ecs-screen-inset-area');
// Unique owner for the interactable area module variable.
const interactableAreaOwner = Symbol('react-ecs-interactable-area');
function getActiveVirtualSize() {
// Main renderer options win; otherwise use the first additional renderer option.
if (virtualSize)
return virtualSize;
for (const entry of additionalRenderers.values()) {
if (entry.options)
return entry.options;
}
return undefined;
}
function ReactBasedUiSystem() {
const components = [];
// Add main UI component
if (uiComponent) {
components.push(React.createElement(uiComponent, { key: '__main__' }));
}
const entitiesToRemove = [];
for (const [entity, entry] of additionalRenderers) {
// Check for entity-based cleanup
if (engine.getEntityState(entity) === EntityState.Removed) {
entitiesToRemove.push(entity);
}
else {
components.push(React.createElement(entry.ui, { key: `__entity_${entity}__` }));
}
}
// Entity-based cleanup
for (const entity of entitiesToRemove) {
additionalRenderers.delete(entity);
}
// Always update the renderer - pass null when empty to clear the UI
if (components.length > 0) {
renderer.update(React.createElement(React.Fragment, null, ...components));
}
else {
renderer.update(null);
}
}
function UiScaleSystem() {
const canvasInfo = UiCanvasInformation.getOrNull(engine.RootEntity);
// Update the screen inset module variable unconditionally — it is
// independent of the virtual size and useful even when the renderer has no
// virtual canvas.
if (canvasInfo?.screenInsetArea) {
setScreenInsetArea(canvasInfo.screenInsetArea, screenInsetAreaOwner);
}
// Update the interactable area module variable unconditionally.
if (canvasInfo?.interactableArea) {
setInteractableArea(canvasInfo.interactableArea, interactableAreaOwner);
}
const activeVirtualSize = getActiveVirtualSize();
if (!activeVirtualSize) {
// Reset only if this system owns the scale factor.
resetUiScaleFactor(uiScaleFactorOwner);
return;
}
if (!canvasInfo)
return;
const { width, height, devicePixelRatio } = canvasInfo;
const { virtualWidth, virtualHeight } = activeVirtualSize;
if (!virtualWidth || !virtualHeight)
return;
// Normalize by devicePixelRatio so virtual px map to logical px (matching the
// vw/vh path); without it the scale was inflated on high-dpr mobile screens.
const ratio = devicePixelRatio || 1;
const nextScale = Math.min(width / virtualWidth, height / virtualHeight) / ratio;
if (Number.isFinite(nextScale) && nextScale !== getUiScaleFactor()) {
// Track ownership when updating to avoid cross-system conflicts.
setUiScaleFactor(nextScale, uiScaleFactorOwner);
}
}
engine.addSystem(UiScaleSystem, 100e3 + 1, '@dcl/react-ecs-ui-scale');
engine.addSystem(ReactBasedUiSystem, 100e3, '@dcl/react-ecs');
return {
destroy() {
engine.removeSystem(UiScaleSystem);
engine.removeSystem(ReactBasedUiSystem);
resetUiScaleFactor(uiScaleFactorOwner);
resetScreenInsetArea(screenInsetAreaOwner);
resetInteractableArea(interactableAreaOwner);
for (const entity of renderer.getEntities()) {
engine.removeEntity(entity);
}
},
setUiRenderer(ui, options) {
uiComponent = ui;
virtualSize = options;
},
addUiRenderer(entity, ui, options) {
additionalRenderers.set(entity, { ui, options });
},
removeUiRenderer(entity) {
additionalRenderers.delete(entity);
}
};
}