From 2d87f33fc05df228237344a98b985c37d4229419 Mon Sep 17 00:00:00 2001 From: Dmytro Soldatov Date: Thu, 29 Jun 2023 21:24:35 +0300 Subject: [PATCH 1/8] refactor --- src/List.ts | 7 +++++++ src/ScrollBox.ts | 32 +++++++++++++------------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/List.ts b/src/List.ts index af62d920..a7949d41 100644 --- a/src/List.ts +++ b/src/List.ts @@ -7,6 +7,7 @@ export type ListOptions = { children?: Container[]; vertPadding?: number; horPadding?: number; + items?: Container[]; }; /** @@ -50,6 +51,8 @@ export class List extends Container this.init(options); } + options?.items?.forEach((item) => this.addChild(item)); + this.on('added', () => this.arrangeChildren()); this.on('childAdded', () => this.arrangeChildren()); } @@ -149,6 +152,10 @@ export class List extends Container return this.options.horPadding; } + /** + * Arrange all elements basing in their sizes and component options. + * Can be arranged vertically, horizontally or bidirectional. + */ protected arrangeChildren() { let x = this.options?.horPadding ?? 0; diff --git a/src/ScrollBox.ts b/src/ScrollBox.ts index ba0534c9..b555d2d3 100644 --- a/src/ScrollBox.ts +++ b/src/ScrollBox.ts @@ -25,7 +25,8 @@ export type ScrollBoxOptions = { /** * Scrollable view, for arranging lists of Pixi container-based elements. * - * Items, that are out of the visible area, are not rendered. + * Items, that are out of the visible area, are not rendered by default. + * This behavior can be changed by setting 'disableDynamicRendering' option to true. * @example * new ScrollBox({ * background: 0XFFFFFF, @@ -82,6 +83,16 @@ export class ScrollBox extends Container /** * Initiates ScrollBox. * @param options + * @param {number} [options.background=0xFFFFFF] - background color of the ScrollBox. + * @param {number} [options.width] - width of the ScrollBox. + * @param {number} [options.height] - height of the ScrollBox. + * @param {number} [options.radius] - radius of the ScrollBox and its masks corners. + * @param {number} [options.elementsMargin=0] - margin between elements. + * @param {number} [options.vertPadding=0] - vertical padding of the ScrollBox. + * @param {number} [options.horPadding=0] - horizontal padding of the ScrollBox. + * @param {number} [options.padding=0] - padding of the ScrollBox (same horizontal and vertical). + * @param {boolean} [options.disableDynamicRendering=false] - disables dynamic rendering of the ScrollBox, + * so even elements the are not visible will be rendered. Be careful with this options as it can impact performance. */ init(options: ScrollBoxOptions) { @@ -106,10 +117,9 @@ export class ScrollBox extends Container elementsMargin: options.elementsMargin, vertPadding: options.vertPadding, horPadding: options.horPadding, + items: options.items, }); - this.addItems(options.items); - if (this.hasBounds) { this.addMask(); @@ -168,28 +178,12 @@ export class ScrollBox extends Container console.error('ScrollBox item should have size'); } - child.x = this.freeSlot.x; - child.y = this.freeSlot.y; - this.list.addChild(child); if (!this.options.disableDynamicRendering) { child.renderable = this.isItemVisible(child); } - - const elementsMargin = this.options?.elementsMargin ?? 0; - - switch (this.options.type) - { - case 'horizontal': - this.freeSlot.x += elementsMargin + child.width; - break; - - default: - this.freeSlot.y += elementsMargin + child.height; - break; - } } this.resize(); From 42f22deb5898afa2d1cdad4c3606a78b933ea417 Mon Sep 17 00:00:00 2001 From: Dmytro Soldatov Date: Thu, 29 Jun 2023 22:34:43 +0300 Subject: [PATCH 2/8] Fix issue --- src/ScrollBox.ts | 156 ++++++++++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 63 deletions(-) diff --git a/src/ScrollBox.ts b/src/ScrollBox.ts index b555d2d3..32e13479 100644 --- a/src/ScrollBox.ts +++ b/src/ScrollBox.ts @@ -1,6 +1,6 @@ -import { Ticker } from '@pixi/core'; +import { Ticker, utils } from '@pixi/core'; import { Container, DisplayObject } from '@pixi/display'; -import { FederatedPointerEvent } from '@pixi/events'; +import { EventMode, FederatedPointerEvent } from '@pixi/events'; import { Graphics } from '@pixi/graphics'; import { Sprite } from '@pixi/sprite'; import type { ListType } from './List'; @@ -62,10 +62,27 @@ export class ScrollBox extends Container protected _trackpad: Trackpad; protected isDragging = 0; - protected interactiveStorage: Map = new Map(); + protected interactiveStorage: { + item: DisplayObject; + eventMode: EventMode; + }[] = []; + protected pressedChild: Container; protected ticker = Ticker.shared; protected options: ScrollBoxOptions; + /** + * @param options + * @param {number} options.background - background color of the ScrollBox. + * @param {number} options.width - width of the ScrollBox. + * @param {number} options.height - height of the ScrollBox. + * @param {number} options.radius - radius of the ScrollBox and its masks corners. + * @param {number} options.elementsMargin - margin between elements. + * @param {number} options.vertPadding - vertical padding of the ScrollBox. + * @param {number} options.horPadding - horizontal padding of the ScrollBox. + * @param {number} options.padding - padding of the ScrollBox (same horizontal and vertical). + * @param {boolean} options.disableDynamicRendering - disables dynamic rendering of the ScrollBox, + * so even elements the are not visible will be rendered. Be careful with this options as it can impact performance. + */ constructor(options?: ScrollBoxOptions) { super(); @@ -83,15 +100,15 @@ export class ScrollBox extends Container /** * Initiates ScrollBox. * @param options - * @param {number} [options.background=0xFFFFFF] - background color of the ScrollBox. - * @param {number} [options.width] - width of the ScrollBox. - * @param {number} [options.height] - height of the ScrollBox. - * @param {number} [options.radius] - radius of the ScrollBox and its masks corners. - * @param {number} [options.elementsMargin=0] - margin between elements. - * @param {number} [options.vertPadding=0] - vertical padding of the ScrollBox. - * @param {number} [options.horPadding=0] - horizontal padding of the ScrollBox. - * @param {number} [options.padding=0] - padding of the ScrollBox (same horizontal and vertical). - * @param {boolean} [options.disableDynamicRendering=false] - disables dynamic rendering of the ScrollBox, + * @param {number} options.background - background color of the ScrollBox. + * @param {number} options.width - width of the ScrollBox. + * @param {number} options.height - height of the ScrollBox. + * @param {number} options.radius - radius of the ScrollBox and its masks corners. + * @param {number} options.elementsMargin - margin between elements. + * @param {number} options.vertPadding - vertical padding of the ScrollBox. + * @param {number} options.horPadding - horizontal padding of the ScrollBox. + * @param {number} options.padding - padding of the ScrollBox (same horizontal and vertical). + * @param {boolean} options.disableDynamicRendering - disables dynamic rendering of the ScrollBox, * so even elements the are not visible will be rendered. Be careful with this options as it can impact performance. */ init(options: ScrollBoxOptions) @@ -143,8 +160,8 @@ export class ScrollBox extends Container } /** - * Add an items to a scrollable list. - * @param {...any} items + * Adds array of items to a scrollable list. + * @param {Container[]} items - items to add. */ addItems(items: Container[]) { @@ -160,8 +177,8 @@ export class ScrollBox extends Container } /** - * Adds an item to a scrollable list. - * @param {...any} items + * Adds one or more items to a scrollable list. + * @param {Container} items - one or more items to add. */ addItem(...items: T): T[0] { @@ -178,6 +195,21 @@ export class ScrollBox extends Container console.error('ScrollBox item should have size'); } + child.eventMode = 'static'; + + if (utils.isMobile.any) + { + child.on('pointerdown', () => { this.pressedChild = child; }); + child.on('pointerup', () => { this.pressedChild = null; }); + child.on('pointerupoutside', () => { this.pressedChild = null; }); + } + else + { + child.on('mousedown', () => { this.pressedChild = child; }); + child.on('mouseup', () => { this.pressedChild = null; }); + child.on('mouseupoutside', () => { this.pressedChild = null; }); + } + this.list.addChild(child); if (!this.options.disableDynamicRendering) @@ -193,7 +225,7 @@ export class ScrollBox extends Container /** * Removes an item from a scrollable list. - * @param itemID + * @param {number} itemID - id of the item to remove. */ removeItem(itemID: number) { @@ -211,7 +243,7 @@ export class ScrollBox extends Container /** * Checks if the item is visible or scrolled out of the visible part of the view.* Adds an item to a scrollable list. - * @param item + * @param {Container} item - item to check. */ isItemVisible(item: Container): boolean { @@ -244,7 +276,10 @@ export class ScrollBox extends Container return isVisible; } - /** Returns all inner items in a list. */ + /** + * Returns all inner items in a list. + * @returns {Array | Array} - list of items. + */ get items(): Container[] | [] { return this.list?.children ?? []; @@ -310,14 +345,14 @@ export class ScrollBox extends Container { this.isDragging = 0; this._trackpad.pointerUp(); - this.restoreInteractivity(); + this.restoreItemsInteractivity(); }); this.on('pointerupoutside', () => { this.isDragging = 0; this._trackpad.pointerUp(); - this.restoreInteractivity(); + this.restoreItemsInteractivity(); }); this.on('globalpointermove', (e: FederatedPointerEvent) => @@ -328,9 +363,11 @@ export class ScrollBox extends Container if (!this.isDragging) return; - if (this.interactiveStorage.size === 0) + if (this.pressedChild) { - this.disableInteractivity(this.items); + this.revertClick(this.pressedChild); + + this.pressedChild = null; } }); @@ -339,46 +376,6 @@ export class ScrollBox extends Container this.on('mouseover', onMouseHover, this).on('mouseout', onMouseOut, this); } - // prevent interactivity on all children - protected disableInteractivity(items: DisplayObject[]) - { - items.forEach((item, id) => - { - this.emitPointerOpOutside(item); - - if (item.interactive) - { - this.interactiveStorage.set(id, item); - item.eventMode = 'auto'; - item.interactiveChildren = false; - } - }); - } - - protected emitPointerOpOutside(item: DisplayObject) - { - if (item.eventMode !== 'auto') - { - item.emit('pointerupoutside', null); - } - - if (item instanceof Container && item.children) - { - item.children.forEach((child) => this.emitPointerOpOutside(child)); - } - } - - // restore interactivity on all children that had it - protected restoreInteractivity() - { - this.interactiveStorage.forEach((item, itemID) => - { - item.eventMode = 'static'; - item.interactiveChildren = false; - this.interactiveStorage.delete(itemID); - }); - } - protected setInteractive(interactive: boolean) { this.eventMode = interactive ? 'static' : 'auto'; @@ -703,4 +700,37 @@ export class ScrollBox extends Container super.destroy(); } + + protected restoreItemsInteractivity() + { + this.interactiveStorage.forEach((element) => + { + element.item.eventMode = element.eventMode; + }); + + this.interactiveStorage.length = 0; + } + + protected revertClick(item: DisplayObject) + { + if (item.eventMode !== 'auto') + { + utils.isMobile.any + ? item.emit('pointerupoutside', null) + : item.emit('mouseupoutside', null); + + this.interactiveStorage.push({ + item, + eventMode: item.eventMode, + }); + + item.eventMode = 'auto'; + } + + // need to disable click for all children too + if (item instanceof Container && item.children) + { + item.children.forEach((child) => this.revertClick(child)); + } + } } From 0ea45f82bfce8c14d9a36a35f1ecf5cbaf96f37c Mon Sep 17 00:00:00 2001 From: Dmytro Soldatov Date: Thu, 29 Jun 2023 23:10:05 +0300 Subject: [PATCH 3/8] optimize --- src/ScrollBox.ts | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/ScrollBox.ts b/src/ScrollBox.ts index 32e13479..d4593c67 100644 --- a/src/ScrollBox.ts +++ b/src/ScrollBox.ts @@ -66,6 +66,7 @@ export class ScrollBox extends Container item: DisplayObject; eventMode: EventMode; }[] = []; + protected visibleItems: Container[] = []; protected pressedChild: Container; protected ticker = Ticker.shared; protected options: ScrollBoxOptions; @@ -197,19 +198,6 @@ export class ScrollBox extends Container child.eventMode = 'static'; - if (utils.isMobile.any) - { - child.on('pointerdown', () => { this.pressedChild = child; }); - child.on('pointerup', () => { this.pressedChild = null; }); - child.on('pointerupoutside', () => { this.pressedChild = null; }); - } - else - { - child.on('mousedown', () => { this.pressedChild = child; }); - child.on('mouseup', () => { this.pressedChild = null; }); - child.on('mouseupoutside', () => { this.pressedChild = null; }); - } - this.list.addChild(child); if (!this.options.disableDynamicRendering) @@ -339,6 +327,19 @@ export class ScrollBox extends Container const touchPoint = this.worldTransform.applyInverse(e.global); this._trackpad.pointerDown(touchPoint); + + const listTouchPoint = this.list.worldTransform.applyInverse(e.global); + + this.visibleItems.forEach((item) => + { + if (item.x < listTouchPoint.x + && item.x + item.width > listTouchPoint.x + && item.y < listTouchPoint.y + && item.y + item.height > listTouchPoint.y) + { + this.pressedChild = item; + } + }); }); this.on('pointerup', () => @@ -346,6 +347,8 @@ export class ScrollBox extends Container this.isDragging = 0; this._trackpad.pointerUp(); this.restoreItemsInteractivity(); + + this.pressedChild = null; }); this.on('pointerupoutside', () => @@ -353,6 +356,8 @@ export class ScrollBox extends Container this.isDragging = 0; this._trackpad.pointerUp(); this.restoreItemsInteractivity(); + + this.pressedChild = null; }); this.on('globalpointermove', (e: FederatedPointerEvent) => @@ -607,9 +612,12 @@ export class ScrollBox extends Container return; } + this.visibleItems.length = 0; + this.items.forEach((child) => { child.renderable = this.isItemVisible(child); + this.visibleItems.push(child); }); } From 399c883dc810cdb7a26a8424298b08b62d41b7a9 Mon Sep 17 00:00:00 2001 From: Dmytro Soldatov Date: Thu, 29 Jun 2023 23:14:40 +0300 Subject: [PATCH 4/8] Update story for better testing this edge case --- src/stories/scrollBox/ScrollBoxGraphics.stories.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/stories/scrollBox/ScrollBoxGraphics.stories.ts b/src/stories/scrollBox/ScrollBoxGraphics.stories.ts index 902e7b0e..4aef545d 100644 --- a/src/stories/scrollBox/ScrollBoxGraphics.stories.ts +++ b/src/stories/scrollBox/ScrollBoxGraphics.stories.ts @@ -47,7 +47,10 @@ export const UseGraphics: StoryFn = ({ for (let i = 0; i < itemsAmount; i++) { - const buttonWrapper = new Container(); + const buttonWrapper1 = new Container(); + const buttonWrapper2 = new Container(); + + buttonWrapper1.addChild(buttonWrapper2); const button = new FancyButton({ defaultView: new Graphics().beginFill(0xa5e24d).drawRoundedRect(0, 0, elementsWidth, elementsHeight, radius), @@ -59,12 +62,12 @@ export const UseGraphics: StoryFn = ({ }) }); - buttonWrapper.addChild(button); + buttonWrapper2.addChild(button); button.anchor.set(0); button.onPress.connect(() => onPress(i + 1)); - items.push(buttonWrapper); + items.push(buttonWrapper1); } // Component usage !!! From da6636122b05e90d0952d7c6b7b081c572b296bd Mon Sep 17 00:00:00 2001 From: Dmytro Soldatov Date: Thu, 29 Jun 2023 23:25:17 +0300 Subject: [PATCH 5/8] cleanup --- src/ScrollBox.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ScrollBox.ts b/src/ScrollBox.ts index d4593c67..cd17379b 100644 --- a/src/ScrollBox.ts +++ b/src/ScrollBox.ts @@ -55,11 +55,6 @@ export class ScrollBox extends Container protected list: List; - protected readonly freeSlot = { - x: 0, - y: 0, - }; - protected _trackpad: Trackpad; protected isDragging = 0; protected interactiveStorage: { From a041f30695aa2e6005928c4c1acea39fe2cf7b79 Mon Sep 17 00:00:00 2001 From: Dmytro Soldatov Date: Thu, 29 Jun 2023 23:29:56 +0300 Subject: [PATCH 6/8] cleanup --- src/stories/scrollBox/ScrollBoxGraphics.stories.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/stories/scrollBox/ScrollBoxGraphics.stories.ts b/src/stories/scrollBox/ScrollBoxGraphics.stories.ts index 4aef545d..16cf054d 100644 --- a/src/stories/scrollBox/ScrollBoxGraphics.stories.ts +++ b/src/stories/scrollBox/ScrollBoxGraphics.stories.ts @@ -47,11 +47,6 @@ export const UseGraphics: StoryFn = ({ for (let i = 0; i < itemsAmount; i++) { - const buttonWrapper1 = new Container(); - const buttonWrapper2 = new Container(); - - buttonWrapper1.addChild(buttonWrapper2); - const button = new FancyButton({ defaultView: new Graphics().beginFill(0xa5e24d).drawRoundedRect(0, 0, elementsWidth, elementsHeight, radius), hoverView: new Graphics().beginFill(0xfec230).drawRoundedRect(0, 0, elementsWidth, elementsHeight, radius), @@ -62,12 +57,10 @@ export const UseGraphics: StoryFn = ({ }) }); - buttonWrapper2.addChild(button); - button.anchor.set(0); button.onPress.connect(() => onPress(i + 1)); - items.push(buttonWrapper1); + items.push(button); } // Component usage !!! From 29fca974832a5a1c4ee3f2df9836eacbde9a7e6c Mon Sep 17 00:00:00 2001 From: Dmytro Soldatov Date: Fri, 30 Jun 2023 10:28:56 +0300 Subject: [PATCH 7/8] fix issue --- src/ScrollBox.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ScrollBox.ts b/src/ScrollBox.ts index cd17379b..c51461a0 100644 --- a/src/ScrollBox.ts +++ b/src/ScrollBox.ts @@ -130,9 +130,10 @@ export class ScrollBox extends Container elementsMargin: options.elementsMargin, vertPadding: options.vertPadding, horPadding: options.horPadding, - items: options.items, }); + this.addItems(options.items); + if (this.hasBounds) { this.addMask(); From 41a9b25761bd7c29089a9d0766cbc48993d71234 Mon Sep 17 00:00:00 2001 From: Dmytro Soldatov Date: Fri, 30 Jun 2023 11:17:09 +0300 Subject: [PATCH 8/8] refactor --- src/ScrollBox.ts | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/ScrollBox.ts b/src/ScrollBox.ts index c51461a0..f352dab5 100644 --- a/src/ScrollBox.ts +++ b/src/ScrollBox.ts @@ -670,27 +670,11 @@ export class ScrollBox extends Container this._trackpad.update(); - if (this.options.type === 'horizontal') - { - if (this.list.x !== this._trackpad.x) - { - this.renderAllItems(); - this.list.x = this._trackpad.x; - } - else - { - this.stopRenderHiddenItems(); - } - } - else - if (this.list.y !== this._trackpad.y) - { - this.renderAllItems(); - this.list.y = this._trackpad.y; - } - else + const type = this.options.type === 'horizontal' ? 'x' : 'y'; + + if (this.list[type] !== this._trackpad[type]) { - this.stopRenderHiddenItems(); + this.list[type] = this._trackpad[type]; } }