rexrainbow / phaser3-rex-notes

Notes of phaser3 engine
MIT License
1.18k stars 260 forks source link

ScrollTo element in scrollable container, similar to HTML <a> tag #362

Closed dtturcotte closed 1 week ago

dtturcotte commented 1 year ago

Hi, I'm looking for functionality similar to anchor tag in your scrollablePanel. Basically, I need the scrollable panel to automatically scroll down to a textElement added using "this.scene.rexUI.add.BBCodeText(...)". I've tried doing this manually by getting the y coordinate of the target scrollTo element and getting a scroll percentage based on targetY/scrollablePanel.height, but this doesn't get me exactly where I want. Just like anchor tag, I want it to scroll to where the target element begins, just to reveal the full target element, and not past it to show content below.

        let scrollToElement = this.todoTextElements.find((t) => t.isCurrentMission)
        let scrollToElementY = scrollToElement.textElement.y + scrollToElement.textElement.height
        let contentHeight = this.todoTextElements.reduce((totalHeight, currentElement) => {
            return totalHeight + currentElement.textElement.height
        }, 0)
        let scrollToPercentage = scrollToElementY / contentHeight

        leftScrollablePanel = this.updateScrollablePanel(
            leftScrollablePanel,
            this.todoTextElements.map((t) => t.textElement),
            scrollToPercentage,
            true
        )

Which calls:

    updateScrollablePanel(scrollablePanel: any, elementArr: Array<any>, scrollToY: number, usePadding: boolean) {
        let sizer = scrollablePanel.getElement('panel')
        sizer.clear(true)
        elementArr.forEach((element) => {
            sizer.add(element)
        })
        scrollablePanel.layout()

        scrollablePanel.setT(scrollToY, true)

        return scrollablePanel
    }

In this example: it should scroll to "Objective: Scenario 4" so that it's fully visible:

Screen Shot 2023-06-17 at 1 49 04 PM

In the result of the code, however, it's scroll way past it:

Screen Shot 2023-06-17 at 1 48 44 PM
rexrainbow commented 1 year ago

Add method scrollToChild to align top/left of child to top/left of panel. See this demo, line 41.

dtturcotte commented 1 year ago

I was hoping for it to only scroll down to the element to reveal it, but not past it. So, for example, if scrolling to item-20 (panel.scrollToChild(panel.getByName('item-20', true));), it would look like this:

Screen Shot 2023-06-18 at 1 36 05 PM
rexrainbow commented 1 year ago

Add align parameter to scrollToChild method. Now item-20 will be scrolled at bottom of panel in this demo.

dtturcotte commented 1 year ago

Thank you. Please let me know when you're able to update the npm package. Hopefully soon?

rexrainbow commented 1 year ago

NPM package will be upgraded at end of this month.

dtturcotte commented 1 month ago

Re-opening this. I'm trying to use scroll to child, but with a Phaser.GameObjects.Container. The scrollToChild works with anything that's not a container, but I need it to work with a container. I've set the size of my containers to the total size of their contents, but I still get this error.

ScrollToChild.js:60 Uncaught (in promise) TypeError: child.getTopLeft is not a function at RexUIScrollablePanel.AlignChild (ScrollToChild.js:60:56) at RexUIScrollablePanel.ScrollToChild [as scrollToChild] (ScrollToChild.js:9:24) at RexUIScrollablePanel.update (rexUIScrollablePanel.ts:151:1) at QuestStatusUI.addMissionJourney (questStatusUI.ts:229:1) at new QuestStatusUI (questStatusUI.ts:22:1) at WatchComponent.addSubUi (watchComponent.ts:91:1) at WatchComponent.addUi (watchComponent.ts:122:1) at new WatchComponent (watchComponent.ts:21:1) at GUIScene.addComponents (guiScene.ts:709:1) at GUIScene.addUi (guiScene.ts:719:1)

Referencing the source code, it's happening here

default:
    var dTop = scrollableBlock.top - child.getTopLeft().y; <---------- ERROR HERE
    var dBottom = scrollableBlock.bottom - child.getBottomLeft().y;
    if ((dTop <= 0) && (dBottom >= 0)) {
        delta = 0;
    } else {
        delta = (Math.abs(dTop) <= Math.abs(dBottom)) ? dTop : dBottom;
    }
    break;
}

Relevant parts of my code:

createCircleContainers() snippet:

    const circleContainer = this.scene.add
        .container(xPos, yPos)
        .setSize(circle.width, circle.height)
        .setDepth(1006)
        .add(circle)

    circleContainer.isScrollTo = isScrollTo
    circleContainer.name = `circle_container_${i}`

Add scrollable panel

this.journeyScrollablePanel = new RexUIScrollablePanel(this.scene, this.width / 2, 255, 400, 250, 'aqua', {}, 'sizer')
this.scene.add.existing(this.journeyScrollablePanel)
const circleContainers = this.createCircleContainers()
this.journeyScrollablePanel.update(circleContainers, true, true, false, null)

this.journeyScrollablePanel.update is

    public update(elementArr: any[], useXAsLeftPadding: boolean, isScrollToItem: boolean, isAddNewLines: boolean, config: any): void {
        const sizer = this.getSizerElement()

        sizer.clear(true)

        const scrollablePanelConfig = {
            padding: {
                top: config && config.paddingTop != null ? config.paddingTop : 0,
                left: config && config.paddingLeft != null ? config.paddingLeft : 0,
                bottom: config && config.paddingBottom != null ? config.paddingBottom : 0,
                right: config && config.paddingRight != null ? config.paddingRight : 0,
            },
            align: config && config.align ? config.align : 'left',
        }

        elementArr.forEach((element): void => {
            if (useXAsLeftPadding) {
                scrollablePanelConfig.padding.left = element.x
            }
            if (isAddNewLines && sizer instanceof FixWidthSizer) {
                sizer.addNewLine()
            }

            sizer.add(element, scrollablePanelConfig)
        })

        this.layout()

        // https://rexrainbow.github.io/phaser3-rex-notes/docs/site/ui-scrollablepanel/#scroll-to-child
        if (isScrollToItem) {
            const firstScrollToElement = elementArr.find((el) => el.isScrollTo)
            if (firstScrollToElement) {
                this.scrollToChild(firstScrollToElement)
            }
        }
    }

Can you help?

dtturcotte commented 1 month ago

The way I've tried to solve this is to manually add the required methods: see the forked scrollTo rexUI demo here.

I'd love a better solution though.

    for (var i = 0; i < 50; i++) {
        var name = `item-${i}`;
        var label = scene.rexUI.add.label({
            background: scene.rexUI.add.roundRectangle({
                color: COLOR_PRIMARY
            }),
            text: scene.add.text(0, 0, name),
            space: { left: 10, right: 10, top: 10, bottom: 10 },
            name: name,
        });

        let container = scene.add.container(0, 0, label).setSize(label.width, label.height);

        container.getTopLeft = function (output) {
            if (!output) {
                output = new Phaser.Math.Vector2();
            }
            output.x = this.x - this.displayWidth * this.originX;
            output.y = this.y - this.displayHeight * this.originY;
            return output;
        };

        container.getBottomLeft = function (output) {
            if (!output) {
                output = new Phaser.Math.Vector2();
            }
            output.x = this.x - this.displayWidth * this.originX;
            output.y = this.y + this.displayHeight * (1 - this.originY);
            return output;
        };

        container.name = name;
        panel.add(container, { expand: true });
    }
rexrainbow commented 1 month ago

Sorry for missing this thread. I had rewrote some logic of scrollToChild method, to avoid using built-in method to get top-left/bottom-left position. Thus you don't have to add extra method at all. NPM package will be upgraded at end of this month.