@dcl/react-ecs 7.28.1-34955459026.commit-7db2d46 → 7.29.1-34986384248.commit-bb45080

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,7 +46,10 @@ export function createReconciler(engine, pointerEvents) {
46
46
  return;
47
47
  }
48
48
  function updateTree(instance, props) {
49
- upsertComponent(instance, props, 'uiTransform');
49
+ // `rightOf` is a required int32 where 0 means "first sibling". A missing left
50
+ // neighbour is written as 0 so the first child of any parent (the canvas root
51
+ // included) serializes the same way as an explicit `rightOf: 0`.
52
+ upsertComponent(instance, { ...props, rightOf: props.rightOf ?? 0 }, 'uiTransform');
50
53
  }
51
54
  function upsertListener(instance, update) {
52
55
  if (update.type === 'delete' || !update.props) {
@@ -147,8 +150,6 @@ export function createReconciler(engine, pointerEvents) {
147
150
  }
148
151
  }
149
152
  function appendChild(parent, child) {
150
- if (!child || !Object.keys(parent).length)
151
- return;
152
153
  const isReorder = parent._child.find((c) => c.entity === child.entity);
153
154
  // If its a reorder its seems that its a mutation of an array with key prop
154
155
  // We need to move the child to the end of the array
@@ -191,6 +192,36 @@ export function createReconciler(engine, pointerEvents) {
191
192
  }
192
193
  removeChildEntity(child);
193
194
  }
195
+ function insertBefore(parentInstance, child, beforeChild) {
196
+ // Handle reorder: if child already exists in this parent, remove it from its old position
197
+ // and fix up the old neighbor's rightOf before inserting at the new position.
198
+ const existingIndex = parentInstance._child.findIndex((c) => c.entity === child.entity);
199
+ if (existingIndex !== -1) {
200
+ // If beforeChild.rightOf already points to child, child is already in the correct
201
+ // position in the rightOf chain. Setting child.rightOf = beforeChild.rightOf would
202
+ // produce a self-cycle (child.rightOf = child.entity).
203
+ if (beforeChild.rightOf === child.entity) {
204
+ return;
205
+ }
206
+ const oldNextSibling = parentInstance._child[existingIndex + 1];
207
+ if (oldNextSibling) {
208
+ oldNextSibling.rightOf = child.rightOf;
209
+ updateTree(oldNextSibling, { rightOf: oldNextSibling.rightOf });
210
+ }
211
+ parentInstance._child.splice(existingIndex, 1);
212
+ }
213
+ const beforeChildIndex = parentInstance._child.findIndex((c) => c.entity === beforeChild.entity);
214
+ parentInstance._child = [
215
+ ...parentInstance._child.slice(0, beforeChildIndex),
216
+ child,
217
+ ...parentInstance._child.slice(beforeChildIndex)
218
+ ];
219
+ child.rightOf = beforeChild.rightOf;
220
+ beforeChild.rightOf = child.entity;
221
+ child.parent = parentInstance.entity;
222
+ updateTree(child, { rightOf: child.rightOf, parent: child.parent });
223
+ updateTree(beforeChild, { rightOf: beforeChild.rightOf });
224
+ }
194
225
  function updateOnChange(entity, componentId, state) {
195
226
  const hasEvent = changeEvents.has(entity);
196
227
  const event = changeEvents.get(entity) || changeEvents.set(entity, new Map()).get(entity);
@@ -245,6 +276,7 @@ export function createReconciler(engine, pointerEvents) {
245
276
  appendChild,
246
277
  appendChildToContainer: appendChild,
247
278
  appendInitialChild: appendChild,
279
+ insertInContainerBefore: insertBefore,
248
280
  removeChild: removeChild,
249
281
  prepareUpdate(_instance, _type, oldProps, newProps) {
250
282
  return componentKeys
@@ -265,42 +297,20 @@ export function createReconciler(engine, pointerEvents) {
265
297
  }
266
298
  }
267
299
  },
268
- insertBefore(parentInstance, child, beforeChild) {
269
- // Handle reorder: if child already exists in this parent, remove it from its old position
270
- // and fix up the old neighbor's rightOf before inserting at the new position.
271
- const existingIndex = parentInstance._child.findIndex((c) => c.entity === child.entity);
272
- if (existingIndex !== -1) {
273
- // If beforeChild.rightOf already points to child, child is already in the correct
274
- // position in the rightOf chain. Setting child.rightOf = beforeChild.rightOf would
275
- // produce a self-cycle (child.rightOf = child.entity).
276
- if (beforeChild.rightOf === child.entity) {
277
- return;
278
- }
279
- const oldNextSibling = parentInstance._child[existingIndex + 1];
280
- if (oldNextSibling) {
281
- oldNextSibling.rightOf = child.rightOf;
282
- updateTree(oldNextSibling, { rightOf: oldNextSibling.rightOf });
283
- }
284
- parentInstance._child.splice(existingIndex, 1);
285
- }
286
- const beforeChildIndex = parentInstance._child.findIndex((c) => c.entity === beforeChild.entity);
287
- parentInstance._child = [
288
- ...parentInstance._child.slice(0, beforeChildIndex),
289
- child,
290
- ...parentInstance._child.slice(beforeChildIndex)
291
- ];
292
- child.rightOf = beforeChild.rightOf;
293
- beforeChild.rightOf = child.entity;
294
- child.parent = parentInstance.entity;
295
- updateTree(child, { rightOf: child.rightOf, parent: child.parent });
296
- updateTree(beforeChild, { rightOf: beforeChild.rightOf });
297
- },
298
- removeChildFromContainer(parenInstance, child) {
299
- removeChildEntity(child);
300
- }
300
+ insertBefore,
301
+ removeChildFromContainer: removeChild
301
302
  };
302
303
  const reconciler = Reconciler(hostConfig);
303
- const root = reconciler.createContainer({}, 0, null, false, null, '',
304
+ // The container is a real Instance standing in for the canvas root, so
305
+ // top-level elements get the same parent/rightOf bookkeeping as any other
306
+ // sibling group. It is never rendered itself.
307
+ const containerInstance = {
308
+ entity: CANVAS_ROOT_ENTITY,
309
+ _child: [],
310
+ parent: undefined,
311
+ rightOf: undefined
312
+ };
313
+ const root = reconciler.createContainer(containerInstance, 0, null, false, null, '',
304
314
  /* istanbul ignore next */
305
315
  function () { }, null);
306
316
  return {
package/dist/system.d.ts CHANGED
@@ -27,6 +27,16 @@ export type UiRendererOptions = {
27
27
  * can use different insets simultaneously.
28
28
  */
29
29
  screenInset?: UiScreenInset;
30
+ /**
31
+ * Stacking order of this renderer's UI relative to the other renderers (the main
32
+ * one and every `addUiRenderer`). Higher values render in front. When omitted,
33
+ * renderers stack in the order they first render, later ones on top; among those
34
+ * first rendered in the same tick the main UI goes at the back.
35
+ *
36
+ * It is applied to the renderer's root container entity, so it only orders whole
37
+ * renderers against each other; elements inside a renderer keep their own `zIndex`.
38
+ */
39
+ zIndex?: number;
30
40
  };
31
41
  /**
32
42
  * @public
package/dist/system.js CHANGED
@@ -3,7 +3,7 @@ import * as ecsComponents from '@dcl/ecs/dist/components';
3
3
  import React from 'react';
4
4
  import { createReconciler } from './reconciler';
5
5
  import { getUiScaleFactor, resetInteractableArea, resetScreenInsetArea, resetUiScaleFactor, setInteractableArea, setScreenInsetArea, setUiScaleFactor } from './components/utils';
6
- import { InteractableArea, ScreenInsetArea } from './components';
6
+ import { InteractableArea, ScreenInsetArea, UiEntity } from './components';
7
7
  import { isMobile } from './platform';
8
8
  /**
9
9
  * Default virtual screen size used on mobile platforms, and the size 16:9
@@ -47,6 +47,14 @@ export function createReactBasedUiSystem(engine, pointerSystem) {
47
47
  let uiComponent = undefined;
48
48
  let mainOptions = undefined;
49
49
  const additionalRenderers = new Map();
50
+ // Renderers are laid out in the order they first render, later ones on top, with
51
+ // the main UI first among those first rendered in the same tick. That is the order
52
+ // their entities were created in before the stacking was explicit, so scenes that
53
+ // set no zIndex keep the stacking they had. A renderer gets its sequence number the
54
+ // first time it renders and keeps it when replaced; the Map iterates in insertion
55
+ // order, so it is already sorted by `order` and only the main UI has to be slotted in.
56
+ let nextOrder = 0;
57
+ let mainOrder = undefined;
50
58
  const UiCanvasInformation = ecsComponents.UiCanvasInformation(engine);
51
59
  // Unique owner to prevent other UI systems resetting this scale factor.
52
60
  const uiScaleFactorOwner = Symbol('react-ecs-ui-scale');
@@ -119,37 +127,58 @@ export function createReactBasedUiSystem(engine, pointerSystem) {
119
127
  /**
120
128
  * Wraps a renderer's component in a container positioned within the selected
121
129
  * screen inset area. `'none'` adds no wrapper, leaving the UI on the whole
122
- * screen. Applied per renderer, so each renderer can use a different inset.
130
+ * screen, unless a `zIndex` needs a root entity to live on. Applied per
131
+ * renderer, so each renderer can use a different inset.
123
132
  */
124
- function wrapWithScreenInset(ui, inset, key) {
133
+ function wrapWithScreenInset(ui, options, key) {
134
+ const zIndex = options?.zIndex;
135
+ // Only forward a provided zIndex: `{ zIndex: undefined }` would override the
136
+ // transform parser default and change what is written for every renderer.
137
+ const wrapperProps = zIndex === undefined ? { key } : { key, uiTransform: { zIndex } };
125
138
  // An omitted inset is resolved to DEFAULT_SCREEN_INSET ('device') before the
126
139
  // switch, so it lands on `case 'device'`. Only an explicit 'none' reaches the
127
140
  // `default` clause below — the two are unrelated despite sharing a name.
128
- switch (inset ?? DEFAULT_SCREEN_INSET) {
141
+ switch (options?.screenInset ?? DEFAULT_SCREEN_INSET) {
129
142
  case 'device':
130
- return React.createElement(ScreenInsetArea, { key }, React.createElement(ui));
143
+ return React.createElement(ScreenInsetArea, wrapperProps, React.createElement(ui));
131
144
  case 'interactable':
132
- return React.createElement(InteractableArea, { key }, React.createElement(ui));
145
+ return React.createElement(InteractableArea, wrapperProps, React.createElement(ui));
133
146
  // 'none' — the whole screen, no wrapper entity
134
147
  default:
135
- return React.createElement(ui, { key });
148
+ if (zIndex === undefined)
149
+ return React.createElement(ui, { key });
150
+ // A whole-screen container: absolutely positioned on every edge so it
151
+ // fills the canvas, with the zIndex the renderer is stacked by.
152
+ return React.createElement(UiEntity, {
153
+ key,
154
+ uiTransform: { positionType: 'absolute', position: { top: 0, left: 0, right: 0, bottom: 0 }, zIndex }
155
+ }, React.createElement(ui));
136
156
  }
137
157
  }
138
158
  function ReactBasedUiSystem() {
139
159
  const components = [];
140
- // Add main UI component
141
- if (uiComponent) {
142
- components.push(wrapWithScreenInset(uiComponent, mainOptions?.screenInset, '__main__'));
143
- }
160
+ if (uiComponent !== undefined && mainOrder === undefined)
161
+ mainOrder = nextOrder++;
162
+ let mainPushed = uiComponent === undefined;
144
163
  const entitiesToRemove = [];
145
164
  for (const [entity, entry] of additionalRenderers) {
146
165
  // Check for entity-based cleanup
147
166
  if (engine.getEntityState(entity) === EntityState.Removed) {
148
167
  entitiesToRemove.push(entity);
168
+ continue;
149
169
  }
150
- else {
151
- components.push(wrapWithScreenInset(entry.ui, entry.options?.screenInset, `__entity_${entity}__`));
170
+ if (entry.order === undefined)
171
+ entry.order = nextOrder++;
172
+ // The main UI goes right before the first renderer that rendered after it.
173
+ if (!mainPushed && mainOrder < entry.order) {
174
+ components.push(wrapWithScreenInset(uiComponent, mainOptions, '__main__'));
175
+ mainPushed = true;
152
176
  }
177
+ components.push(wrapWithScreenInset(entry.ui, entry.options, `__entity_${entity}__`));
178
+ }
179
+ // Main UI first rendered after every other renderer (or alone): it goes on top.
180
+ if (!mainPushed) {
181
+ components.push(wrapWithScreenInset(uiComponent, mainOptions, '__main__'));
153
182
  }
154
183
  // Entity-based cleanup
155
184
  for (const entity of entitiesToRemove) {
@@ -225,7 +254,8 @@ export function createReactBasedUiSystem(engine, pointerSystem) {
225
254
  mainOptions = options;
226
255
  },
227
256
  addUiRenderer(entity, ui, options) {
228
- additionalRenderers.set(entity, { ui, options });
257
+ // A replacement keeps its place in the stacking order.
258
+ additionalRenderers.set(entity, { ui, options, order: additionalRenderers.get(entity)?.order });
229
259
  },
230
260
  removeUiRenderer(entity) {
231
261
  additionalRenderers.delete(entity);
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@dcl/react-ecs",
3
3
  "description": "Decentraland ECS",
4
- "version": "7.28.1-34955459026.commit-7db2d46",
4
+ "version": "7.29.1-34986384248.commit-bb45080",
5
5
  "author": "DCL",
6
6
  "bugs": "https://github.com/decentraland/js-sdk-toolchain/issues",
7
7
  "dependencies": {
8
- "@dcl/ecs": "7.28.1-34955459026.commit-7db2d46",
8
+ "@dcl/ecs": "7.29.1-34986384248.commit-bb45080",
9
9
  "react": "^18.2.0",
10
10
  "react-reconciler": "^0.29.0"
11
11
  },
@@ -40,5 +40,5 @@
40
40
  "tsconfig": "./tsconfig.json"
41
41
  },
42
42
  "types": "./dist/index.d.ts",
43
- "commit": "7db2d4613572a8d72af9166b3df228a3b8f8c427"
43
+ "commit": "bb450800d3d88bfed04e25cca38b2c833eb3cd53"
44
44
  }