Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/ui/src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ export class Utils {
entity = entity.parent;
count++;
}
// A shorter chain drops tail entries — they must release the listener, or the bound
// element stays reachable from (and invocable by) entities it no longer tracks.
for (let i = count, n = listeningEntities.length; i < n; i++) {
// @ts-ignore
listeningEntities[i]._unRegisterModifyListener(listener);
}
listeningEntities.length = count;
}

Expand Down
17 changes: 12 additions & 5 deletions packages/ui/src/component/UICanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,9 @@ export class UICanvas extends Component implements IElement {
this._setIsRootCanvas(!rootCanvas);
Utils.setRootCanvas(this, rootCanvas);
} else if (flag === EntityUIModifyFlags.CanvasEnableInScene) {
this._setIsRootCanvas(false);
// The enabling canvas has not claimed root status at dispatch time, so searches
// inside the demote cascade cannot find it — hand it down as the successor.
this._setIsRootCanvas(false, <UICanvas>param);
Utils.setRootCanvas(this, <UICanvas>param);
}
} else {
Expand Down Expand Up @@ -632,7 +634,7 @@ export class UICanvas extends Component implements IElement {
}
}

private _setIsRootCanvas(isRootCanvas: boolean): void {
private _setIsRootCanvas(isRootCanvas: boolean, successor: UICanvas = null): void {
if (this._isRootCanvas !== isRootCanvas) {
this._isRootCanvas = isRootCanvas;
this._updateCameraObserver();
Expand All @@ -643,11 +645,16 @@ export class UICanvas extends Component implements IElement {
const { _disorderedElements: disorderedElements } = this;
disorderedElements.forEach((element: IElement) => {
if (element instanceof UICanvas) {
const rootCanvas = Utils.searchRootCanvasInParents(element);
element._setIsRootCanvas(!rootCanvas);
// `successor` covers the enable-driven demote, where the search cannot see the
// enabling canvas yet (it claims root status only after its dispatch returns).
const rootCanvas = Utils.searchRootCanvasInParents(element) ?? successor;
element._setIsRootCanvas(!rootCanvas, successor);
Utils.setRootCanvas(element, rootCanvas);
} else {
Utils.setRootCanvasDirty(this);
// Reset the element's own canvas state (stale _rootCanvas/_indexInRootCanvas and
// the dirty flag the new root's walk re-assignment is gated on) — the bulk
// clear below only empties this canvas's list.
Utils.setRootCanvasDirty(element);
Utils.setGroupDirty(<IGroupAble>element);
}
});
Expand Down
65 changes: 64 additions & 1 deletion tests/src/ui/UICanvas.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Camera } from "@galacean/engine-core";
import { Vector2 } from "@galacean/engine-math";
import { WebGLEngine } from "@galacean/engine";
import { CanvasRenderMode, ResolutionAdaptationMode, UICanvas, UITransform } from "@galacean/engine-ui";
import { CanvasRenderMode, Image, ResolutionAdaptationMode, UICanvas, UITransform } from "@galacean/engine-ui";
import { describe, expect, it } from "vitest";

describe("UICanvas", async () => {
Expand Down Expand Up @@ -351,4 +351,67 @@ describe("UICanvas", async () => {
// @ts-ignore
expect(rootCanvas._canDispatchEvent(camera2)).to.be.false;
});
it("re-homes elements to the new root canvas when theirs loses root status", () => {
const outerEntity = root.createChild("outer");
const innerEntity = outerEntity.createChild("inner");
const innerCanvas = innerEntity.addComponent(UICanvas);
const imageEntity = innerEntity.createChild("image");
const image = imageEntity.addComponent(Image);

// Collected by the inner canvas while it is the root.
// @ts-ignore
innerCanvas._getRenderers();
// @ts-ignore
expect(image._getRootCanvas()).to.eq(innerCanvas);
// @ts-ignore
expect(innerCanvas._disorderedElements.length).to.be.greaterThan(0);

// Enabling a canvas on an ancestor demotes the inner one.
const outerCanvas = outerEntity.addComponent(UICanvas);
// @ts-ignore
expect(innerCanvas._isRootCanvas).to.be.false;
// @ts-ignore
expect(outerCanvas._isRootCanvas).to.be.true;
// The element's canvas state must be reset (the walk's re-assignment is gated on the
// dirty flag), otherwise it keeps rendering into the demoted canvas's dead queue.
// @ts-ignore
expect(image._isRootCanvasDirty).to.be.true;
// @ts-ignore
expect(image._rootCanvas).to.be.null;

// The new root adopts it on its next walk.
// @ts-ignore
outerCanvas._getRenderers();
// @ts-ignore
expect(image._getRootCanvas()).to.eq(outerCanvas);

outerEntity.destroy();
});
it("re-roots nested canvases to the enabling ancestor canvas", () => {
const outerEntity = root.createChild("outer");
const midEntity = outerEntity.createChild("mid");
const midCanvas = midEntity.addComponent(UICanvas);
const innerEntity = midEntity.createChild("inner");
const innerCanvas = innerEntity.addComponent(UICanvas);
// @ts-ignore
expect(midCanvas._isRootCanvas).to.be.true;
// @ts-ignore
expect(innerCanvas._isRootCanvas).to.be.false;

// The enabling canvas claims root status only after its enable dispatch returns, so the
// demote cascade must hand it down instead of searching for it.
const outerCanvas = outerEntity.addComponent(UICanvas);
// @ts-ignore
expect(outerCanvas._isRootCanvas).to.be.true;
// @ts-ignore
expect(midCanvas._isRootCanvas).to.be.false;
// @ts-ignore
expect(innerCanvas._isRootCanvas).to.be.false;
// @ts-ignore
expect(midCanvas._getRootCanvas()).to.eq(outerCanvas);
// @ts-ignore
expect(innerCanvas._getRootCanvas()).to.eq(outerCanvas);

outerEntity.destroy();
});
});
Loading