Skip to content

Commit

Permalink
Merge branch 'dev' into fix/scrollbox-bidirectional-flow-v8
Browse files Browse the repository at this point in the history
  • Loading branch information
Zyie authored May 22, 2024
2 parents 525d510 + 6ca85ef commit db12287
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 60 deletions.
81 changes: 76 additions & 5 deletions src/FancyButton.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-len */
import { Container, isMobile, NineSliceSprite, ObservablePoint, Rectangle, Texture, Ticker } from 'pixi.js';
import { Group, Tween } from 'tweedle.js';
import { ButtonContainer } from './Button';
Expand Down Expand Up @@ -63,6 +64,8 @@ export type ButtonOptions = ViewsInput & {
iconOffset?: Offset;
defaultTextScale?: Pos | number;
defaultIconScale?: Pos | number;
defaultTextAnchor?: Pos | number;
defaultIconAnchor?: Pos | number;
animations?: StateAnimations;
nineSliceSprite?: [number, number, number, number];
ignoreRefitting?: boolean;
Expand Down Expand Up @@ -144,6 +147,12 @@ export class FancyButton extends ButtonContainer
/** Base icon scaling to take into account when fitting inside the button */
protected _defaultIconScale: Pos = { x: 1, y: 1 };

/** Base text anchor to take into account when fitting and placing inside the button */
protected _defaultTextAnchor: Pos = { x: 0.5, y: 0.5 };

/** Base icon anchor to take into account when fitting and placing inside the button */
protected _defaultIconAnchor: Pos = { x: 0.5, y: 0.5 };

/**
* Creates a button with a lot of tweaks.
* @param {object} options - Button options.
Expand All @@ -162,6 +171,8 @@ export class FancyButton extends ButtonContainer
* when all animations scales will be applied to the inner view.
* @param {number} options.defaultTextScale - Base text scaling to take into account when fitting inside the button.
* @param {number} options.defaultIconScale - Base icon scaling to take into account when fitting inside the button.
* @param {number} options.defaultTextAnchor - Base text anchor to take into account when fitting and placing inside the button.
* @param {number} options.defaultIconAnchor - Base icon anchor to take into account when fitting and placing inside the button.
* @param {number} options.anchor - Anchor point of the button.
* @param {number} options.anchorX - Horizontal anchor point of the button.
* @param {number} options.anchorY - Vertical anchor point of the button.
Expand All @@ -185,6 +196,8 @@ export class FancyButton extends ButtonContainer
iconOffset,
defaultTextScale: textScale,
defaultIconScale: iconScale,
defaultTextAnchor: textAnchor,
defaultIconAnchor: iconAnchor,
scale,
anchor,
anchorX,
Expand All @@ -206,6 +219,8 @@ export class FancyButton extends ButtonContainer
this.iconOffset = iconOffset;
this.defaultTextScale = textScale;
this.defaultIconScale = iconScale;
this.defaultTextAnchor = textAnchor;
this.defaultIconAnchor = iconAnchor;
this.scale.set(scale ?? 1);

if (animations)
Expand Down Expand Up @@ -323,7 +338,6 @@ export class FancyButton extends ButtonContainer
this._defaultTextScale = { x, y };
}

this._views.textView.anchor.set(0);
this.innerView.addChild(this._views.textView);

this.adjustTextView(this.state);
Expand Down Expand Up @@ -395,6 +409,7 @@ export class FancyButton extends ButtonContainer
if (!this.text) return;

const activeView = this.getStateView(this.state);
const { x: anchorX, y: anchorY } = this._defaultTextAnchor;

if (activeView)
{
Expand All @@ -409,7 +424,7 @@ export class FancyButton extends ButtonContainer
this._views.textView.y = activeView.y + (activeView.height / 2);
}

this._views.textView.anchor.set(0.5);
this._views.textView.anchor.set(anchorX, anchorY);

this.setOffset(this._views.textView, state, this.textOffset);
}
Expand Down Expand Up @@ -437,12 +452,24 @@ export class FancyButton extends ButtonContainer
this._views.iconView.scale.set(this._defaultIconScale.x, this._defaultIconScale.y);
}

const { x: anchorX, y: anchorY } = this._defaultIconAnchor;

fitToView(activeView, this._views.iconView, this.padding, false);

(this._views.iconView as Sprite).anchor?.set(0);
if ('anchor' in this._views.iconView)
{
(this._views.iconView.anchor as ObservablePoint).set(anchorX, anchorY);
}
else
{
this._views.iconView.pivot.set(
anchorX * (this._views.iconView.width / this._views.iconView.scale.x),
anchorY * (this._views.iconView.height / this._views.iconView.scale.y)
);
}

this._views.iconView.x = activeView.x + (activeView.width / 2) - (this._views.iconView.width / 2);
this._views.iconView.y = activeView.y + (activeView.height / 2) - (this._views.iconView.height / 2);
this._views.iconView.x = activeView.x + (activeView.width / 2);
this._views.iconView.y = activeView.y + (activeView.height / 2);

this.setOffset(this._views.iconView, state, this.iconOffset);
}
Expand Down Expand Up @@ -888,6 +915,50 @@ export class FancyButton extends ButtonContainer
return this.defaultIconScale;
}

/**
* Sets the base anchor for the text view to take into account when fitting and placing inside the button.
* @param {Pos | number} anchor - base anchor of the text view.
*/
set defaultTextAnchor(anchor: Pos | number)
{
if (anchor === undefined) return;
// Apply to the options so that the manual anchor is prioritized.
this.options.defaultTextAnchor = anchor;
const isNumber = typeof anchor === 'number';

this._defaultTextAnchor.x = isNumber ? anchor : anchor.x ?? 1;
this._defaultTextAnchor.y = isNumber ? anchor : anchor.y ?? 1;
this.adjustTextView(this.state);
}

/** Returns the text view base anchor. */
get defaultTextAnchor(): Pos
{
return this.defaultTextAnchor;
}

/**
* Sets the base anchor for the icon view to take into account when fitting and placing inside the button.
* @param {Pos | number} anchor - base anchor of the icon view.
*/
set defaultIconAnchor(anchor: Pos | number)
{
if (anchor === undefined) return;
// Apply to the options so that the manual anchor is prioritized.
this.options.defaultIconAnchor = anchor;
const isNumber = typeof anchor === 'number';

this._defaultIconAnchor.x = isNumber ? anchor : anchor.x ?? 1;
this._defaultIconAnchor.y = isNumber ? anchor : anchor.y ?? 1;
this.adjustIconView(this.state);
}

/** Returns the icon view base anchor. */
get defaultIconAnchor(): Pos
{
return this.defaultIconAnchor;
}

/**
* Sets width of a FancyButtons state views.
* If nineSliceSprite is set, then width will be set to nineSliceSprites of a views.
Expand Down
83 changes: 33 additions & 50 deletions src/ScrollBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type ScrollBoxOptions = {
globalScroll?: boolean;
shiftScroll?: boolean;
proximityRange?: number;
proximityDebounce?: number;
disableProximityCheck?: boolean;
} & Omit<ListOptions, 'children'>;

type ProximityEventData = {
Expand All @@ -36,9 +38,6 @@ type ProximityEventData = {
inRange: boolean;
};

const scrollerBounds = new Bounds();
const itemBounds = new Bounds();

/**
* Scrollable view, for arranging lists of Pixi container-based elements.
*
Expand Down Expand Up @@ -89,9 +88,10 @@ export class ScrollBox extends Container
protected isOver = false;

protected proximityRange: number;
protected proximityCache: boolean[] = [];
private lastScrollX!: number | null;
private lastScrollY!: number | null;
protected proximityStatusCache: boolean[] = [];
protected lastScrollX!: number | null;
protected lastScrollY!: number | null;
protected proximityCheckFrameCounter = 0;
public onProximityChange = new Signal<(data: ProximityEventData) => void>();

/**
Expand Down Expand Up @@ -202,7 +202,7 @@ export class ScrollBox extends Container
/** Remove all items from a scrollable list. */
removeItems()
{
this.proximityCache.length = 0;
this.proximityStatusCache.length = 0;
this.list.removeChildren();
}

Expand All @@ -228,7 +228,7 @@ export class ScrollBox extends Container
child.eventMode = 'static';

this.list.addChild(child);
this.proximityCache.push(false);
this.proximityStatusCache.push(false);

if (!this.options.disableDynamicRendering)
{
Expand All @@ -248,15 +248,16 @@ export class ScrollBox extends Container
removeItem(itemID: number)
{
this.list.removeItem(itemID);
this.proximityCache.splice(itemID, 1);
this.proximityStatusCache.splice(itemID, 1);
this.resize();
}

/**
* Checks if the item is visible or scrolled out of the visible part of the view.* Adds an item to a scrollable list.
* @param {Container} item - item to check.
* @param padding - proximity padding to consider the item visible.
*/
isItemVisible(item: Container): boolean
isItemVisible(item: Container, padding = 0): boolean
{
const isVertical = this.options.type === 'vertical' || !this.options.type;
let isVisible = false;
Expand All @@ -266,10 +267,7 @@ export class ScrollBox extends Container
{
const posY = item.y + list.y;

if (
posY + item.height + this.list.bottomPadding >= 0
&& posY - this.list.topPadding <= this.options.height
)
if (posY + item.height >= -padding && posY <= this.options.height + padding)
{
isVisible = true;
}
Expand All @@ -278,7 +276,7 @@ export class ScrollBox extends Container
{
const posX = item.x + list.x;

if (posX + item.width >= 0 && posX <= this.options.width)
if (posX + item.width >= -padding && posX <= this.options.width + padding)
{
isVisible = true;
}
Expand Down Expand Up @@ -758,43 +756,28 @@ export class ScrollBox extends Container
this.list[type] = this._trackpad[type];
}

if (this._trackpad.x !== this.lastScrollX || this._trackpad.y !== this.lastScrollY)
if (!this.options.disableProximityCheck && (
this._trackpad.x !== this.lastScrollX || this._trackpad.y !== this.lastScrollY
))
{
/**
* Wait a frame to ensure that the transforms of the scene graph are up-to-date.
* Since we are skipping this step on the 'getBounds' calls for performance's sake,
* this is necessary to ensure that the bounds are accurate.
*/
requestAnimationFrame(() => this.items.forEach((item, index) => this.checkItemProximity(item, index)));
this.lastScrollX = this._trackpad.x;
this.lastScrollY = this._trackpad.y;
}
}

private checkItemProximity(item: Container, index: number): void
{
/** Get the item bounds, capping the width and height to at least 1 for the purposes of intersection checking. */
item.getBounds(true, itemBounds);
itemBounds.width = Math.max(itemBounds.width, 1);
itemBounds.height = Math.max(itemBounds.height, 1);

// Get the scroller bounds, expanding them by the defined max distance.
this.getBounds(true, scrollerBounds);

scrollerBounds.x -= this.proximityRange;
scrollerBounds.y -= this.proximityRange;
scrollerBounds.width += this.proximityRange * 2;
scrollerBounds.height += this.proximityRange * 2;

// Check for intersection
const inRange = scrollerBounds.rectangle.intersects(itemBounds.rectangle);
const wasInRange = this.proximityCache[index];
this.proximityCheckFrameCounter++;
if (this.proximityCheckFrameCounter >= (this.options.proximityDebounce ?? 10))
{
this.items.forEach((item, index) =>
{
const inRange = this.isItemVisible(item, this.proximityRange);
const wasInRange = this.proximityStatusCache[index];

// If the item's proximity state has changed, emit the event
if (inRange !== wasInRange)
{
this.proximityCache[index] = inRange;
this.onProximityChange.emit({ item, index, inRange });
if (inRange !== wasInRange)
{
this.proximityStatusCache[index] = inRange;
this.onProximityChange.emit({ item, index, inRange });
}
});
this.lastScrollX = this._trackpad.x;
this.lastScrollY = this._trackpad.y;
this.proximityCheckFrameCounter = 0;
}
}
}

Expand Down
10 changes: 7 additions & 3 deletions src/stories/fancyButton/FancyButtonBitmapText.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const args = {
textOffsetX: 0,
textOffsetY: -7,
defaultTextScale: 0.99,
defaultTextAnchorX: 0.5,
defaultTextAnchorY: 0.5,
anchorX: 0.5,
anchorY: 0.5,
animationDuration: 100,
Expand All @@ -31,9 +33,11 @@ export const UsingSpriteAndBitmapText: StoryFn<typeof args> = (
textOffsetX,
textOffsetY,
defaultTextScale,
defaultTextAnchorX,
defaultTextAnchorY,
anchorX,
anchorY,
animationDuration
animationDuration,
},
context
) =>
Expand All @@ -54,14 +58,13 @@ export const UsingSpriteAndBitmapText: StoryFn<typeof args> = (
name: 'TitleFont',
style: {
...defaultTextStyle,

fill: textColor || defaultTextStyle.fill,
},
});

const title = new BitmapText({
text,
style: { fontFamily: 'TitleFont' },
style: { fontFamily: 'TitleFont', fontSize: defaultTextStyle.fontSize },
});

// Component usage !!!
Expand All @@ -74,6 +77,7 @@ export const UsingSpriteAndBitmapText: StoryFn<typeof args> = (
padding,
textOffset: { x: textOffsetX, y: textOffsetY },
defaultTextScale,
defaultTextAnchor: { x: defaultTextAnchorX, y: defaultTextAnchorY },
animations: {
hover: {
props: {
Expand Down
10 changes: 10 additions & 0 deletions src/stories/fancyButton/FancyButtonDynamicUpdate.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const args = {
textColor: '#FFFFFF',
defaultTextScale: 0.99,
defaultIconScale: 0.2,
defaultTextAnchorX: 0.5,
defaultTextAnchorY: 0.5,
defaultIconAnchorX: 0.5,
defaultIconAnchorY: 0.5,
padding: 11,
anchorX: 0.5,
anchorY: 0.5,
Expand All @@ -25,6 +29,10 @@ export const DynamicUpdate: StoryFn<typeof args> = ({
textColor,
defaultTextScale,
defaultIconScale,
defaultTextAnchorX,
defaultTextAnchorY,
defaultIconAnchorX,
defaultIconAnchorY,
disabled,
onPress,
padding,
Expand All @@ -50,6 +58,7 @@ export const DynamicUpdate: StoryFn<typeof args> = ({

button.iconView = Sprite.from(icon);
button.defaultIconScale = defaultIconScale;
button.defaultIconAnchor = { x: defaultIconAnchorX, y: defaultIconAnchorY };
button.iconOffset = { x: -100, y: -7 };

button.textView = new Text({
Expand All @@ -59,6 +68,7 @@ export const DynamicUpdate: StoryFn<typeof args> = ({
}
});
button.defaultTextScale = defaultTextScale;
button.defaultTextAnchor = { x: defaultTextAnchorX, y: defaultTextAnchorY };
button.textOffset = { x: 30, y: -7 };

button.padding = padding;
Expand Down
Loading

0 comments on commit db12287

Please sign in to comment.