@dcl/react-ecs 7.24.5-29247185020.commit-e92dfe6 → 7.24.5-29264961047.commit-77493c3

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.
Files changed (33) hide show
  1. package/dist/components/Button/index.js +2 -2
  2. package/dist/components/Dropdown/index.js +2 -10
  3. package/dist/components/Input/index.js +2 -10
  4. package/dist/components/InteractableArea/index.d.ts +23 -0
  5. package/dist/components/InteractableArea/index.js +33 -0
  6. package/dist/components/InteractableArea/types.d.ts +23 -0
  7. package/dist/components/InteractableArea/types.js +1 -0
  8. package/dist/components/Label/index.js +3 -13
  9. package/dist/components/Label/types.d.ts +0 -4
  10. package/dist/components/ScreenInsetArea/index.d.ts +22 -0
  11. package/dist/components/ScreenInsetArea/index.js +32 -0
  12. package/dist/components/ScreenInsetArea/types.d.ts +22 -0
  13. package/dist/components/ScreenInsetArea/types.js +1 -0
  14. package/dist/components/index.d.ts +4 -0
  15. package/dist/components/index.js +4 -0
  16. package/dist/components/listeners/types.d.ts +6 -37
  17. package/dist/components/listeners/types.js +1 -9
  18. package/dist/components/uiBackground/types.d.ts +1 -12
  19. package/dist/components/uiBackground/utils.js +0 -8
  20. package/dist/components/uiTransform/index.d.ts +1 -1
  21. package/dist/components/uiTransform/index.js +3 -6
  22. package/dist/components/uiTransform/types.d.ts +0 -12
  23. package/dist/components/uiTransform/utils.js +0 -37
  24. package/dist/components/utils.js +89 -2
  25. package/dist/platform.d.ts +12 -0
  26. package/dist/platform.js +26 -0
  27. package/dist/react-ecs.d.ts +6 -14
  28. package/dist/reconciler/index.d.ts +3 -4
  29. package/dist/reconciler/index.js +91 -68
  30. package/dist/reconciler/utils.js +4 -12
  31. package/dist/system.d.ts +7 -6
  32. package/dist/system.js +82 -22
  33. package/package.json +3 -3
@@ -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, onMouseDrag, onMouseDragLocked, onMouseDragEnd, onInputDown, onInputUp, onInputDrag, onInputDragLocked, onInputDragEnd, ...otherProps } = props;
32
+ const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...otherProps } = props;
33
33
  const buttonProps = getButtonProps(props);
34
34
  const uiBackgroundProps = parseUiBackground({
35
35
  ...buttonProps.uiBackground,
@@ -54,5 +54,5 @@ export function Button(props) {
54
54
  if (uiBackgroundProps && uiBackgroundProps.color)
55
55
  uiBackgroundProps.color.a /= 2;
56
56
  }
57
- return (ReactEcs.createElement("entity", { onMouseDown: !!props.disabled ? undefined : onMouseDown, onMouseUp: !!props.disabled ? undefined : onMouseUp, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onMouseDrag: onMouseDrag, onMouseDragLocked: onMouseDragLocked, onMouseDragEnd: onMouseDragEnd, onInputDown: !!props.disabled ? undefined : onInputDown, onInputUp: !!props.disabled ? undefined : onInputUp, onInputDrag: onInputDrag, onInputDragLocked: onInputDragLocked, onInputDragEnd: onInputDragEnd, uiTransform: uiTransformProps, uiText: textProps, uiBackground: uiBackgroundProps }));
57
+ return (ReactEcs.createElement("entity", { onMouseDown: !!props.disabled ? undefined : onMouseDown, onMouseUp: !!props.disabled ? undefined : onMouseUp, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, uiTransform: uiTransformProps, uiText: textProps, uiBackground: uiBackgroundProps }));
58
58
  }
@@ -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, onMouseDrag, onMouseDragLocked, onMouseDragEnd, onInputDown, onInputUp, onInputDrag, onInputDragLocked, onInputDragEnd, ...otherProps } = props;
37
+ const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...otherProps } = props;
38
38
  const dropdownProps = parseUiDropdown(otherProps);
39
39
  const commonProps = parseProps({
40
40
  uiTransform,
@@ -42,15 +42,7 @@ export function Dropdown(props) {
42
42
  onMouseDown,
43
43
  onMouseUp,
44
44
  onMouseEnter,
45
- onMouseLeave,
46
- onMouseDrag,
47
- onMouseDragLocked,
48
- onMouseDragEnd,
49
- onInputDown,
50
- onInputUp,
51
- onInputDrag,
52
- onInputDragLocked,
53
- onInputDragEnd
45
+ onMouseLeave
54
46
  });
55
47
  return ReactEcs.createElement("entity", { ...commonProps, uiDropdown: dropdownProps });
56
48
  }
@@ -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, onMouseDrag, onMouseDragLocked, onMouseDragEnd, onInputDown, onInputUp, onInputDrag, onInputDragLocked, onInputDragEnd, ...otherProps } = props;
38
+ const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...otherProps } = props;
39
39
  const inputProps = parseUiInput(otherProps);
40
40
  const commonProps = parseProps({
41
41
  uiTransform,
@@ -43,15 +43,7 @@ export function Input(props) {
43
43
  onMouseDown,
44
44
  onMouseUp,
45
45
  onMouseEnter,
46
- onMouseLeave,
47
- onMouseDrag,
48
- onMouseDragLocked,
49
- onMouseDragEnd,
50
- onInputDown,
51
- onInputUp,
52
- onInputDrag,
53
- onInputDragLocked,
54
- onInputDragEnd
46
+ onMouseLeave
55
47
  });
56
48
  return ReactEcs.createElement("entity", { ...commonProps, uiInput: inputProps });
57
49
  }
@@ -0,0 +1,23 @@
1
+ import { ReactEcs } from '../../react-ecs';
2
+ import { UiInteractableAreaProps } from './types';
3
+ /**
4
+ *
5
+ * @public
6
+ * InteractableArea component
7
+ *
8
+ * Constrains its children to the area inside the renderer-reported interactable
9
+ * area. This is the portion of the screen not covered by client UI such as the
10
+ * minimap, chat window, or other platform overlays. On the Unity desktop client
11
+ * the left 25% of the screen is reserved for client UI, so this container
12
+ * positions its children within the remaining 75%.
13
+ *
14
+ * The container is absolutely positioned with top/left/right/bottom matching
15
+ * the current `UiCanvasInformation.interactableArea`, so a child sized
16
+ * 100%x100% fills the interactable area exactly.
17
+ *
18
+ * @example
19
+ * <InteractableArea><MyHud /></InteractableArea>
20
+ *
21
+ * @category Component
22
+ */
23
+ export declare function InteractableArea(props: UiInteractableAreaProps): ReactEcs.JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { ReactEcs } from '../../react-ecs';
2
+ import { UiEntity } from '../index';
3
+ import { getInteractableArea } from '../utils';
4
+ /**
5
+ *
6
+ * @public
7
+ * InteractableArea component
8
+ *
9
+ * Constrains its children to the area inside the renderer-reported interactable
10
+ * area. This is the portion of the screen not covered by client UI such as the
11
+ * minimap, chat window, or other platform overlays. On the Unity desktop client
12
+ * the left 25% of the screen is reserved for client UI, so this container
13
+ * positions its children within the remaining 75%.
14
+ *
15
+ * The container is absolutely positioned with top/left/right/bottom matching
16
+ * the current `UiCanvasInformation.interactableArea`, so a child sized
17
+ * 100%x100% fills the interactable area exactly.
18
+ *
19
+ * @example
20
+ * <InteractableArea><MyHud /></InteractableArea>
21
+ *
22
+ * @category Component
23
+ */
24
+ /* @__PURE__ */
25
+ export function InteractableArea(props) {
26
+ const { top, left, right, bottom } = getInteractableArea();
27
+ const { uiTransform, ...otherProps } = props;
28
+ return (ReactEcs.createElement(UiEntity, { ...otherProps, uiTransform: {
29
+ ...uiTransform,
30
+ positionType: 'absolute',
31
+ position: { top, left, right, bottom }
32
+ } }));
33
+ }
@@ -0,0 +1,23 @@
1
+ import { EntityPropTypes } from '../types';
2
+ /**
3
+ * InteractableArea component props
4
+ *
5
+ * The container reads the current `interactableArea` reported by the renderer
6
+ * via `UiCanvasInformation` (the HUD-safe zone — the portion of the screen not
7
+ * covered by client UI such as the minimap, chat window, or other overlays) and
8
+ * constrains its children to the area inside those insets using absolute
9
+ * positioning. Layout props that control the container's own position
10
+ * (`positionType`, `position`) are owned by the component and are not
11
+ * configurable from props — every other layout, background and event prop is
12
+ * forwarded as usual.
13
+ *
14
+ * @public
15
+ */
16
+ export type UiInteractableAreaProps = Omit<EntityPropTypes, 'uiTransform'> & {
17
+ /**
18
+ * Layout overrides forwarded to the underlying entity. The
19
+ * `positionType` and `position` fields are reserved by the container and
20
+ * any value provided here is ignored.
21
+ */
22
+ uiTransform?: Omit<NonNullable<EntityPropTypes['uiTransform']>, 'positionType' | 'position'>;
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -16,22 +16,14 @@ export { scaleFontSize } from './utils';
16
16
  */
17
17
  /* @__PURE__ */
18
18
  export function Label(props) {
19
- const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onMouseDrag, onMouseDragLocked, onMouseDragEnd, onInputDown, onInputUp, onInputDrag, onInputDragLocked, onInputDragEnd, ...uiTextProps } = props;
19
+ const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...uiTextProps } = props;
20
20
  const commonProps = parseProps({
21
21
  uiTransform,
22
22
  uiBackground,
23
23
  onMouseDown,
24
24
  onMouseUp,
25
25
  onMouseEnter,
26
- onMouseLeave,
27
- onMouseDrag,
28
- onMouseDragLocked,
29
- onMouseDragEnd,
30
- onInputDown,
31
- onInputUp,
32
- onInputDrag,
33
- onInputDragLocked,
34
- onInputDragEnd
26
+ onMouseLeave
35
27
  });
36
28
  const { font, textAlign, fontSize, textWrap, ...textProps } = uiTextProps;
37
29
  const uiText = {
@@ -39,9 +31,7 @@ export function Label(props) {
39
31
  ...getFont(font),
40
32
  ...getTextAlign(textAlign),
41
33
  ...getFontSize(fontSize),
42
- ...getTextWrap(textWrap),
43
- outlineWidth: props.outlineWidth,
44
- outlineColor: props.outlineColor
34
+ ...getTextWrap(textWrap)
45
35
  };
46
36
  return ReactEcs.createElement("entity", { ...commonProps, uiText: uiText });
47
37
  }
@@ -15,10 +15,6 @@ export interface UiLabelProps {
15
15
  textAlign?: TextAlignType | undefined;
16
16
  /** Label font type. @defaultValue 'sans-serif' */
17
17
  font?: UiFontType | undefined;
18
- /** Outline width of the text. @defaultValue 0 */
19
- outlineWidth?: number | undefined;
20
- /** Outline color of the text. @defaultValue `{ r: 0, g: 0, b: 0, a: 1 }` */
21
- outlineColor?: Color4 | undefined;
22
18
  /** Behaviour when text reached. @defaultValue 'wrap' */
23
19
  textWrap?: UiTextWrapType | undefined;
24
20
  }
@@ -0,0 +1,22 @@
1
+ import { ReactEcs } from '../../react-ecs';
2
+ import { UiScreenInsetAreaProps } from './types';
3
+ /**
4
+ *
5
+ * @public
6
+ * ScreenInsetArea component
7
+ *
8
+ * Constrains its children to the area inside the renderer-reported screen
9
+ * inset (safe margins). On mobile this is the area excluding the notch,
10
+ * status bar, home indicator and rounded corners. On desktop the insets are
11
+ * typically zero, so the container fills the canvas.
12
+ *
13
+ * The container is absolutely positioned with top/left/right/bottom matching
14
+ * the current `UiCanvasInformation.screenInsetArea`, so a child sized
15
+ * 100%x100% fills the safe area exactly.
16
+ *
17
+ * @example
18
+ * <ScreenInsetArea><MyHud /></ScreenInsetArea>
19
+ *
20
+ * @category Component
21
+ */
22
+ export declare function ScreenInsetArea(props: UiScreenInsetAreaProps): ReactEcs.JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { ReactEcs } from '../../react-ecs';
2
+ import { UiEntity } from '../index';
3
+ import { getScreenInsetArea } from '../utils';
4
+ /**
5
+ *
6
+ * @public
7
+ * ScreenInsetArea component
8
+ *
9
+ * Constrains its children to the area inside the renderer-reported screen
10
+ * inset (safe margins). On mobile this is the area excluding the notch,
11
+ * status bar, home indicator and rounded corners. On desktop the insets are
12
+ * typically zero, so the container fills the canvas.
13
+ *
14
+ * The container is absolutely positioned with top/left/right/bottom matching
15
+ * the current `UiCanvasInformation.screenInsetArea`, so a child sized
16
+ * 100%x100% fills the safe area exactly.
17
+ *
18
+ * @example
19
+ * <ScreenInsetArea><MyHud /></ScreenInsetArea>
20
+ *
21
+ * @category Component
22
+ */
23
+ /* @__PURE__ */
24
+ export function ScreenInsetArea(props) {
25
+ const { top, left, right, bottom } = getScreenInsetArea();
26
+ const { uiTransform, ...otherProps } = props;
27
+ return (ReactEcs.createElement(UiEntity, { ...otherProps, uiTransform: {
28
+ ...uiTransform,
29
+ positionType: 'absolute',
30
+ position: { top, left, right, bottom }
31
+ } }));
32
+ }
@@ -0,0 +1,22 @@
1
+ import { EntityPropTypes } from '../types';
2
+ /**
3
+ * ScreenInsetArea component props
4
+ *
5
+ * The container reads the current `screenInsetArea` reported by the renderer
6
+ * via `UiCanvasInformation` (the device safe margins — notch, status bar,
7
+ * home indicator, rounded corners) and constrains its children to the area
8
+ * inside those insets using absolute positioning. Layout props that control
9
+ * the container's own position (`positionType`, `position`) are owned by the
10
+ * component and are not configurable from props — every other layout,
11
+ * background and event prop is forwarded as usual.
12
+ *
13
+ * @public
14
+ */
15
+ export type UiScreenInsetAreaProps = Omit<EntityPropTypes, 'uiTransform'> & {
16
+ /**
17
+ * Layout overrides forwarded to the underlying entity. The
18
+ * `positionType` and `position` fields are reserved by the container and
19
+ * any value provided here is ignored.
20
+ */
21
+ uiTransform?: Omit<NonNullable<EntityPropTypes['uiTransform']>, 'positionType' | 'position'>;
22
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -9,10 +9,14 @@ export * from './uiBackground/types';
9
9
  export * from './Dropdown/types';
10
10
  export * from './Label/types';
11
11
  export * from './Button/types';
12
+ export * from './ScreenInsetArea/types';
13
+ export * from './InteractableArea/types';
12
14
  export { Dropdown } from './Dropdown';
13
15
  export { Input } from './Input';
14
16
  export { Label, scaleFontSize } from './Label';
15
17
  export { Button } from './Button';
18
+ export { ScreenInsetArea } from './ScreenInsetArea';
19
+ export { InteractableArea } from './InteractableArea';
16
20
  /**
17
21
  * @public
18
22
  * @category Component
@@ -9,10 +9,14 @@ export * from './uiBackground/types';
9
9
  export * from './Dropdown/types';
10
10
  export * from './Label/types';
11
11
  export * from './Button/types';
12
+ export * from './ScreenInsetArea/types';
13
+ export * from './InteractableArea/types';
12
14
  export { Dropdown } from './Dropdown';
13
15
  export { Input } from './Input';
14
16
  export { Label, scaleFontSize } from './Label';
15
17
  export { Button } from './Button';
18
+ export { ScreenInsetArea } from './ScreenInsetArea';
19
+ export { InteractableArea } from './InteractableArea';
16
20
  /**
17
21
  * @public
18
22
  * @category Component
@@ -1,50 +1,19 @@
1
- import { EventSystemCallback, InputAction } from '@dcl/ecs';
2
1
  /**
3
- * legacy Callback function
4
- *
5
- * @public @deprecated This type is no longer used in the sdk api, EventSystemCallback is
6
- * used for listeners instead
7
- */
8
- export type Callback = () => void;
9
- /**
10
- * a record object mapping `InputAction`s to functions.
11
- *
12
- * @example
13
- * onInputDown={{
14
- * [InputAction.IA_PRIMARY]: (eventData) => { console.log("primary") },
15
- * [InputAction.IA_SECONDARY]: () => { console.log("secondary") },
16
- * }}
17
- *
2
+ * Callback function to be triggered on a specified event
18
3
  * @public
19
4
  */
20
- export type MultiCallback = Partial<Record<InputAction, EventSystemCallback>>;
5
+ export type Callback = () => void;
21
6
  /**
22
7
  * User key event Listeners
23
8
  * @public
24
9
  */
25
10
  export type Listeners = {
26
11
  /** triggered on mouse down event */
27
- onMouseDown?: EventSystemCallback;
12
+ onMouseDown?: Callback;
28
13
  /** triggered on mouse up event */
29
- onMouseUp?: EventSystemCallback;
14
+ onMouseUp?: Callback;
30
15
  /** triggered on mouse hover event */
31
- onMouseEnter?: EventSystemCallback;
16
+ onMouseEnter?: Callback;
32
17
  /** triggered on mouse leave event */
33
- onMouseLeave?: EventSystemCallback;
34
- /** triggered on mouse drag event */
35
- onMouseDrag?: EventSystemCallback;
36
- /** triggered on mouse drag event */
37
- onMouseDragLocked?: EventSystemCallback;
38
- /** triggered on mouse drag event */
39
- onMouseDragEnd?: EventSystemCallback;
40
- /** triggered on input down event */
41
- onInputDown?: MultiCallback;
42
- /** triggered on input up event */
43
- onInputUp?: MultiCallback;
44
- /** triggered on input drag event */
45
- onInputDrag?: MultiCallback;
46
- /** triggered on input drag event */
47
- onInputDragLocked?: MultiCallback;
48
- /** triggered on input drag event */
49
- onInputDragEnd?: MultiCallback;
18
+ onMouseLeave?: Callback;
50
19
  };
@@ -2,15 +2,7 @@ const listeners = {
2
2
  onMouseDown: undefined,
3
3
  onMouseUp: undefined,
4
4
  onMouseEnter: undefined,
5
- onMouseLeave: undefined,
6
- onMouseDrag: undefined,
7
- onMouseDragLocked: undefined,
8
- onMouseDragEnd: undefined,
9
- onInputDown: undefined,
10
- onInputUp: undefined,
11
- onInputDrag: undefined,
12
- onInputDragLocked: undefined,
13
- onInputDragEnd: undefined
5
+ onMouseLeave: undefined
14
6
  };
15
7
  const listenersKey = Object.keys(listeners);
16
8
  /**
@@ -1,4 +1,4 @@
1
- import { BorderRect, Entity } from '@dcl/ecs';
1
+ import { BorderRect } from '@dcl/ecs';
2
2
  import { Color4 } from '@dcl/ecs/dist/components/generated/pb/decentraland/common/colors.gen';
3
3
  /**
4
4
  * @public
@@ -15,8 +15,6 @@ export interface UiBackgroundProps {
15
15
  uvs?: number[];
16
16
  /** AvatarTexture for the background */
17
17
  avatarTexture?: UiAvatarTexture;
18
- /** VideoTexture for the background */
19
- videoTexture?: UiVideoTexture;
20
18
  /** Texture for the background */
21
19
  texture?: UiTexture;
22
20
  }
@@ -29,15 +27,6 @@ export interface UiAvatarTexture {
29
27
  wrapMode?: TextureWrapType;
30
28
  filterMode?: TextureFilterType;
31
29
  }
32
- /**
33
- * Texture
34
- * @public
35
- */
36
- export interface UiVideoTexture {
37
- videoPlayerEntity: Entity;
38
- wrapMode?: TextureWrapType;
39
- filterMode?: TextureFilterType;
40
- }
41
30
  /**
42
31
  * Texture
43
32
  * @public
@@ -30,14 +30,6 @@ 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
- }
41
33
  return undefined;
42
34
  }
43
35
  function parseTexture(texture) {
@@ -1,5 +1,5 @@
1
- import { PBUiTransform } from '@dcl/ecs/dist/components';
2
1
  import { UiTransformProps } from './types';
2
+ import { PBUiTransform } from '@dcl/ecs/dist/components';
3
3
  /**
4
4
  * @public
5
5
  */
@@ -1,4 +1,4 @@
1
- import { getAlign, getDisplay, getFlexDirection, getFlexWrap, getJustify, getOverflow, getPointerFilter, getPositionType, parseBorderColor, parseBorderRadius, parseBorderWidth, getScrollPosition, getScrollVisible, parsePosition, parseSize } from './utils';
1
+ import { getAlign, getDisplay, getFlexDirection, getFlexWrap, getJustify, getOverflow, getPointerFilter, getPositionType, 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 { scrollPosition, scrollVisible, height, minHeight, maxHeight, width, minWidth, maxWidth, alignItems, alignContent, flexWrap, borderRadius, borderWidth, borderColor, zIndex, ...otherProps } = props;
63
+ const { height, minHeight, maxHeight, width, minWidth, maxWidth, alignItems, alignContent, flexWrap, borderRadius, borderWidth, borderColor, ...otherProps } = props;
64
64
  return {
65
65
  ...defaultUiTransform,
66
66
  ...otherProps,
@@ -86,9 +86,6 @@ export function parseUiTransform(props = {}) {
86
86
  ...(flexWrap && getFlexWrap(flexWrap)),
87
87
  ...(borderRadius && parseBorderRadius(borderRadius)),
88
88
  ...(borderWidth && parseBorderWidth(borderWidth)),
89
- ...(borderColor && parseBorderColor(borderColor)),
90
- ...(scrollPosition && getScrollPosition(scrollPosition)),
91
- ...(scrollVisible && getScrollVisible(scrollVisible)),
92
- ...(zIndex && { zIndex })
89
+ ...(borderColor && parseBorderColor(borderColor))
93
90
  };
94
91
  }
@@ -1,5 +1,4 @@
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';
3
2
  import { ScaleUnit } from '../types';
4
3
  /**
5
4
  * unit value specified. i.e. 1 || '100%' || '1px' || '10vw'
@@ -74,11 +73,6 @@ export type PositionType = 'absolute' | 'relative';
74
73
  * The pointer filter property determines if the ui element blocks the pointer or not (elements with pointer events always block the pointer regardless of this property)
75
74
  */
76
75
  export type PointerFilterType = 'none' | 'block';
77
- /**
78
- * @public
79
- * The scroll-visible determines if the scrollbars are shown when the scroll overflow is enabled
80
- */
81
- export type ScrollVisibleType = 'horizontal' | 'vertical' | 'both' | 'hidden';
82
76
  /**
83
77
  * Layout props to position things in the canvas
84
78
  * @public
@@ -135,12 +129,6 @@ export interface UiTransformProps {
135
129
  borderWidth?: Partial<Position> | PositionUnit;
136
130
  /** The opacity property sets the opacity level for an element, it's accumulated across children @defaultValue 1 */
137
131
  opacity?: number;
138
- /** A reference value to identify the element, default empty */
139
- elementId?: string;
140
- /** default position=(0,0) if it aplies, a vector or a reference-id */
141
- scrollPosition?: Vector2 | string;
142
- /** default ShowScrollBar.SSB_BOTH */
143
- scrollVisible?: ScrollVisibleType;
144
132
  /** default 0 */
145
133
  zIndex?: number;
146
134
  }
@@ -244,40 +244,3 @@ 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 {
253
- scrollPosition: {
254
- value: {
255
- $case: 'reference',
256
- reference: scrollPosition
257
- }
258
- }
259
- };
260
- }
261
- else {
262
- return {
263
- scrollPosition: {
264
- value: {
265
- $case: 'position',
266
- position: scrollPosition
267
- }
268
- }
269
- };
270
- }
271
- }
272
- const parseScrollVisible = {
273
- both: 0 /* ShowScrollBar.SSB_BOTH */,
274
- hidden: 3 /* ShowScrollBar.SSB_HIDDEN */,
275
- horizontal: 2 /* ShowScrollBar.SSB_ONLY_HORIZONTAL */,
276
- vertical: 1 /* ShowScrollBar.SSB_ONLY_VERTICAL */
277
- };
278
- /**
279
- * @internal
280
- */
281
- export function getScrollVisible(scrollVisible) {
282
- return { scrollVisible: parseScrollVisible[scrollVisible] };
283
- }
@@ -3,6 +3,11 @@ import { parseUiBackground } from './uiBackground';
3
3
  import { parseUiTransform } from './uiTransform';
4
4
  let uiScaleFactor = 1;
5
5
  let uiScaleOwner = undefined;
6
+ const ZERO_INSETS = { top: 0, left: 0, right: 0, bottom: 0 };
7
+ let screenInsetArea = { ...ZERO_INSETS };
8
+ let screenInsetAreaOwner = undefined;
9
+ let interactableArea = { ...ZERO_INSETS };
10
+ let interactableAreaOwner = undefined;
6
11
  /**
7
12
  * @internal
8
13
  */
@@ -54,13 +59,22 @@ export function getUiScaleFactor() {
54
59
  return uiScaleFactor;
55
60
  }
56
61
  /**
62
+ * Sets the global UI scale factor.
63
+ *
64
+ * The `owner` symbol implements a cooperative reset-protection scheme shared
65
+ * with {@link resetUiScaleFactor}:
66
+ * - Writes always succeed — last writer claims ownership (the most recent
67
+ * `owner` passed to `set` is the one allowed to `reset`).
68
+ * - Resets from a non-matching owner are ignored, so a stale system can't
69
+ * stomp the active scale while another system is driving it.
70
+ * - A reset called without an owner always wins (used by tests / teardown).
71
+ *
57
72
  * @internal
58
73
  */
59
74
  export function setUiScaleFactor(nextScale, owner) {
60
75
  if (!Number.isFinite(nextScale) || nextScale < 0)
61
76
  return;
62
77
  if (owner) {
63
- // Mark ownership so only that system can reset the scale.
64
78
  uiScaleOwner = owner;
65
79
  }
66
80
  uiScaleFactor = nextScale;
@@ -69,12 +83,85 @@ export function setUiScaleFactor(nextScale, owner) {
69
83
  * @internal
70
84
  */
71
85
  export function resetUiScaleFactor(owner) {
72
- // Ignore resets from non-owners to avoid stomping active scale.
86
+ // No-op for non-owners (see ownership rules on `setUiScaleFactor`).
87
+ // A reset with no owner always wins — used by tests and teardown.
73
88
  if (owner && uiScaleOwner !== owner)
74
89
  return;
75
90
  uiScaleOwner = undefined;
76
91
  uiScaleFactor = 1;
77
92
  }
93
+ /**
94
+ * @internal
95
+ */
96
+ export function getScreenInsetArea() {
97
+ return { ...screenInsetArea };
98
+ }
99
+ /**
100
+ * Sets the global screen inset area.
101
+ *
102
+ * The `owner` symbol implements a cooperative reset-protection scheme shared
103
+ * with {@link resetScreenInsetArea}:
104
+ * - Writes always succeed — last writer claims ownership (the most recent
105
+ * `owner` passed to `set` is the one allowed to `reset`).
106
+ * - Resets from a non-matching owner are ignored, so a stale system can't
107
+ * stomp the active insets while another system is driving them.
108
+ * - A reset called without an owner always wins (used by tests / teardown).
109
+ *
110
+ * @internal
111
+ */
112
+ export function setScreenInsetArea(next, owner) {
113
+ if (owner) {
114
+ screenInsetAreaOwner = owner;
115
+ }
116
+ screenInsetArea = { top: next.top, left: next.left, right: next.right, bottom: next.bottom };
117
+ }
118
+ /**
119
+ * @internal
120
+ */
121
+ export function resetScreenInsetArea(owner) {
122
+ // No-op for non-owners (see ownership rules on `setScreenInsetArea`).
123
+ // A reset with no owner always wins — used by tests and teardown.
124
+ if (owner && screenInsetAreaOwner !== owner)
125
+ return;
126
+ screenInsetAreaOwner = undefined;
127
+ screenInsetArea = { ...ZERO_INSETS };
128
+ }
129
+ /**
130
+ * @internal
131
+ */
132
+ export function getInteractableArea() {
133
+ return { ...interactableArea };
134
+ }
135
+ /**
136
+ * Sets the global interactable area.
137
+ *
138
+ * The `owner` symbol implements a cooperative reset-protection scheme shared
139
+ * with {@link resetInteractableArea}:
140
+ * - Writes always succeed — last writer claims ownership (the most recent
141
+ * `owner` passed to `set` is the one allowed to `reset`).
142
+ * - Resets from a non-matching owner are ignored, so a stale system can't
143
+ * stomp the active area while another system is driving it.
144
+ * - A reset called without an owner always wins (used by tests / teardown).
145
+ *
146
+ * @internal
147
+ */
148
+ export function setInteractableArea(next, owner) {
149
+ if (owner) {
150
+ interactableAreaOwner = owner;
151
+ }
152
+ interactableArea = { top: next.top, left: next.left, right: next.right, bottom: next.bottom };
153
+ }
154
+ /**
155
+ * @internal
156
+ */
157
+ export function resetInteractableArea(owner) {
158
+ // No-op for non-owners (see ownership rules on `setInteractableArea`).
159
+ // A reset with no owner always wins — used by tests and teardown.
160
+ if (owner && interactableAreaOwner !== owner)
161
+ return;
162
+ interactableAreaOwner = undefined;
163
+ interactableArea = { ...ZERO_INSETS };
164
+ }
78
165
  /**
79
166
  * @internal
80
167
  */
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Injects the function used to detect whether the scene runs on a mobile
3
+ * platform. Called by `@dcl/sdk` with its `isMobile()` platform helper.
4
+ */
5
+ export declare function setIsMobileProvider(provider: () => boolean): void;
6
+ /**
7
+ * Whether the scene is running on a mobile platform, according to the
8
+ * injected provider. Platform detection is asynchronous on the SDK side, so
9
+ * this may return false during the first ticks and flip to true once the
10
+ * explorer information arrives.
11
+ */
12
+ export declare function isMobile(): boolean;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Platform detection hook for the UI scale system.
3
+ *
4
+ * react-ecs stays independent of the scene runtime (`~system/*` modules), so
5
+ * it cannot ask the explorer which platform it runs on. Instead, the host SDK
6
+ * injects the check at module-load time (see `@dcl/sdk/react-ecs`). Until a
7
+ * provider is injected — or when none ever is — the platform is assumed to be
8
+ * non-mobile.
9
+ */
10
+ let isMobileProvider = () => false;
11
+ /**
12
+ * Injects the function used to detect whether the scene runs on a mobile
13
+ * platform. Called by `@dcl/sdk` with its `isMobile()` platform helper.
14
+ */
15
+ export function setIsMobileProvider(provider) {
16
+ isMobileProvider = provider;
17
+ }
18
+ /**
19
+ * Whether the scene is running on a mobile platform, according to the
20
+ * injected provider. Platform detection is asynchronous on the SDK side, so
21
+ * this may return false during the first ticks and flip to true once the
22
+ * explorer information arrives.
23
+ */
24
+ export function isMobile() {
25
+ return isMobileProvider();
26
+ }
@@ -1,5 +1,5 @@
1
- import { PBUiBackground, PBUiText, PBUiTransform, PBUiInput, PBUiDropdown, EventSystemCallback } from '@dcl/ecs';
2
- import { Key, MultiCallback } from './components';
1
+ import { PBUiBackground, PBUiText, PBUiTransform, PBUiInput, PBUiDropdown } from '@dcl/ecs';
2
+ import { Callback, Key } from './components';
3
3
  /**
4
4
  * @public
5
5
  */
@@ -18,18 +18,10 @@ export type EntityComponents = {
18
18
  uiBackground: PBUiBackground;
19
19
  uiInput: PBUiInput;
20
20
  uiDropdown: PBUiDropdown;
21
- onMouseDown: EventSystemCallback;
22
- onMouseUp: EventSystemCallback;
23
- onMouseEnter: EventSystemCallback;
24
- onMouseLeave: EventSystemCallback;
25
- onMouseDrag: EventSystemCallback;
26
- onMouseDragLocked: EventSystemCallback;
27
- onMouseDragEnd: EventSystemCallback;
28
- onInputDown: MultiCallback;
29
- onInputUp: MultiCallback;
30
- onInputDrag: MultiCallback;
31
- onInputDragLocked: MultiCallback;
32
- onInputDragEnd: MultiCallback;
21
+ onMouseDown: Callback;
22
+ onMouseUp: Callback;
23
+ onMouseEnter: Callback;
24
+ onMouseLeave: Callback;
33
25
  };
34
26
  /**
35
27
  * @hidden
@@ -1,7 +1,6 @@
1
1
  import { Entity, IEngine, PointerEventsSystem } from '@dcl/ecs';
2
2
  import { ReactEcs } from '../react-ecs';
3
- export interface DclReconciler {
4
- update: (component: ReactEcs.JSX.ReactNode) => void;
3
+ export declare function createReconciler(engine: Pick<IEngine, 'getComponent' | 'addEntity' | 'removeEntity' | 'defineComponentFromSchema' | 'getEntitiesWith'>, pointerEvents: PointerEventsSystem): {
4
+ update: (component: ReactEcs.JSX.ReactNode) => number;
5
5
  getEntities: () => Entity[];
6
- }
7
- export declare function createReconciler(engine: Pick<IEngine, 'getComponent' | 'addEntity' | 'removeEntity' | 'defineComponentFromSchema' | 'getEntitiesWith'>, pointerEvents: PointerEventsSystem, rootEntity: Entity | undefined): DclReconciler;
6
+ };
@@ -3,11 +3,24 @@ import Reconciler from 'react-reconciler';
3
3
  import { isListener } from '../components';
4
4
  import { CANVAS_ROOT_ENTITY } from '../components/uiTransform';
5
5
  import { componentKeys, isNotUndefined, noopConfig, propsChanged } from './utils';
6
- export function createReconciler(engine, pointerEvents, rootEntity) {
6
+ function getPointerEnum(pointerKey) {
7
+ const pointers = {
8
+ onMouseDown: 1 /* PointerEventType.PET_DOWN */,
9
+ onMouseUp: 0 /* PointerEventType.PET_UP */,
10
+ onMouseEnter: 2 /* PointerEventType.PET_HOVER_ENTER */,
11
+ onMouseLeave: 3 /* PointerEventType.PET_HOVER_LEAVE */
12
+ };
13
+ return pointers[pointerKey];
14
+ }
15
+ export function createReconciler(engine, pointerEvents) {
7
16
  // Store all the entities so when we destroy the UI we can also destroy them
8
17
  const entities = new Set();
9
18
  // Store the onChange callbacks to be runned every time a Result has changed
10
19
  const changeEvents = new Map();
20
+ const clickEvents = new Map();
21
+ // Track the last value reported by the renderer for each input entity,
22
+ // so we can avoid echoing it back and causing keystroke drops.
23
+ const lastInputResultValues = new Map();
11
24
  // Initialize components
12
25
  const UiTransform = components.UiTransform(engine);
13
26
  const UiText = components.UiText(engine);
@@ -16,8 +29,6 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
16
29
  const UiInputResult = components.UiInputResult(engine);
17
30
  const UiDropdown = components.UiDropdown(engine);
18
31
  const UiDropdownResult = components.UiDropdownResult(engine);
19
- const UiScrollResult = components.UiScrollResult(engine);
20
- const Transform = components.Transform(engine);
21
32
  // Component ID Helper
22
33
  const getComponentId = {
23
34
  uiTransform: UiTransform.componentId,
@@ -26,15 +37,22 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
26
37
  uiInput: UiInput.componentId,
27
38
  uiDropdown: UiDropdown.componentId
28
39
  };
40
+ function pointerEventCallback(entity, pointerEvent) {
41
+ const callback = clickEvents.get(entity)?.get(pointerEvent);
42
+ if (callback)
43
+ callback();
44
+ return;
45
+ }
29
46
  function updateTree(instance, props) {
30
47
  upsertComponent(instance, props, 'uiTransform');
31
48
  }
32
49
  function upsertListener(instance, update) {
33
50
  if (update.type === 'delete' || !update.props) {
34
- if (update.component === 'onMouseDown' || update.component === 'onInputDown') {
51
+ clickEvents.get(instance.entity)?.delete(getPointerEnum(update.component));
52
+ if (update.component === 'onMouseDown') {
35
53
  pointerEvents.removeOnPointerDown(instance.entity);
36
54
  }
37
- else if (update.component === 'onMouseUp' || update.component === 'onInputUp') {
55
+ else if (update.component === 'onMouseUp') {
38
56
  pointerEvents.removeOnPointerUp(instance.entity);
39
57
  }
40
58
  else if (update.component === 'onMouseEnter') {
@@ -43,57 +61,32 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
43
61
  else if (update.component === 'onMouseLeave') {
44
62
  pointerEvents.removeOnPointerHoverLeave(instance.entity);
45
63
  }
46
- else if (update.component === 'onMouseDrag' || update.component === 'onInputDrag') {
47
- pointerEvents.removeOnPointerDrag(instance.entity);
48
- }
49
- else if (update.component === 'onMouseDragLocked' || update.component === 'onInputDragLocked') {
50
- pointerEvents.removeOnPointerDragLocked(instance.entity);
51
- }
52
- else if (update.component === 'onMouseDragEnd' || update.component === 'onInputDragEnd') {
53
- pointerEvents.removeOnPointerDragEnd(instance.entity);
54
- }
55
64
  return;
56
65
  }
57
66
  if (update.props) {
58
- const pointerEventSystem = update.component === 'onMouseDown' || update.component === 'onInputDown'
67
+ const pointerEvent = getPointerEnum(update.component);
68
+ const entityEvent = clickEvents.get(instance.entity) || clickEvents.set(instance.entity, new Map()).get(instance.entity);
69
+ const alreadyHasPointerEvent = entityEvent.get(pointerEvent);
70
+ entityEvent.set(pointerEvent, update.props);
71
+ if (alreadyHasPointerEvent)
72
+ return;
73
+ const pointerEventSystem = update.component === 'onMouseDown'
59
74
  ? pointerEvents.onPointerDown
60
- : update.component === 'onMouseUp' || update.component === 'onInputUp'
75
+ : update.component === 'onMouseUp'
61
76
  ? pointerEvents.onPointerUp
62
77
  : update.component === 'onMouseEnter'
63
78
  ? pointerEvents.onPointerHoverEnter
64
- : update.component === 'onMouseLeave'
65
- ? pointerEvents.onPointerHoverLeave
66
- : update.component === 'onMouseDrag' || update.component === 'onInputDrag'
67
- ? pointerEvents.onPointerDrag
68
- : update.component === 'onMouseDragLocked' || update.component === 'onInputDragLocked'
69
- ? pointerEvents.onPointerDragLocked
70
- : (update.component === 'onMouseDragEnd' || update.component === 'onInputDragEnd') &&
71
- pointerEvents.onPointerDragEnd;
79
+ : update.component === 'onMouseLeave' && pointerEvents.onPointerHoverLeave;
72
80
  if (pointerEventSystem) {
73
- if (typeof update.props === 'function') {
74
- pointerEventSystem({
75
- entity: instance.entity,
76
- opts: {
77
- button: 0 /* InputAction.IA_POINTER */,
78
- // We add this showFeedBack so the pointerEventSystem creates a PointerEvent component with our entity
79
- // This is needed for the renderer to know which entities are clickeables
80
- showFeedback: true
81
- }
82
- }, update.props);
83
- }
84
- else {
85
- // force the right overload (the single arg version doesn't exist for onPointerHoverEnter or onPointerHoverLeave,
86
- // but we don't have onInputXXX components for those)
87
- ;
88
- pointerEventSystem({
89
- entity: instance.entity,
90
- optsList: Object.entries(update.props).map(([button, cb]) => ({
91
- button: Number(button),
92
- showFeedback: true,
93
- cb
94
- }))
95
- });
96
- }
81
+ pointerEventSystem({
82
+ entity: instance.entity,
83
+ opts: {
84
+ button: 0 /* InputAction.IA_POINTER */,
85
+ // We add this showFeedBack so the pointerEventSystem creates a PointerEvent component with our entity
86
+ // This is needed for the renderer to know which entities are clickeables
87
+ showFeedback: true
88
+ }
89
+ }, () => pointerEventCallback(instance.entity, pointerEvent));
97
90
  }
98
91
  }
99
92
  }
@@ -121,6 +114,15 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
121
114
  delete props.onChange;
122
115
  delete props.onSubmit;
123
116
  }
117
+ // Prevent keystroke drops: when React echoes back the same value the renderer
118
+ // reported, strip it from the props so the component isn't marked dirty for it.
119
+ // This avoids sending a stale value that overwrites what the user is currently typing.
120
+ if (componentName === 'uiInput' &&
121
+ 'value' in props &&
122
+ lastInputResultValues.has(instance.entity) &&
123
+ props.value === lastInputResultValues.get(instance.entity)) {
124
+ delete props.value;
125
+ }
124
126
  // We check if there is any key pending to be changed to avoid updating the existing component
125
127
  if (!Object.keys(props).length) {
126
128
  return;
@@ -134,6 +136,8 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
134
136
  }
135
137
  function removeChildEntity(instance) {
136
138
  changeEvents.delete(instance.entity);
139
+ clickEvents.delete(instance.entity);
140
+ lastInputResultValues.delete(instance.entity);
137
141
  engine.removeEntity(instance.entity);
138
142
  for (const child of instance._child) {
139
143
  removeChildEntity(child);
@@ -152,11 +156,15 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
152
156
  const rightOfChild = parent._child.find((c) => c.rightOf === child.entity);
153
157
  if (rightOfChild) {
154
158
  rightOfChild.rightOf = child.rightOf;
155
- // Re-order parent._child array
156
- parent._child = parent._child.filter((c) => c.entity !== child.entity);
157
- parent._child.push(child);
158
159
  updateTree(rightOfChild, { rightOf: rightOfChild.rightOf });
159
160
  }
161
+ // Always remove from old position and push to end for reorders.
162
+ // Previously, filter/push was inside the if(rightOfChild) block, so when
163
+ // rightOfChild was not found (child was last), the child stayed at its old
164
+ // position. This caused _child[length-2] to potentially be the child itself,
165
+ // producing a self-referencing rightOf.
166
+ parent._child = parent._child.filter((c) => c.entity !== child.entity);
167
+ parent._child.push(child);
160
168
  // Its a re-order. We are the last element, so we need to fetch the element before us.
161
169
  child.rightOf = parent._child[parent._child.length - 2]?.entity;
162
170
  }
@@ -170,13 +178,14 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
170
178
  }
171
179
  function removeChild(parentInstance, child) {
172
180
  const childIndex = parentInstance._child.findIndex((c) => c.entity === child.entity);
173
- const childToModify = parentInstance._child[childIndex + 1];
174
- if (childToModify) {
175
- childToModify.rightOf = child.rightOf;
176
- updateTree(childToModify, { rightOf: child.rightOf });
181
+ if (childIndex !== -1) {
182
+ const childToModify = parentInstance._child[childIndex + 1];
183
+ if (childToModify) {
184
+ childToModify.rightOf = child.rightOf;
185
+ updateTree(childToModify, { rightOf: child.rightOf });
186
+ }
187
+ parentInstance._child.splice(childIndex, 1);
177
188
  }
178
- // Mutate 💀
179
- parentInstance._child.splice(childIndex, 1);
180
189
  removeChildEntity(child);
181
190
  }
182
191
  function updateOnChange(entity, componentId, state) {
@@ -189,6 +198,9 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
189
198
  if (!hasEvent) {
190
199
  const resultComponentId = componentId === UiDropdown.componentId ? UiDropdownResult.componentId : UiInputResult.componentId;
191
200
  engine.getComponent(resultComponentId).onChange(entity, (value) => {
201
+ if (resultComponentId === UiInputResult.componentId) {
202
+ lastInputResultValues.set(entity, value?.value);
203
+ }
192
204
  if (value?.isSubmit) {
193
205
  const onSubmit = changeEvents.get(entity)?.get(componentId)?.onSubmitCallback;
194
206
  onSubmit && onSubmit(value?.value);
@@ -202,10 +214,6 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
202
214
  ...noopConfig,
203
215
  createInstance(type, props) {
204
216
  const entity = engine.addEntity();
205
- // set root
206
- if (rootEntity !== undefined) {
207
- Transform.createOrReplace(entity, { parent: rootEntity });
208
- }
209
217
  entities.add(entity);
210
218
  const instance = {
211
219
  entity,
@@ -219,13 +227,11 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
219
227
  continue;
220
228
  }
221
229
  if (isListener(keyTyped)) {
222
- if (props[keyTyped] !== undefined) {
223
- upsertListener(instance, {
224
- type: 'add',
225
- props: props[keyTyped],
226
- component: keyTyped
227
- });
228
- }
230
+ upsertListener(instance, {
231
+ type: 'add',
232
+ props: props[keyTyped],
233
+ component: keyTyped
234
+ });
229
235
  }
230
236
  else {
231
237
  upsertComponent(instance, props[keyTyped], keyTyped);
@@ -257,6 +263,23 @@ export function createReconciler(engine, pointerEvents, rootEntity) {
257
263
  }
258
264
  },
259
265
  insertBefore(parentInstance, child, beforeChild) {
266
+ // Handle reorder: if child already exists in this parent, remove it from its old position
267
+ // and fix up the old neighbor's rightOf before inserting at the new position.
268
+ const existingIndex = parentInstance._child.findIndex((c) => c.entity === child.entity);
269
+ if (existingIndex !== -1) {
270
+ // If beforeChild.rightOf already points to child, child is already in the correct
271
+ // position in the rightOf chain. Setting child.rightOf = beforeChild.rightOf would
272
+ // produce a self-cycle (child.rightOf = child.entity).
273
+ if (beforeChild.rightOf === child.entity) {
274
+ return;
275
+ }
276
+ const oldNextSibling = parentInstance._child[existingIndex + 1];
277
+ if (oldNextSibling) {
278
+ oldNextSibling.rightOf = child.rightOf;
279
+ updateTree(oldNextSibling, { rightOf: oldNextSibling.rightOf });
280
+ }
281
+ parentInstance._child.splice(existingIndex, 1);
282
+ }
260
283
  const beforeChildIndex = parentInstance._child.findIndex((c) => c.entity === beforeChild.entity);
261
284
  parentInstance._child = [
262
285
  ...parentInstance._child.slice(0, beforeChildIndex),
@@ -23,10 +23,10 @@ export function propsChanged(component, prevProps, nextProps) {
23
23
  }
24
24
  }
25
25
  // ...then catch keys present only in nextProps (an optional prop set for the first time,
26
- // e.g. `scrollPosition` going from undefined to a value). Iterating prevProps alone would
27
- // drop these. Two allocation-free loops are used instead of a Set union to keep this hot
28
- // path cheap. These keys aren't in prevProps, so emitting them whenever they're defined is
29
- // both cheaper than (and avoids the falsy-value trap of) an isEqual(undefined, ...) compare.
26
+ // e.g. an undefined value becoming defined). Iterating prevProps alone would drop these.
27
+ // Two allocation-free loops are used instead of a Set union to keep this hot path cheap.
28
+ // These keys aren't in prevProps, so emitting them whenever they're defined is both cheaper
29
+ // than (and avoids the falsy-value trap of) an isEqual(undefined, ...) comparison.
30
30
  for (const k in nextProps) {
31
31
  if (k in prevProps)
32
32
  continue;
@@ -49,14 +49,6 @@ const entityComponent = {
49
49
  onMouseUp: undefined,
50
50
  onMouseEnter: undefined,
51
51
  onMouseLeave: undefined,
52
- onMouseDrag: undefined,
53
- onMouseDragLocked: undefined,
54
- onMouseDragEnd: undefined,
55
- onInputDown: undefined,
56
- onInputUp: undefined,
57
- onInputDrag: undefined,
58
- onInputDragLocked: undefined,
59
- onInputDragEnd: undefined,
60
52
  uiInput: undefined,
61
53
  uiDropdown: undefined
62
54
  };
package/dist/system.d.ts CHANGED
@@ -21,13 +21,13 @@ export interface ReactBasedUiSystem {
21
21
  destroy(): void;
22
22
  /**
23
23
  * Set the main UI renderer. Optional virtual size defines the global UI scale factor.
24
+ *
25
+ * When no virtual size is provided, a platform default is used: 1600x720 on
26
+ * mobile, 1920x1080 otherwise. Providing an invalid size (values \<= 0)
27
+ * disables the virtual screen (no UI scaling). On mobile, a provided 16:9
28
+ * virtual size is overridden to 1600x720 to fit phone screens.
24
29
  */
25
30
  setUiRenderer(ui: UiComponent, options?: UiRendererOptions): void;
26
- /**
27
- * Set a texture renderer for a specific entity.
28
- * @deprecated Use addUiRenderer instead
29
- */
30
- setTextureRenderer(entity: Entity, ui: UiComponent): void;
31
31
  /**
32
32
  * Add a UI renderer associated with an entity. The UI will be automatically cleaned up
33
33
  * when the entity is removed from the engine.
@@ -40,7 +40,8 @@ export interface ReactBasedUiSystem {
40
40
  * @param entity - The entity to associate with this UI renderer. When the entity is removed,
41
41
  * the UI renderer is automatically cleaned up.
42
42
  * @param ui - The UI component to render
43
- * @param options - Optional virtual size used for UI scale factor when main UI has none
43
+ * @param options - Optional virtual size used for UI scale factor when main UI has none.
44
+ * Defaults and the mobile 16:9 override behave as in {@link ReactBasedUiSystem.setUiRenderer}.
44
45
  */
45
46
  addUiRenderer(entity: Entity, ui: UiComponent, options?: UiRendererOptions): void;
46
47
  /**
package/dist/system.js CHANGED
@@ -2,19 +2,44 @@ import { EntityState } from '@dcl/ecs';
2
2
  import * as ecsComponents from '@dcl/ecs/dist/components';
3
3
  import React from 'react';
4
4
  import { createReconciler } from './reconciler';
5
- import { getUiScaleFactor, resetUiScaleFactor, setUiScaleFactor } from './components/utils';
5
+ import { getUiScaleFactor, resetInteractableArea, resetScreenInsetArea, resetUiScaleFactor, setInteractableArea, setScreenInsetArea, setUiScaleFactor } from './components/utils';
6
+ import { isMobile } from './platform';
7
+ /**
8
+ * Default virtual screen size used on mobile platforms, and the size 16:9
9
+ * virtual screens are overridden to on mobile (phone screens are much wider
10
+ * than 16:9, so a 16:9 virtual canvas would letterbox the UI).
11
+ */
12
+ const DEFAULT_MOBILE_VIRTUAL_SIZE = { virtualWidth: 1600, virtualHeight: 720 };
13
+ /**
14
+ * Default virtual screen size used on non-mobile platforms.
15
+ */
16
+ const DEFAULT_VIRTUAL_SIZE = { virtualWidth: 1920, virtualHeight: 1080 };
17
+ function isValidVirtualSize(options) {
18
+ return !!options && options.virtualWidth > 0 && options.virtualHeight > 0;
19
+ }
20
+ function is16by9(options) {
21
+ return options.virtualWidth * 9 === options.virtualHeight * 16;
22
+ }
6
23
  /**
7
24
  * @public
8
25
  */
9
26
  export function createReactBasedUiSystem(engine, pointerSystem) {
10
- const renderer = createReconciler(engine, pointerSystem, undefined);
27
+ const renderer = createReconciler(engine, pointerSystem);
11
28
  let uiComponent = undefined;
12
29
  let virtualSize = undefined;
13
30
  const additionalRenderers = new Map();
14
- const textureRenderersAndUis = [];
15
31
  const UiCanvasInformation = ecsComponents.UiCanvasInformation(engine);
16
32
  // Unique owner to prevent other UI systems resetting this scale factor.
17
33
  const uiScaleFactorOwner = Symbol('react-ecs-ui-scale');
34
+ // Unique owner for the screen inset module variable.
35
+ const screenInsetAreaOwner = Symbol('react-ecs-screen-inset-area');
36
+ // Unique owner for the interactable area module variable.
37
+ const interactableAreaOwner = Symbol('react-ecs-interactable-area');
38
+ // Last 16:9 size we already logged the mobile override for, so the log
39
+ // fires once per provided size instead of every tick. Tracked as raw numbers
40
+ // to avoid allocating a comparison string every tick.
41
+ let loggedMobileOverrideW = 0;
42
+ let loggedMobileOverrideH = 0;
18
43
  function getActiveVirtualSize() {
19
44
  // Main renderer options win; otherwise use the first additional renderer option.
20
45
  if (virtualSize)
@@ -25,6 +50,33 @@ export function createReactBasedUiSystem(engine, pointerSystem) {
25
50
  }
26
51
  return undefined;
27
52
  }
53
+ /**
54
+ * Resolves the virtual screen to scale the UI against, or `undefined` when
55
+ * the virtual screen is disabled.
56
+ */
57
+ function resolveVirtualSize() {
58
+ const provided = getActiveVirtualSize();
59
+ const mobile = isMobile();
60
+ // No creator-provided size: fall back to the platform default.
61
+ if (!provided) {
62
+ return mobile ? DEFAULT_MOBILE_VIRTUAL_SIZE : DEFAULT_VIRTUAL_SIZE;
63
+ }
64
+ // An explicitly provided but invalid size (values <= 0) disables the
65
+ // virtual screen — no UI scaling at all.
66
+ if (!isValidVirtualSize(provided)) {
67
+ return undefined;
68
+ }
69
+ // On mobile, 16:9 virtual screens don't fit phone aspect ratios — override them.
70
+ if (mobile && is16by9(provided)) {
71
+ if (loggedMobileOverrideW !== provided.virtualWidth || loggedMobileOverrideH !== provided.virtualHeight) {
72
+ loggedMobileOverrideW = provided.virtualWidth;
73
+ loggedMobileOverrideH = provided.virtualHeight;
74
+ console.log(`Mobile platform detected: overriding 16:9 virtual screen size ${provided.virtualWidth}x${provided.virtualHeight} with ${DEFAULT_MOBILE_VIRTUAL_SIZE.virtualWidth}x${DEFAULT_MOBILE_VIRTUAL_SIZE.virtualHeight}`);
75
+ }
76
+ return DEFAULT_MOBILE_VIRTUAL_SIZE;
77
+ }
78
+ return provided;
79
+ }
28
80
  function ReactBasedUiSystem() {
29
81
  const components = [];
30
82
  // Add main UI component
@@ -52,26 +104,40 @@ export function createReactBasedUiSystem(engine, pointerSystem) {
52
104
  else {
53
105
  renderer.update(null);
54
106
  }
55
- // Update texture renderers (deprecated, kept for backwards compatibility)
56
- for (const [textureRenderer, ui] of textureRenderersAndUis) {
57
- textureRenderer.update(React.createElement(ui));
58
- }
59
107
  }
60
108
  function UiScaleSystem() {
61
- const activeVirtualSize = getActiveVirtualSize();
62
- if (!activeVirtualSize) {
109
+ const canvasInfo = UiCanvasInformation.getOrNull(engine.RootEntity);
110
+ // Update the screen inset module variable unconditionally — it is
111
+ // independent of the virtual size and useful even when the renderer has no
112
+ // virtual canvas.
113
+ if (canvasInfo?.screenInsetArea) {
114
+ setScreenInsetArea(canvasInfo.screenInsetArea, screenInsetAreaOwner);
115
+ }
116
+ // Update the interactable area module variable unconditionally.
117
+ if (canvasInfo?.interactableArea) {
118
+ setInteractableArea(canvasInfo.interactableArea, interactableAreaOwner);
119
+ }
120
+ // The virtual screen (provided or defaulted) only applies while some
121
+ // renderer is registered; with no UI at all the scale factor is released.
122
+ if (uiComponent === undefined && additionalRenderers.size === 0) {
63
123
  // Reset only if this system owns the scale factor.
64
124
  resetUiScaleFactor(uiScaleFactorOwner);
65
125
  return;
66
126
  }
67
- const canvasInfo = UiCanvasInformation.getOrNull(engine.RootEntity);
127
+ const activeVirtualSize = resolveVirtualSize();
128
+ if (!activeVirtualSize) {
129
+ // Virtual screen explicitly disabled by an invalid provided size.
130
+ resetUiScaleFactor(uiScaleFactorOwner);
131
+ return;
132
+ }
68
133
  if (!canvasInfo)
69
134
  return;
70
- const { width, height } = canvasInfo;
135
+ const { width, height, devicePixelRatio } = canvasInfo;
71
136
  const { virtualWidth, virtualHeight } = activeVirtualSize;
72
- if (!virtualWidth || !virtualHeight)
73
- return;
74
- const nextScale = Math.min(width / virtualWidth, height / virtualHeight);
137
+ // Normalize by devicePixelRatio so virtual px map to logical px (matching the
138
+ // vw/vh path); without it the scale was inflated on high-dpr mobile screens.
139
+ const ratio = devicePixelRatio || 1;
140
+ const nextScale = Math.min(width / virtualWidth, height / virtualHeight) / ratio;
75
141
  if (Number.isFinite(nextScale) && nextScale !== getUiScaleFactor()) {
76
142
  // Track ownership when updating to avoid cross-system conflicts.
77
143
  setUiScaleFactor(nextScale, uiScaleFactorOwner);
@@ -84,22 +150,16 @@ export function createReactBasedUiSystem(engine, pointerSystem) {
84
150
  engine.removeSystem(UiScaleSystem);
85
151
  engine.removeSystem(ReactBasedUiSystem);
86
152
  resetUiScaleFactor(uiScaleFactorOwner);
153
+ resetScreenInsetArea(screenInsetAreaOwner);
154
+ resetInteractableArea(interactableAreaOwner);
87
155
  for (const entity of renderer.getEntities()) {
88
156
  engine.removeEntity(entity);
89
157
  }
90
- for (const [textureRenderer, _] of textureRenderersAndUis) {
91
- for (const entity of textureRenderer.getEntities()) {
92
- engine.removeEntity(entity);
93
- }
94
- }
95
158
  },
96
159
  setUiRenderer(ui, options) {
97
160
  uiComponent = ui;
98
161
  virtualSize = options;
99
162
  },
100
- setTextureRenderer(entity, ui) {
101
- textureRenderersAndUis.push([createReconciler(engine, pointerSystem, entity), ui]);
102
- },
103
163
  addUiRenderer(entity, ui, options) {
104
164
  additionalRenderers.set(entity, { ui, options });
105
165
  },
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@dcl/react-ecs",
3
3
  "description": "Decentraland ECS",
4
- "version": "7.24.5-29247185020.commit-e92dfe6",
4
+ "version": "7.24.5-29264961047.commit-77493c3",
5
5
  "author": "DCL",
6
6
  "bugs": "https://github.com/decentraland/js-sdk-toolchain/issues",
7
7
  "dependencies": {
8
- "@dcl/ecs": "7.24.5-29247185020.commit-e92dfe6",
8
+ "@dcl/ecs": "7.24.5-29264961047.commit-77493c3",
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": "e92dfe6298e3d808e58add843780cdbe7eb5af7d"
43
+ "commit": "77493c38bf6fe58d9ece07c8f886e6b1d39438ab"
44
44
  }