@energy8platform/game-engine 0.10.10 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -74
- package/dist/index.cjs.js +1280 -296
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +362 -46
- package/dist/index.esm.js +1281 -298
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +16 -21
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +0 -2
- package/dist/lua.esm.js +16 -21
- package/dist/lua.esm.js.map +1 -1
- package/dist/react.cjs.js +2372 -11
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +17 -6
- package/dist/react.esm.js +2372 -12
- package/dist/react.esm.js.map +1 -1
- package/dist/ui.cjs.js +1553 -632
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +374 -46
- package/dist/ui.esm.js +1553 -634
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +1 -11
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.esm.js +1 -11
- package/dist/vite.esm.js.map +1 -1
- package/package.json +3 -18
- package/src/index.ts +3 -3
- package/src/lua/LuaEngine.ts +8 -18
- package/src/lua/SimulationRunner.ts +7 -3
- package/src/react/applyProps.ts +86 -0
- package/src/react/extendAll.ts +27 -6
- package/src/react/index.ts +1 -1
- package/src/react/jsx.d.ts +222 -0
- package/src/react/reconciler.ts +22 -5
- package/src/ui/BalanceDisplay.ts +31 -38
- package/src/ui/Button.ts +217 -53
- package/src/ui/FlexContainer.ts +479 -0
- package/src/ui/Label.ts +13 -0
- package/src/ui/Layout.ts +86 -87
- package/src/ui/Modal.ts +11 -1
- package/src/ui/Panel.ts +108 -36
- package/src/ui/ProgressBar.ts +85 -31
- package/src/ui/ScrollContainer.ts +397 -45
- package/src/ui/Toast.ts +47 -17
- package/src/ui/WinDisplay.ts +51 -39
- package/src/ui/index.ts +5 -11
- package/src/ui/view.ts +28 -0
- package/src/vite/index.ts +1 -11
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Container, type ColorSource } from 'pixi.js';
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
1
|
+
import { Container, Graphics, type ColorSource, Ticker } from 'pixi.js';
|
|
2
|
+
import { resolveView } from './view';
|
|
3
|
+
import type { ViewInput } from './view';
|
|
4
4
|
|
|
5
5
|
// ─── Types ───────────────────────────────────────────────
|
|
6
6
|
|
|
@@ -28,27 +28,33 @@ export interface ScrollContainerConfig {
|
|
|
28
28
|
/** Padding */
|
|
29
29
|
padding?: number;
|
|
30
30
|
|
|
31
|
-
/** Disable dynamic rendering (render all items even when offscreen) */
|
|
32
|
-
disableDynamicRendering?: boolean;
|
|
33
|
-
|
|
34
31
|
/** Disable easing/inertia */
|
|
35
32
|
disableEasing?: boolean;
|
|
36
33
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
34
|
+
/** Show scrollbar indicator (default: false) */
|
|
35
|
+
scrollbar?: boolean;
|
|
36
|
+
|
|
37
|
+
/** Custom scrollbar thumb view (string texture name, Texture, or Container) */
|
|
38
|
+
thumbView?: ViewInput;
|
|
39
|
+
|
|
40
|
+
/** Scrollbar width in px (default: 6) */
|
|
41
|
+
scrollbarWidth?: number;
|
|
42
|
+
|
|
43
|
+
/** Scrollbar padding from edge (default: 4) */
|
|
44
|
+
scrollbarPadding?: number;
|
|
45
|
+
|
|
46
|
+
/** Scrollbar color (when no thumbView, default: 0xaaaaaa) */
|
|
47
|
+
scrollbarColor?: number;
|
|
48
|
+
|
|
49
|
+
/** Scrollbar alpha (default: 0.5) */
|
|
50
|
+
scrollbarAlpha?: number;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
horizontal: 'horizontal',
|
|
44
|
-
both: 'bidirectional',
|
|
45
|
-
};
|
|
53
|
+
const DECELERATION = 0.95;
|
|
54
|
+
const MIN_VELOCITY = 0.5;
|
|
46
55
|
|
|
47
56
|
/**
|
|
48
|
-
* Scrollable container
|
|
49
|
-
*
|
|
50
|
-
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
51
|
-
* dynamic rendering optimization for off-screen items.
|
|
57
|
+
* Scrollable container with touch/drag, mouse wheel, and inertia.
|
|
52
58
|
*
|
|
53
59
|
* @example
|
|
54
60
|
* ```ts
|
|
@@ -66,61 +72,407 @@ const DIRECTION_MAP: Record<ScrollDirection, 'vertical' | 'horizontal' | 'bidire
|
|
|
66
72
|
* scene.container.addChild(scroll);
|
|
67
73
|
* ```
|
|
68
74
|
*/
|
|
69
|
-
export class ScrollContainer extends
|
|
70
|
-
|
|
75
|
+
export class ScrollContainer extends Container {
|
|
76
|
+
readonly __uiComponent = true as const;
|
|
77
|
+
|
|
78
|
+
private _viewport: { width: number; height: number };
|
|
79
|
+
private _internalSetup = true;
|
|
80
|
+
private _content: Container;
|
|
81
|
+
private _maskGfx: Graphics;
|
|
82
|
+
private _bg: Graphics | null = null;
|
|
83
|
+
private _scrollConfig: Required<Pick<ScrollContainerConfig, 'direction' | 'elementsMargin' | 'padding' | 'borderRadius' | 'disableEasing'>>;
|
|
84
|
+
private _items: Container[] = [];
|
|
85
|
+
|
|
86
|
+
// Scrollbar
|
|
87
|
+
private _scrollbar: Container | null = null;
|
|
88
|
+
private _scrollbarConfig: { width: number; padding: number };
|
|
89
|
+
|
|
90
|
+
// Drag state
|
|
91
|
+
private _dragging = false;
|
|
92
|
+
private _dragStart = { x: 0, y: 0 };
|
|
93
|
+
private _contentStart = { x: 0, y: 0 };
|
|
94
|
+
private _velocity = { x: 0, y: 0 };
|
|
95
|
+
private _lastDragPos = { x: 0, y: 0 };
|
|
96
|
+
private _lastDragTime = 0;
|
|
97
|
+
private _inertiaActive = false;
|
|
98
|
+
|
|
99
|
+
// Bound handlers for cleanup
|
|
100
|
+
private _onTickBound: ((ticker: Ticker) => void) | null = null;
|
|
101
|
+
private _onWheelBound: ((e: WheelEvent) => void) | null = null;
|
|
71
102
|
|
|
72
103
|
constructor(config: ScrollContainerConfig) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
super();
|
|
105
|
+
|
|
106
|
+
this._viewport = { width: config.width, height: config.height };
|
|
107
|
+
this._scrollConfig = {
|
|
108
|
+
direction: config.direction ?? 'vertical',
|
|
78
109
|
elementsMargin: config.elementsMargin ?? 0,
|
|
79
110
|
padding: config.padding ?? 0,
|
|
80
|
-
|
|
111
|
+
borderRadius: config.borderRadius ?? 0,
|
|
81
112
|
disableEasing: config.disableEasing ?? false,
|
|
82
|
-
globalScroll: config.globalScroll ?? true,
|
|
83
113
|
};
|
|
84
114
|
|
|
115
|
+
// Background
|
|
85
116
|
if (config.backgroundColor !== undefined) {
|
|
86
|
-
|
|
117
|
+
this._bg = new Graphics();
|
|
118
|
+
this._bg.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
119
|
+
.fill(config.backgroundColor);
|
|
120
|
+
this.addChild(this._bg);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Mask
|
|
124
|
+
this._maskGfx = new Graphics();
|
|
125
|
+
this._maskGfx.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
126
|
+
.fill(0xffffff);
|
|
127
|
+
this.addChild(this._maskGfx);
|
|
128
|
+
|
|
129
|
+
// Content container
|
|
130
|
+
this._content = new Container();
|
|
131
|
+
this._content.mask = this._maskGfx;
|
|
132
|
+
this.addChild(this._content);
|
|
133
|
+
|
|
134
|
+
// Interaction
|
|
135
|
+
this.eventMode = 'static';
|
|
136
|
+
this.hitArea = { contains: (x: number, y: number) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
137
|
+
|
|
138
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
139
|
+
this.on('pointermove', this._onPointerMove, this);
|
|
140
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
141
|
+
this.on('pointerupoutside', this._onPointerUp, this);
|
|
142
|
+
|
|
143
|
+
// Mouse wheel
|
|
144
|
+
this._onWheelBound = this._onWheel.bind(this);
|
|
145
|
+
|
|
146
|
+
// Scrollbar
|
|
147
|
+
const sbWidth = config.scrollbarWidth ?? 6;
|
|
148
|
+
const sbPadding = config.scrollbarPadding ?? 4;
|
|
149
|
+
this._scrollbarConfig = { width: sbWidth, padding: sbPadding };
|
|
150
|
+
|
|
151
|
+
if (config.scrollbar) {
|
|
152
|
+
const customThumb = resolveView(config.thumbView);
|
|
153
|
+
if (customThumb) {
|
|
154
|
+
this._scrollbar = customThumb;
|
|
155
|
+
} else {
|
|
156
|
+
const g = new Graphics();
|
|
157
|
+
g.roundRect(0, 0, sbWidth, 40, sbWidth / 2).fill(config.scrollbarColor ?? 0xaaaaaa);
|
|
158
|
+
g.alpha = config.scrollbarAlpha ?? 0.5;
|
|
159
|
+
this._scrollbar = g;
|
|
160
|
+
}
|
|
161
|
+
this._scrollbar.visible = false;
|
|
162
|
+
super.addChild(this._scrollbar);
|
|
87
163
|
}
|
|
88
164
|
|
|
89
|
-
|
|
165
|
+
this._internalSetup = false;
|
|
166
|
+
}
|
|
90
167
|
|
|
91
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Override addChild so external children are routed to scroll content.
|
|
170
|
+
* Enables `<scrollContainer><label /><panel /></scrollContainer>` in React JSX.
|
|
171
|
+
*/
|
|
172
|
+
override addChild<T extends Container>(...children: T[]): T {
|
|
173
|
+
if (this._internalSetup) {
|
|
174
|
+
return super.addChild(...children);
|
|
175
|
+
}
|
|
176
|
+
for (const child of children) {
|
|
177
|
+
this.addItem(child as any);
|
|
178
|
+
}
|
|
179
|
+
return children[0];
|
|
92
180
|
}
|
|
93
181
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
182
|
+
override removeChild<T extends Container>(...children: T[]): T {
|
|
183
|
+
if (this._internalSetup) {
|
|
184
|
+
return super.removeChild(...children);
|
|
185
|
+
}
|
|
186
|
+
for (const child of children) {
|
|
187
|
+
const idx = this._items.indexOf(child as any);
|
|
188
|
+
if (idx !== -1) {
|
|
189
|
+
this._items.splice(idx, 1);
|
|
190
|
+
this._content.removeChild(child);
|
|
101
191
|
}
|
|
102
192
|
}
|
|
193
|
+
this.layoutItems();
|
|
194
|
+
return children[0];
|
|
195
|
+
}
|
|
103
196
|
|
|
104
|
-
|
|
197
|
+
/** React reconciler update hook */
|
|
198
|
+
updateConfig(changed: Record<string, any>): void {
|
|
199
|
+
if ('width' in changed || 'height' in changed) {
|
|
200
|
+
this.setViewportSize(
|
|
201
|
+
changed.width ?? this._viewport.width,
|
|
202
|
+
changed.height ?? this._viewport.height,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Enable mouse wheel scrolling (call after adding to stage) */
|
|
208
|
+
enableWheel(canvas: HTMLCanvasElement): void {
|
|
209
|
+
if (this._onWheelBound) {
|
|
210
|
+
canvas.addEventListener('wheel', this._onWheelBound, { passive: false });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Set scrollable content. Replaces any existing items. */
|
|
215
|
+
setContent(content: Container): void {
|
|
216
|
+
this.clearItems();
|
|
105
217
|
const children = [...content.children] as Container[];
|
|
106
|
-
|
|
107
|
-
this.
|
|
218
|
+
for (const child of children) {
|
|
219
|
+
this.addItem(child);
|
|
108
220
|
}
|
|
109
221
|
}
|
|
110
222
|
|
|
111
223
|
/** Add a single item */
|
|
112
|
-
addItem(
|
|
113
|
-
this.
|
|
114
|
-
|
|
224
|
+
addItem(child: Container): this {
|
|
225
|
+
this._items.push(child);
|
|
226
|
+
this._content.addChild(child);
|
|
227
|
+
this.layoutItems();
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Remove all items */
|
|
232
|
+
clearItems(): void {
|
|
233
|
+
for (const item of this._items) {
|
|
234
|
+
this._content.removeChild(item);
|
|
235
|
+
}
|
|
236
|
+
this._items.length = 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Get items */
|
|
240
|
+
get items(): readonly Container[] {
|
|
241
|
+
return this._items;
|
|
115
242
|
}
|
|
116
243
|
|
|
117
|
-
/** Scroll to make a specific item
|
|
244
|
+
/** Scroll to make a specific item index visible */
|
|
118
245
|
scrollToItem(index: number): void {
|
|
119
|
-
this.
|
|
246
|
+
if (index < 0 || index >= this._items.length) return;
|
|
247
|
+
const item = this._items[index];
|
|
248
|
+
const isVert = this._scrollConfig.direction !== 'horizontal';
|
|
249
|
+
|
|
250
|
+
if (isVert) {
|
|
251
|
+
this._content.y = -item.y + this._scrollConfig.padding;
|
|
252
|
+
} else {
|
|
253
|
+
this._content.x = -item.x + this._scrollConfig.padding;
|
|
254
|
+
}
|
|
255
|
+
this.clampScroll();
|
|
120
256
|
}
|
|
121
257
|
|
|
122
258
|
/** Current scroll position */
|
|
123
259
|
get scrollPosition(): { x: number; y: number } {
|
|
124
|
-
return { x: this.
|
|
260
|
+
return { x: this._content.x, y: this._content.y };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Resize the scroll viewport */
|
|
264
|
+
setViewportSize(width: number, height: number): void {
|
|
265
|
+
this._viewport.width = width;
|
|
266
|
+
this._viewport.height = height;
|
|
267
|
+
|
|
268
|
+
this._maskGfx.clear();
|
|
269
|
+
this._maskGfx.roundRect(0, 0, width, height, this._scrollConfig.borderRadius).fill(0xffffff);
|
|
270
|
+
|
|
271
|
+
if (this._bg) {
|
|
272
|
+
this._bg.clear();
|
|
273
|
+
this._bg.roundRect(0, 0, width, height, this._scrollConfig.borderRadius)
|
|
274
|
+
.fill(0xffffff); // color will be overridden if needed
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.clampScroll();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Layout ──────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
private layoutItems(): void {
|
|
283
|
+
const { direction, elementsMargin, padding } = this._scrollConfig;
|
|
284
|
+
const isVert = direction !== 'horizontal';
|
|
285
|
+
let pos = padding;
|
|
286
|
+
|
|
287
|
+
for (const item of this._items) {
|
|
288
|
+
if (isVert) {
|
|
289
|
+
item.x = padding;
|
|
290
|
+
item.y = pos;
|
|
291
|
+
pos += item.height + elementsMargin;
|
|
292
|
+
} else {
|
|
293
|
+
item.x = pos;
|
|
294
|
+
item.y = padding;
|
|
295
|
+
pos += item.width + elementsMargin;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─── Drag handling ───────────────────────────────────
|
|
301
|
+
|
|
302
|
+
private _onPointerDown(e: import('pixi.js').FederatedPointerEvent): void {
|
|
303
|
+
this._dragging = true;
|
|
304
|
+
this._inertiaActive = false;
|
|
305
|
+
this._dragStart.x = e.globalX;
|
|
306
|
+
this._dragStart.y = e.globalY;
|
|
307
|
+
this._contentStart.x = this._content.x;
|
|
308
|
+
this._contentStart.y = this._content.y;
|
|
309
|
+
this._lastDragPos.x = e.globalX;
|
|
310
|
+
this._lastDragPos.y = e.globalY;
|
|
311
|
+
this._lastDragTime = Date.now();
|
|
312
|
+
this._velocity.x = 0;
|
|
313
|
+
this._velocity.y = 0;
|
|
314
|
+
|
|
315
|
+
this.stopInertia();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private _onPointerMove(e: import('pixi.js').FederatedPointerEvent): void {
|
|
319
|
+
if (!this._dragging) return;
|
|
320
|
+
|
|
321
|
+
const dx = e.globalX - this._dragStart.x;
|
|
322
|
+
const dy = e.globalY - this._dragStart.y;
|
|
323
|
+
const { direction } = this._scrollConfig;
|
|
324
|
+
|
|
325
|
+
if (direction !== 'horizontal') {
|
|
326
|
+
this._content.y = this._contentStart.y + dy;
|
|
327
|
+
}
|
|
328
|
+
if (direction !== 'vertical') {
|
|
329
|
+
this._content.x = this._contentStart.x + dx;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Track velocity
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
const dt = now - this._lastDragTime;
|
|
335
|
+
if (dt > 0) {
|
|
336
|
+
this._velocity.x = (e.globalX - this._lastDragPos.x) / dt * 16;
|
|
337
|
+
this._velocity.y = (e.globalY - this._lastDragPos.y) / dt * 16;
|
|
338
|
+
}
|
|
339
|
+
this._lastDragPos.x = e.globalX;
|
|
340
|
+
this._lastDragPos.y = e.globalY;
|
|
341
|
+
this._lastDragTime = now;
|
|
342
|
+
|
|
343
|
+
this.clampScroll();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private _onPointerUp(): void {
|
|
347
|
+
if (!this._dragging) return;
|
|
348
|
+
this._dragging = false;
|
|
349
|
+
|
|
350
|
+
if (!this._scrollConfig.disableEasing &&
|
|
351
|
+
(Math.abs(this._velocity.x) > MIN_VELOCITY || Math.abs(this._velocity.y) > MIN_VELOCITY)) {
|
|
352
|
+
this.startInertia();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─── Inertia ─────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
private startInertia(): void {
|
|
359
|
+
this._inertiaActive = true;
|
|
360
|
+
this._onTickBound = this._inertiaTick.bind(this);
|
|
361
|
+
Ticker.shared.add(this._onTickBound);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private stopInertia(): void {
|
|
365
|
+
if (this._onTickBound && this._inertiaActive) {
|
|
366
|
+
Ticker.shared.remove(this._onTickBound);
|
|
367
|
+
this._inertiaActive = false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private _inertiaTick(): void {
|
|
372
|
+
const { direction } = this._scrollConfig;
|
|
373
|
+
|
|
374
|
+
if (direction !== 'horizontal') {
|
|
375
|
+
this._content.y += this._velocity.y;
|
|
376
|
+
this._velocity.y *= DECELERATION;
|
|
377
|
+
}
|
|
378
|
+
if (direction !== 'vertical') {
|
|
379
|
+
this._content.x += this._velocity.x;
|
|
380
|
+
this._velocity.x *= DECELERATION;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.clampScroll();
|
|
384
|
+
|
|
385
|
+
if (Math.abs(this._velocity.x) < MIN_VELOCITY && Math.abs(this._velocity.y) < MIN_VELOCITY) {
|
|
386
|
+
this.stopInertia();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─── Mouse wheel ─────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
private _onWheel(e: WheelEvent): void {
|
|
393
|
+
const { direction } = this._scrollConfig;
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
|
|
396
|
+
if (direction !== 'horizontal') {
|
|
397
|
+
this._content.y -= e.deltaY;
|
|
398
|
+
}
|
|
399
|
+
if (direction !== 'vertical') {
|
|
400
|
+
this._content.x -= e.deltaX;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
this.clampScroll();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ─── Scroll bounds ───────────────────────────────────
|
|
407
|
+
|
|
408
|
+
private clampScroll(): void {
|
|
409
|
+
const { direction } = this._scrollConfig;
|
|
410
|
+
const bounds = this._content.getLocalBounds();
|
|
411
|
+
|
|
412
|
+
if (direction !== 'horizontal') {
|
|
413
|
+
const contentHeight = bounds.height + bounds.y;
|
|
414
|
+
const maxScroll = Math.min(0, this._viewport.height - contentHeight);
|
|
415
|
+
this._content.y = Math.max(maxScroll, Math.min(0, this._content.y));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (direction !== 'vertical') {
|
|
419
|
+
const contentWidth = bounds.width + bounds.x;
|
|
420
|
+
const maxScroll = Math.min(0, this._viewport.width - contentWidth);
|
|
421
|
+
this._content.x = Math.max(maxScroll, Math.min(0, this._content.x));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this.updateScrollbar();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private updateScrollbar(): void {
|
|
428
|
+
if (!this._scrollbar) return;
|
|
429
|
+
const { direction } = this._scrollConfig;
|
|
430
|
+
const { width: sbW, padding: sbPad } = this._scrollbarConfig;
|
|
431
|
+
const bounds = this._content.getLocalBounds();
|
|
432
|
+
const isVert = direction !== 'horizontal';
|
|
433
|
+
|
|
434
|
+
if (isVert) {
|
|
435
|
+
const contentH = bounds.height + bounds.y;
|
|
436
|
+
if (contentH <= this._viewport.height) {
|
|
437
|
+
this._scrollbar.visible = false;
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
this._scrollbar.visible = true;
|
|
441
|
+
const ratio = this._viewport.height / contentH;
|
|
442
|
+
const thumbH = Math.max(20, this._viewport.height * ratio);
|
|
443
|
+
const scrollRange = this._viewport.height - thumbH;
|
|
444
|
+
const scrollProgress = -this._content.y / (contentH - this._viewport.height);
|
|
445
|
+
|
|
446
|
+
this._scrollbar.x = this._viewport.width - sbW - sbPad;
|
|
447
|
+
this._scrollbar.y = scrollProgress * scrollRange;
|
|
448
|
+
this._scrollbar.height = thumbH;
|
|
449
|
+
this._scrollbar.width = sbW;
|
|
450
|
+
} else {
|
|
451
|
+
const contentW = bounds.width + bounds.x;
|
|
452
|
+
if (contentW <= this._viewport.width) {
|
|
453
|
+
this._scrollbar.visible = false;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this._scrollbar.visible = true;
|
|
457
|
+
const ratio = this._viewport.width / contentW;
|
|
458
|
+
const thumbW = Math.max(20, this._viewport.width * ratio);
|
|
459
|
+
const scrollRange = this._viewport.width - thumbW;
|
|
460
|
+
const scrollProgress = -this._content.x / (contentW - this._viewport.width);
|
|
461
|
+
|
|
462
|
+
this._scrollbar.y = this._viewport.height - sbW - sbPad;
|
|
463
|
+
this._scrollbar.x = scrollProgress * scrollRange;
|
|
464
|
+
this._scrollbar.width = thumbW;
|
|
465
|
+
this._scrollbar.height = sbW;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
|
|
470
|
+
this.stopInertia();
|
|
471
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
472
|
+
this.off('pointermove', this._onPointerMove, this);
|
|
473
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
474
|
+
this.off('pointerupoutside', this._onPointerUp, this);
|
|
475
|
+
this._items.length = 0;
|
|
476
|
+
super.destroy(options);
|
|
125
477
|
}
|
|
126
478
|
}
|
package/src/ui/Toast.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Container, Graphics, Text } from 'pixi.js';
|
|
2
2
|
import { Tween } from '../animation/Tween';
|
|
3
3
|
import { Easing } from '../animation/Easing';
|
|
4
|
+
import { resolveView } from './view';
|
|
5
|
+
import type { ViewInput } from './view';
|
|
4
6
|
|
|
5
7
|
export type ToastType = 'info' | 'success' | 'warning' | 'error';
|
|
6
8
|
|
|
@@ -9,6 +11,8 @@ export interface ToastConfig {
|
|
|
9
11
|
duration?: number;
|
|
10
12
|
/** Toast position from bottom */
|
|
11
13
|
bottomOffset?: number;
|
|
14
|
+
/** Custom background view (string texture name, Texture, or Container). Sized to fit text. */
|
|
15
|
+
backgroundView?: ViewInput;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
const TOAST_COLORS: Record<ToastType, number> = {
|
|
@@ -29,10 +33,13 @@ const TOAST_COLORS: Record<ToastType, number> = {
|
|
|
29
33
|
* ```
|
|
30
34
|
*/
|
|
31
35
|
export class Toast extends Container {
|
|
32
|
-
|
|
36
|
+
readonly __uiComponent = true as const;
|
|
37
|
+
|
|
38
|
+
private _bg: Container;
|
|
39
|
+
private _customBg: boolean;
|
|
33
40
|
private _text: Text;
|
|
34
|
-
private _config: Required<ToastConfig
|
|
35
|
-
private
|
|
41
|
+
private _config: Required<Pick<ToastConfig, 'duration' | 'bottomOffset'>>;
|
|
42
|
+
private _dismissPending = false;
|
|
36
43
|
|
|
37
44
|
constructor(config: ToastConfig = {}) {
|
|
38
45
|
super();
|
|
@@ -42,7 +49,9 @@ export class Toast extends Container {
|
|
|
42
49
|
bottomOffset: config.bottomOffset ?? 60,
|
|
43
50
|
};
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
const customBg = resolveView(config.backgroundView);
|
|
53
|
+
this._customBg = !!customBg;
|
|
54
|
+
this._bg = customBg ?? new Graphics();
|
|
46
55
|
this.addChild(this._bg);
|
|
47
56
|
|
|
48
57
|
this._text = new Text({
|
|
@@ -68,9 +77,9 @@ export class Toast extends Container {
|
|
|
68
77
|
viewWidth?: number,
|
|
69
78
|
viewHeight?: number,
|
|
70
79
|
): Promise<void> {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
// Cancel any pending dismiss
|
|
81
|
+
Tween.killTweensOf(this);
|
|
82
|
+
this._dismissPending = false;
|
|
74
83
|
|
|
75
84
|
this._text.text = message;
|
|
76
85
|
|
|
@@ -80,9 +89,17 @@ export class Toast extends Container {
|
|
|
80
89
|
const radius = 8;
|
|
81
90
|
|
|
82
91
|
// Draw the background
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
if (this._customBg) {
|
|
93
|
+
this._bg.width = width;
|
|
94
|
+
this._bg.height = height;
|
|
95
|
+
this._bg.x = -width / 2;
|
|
96
|
+
this._bg.y = -height / 2;
|
|
97
|
+
} else {
|
|
98
|
+
const g = this._bg as Graphics;
|
|
99
|
+
g.clear();
|
|
100
|
+
g.roundRect(-width / 2, -height / 2, width, height, radius);
|
|
101
|
+
g.fill(TOAST_COLORS[type]);
|
|
102
|
+
}
|
|
86
103
|
|
|
87
104
|
// Position
|
|
88
105
|
if (viewWidth && viewHeight) {
|
|
@@ -97,9 +114,12 @@ export class Toast extends Container {
|
|
|
97
114
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
98
115
|
|
|
99
116
|
if (this._config.duration > 0) {
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
this._dismissPending = true;
|
|
118
|
+
await Tween.delay(this._config.duration);
|
|
119
|
+
if (this._dismissPending) {
|
|
120
|
+
this._dismissPending = false;
|
|
121
|
+
await this.dismiss();
|
|
122
|
+
}
|
|
103
123
|
}
|
|
104
124
|
}
|
|
105
125
|
|
|
@@ -109,12 +129,22 @@ export class Toast extends Container {
|
|
|
109
129
|
async dismiss(): Promise<void> {
|
|
110
130
|
if (!this.visible) return;
|
|
111
131
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this._dismissTimeout = null;
|
|
115
|
-
}
|
|
132
|
+
this._dismissPending = false;
|
|
133
|
+
Tween.killTweensOf(this);
|
|
116
134
|
|
|
117
135
|
await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
|
|
118
136
|
this.visible = false;
|
|
119
137
|
}
|
|
138
|
+
|
|
139
|
+
/** React reconciler update hook */
|
|
140
|
+
updateConfig(changed: Record<string, any>): void {
|
|
141
|
+
if ('duration' in changed) this._config.duration = changed.duration;
|
|
142
|
+
if ('bottomOffset' in changed) this._config.bottomOffset = changed.bottomOffset;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
|
|
146
|
+
this._dismissPending = false;
|
|
147
|
+
Tween.killTweensOf(this);
|
|
148
|
+
super.destroy(options);
|
|
149
|
+
}
|
|
120
150
|
}
|