@dcl/react-ecs 7.29.1-35862852104.commit-1f44e80 → 7.29.1-35901015152.commit-d1b7b96

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.
@@ -29,7 +29,7 @@ function getButtonProps(props) {
29
29
  */
30
30
  /* @__PURE__ */
31
31
  export function Button(props) {
32
- const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...otherProps } = props;
32
+ const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onMouseDrag, onMouseDragLocked, onMouseDragEnd, ...otherProps } = props;
33
33
  const buttonProps = getButtonProps(props);
34
34
  const uiBackgroundProps = parseUiBackground({
35
35
  ...buttonProps.uiBackground,
@@ -58,5 +58,5 @@ export function Button(props) {
58
58
  uiBackgroundProps.color = { ...uiBackgroundProps.color, a: uiBackgroundProps.color.a / 2 };
59
59
  }
60
60
  }
61
- return (ReactEcs.createElement("entity", { onMouseDown: !!props.disabled ? undefined : onMouseDown, onMouseUp: !!props.disabled ? undefined : onMouseUp, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, uiTransform: uiTransformProps, uiText: textProps, uiBackground: uiBackgroundProps }));
61
+ return (ReactEcs.createElement("entity", { onMouseDown: !!props.disabled ? undefined : onMouseDown, onMouseUp: !!props.disabled ? undefined : onMouseUp, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onMouseDrag: onMouseDrag, onMouseDragLocked: onMouseDragLocked, onMouseDragEnd: onMouseDragEnd, uiTransform: uiTransformProps, uiText: textProps, uiBackground: uiBackgroundProps }));
62
62
  }
@@ -34,7 +34,7 @@ function parseUiDropdown(props) {
34
34
  */
35
35
  /* @__PURE__ */
36
36
  export function Dropdown(props) {
37
- const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...otherProps } = props;
37
+ const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onMouseDrag, onMouseDragLocked, onMouseDragEnd, ...otherProps } = props;
38
38
  const dropdownProps = parseUiDropdown(otherProps);
39
39
  const commonProps = parseProps({
40
40
  uiTransform,
@@ -42,7 +42,10 @@ export function Dropdown(props) {
42
42
  onMouseDown,
43
43
  onMouseUp,
44
44
  onMouseEnter,
45
- onMouseLeave
45
+ onMouseLeave,
46
+ onMouseDrag,
47
+ onMouseDragLocked,
48
+ onMouseDragEnd
46
49
  });
47
50
  return ReactEcs.createElement("entity", { ...commonProps, uiDropdown: dropdownProps });
48
51
  }
@@ -35,7 +35,7 @@ function parseUiInput(props) {
35
35
  * @category Component
36
36
  */ /* @__PURE__ */
37
37
  export function Input(props) {
38
- const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...otherProps } = props;
38
+ const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onMouseDrag, onMouseDragLocked, onMouseDragEnd, ...otherProps } = props;
39
39
  const inputProps = parseUiInput(otherProps);
40
40
  const commonProps = parseProps({
41
41
  uiTransform,
@@ -43,7 +43,10 @@ export function Input(props) {
43
43
  onMouseDown,
44
44
  onMouseUp,
45
45
  onMouseEnter,
46
- onMouseLeave
46
+ onMouseLeave,
47
+ onMouseDrag,
48
+ onMouseDragLocked,
49
+ onMouseDragEnd
47
50
  });
48
51
  return ReactEcs.createElement("entity", { ...commonProps, uiInput: inputProps });
49
52
  }
@@ -16,14 +16,17 @@ export { scaleFontSize } from './utils';
16
16
  */
17
17
  /* @__PURE__ */
18
18
  export function Label(props) {
19
- const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...uiTextProps } = props;
19
+ const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onMouseDrag, onMouseDragLocked, onMouseDragEnd, ...uiTextProps } = props;
20
20
  const commonProps = parseProps({
21
21
  uiTransform,
22
22
  uiBackground,
23
23
  onMouseDown,
24
24
  onMouseUp,
25
25
  onMouseEnter,
26
- onMouseLeave
26
+ onMouseLeave,
27
+ onMouseDrag,
28
+ onMouseDragLocked,
29
+ onMouseDragEnd
27
30
  });
28
31
  const { font, textAlign, fontSize, textWrap, ...textProps } = uiTextProps;
29
32
  const uiText = {
@@ -17,6 +17,8 @@ export interface UiLabelProps {
17
17
  font?: UiFontType | undefined;
18
18
  /** Behaviour when text reached. @defaultValue 'wrap' */
19
19
  textWrap?: UiTextWrapType | undefined;
20
+ /** A font file in the scene, or a Google Fonts family name; `font` is the fallback. @defaultValue undefined */
21
+ fontSrc?: string | undefined;
20
22
  }
21
23
  /**
22
24
  * @public
@@ -1,8 +1,10 @@
1
+ import { PBPointerEventsResult } from '@dcl/ecs';
1
2
  /**
2
- * Callback function to be triggered on a specified event
3
+ * Callback function to be triggered on a specified event. Receives the pointer event that
4
+ * fired it (button, hit, and for drags the pointer delta), which handlers may ignore.
3
5
  * @public
4
6
  */
5
- export type Callback = () => void;
7
+ export type Callback = (event: PBPointerEventsResult) => void;
6
8
  /**
7
9
  * User key event Listeners
8
10
  * @public
@@ -16,4 +18,10 @@ export type Listeners = {
16
18
  onMouseEnter?: Callback;
17
19
  /** triggered on mouse leave event */
18
20
  onMouseLeave?: Callback;
21
+ /** triggered while the pointer is dragged after pressing on the element */
22
+ onMouseDrag?: Callback;
23
+ /** triggered while the pointer is dragged with the cursor locked in place */
24
+ onMouseDragLocked?: Callback;
25
+ /** triggered when a drag that started on the element ends */
26
+ onMouseDragEnd?: Callback;
19
27
  };
@@ -2,7 +2,10 @@ const listeners = {
2
2
  onMouseDown: undefined,
3
3
  onMouseUp: undefined,
4
4
  onMouseEnter: undefined,
5
- onMouseLeave: undefined
5
+ onMouseLeave: undefined,
6
+ onMouseDrag: undefined,
7
+ onMouseDragLocked: undefined,
8
+ onMouseDragEnd: undefined
6
9
  };
7
10
  const listenersKey = Object.keys(listeners);
8
11
  /**
@@ -1,4 +1,4 @@
1
- import { BorderRect } from '@dcl/ecs';
1
+ import { BorderRect, Entity } from '@dcl/ecs';
2
2
  import { Color4 } from '@dcl/ecs/dist/components/generated/pb/decentraland/common/colors.gen';
3
3
  /**
4
4
  * @public
@@ -15,6 +15,8 @@ export interface UiBackgroundProps {
15
15
  uvs?: number[];
16
16
  /** AvatarTexture for the background */
17
17
  avatarTexture?: UiAvatarTexture;
18
+ /** VideoTexture for the background: the frames of a VideoPlayer on another entity */
19
+ videoTexture?: UiVideoTexture;
18
20
  /** Texture for the background */
19
21
  texture?: UiTexture;
20
22
  }
@@ -27,6 +29,15 @@ export interface UiAvatarTexture {
27
29
  wrapMode?: TextureWrapType;
28
30
  filterMode?: TextureFilterType;
29
31
  }
32
+ /**
33
+ * Video Texture
34
+ * @public
35
+ */
36
+ export interface UiVideoTexture {
37
+ videoPlayerEntity: Entity;
38
+ wrapMode?: TextureWrapType;
39
+ filterMode?: TextureFilterType;
40
+ }
30
41
  /**
31
42
  * Texture
32
43
  * @public
@@ -30,6 +30,14 @@ export function getTexture(props) {
30
30
  }
31
31
  };
32
32
  }
33
+ if (props.videoTexture) {
34
+ return {
35
+ tex: {
36
+ $case: 'videoTexture',
37
+ videoTexture: parseTexture(props.videoTexture)
38
+ }
39
+ };
40
+ }
33
41
  return undefined;
34
42
  }
35
43
  function parseTexture(texture) {
@@ -1,4 +1,4 @@
1
- import { getAlign, getDisplay, getFlexDirection, getFlexWrap, getJustify, getOverflow, getPointerFilter, getPositionType, parseBorderColor, parseBorderRadius, parseBorderWidth, parsePosition, parseSize } from './utils';
1
+ import { getAlign, getDisplay, getFlexDirection, getFlexWrap, getJustify, getOverflow, getPointerFilter, getPositionType, getScrollPosition, getScrollVisible, parseBorderColor, parseBorderRadius, parseBorderWidth, parsePosition, parseSize } from './utils';
2
2
  /**
3
3
  * @internal
4
4
  */
@@ -60,7 +60,7 @@ const defaultUiTransform = {
60
60
  */
61
61
  /* @__PURE__ */
62
62
  export function parseUiTransform(props = {}) {
63
- const { height, minHeight, maxHeight, width, minWidth, maxWidth, alignItems, alignContent, flexWrap, borderRadius, borderWidth, borderColor, ...otherProps } = props;
63
+ const { height, minHeight, maxHeight, width, minWidth, maxWidth, alignItems, alignContent, flexWrap, borderRadius, borderWidth, borderColor, scrollPosition, scrollVisible, ...otherProps } = props;
64
64
  return {
65
65
  ...defaultUiTransform,
66
66
  ...otherProps,
@@ -86,6 +86,8 @@ export function parseUiTransform(props = {}) {
86
86
  ...(flexWrap && getFlexWrap(flexWrap)),
87
87
  ...(borderRadius && parseBorderRadius(borderRadius)),
88
88
  ...(borderWidth && parseBorderWidth(borderWidth)),
89
- ...(borderColor && parseBorderColor(borderColor))
89
+ ...(borderColor && parseBorderColor(borderColor)),
90
+ ...(scrollPosition && getScrollPosition(scrollPosition)),
91
+ ...(scrollVisible && getScrollVisible(scrollVisible))
90
92
  };
91
93
  }
@@ -1,4 +1,5 @@
1
1
  import { Color4 } from '@dcl/ecs/dist/components/generated/pb/decentraland/common/colors.gen';
2
+ import { Vector2 } from '@dcl/ecs/dist/components/generated/pb/decentraland/common/vectors.gen';
2
3
  import { ScaleUnit } from '../types';
3
4
  /**
4
5
  * unit value specified. i.e. 1 || '100%' || '1px' || '10vw'
@@ -131,4 +132,15 @@ export interface UiTransformProps {
131
132
  opacity?: number;
132
133
  /** default 0 */
133
134
  zIndex?: number;
135
+ /** A reference id for the element, e.g. as a scrollPosition target or for SetUiFocus. default empty */
136
+ elementId?: string;
137
+ /** Scroll offset when overflow is 'scroll': a position, or the elementId of a child to scroll to. default (0, 0) */
138
+ scrollPosition?: Vector2 | string;
139
+ /** Which scrollbars to show when overflow is 'scroll'. default 'both' */
140
+ scrollVisible?: ScrollVisibleType;
134
141
  }
142
+ /**
143
+ * Which scrollbars are shown when the overflow is scrollable
144
+ * @public
145
+ */
146
+ export type ScrollVisibleType = 'horizontal' | 'vertical' | 'both' | 'hidden';
@@ -244,3 +244,24 @@ const parsePointerFilter = {
244
244
  none: 0 /* PointerFilterMode.PFM_NONE */,
245
245
  block: 1 /* PointerFilterMode.PFM_BLOCK */
246
246
  };
247
+ /**
248
+ * @internal
249
+ */
250
+ export function getScrollPosition(scrollPosition) {
251
+ if (typeof scrollPosition === 'string') {
252
+ return { scrollPosition: { value: { $case: 'reference', reference: scrollPosition } } };
253
+ }
254
+ return { scrollPosition: { value: { $case: 'position', position: scrollPosition } } };
255
+ }
256
+ const parseScrollVisible = {
257
+ both: 0 /* ShowScrollBar.SSB_BOTH */,
258
+ hidden: 3 /* ShowScrollBar.SSB_HIDDEN */,
259
+ horizontal: 2 /* ShowScrollBar.SSB_ONLY_HORIZONTAL */,
260
+ vertical: 1 /* ShowScrollBar.SSB_ONLY_VERTICAL */
261
+ };
262
+ /**
263
+ * @internal
264
+ */
265
+ export function getScrollVisible(scrollVisible) {
266
+ return { scrollVisible: parseScrollVisible[scrollVisible] };
267
+ }
@@ -23,6 +23,9 @@ export type EntityComponents = {
23
23
  onMouseUp: Callback;
24
24
  onMouseEnter: Callback;
25
25
  onMouseLeave: Callback;
26
+ onMouseDrag: Callback;
27
+ onMouseDragLocked: Callback;
28
+ onMouseDragEnd: Callback;
26
29
  };
27
30
  /**
28
31
  * @hidden
@@ -1,6 +1,6 @@
1
1
  import { Entity, IEngine, PointerEventsSystem } from '@dcl/ecs';
2
2
  import { ReactEcs } from '../react-ecs';
3
- export declare function createReconciler(engine: Pick<IEngine, 'getComponent' | 'addEntity' | 'removeEntity' | 'defineComponentFromSchema' | 'getEntitiesWith'>, pointerEvents: PointerEventsSystem): {
3
+ export declare function createReconciler(engine: Pick<IEngine, 'getComponent' | 'addEntity' | 'removeEntity' | 'defineComponentFromSchema' | 'getEntitiesWith'>, pointerEvents: PointerEventsSystem, rootEntity?: Entity): {
4
4
  update: (component: ReactEcs.JSX.ReactNode) => number;
5
5
  getEntities: () => Entity[];
6
6
  };
@@ -8,11 +8,17 @@ function getPointerEnum(pointerKey) {
8
8
  onMouseDown: 1 /* PointerEventType.PET_DOWN */,
9
9
  onMouseUp: 0 /* PointerEventType.PET_UP */,
10
10
  onMouseEnter: 2 /* PointerEventType.PET_HOVER_ENTER */,
11
- onMouseLeave: 3 /* PointerEventType.PET_HOVER_LEAVE */
11
+ onMouseLeave: 3 /* PointerEventType.PET_HOVER_LEAVE */,
12
+ onMouseDrag: 7 /* PointerEventType.PET_DRAG */,
13
+ onMouseDragLocked: 6 /* PointerEventType.PET_DRAG_LOCKED */,
14
+ onMouseDragEnd: 8 /* PointerEventType.PET_DRAG_END */
12
15
  };
13
16
  return pointers[pointerKey];
14
17
  }
15
- export function createReconciler(engine, pointerEvents) {
18
+ export function createReconciler(engine, pointerEvents,
19
+ // When set, every entity this reconciler creates is parented to it -- a UiCanvas on the root
20
+ // entity then renders the tree to a texture instead of the screen.
21
+ rootEntity) {
16
22
  // Store all the entities so when we destroy the UI we can also destroy them
17
23
  const entities = new Set();
18
24
  // Store the onChange callbacks to be runned every time a Result has changed
@@ -29,6 +35,7 @@ export function createReconciler(engine, pointerEvents) {
29
35
  const UiInputResult = components.UiInputResult(engine);
30
36
  const UiDropdown = components.UiDropdown(engine);
31
37
  const UiDropdownResult = components.UiDropdownResult(engine);
38
+ const Transform = components.Transform(engine);
32
39
  const UiInputBinding = components.UiInputBinding(engine);
33
40
  // Component ID Helper
34
41
  const getComponentId = {
@@ -39,17 +46,14 @@ export function createReconciler(engine, pointerEvents) {
39
46
  uiDropdown: UiDropdown.componentId,
40
47
  uiInputBinding: UiInputBinding.componentId
41
48
  };
42
- function pointerEventCallback(entity, pointerEvent) {
49
+ function pointerEventCallback(entity, pointerEvent, event) {
43
50
  const callback = clickEvents.get(entity)?.get(pointerEvent);
44
51
  if (callback)
45
- callback();
52
+ callback(event);
46
53
  return;
47
54
  }
48
55
  function updateTree(instance, props) {
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');
56
+ upsertComponent(instance, props, 'uiTransform');
53
57
  }
54
58
  function upsertListener(instance, update) {
55
59
  if (update.type === 'delete' || !update.props) {
@@ -66,6 +70,15 @@ export function createReconciler(engine, pointerEvents) {
66
70
  else if (update.component === 'onMouseLeave') {
67
71
  pointerEvents.removeOnPointerHoverLeave(instance.entity);
68
72
  }
73
+ else if (update.component === 'onMouseDrag') {
74
+ pointerEvents.removeOnPointerDrag(instance.entity);
75
+ }
76
+ else if (update.component === 'onMouseDragLocked') {
77
+ pointerEvents.removeOnPointerDragLocked(instance.entity);
78
+ }
79
+ else if (update.component === 'onMouseDragEnd') {
80
+ pointerEvents.removeOnPointerDragEnd(instance.entity);
81
+ }
69
82
  return;
70
83
  }
71
84
  if (update.props) {
@@ -81,7 +94,13 @@ export function createReconciler(engine, pointerEvents) {
81
94
  ? pointerEvents.onPointerUp
82
95
  : update.component === 'onMouseEnter'
83
96
  ? pointerEvents.onPointerHoverEnter
84
- : update.component === 'onMouseLeave' && pointerEvents.onPointerHoverLeave;
97
+ : update.component === 'onMouseLeave'
98
+ ? pointerEvents.onPointerHoverLeave
99
+ : update.component === 'onMouseDrag'
100
+ ? pointerEvents.onPointerDrag
101
+ : update.component === 'onMouseDragLocked'
102
+ ? pointerEvents.onPointerDragLocked
103
+ : update.component === 'onMouseDragEnd' && pointerEvents.onPointerDragEnd;
85
104
  if (pointerEventSystem) {
86
105
  pointerEventSystem({
87
106
  entity: instance.entity,
@@ -91,7 +110,7 @@ export function createReconciler(engine, pointerEvents) {
91
110
  // This is needed for the renderer to know which entities are clickeables
92
111
  showFeedback: true
93
112
  }
94
- }, () => pointerEventCallback(instance.entity, pointerEvent));
113
+ }, (event) => pointerEventCallback(instance.entity, pointerEvent, event));
95
114
  }
96
115
  }
97
116
  }
@@ -150,6 +169,8 @@ export function createReconciler(engine, pointerEvents) {
150
169
  }
151
170
  }
152
171
  function appendChild(parent, child) {
172
+ if (!child || !Object.keys(parent).length)
173
+ return;
153
174
  const isReorder = parent._child.find((c) => c.entity === child.entity);
154
175
  // If its a reorder its seems that its a mutation of an array with key prop
155
176
  // We need to move the child to the end of the array
@@ -192,36 +213,6 @@ export function createReconciler(engine, pointerEvents) {
192
213
  }
193
214
  removeChildEntity(child);
194
215
  }
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
- }
225
216
  function updateOnChange(entity, componentId, state) {
226
217
  const hasEvent = changeEvents.has(entity);
227
218
  const event = changeEvents.get(entity) || changeEvents.set(entity, new Map()).get(entity);
@@ -248,6 +239,9 @@ export function createReconciler(engine, pointerEvents) {
248
239
  ...noopConfig,
249
240
  createInstance(type, props) {
250
241
  const entity = engine.addEntity();
242
+ if (rootEntity !== undefined) {
243
+ Transform.createOrReplace(entity, { parent: rootEntity });
244
+ }
251
245
  entities.add(entity);
252
246
  const instance = {
253
247
  entity,
@@ -276,7 +270,6 @@ export function createReconciler(engine, pointerEvents) {
276
270
  appendChild,
277
271
  appendChildToContainer: appendChild,
278
272
  appendInitialChild: appendChild,
279
- insertInContainerBefore: insertBefore,
280
273
  removeChild: removeChild,
281
274
  prepareUpdate(_instance, _type, oldProps, newProps) {
282
275
  return componentKeys
@@ -297,20 +290,42 @@ export function createReconciler(engine, pointerEvents) {
297
290
  }
298
291
  }
299
292
  },
300
- insertBefore,
301
- removeChildFromContainer: removeChild
293
+ insertBefore(parentInstance, child, beforeChild) {
294
+ // Handle reorder: if child already exists in this parent, remove it from its old position
295
+ // and fix up the old neighbor's rightOf before inserting at the new position.
296
+ const existingIndex = parentInstance._child.findIndex((c) => c.entity === child.entity);
297
+ if (existingIndex !== -1) {
298
+ // If beforeChild.rightOf already points to child, child is already in the correct
299
+ // position in the rightOf chain. Setting child.rightOf = beforeChild.rightOf would
300
+ // produce a self-cycle (child.rightOf = child.entity).
301
+ if (beforeChild.rightOf === child.entity) {
302
+ return;
303
+ }
304
+ const oldNextSibling = parentInstance._child[existingIndex + 1];
305
+ if (oldNextSibling) {
306
+ oldNextSibling.rightOf = child.rightOf;
307
+ updateTree(oldNextSibling, { rightOf: oldNextSibling.rightOf });
308
+ }
309
+ parentInstance._child.splice(existingIndex, 1);
310
+ }
311
+ const beforeChildIndex = parentInstance._child.findIndex((c) => c.entity === beforeChild.entity);
312
+ parentInstance._child = [
313
+ ...parentInstance._child.slice(0, beforeChildIndex),
314
+ child,
315
+ ...parentInstance._child.slice(beforeChildIndex)
316
+ ];
317
+ child.rightOf = beforeChild.rightOf;
318
+ beforeChild.rightOf = child.entity;
319
+ child.parent = parentInstance.entity;
320
+ updateTree(child, { rightOf: child.rightOf, parent: child.parent });
321
+ updateTree(beforeChild, { rightOf: beforeChild.rightOf });
322
+ },
323
+ removeChildFromContainer(parenInstance, child) {
324
+ removeChildEntity(child);
325
+ }
302
326
  };
303
327
  const reconciler = Reconciler(hostConfig);
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, '',
328
+ const root = reconciler.createContainer({}, 0, null, false, null, '',
314
329
  /* istanbul ignore next */
315
330
  function () { }, null);
316
331
  return {
@@ -49,6 +49,9 @@ const entityComponent = {
49
49
  onMouseUp: undefined,
50
50
  onMouseEnter: undefined,
51
51
  onMouseLeave: undefined,
52
+ onMouseDrag: undefined,
53
+ onMouseDragLocked: undefined,
54
+ onMouseDragEnd: undefined,
52
55
  uiInput: undefined,
53
56
  uiDropdown: undefined,
54
57
  uiInputBinding: undefined
package/dist/system.d.ts CHANGED
@@ -27,17 +27,6 @@ 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; a renderer zIndex of 0 is interpreted as "unset"
38
- * (positional order); elements inside a renderer keep their own `zIndex`.
39
- */
40
- zIndex?: number;
41
30
  };
42
31
  /**
43
32
  * @public
@@ -85,6 +74,20 @@ export interface ReactBasedUiSystem {
85
74
  * @param entity - The entity whose UI renderer should be removed
86
75
  */
87
76
  removeUiRenderer(entity: Entity): void;
77
+ /**
78
+ * Render a UI tree to a texture. The tree is rooted at `entity`, which must carry a UiCanvas
79
+ * describing the texture; the result can be sampled through `TextureUnion.uiTexture`. Replaces
80
+ * any texture renderer already set on the entity, and is torn down when the entity is removed.
81
+ *
82
+ * @param entity - The UiCanvas entity the UI tree hangs from
83
+ * @param ui - The UI component to render
84
+ */
85
+ setTextureRenderer(entity: Entity, ui: UiComponent): void;
86
+ /**
87
+ * Remove a texture renderer previously set with setTextureRenderer(), destroying its entities.
88
+ * @param entity - The UiCanvas entity the renderer was set on
89
+ */
90
+ removeTextureRenderer(entity: Entity): void;
88
91
  }
89
92
  /**
90
93
  * @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, UiEntity } from './components';
6
+ import { InteractableArea, ScreenInsetArea } 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,14 +47,18 @@ 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
+ // UI trees rendered to a texture, keyed by their UiCanvas entity. Each needs its own reconciler
51
+ // because the root parent is a per-reconciler property.
52
+ const textureRenderers = new Map();
53
+ function destroyTextureRenderer(entity) {
54
+ const entry = textureRenderers.get(entity);
55
+ if (!entry)
56
+ return;
57
+ textureRenderers.delete(entity);
58
+ for (const uiEntity of entry.renderer.getEntities()) {
59
+ engine.removeEntity(uiEntity);
60
+ }
61
+ }
58
62
  const UiCanvasInformation = ecsComponents.UiCanvasInformation(engine);
59
63
  // Unique owner to prevent other UI systems resetting this scale factor.
60
64
  const uiScaleFactorOwner = Symbol('react-ecs-ui-scale');
@@ -127,58 +131,37 @@ export function createReactBasedUiSystem(engine, pointerSystem) {
127
131
  /**
128
132
  * Wraps a renderer's component in a container positioned within the selected
129
133
  * screen inset area. `'none'` adds no wrapper, leaving the UI on the whole
130
- * screen, unless a `zIndex` needs a root entity to live on. Applied per
131
- * renderer, so each renderer can use a different inset.
134
+ * screen. Applied per renderer, so each renderer can use a different inset.
132
135
  */
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 } };
136
+ function wrapWithScreenInset(ui, inset, key) {
138
137
  // An omitted inset is resolved to DEFAULT_SCREEN_INSET ('device') before the
139
138
  // switch, so it lands on `case 'device'`. Only an explicit 'none' reaches the
140
139
  // `default` clause below — the two are unrelated despite sharing a name.
141
- switch (options?.screenInset ?? DEFAULT_SCREEN_INSET) {
140
+ switch (inset ?? DEFAULT_SCREEN_INSET) {
142
141
  case 'device':
143
- return React.createElement(ScreenInsetArea, wrapperProps, React.createElement(ui));
142
+ return React.createElement(ScreenInsetArea, { key }, React.createElement(ui));
144
143
  case 'interactable':
145
- return React.createElement(InteractableArea, wrapperProps, React.createElement(ui));
144
+ return React.createElement(InteractableArea, { key }, React.createElement(ui));
146
145
  // 'none' — the whole screen, no wrapper entity
147
146
  default:
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));
147
+ return React.createElement(ui, { key });
156
148
  }
157
149
  }
158
150
  function ReactBasedUiSystem() {
159
151
  const components = [];
160
- if (uiComponent !== undefined && mainOrder === undefined)
161
- mainOrder = nextOrder++;
162
- let mainPushed = uiComponent === undefined;
152
+ // Add main UI component
153
+ if (uiComponent) {
154
+ components.push(wrapWithScreenInset(uiComponent, mainOptions?.screenInset, '__main__'));
155
+ }
163
156
  const entitiesToRemove = [];
164
157
  for (const [entity, entry] of additionalRenderers) {
165
158
  // Check for entity-based cleanup
166
159
  if (engine.getEntityState(entity) === EntityState.Removed) {
167
160
  entitiesToRemove.push(entity);
168
- continue;
169
161
  }
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;
162
+ else {
163
+ components.push(wrapWithScreenInset(entry.ui, entry.options?.screenInset, `__entity_${entity}__`));
176
164
  }
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__'));
182
165
  }
183
166
  // Entity-based cleanup
184
167
  for (const entity of entitiesToRemove) {
@@ -191,6 +174,14 @@ export function createReactBasedUiSystem(engine, pointerSystem) {
191
174
  else {
192
175
  renderer.update(null);
193
176
  }
177
+ for (const [entity, entry] of textureRenderers) {
178
+ if (engine.getEntityState(entity) === EntityState.Removed) {
179
+ destroyTextureRenderer(entity);
180
+ }
181
+ else {
182
+ entry.renderer.update(React.createElement(entry.ui));
183
+ }
184
+ }
194
185
  }
195
186
  function UiScaleSystem() {
196
187
  const canvasInfo = UiCanvasInformation.getOrNull(engine.RootEntity);
@@ -248,17 +239,26 @@ export function createReactBasedUiSystem(engine, pointerSystem) {
248
239
  for (const entity of renderer.getEntities()) {
249
240
  engine.removeEntity(entity);
250
241
  }
242
+ for (const entity of textureRenderers.keys()) {
243
+ destroyTextureRenderer(entity);
244
+ }
251
245
  },
252
246
  setUiRenderer(ui, options) {
253
247
  uiComponent = ui;
254
248
  mainOptions = options;
255
249
  },
256
250
  addUiRenderer(entity, ui, options) {
257
- // A replacement keeps its place in the stacking order.
258
- additionalRenderers.set(entity, { ui, options, order: additionalRenderers.get(entity)?.order });
251
+ additionalRenderers.set(entity, { ui, options });
259
252
  },
260
253
  removeUiRenderer(entity) {
261
254
  additionalRenderers.delete(entity);
255
+ },
256
+ setTextureRenderer(entity, ui) {
257
+ destroyTextureRenderer(entity);
258
+ textureRenderers.set(entity, { renderer: createReconciler(engine, pointerSystem, entity), ui });
259
+ },
260
+ removeTextureRenderer(entity) {
261
+ destroyTextureRenderer(entity);
262
262
  }
263
263
  };
264
264
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@dcl/react-ecs",
3
3
  "description": "Decentraland ECS",
4
- "version": "7.29.1-35862852104.commit-1f44e80",
4
+ "version": "7.29.1-35901015152.commit-d1b7b96",
5
5
  "author": "DCL",
6
6
  "bugs": "https://github.com/decentraland/js-sdk-toolchain/issues",
7
7
  "dependencies": {
8
- "@dcl/ecs": "7.29.1-35862852104.commit-1f44e80",
8
+ "@dcl/ecs": "7.29.1-35901015152.commit-d1b7b96",
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": "1f44e80aec8fa365230f39956a8f6526011c6bd5"
43
+ "commit": "d1b7b964cd184465c1b65118e47b5dc6be68a184"
44
44
  }