From baac552dff4a859e51c17687e2fa2dc479e4505e Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:51:23 +0100 Subject: [PATCH 1/4] fix: Improves hit area detection Adds hit area detection for scrollable containers. - Ensures events only trigger when the pointer is within the container's bounds - Uses visual bounds to accurately determine interaction area --- src/components/LayoutContainer.ts | 77 ++++++++++++++++++++----- tests/__tests__/LayoutContainer.test.ts | 16 ++--- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/src/components/LayoutContainer.ts b/src/components/LayoutContainer.ts index b130591..05747f9 100644 --- a/src/components/LayoutContainer.ts +++ b/src/components/LayoutContainer.ts @@ -117,8 +117,10 @@ export class LayoutContainer extends Container { /** Whether or not the background was created by the user */ private _isUserBackground: boolean = false; - /** The hit area for the container */ - private _hitArea = new Rectangle(); + /** The maximum visual bounds of the layout */ + private _visualBounds = new Rectangle(); + /** The scroll bounds for the container where scroll events can occur */ + private _scrollBounds = new Rectangle(); constructor(params: LayoutContainerOptions = {}) { const { layout, trackpad, background, children, ...options } = params; @@ -156,10 +158,16 @@ export class LayoutContainer extends Container { ...trackpad, }); this.eventMode = 'static'; - this.on('pointerdown', (e) => this._trackpad.pointerDown(e.global)); + this.on( + 'pointerdown', + (e) => this.isPointWithinBounds(e.global.x, e.global.y) && this._trackpad.pointerDown(e.global), + ); this.on('pointerup', () => this._trackpad.pointerUp()); this.on('pointerupoutside', () => this._trackpad.pointerUp()); - this.on('pointermove', (e) => this._trackpad.pointerMove(e.global)); + this.on( + 'pointermove', + (e) => this.isPointWithinBounds(e.global.x, e.global.y) && this._trackpad.pointerMove(e.global), + ); this.on('pointercancel', () => this._trackpad.pointerUp()); this.on('wheel', (e) => { const overflow = this.layout?.style.overflow; @@ -167,6 +175,10 @@ export class LayoutContainer extends Container { if (overflow !== 'scroll') { return; } + // check that the pointer position is within the visual bounds + if (!this.isPointWithinBounds(e.global.x, e.global.y)) { + return; + } const shift = e.shiftKey ? 1 : 0; const deltaX = e.deltaX * (shift ? 1 : -1); const deltaY = e.deltaY * (shift ? -1 : 1); @@ -194,9 +206,6 @@ export class LayoutContainer extends Container { */ override computeLayoutData(computedLayout: ComputedLayout) { this._drawBackground(computedLayout); - this._hitArea.width = computedLayout.width; - this._hitArea.height = computedLayout.height; - this.hitArea = this._hitArea; return { x: computedLayout.left, @@ -233,6 +242,12 @@ export class LayoutContainer extends Container { this._mask.roundRect(0, 0, maskWidth, maskHeight, radius); this._mask.position.set(maskX, maskY); this._mask.fill(0xffffff); + this._scrollBounds.set( + maskX + borderWidth, + maskY + borderWidth, + maskWidth - borderWidth * 2, + maskHeight - borderWidth * 2, + ); } protected _updateBackground(computedLayout: ComputedLayout) { @@ -295,17 +310,14 @@ export class LayoutContainer extends Container { borderWidth, boxSizing === BoxSizing.ContentBox, ); + this._updateScrollArea(); this.setMask({ mask: this._mask }); - // the max value is actually the difference between the container size and the content size and the stroke - const borderOffset = boxSizing === BoxSizing.BorderBox ? borderWidth : 0; - setTimeout(() => { - const maskWidth = computedLayout.width - this.overflowContainer.width - borderOffset * 2; - const maskHeight = computedLayout.height - this.overflowContainer.height - borderOffset * 2; + const maskWidth = computedLayout.width - this._visualBounds.width; + const maskHeight = computedLayout.height - this._visualBounds.height; - this._trackpad.xAxis.max = Math.min(0, maskWidth); - this._trackpad.yAxis.max = Math.min(0, maskHeight); - }, 1); + this._trackpad.xAxis.max = Math.min(0, maskWidth); + this._trackpad.yAxis.max = Math.min(0, maskHeight); } else { this.mask = null; this._trackpad.xAxis.value = 0; @@ -314,6 +326,41 @@ export class LayoutContainer extends Container { } } + protected _updateScrollArea() { + let maxBottom = 0; + let maxRight = 0; + + for (let i = 0; i < this.layout!.yoga.getChildCount(); i++) { + const child = this.layout!.yoga.getChild(i); + const bottom = child.getComputedTop() + child.getComputedHeight(); + const right = child.getComputedLeft() + child.getComputedWidth(); + + if (right > maxRight) maxRight = right; + if (bottom > maxBottom) maxBottom = bottom; + } + + maxRight += this.layout!.yoga.getComputedPadding(Edge.Right); + maxBottom += this.layout!.yoga.getComputedPadding(Edge.Bottom); + + const borderWidth = this.layout!.yoga.getBorder(Edge.All) || 0; + + maxRight += borderWidth; + maxBottom += borderWidth; + + this._visualBounds.width = Math.max(this._visualBounds.width, maxRight); + this._visualBounds.height = Math.max(this._visualBounds.height, maxBottom); + } + + /** + * Checks if a point is within the visual bounds of the container, accounting for world transformations. + * @param x - The x-coordinate of the point to check + * @param y - The y-coordinate of the point to check + * @returns True if the point is within the visual bounds, false otherwise + */ + protected isPointWithinBounds(x: number, y: number): boolean { + return this._scrollBounds.contains(x - this.worldTransform.tx, y - this.worldTransform.ty); + } + protected update(): void { const overflow = this.layout?.style.overflow; diff --git a/tests/__tests__/LayoutContainer.test.ts b/tests/__tests__/LayoutContainer.test.ts index 0ecb717..74871c5 100644 --- a/tests/__tests__/LayoutContainer.test.ts +++ b/tests/__tests__/LayoutContainer.test.ts @@ -1,5 +1,4 @@ -import { Application, type Rectangle } from 'pixi.js'; -import { Container } from 'pixi.js'; +import { Application, Container } from 'pixi.js'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { LayoutContainer } from '../../src/components/LayoutContainer'; import '../../src/index'; @@ -23,18 +22,21 @@ describe('LayoutContainer', async () => { app.destroy(true); }); - it('should have the correct sized hit area', async () => { + it.only('should have the correct sized hit area', async () => { const stage = app.stage; const container = new LayoutContainer({ - layout: { width: 100, height: 100 }, + layout: { width: 100, height: 100, overflow: 'scroll' }, }); stage.addChild(container); app.render(); - - expect((container.hitArea! as Rectangle).width).toBe(100); - expect((container.hitArea! as Rectangle).height).toBe(100); + expect(container['isPointWithinBounds'](50, 50)).toBe(true); + expect(container['isPointWithinBounds'](150, 150)).toBe(false); + expect(container['isPointWithinBounds'](-50, -50)).toBe(false); + expect(container['isPointWithinBounds'](0, 0)).toBe(true); + expect(container['isPointWithinBounds'](99.9, 99.9)).toBe(true); + expect(container['isPointWithinBounds'](100.1, 100.1)).toBe(false); }); }); From 2831d422065e3c7b63d8ade58e0f8ef0f9badf40 Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:08:43 +0100 Subject: [PATCH 2/4] Fix: Re-enables a test case Re-enables a previously isolated test case, ensuring it's now part of the regular test suite. --- tests/__tests__/LayoutContainer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__tests__/LayoutContainer.test.ts b/tests/__tests__/LayoutContainer.test.ts index 74871c5..bb0a57b 100644 --- a/tests/__tests__/LayoutContainer.test.ts +++ b/tests/__tests__/LayoutContainer.test.ts @@ -22,7 +22,7 @@ describe('LayoutContainer', async () => { app.destroy(true); }); - it.only('should have the correct sized hit area', async () => { + it('should have the correct sized hit area', async () => { const stage = app.stage; const container = new LayoutContainer({ layout: { width: 100, height: 100, overflow: 'scroll' }, From 9e81971308c26850d4c51caaac5dbbd6e0fa0e7a Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:41:03 +0100 Subject: [PATCH 3/4] Fix: Avoids errors when updating destroyed containers Adds a check to prevent attempting to update the size of a container that has already been destroyed. This avoids potential errors and improves the stability of the layout system. --- src/core/LayoutSystem.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/LayoutSystem.ts b/src/core/LayoutSystem.ts index 6489544..5a0951e 100644 --- a/src/core/LayoutSystem.ts +++ b/src/core/LayoutSystem.ts @@ -122,6 +122,7 @@ export class LayoutSystem implements System { * @param container - The container to update the size for */ private _updateSize(container: Container) { + if (container.destroyed) return; const layout = container._layout; if (layout) { From d4f21c668ef0478a2141f89bb80afbd1a6792564 Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:13:53 +0100 Subject: [PATCH 4/4] ensure fill --- src/components/LayoutContainer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/LayoutContainer.ts b/src/components/LayoutContainer.ts index 05747f9..b3aed45 100644 --- a/src/components/LayoutContainer.ts +++ b/src/components/LayoutContainer.ts @@ -265,6 +265,8 @@ export class LayoutContainer extends Container { // eslint-disable-next-line no-eq-null, eqeqeq if (backgroundColor != null) { background.fill({ color: backgroundColor }); + } else { + background.fill({ color: 0xffffff, alpha: 0 }); } } }