@energy8platform/game-engine 0.10.11 → 0.12.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 +272 -74
- package/dist/index.cjs.js +1322 -296
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +369 -46
- package/dist/index.esm.js +1323 -298
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +8 -18
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +0 -2
- package/dist/lua.esm.js +8 -18
- package/dist/lua.esm.js.map +1 -1
- package/dist/react.cjs.js +2848 -35
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +17 -6
- package/dist/react.esm.js +2848 -36
- package/dist/react.esm.js.map +1 -1
- package/dist/ui.cjs.js +1913 -592
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +528 -46
- package/dist/ui.esm.js +1911 -594
- 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/react/applyProps.ts +90 -2
- package/src/react/extendAll.ts +29 -6
- package/src/react/index.ts +1 -1
- package/src/react/jsx.d.ts +249 -0
- package/src/react/reconciler.ts +80 -7
- package/src/ui/BalanceDisplay.ts +31 -38
- package/src/ui/Button.ts +217 -53
- package/src/ui/FlexContainer.ts +529 -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/Slider.ts +241 -0
- package/src/ui/Toast.ts +47 -17
- package/src/ui/Toggle.ts +201 -0
- package/src/ui/WinDisplay.ts +51 -39
- package/src/ui/index.ts +9 -11
- package/src/ui/view.ts +28 -0
- package/src/vite/index.ts +1 -11
package/src/ui/Layout.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Container } from 'pixi.js';
|
|
2
|
-
import
|
|
2
|
+
import { FlexContainer } from './FlexContainer';
|
|
3
|
+
import type { FlexDirection, AlignItems, FlexItemConfig } from './FlexContainer';
|
|
3
4
|
|
|
4
5
|
// ─── Types ───────────────────────────────────────────────
|
|
5
6
|
|
|
@@ -41,63 +42,17 @@ export interface LayoutConfig {
|
|
|
41
42
|
|
|
42
43
|
// ─── Helpers ─────────────────────────────────────────────
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
start: 'flex-start',
|
|
46
|
-
center: 'center',
|
|
47
|
-
end: 'flex-end',
|
|
48
|
-
stretch: 'stretch',
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
function normalizePadding(
|
|
52
|
-
padding: number | [number, number, number, number],
|
|
53
|
-
): [number, number, number, number] {
|
|
54
|
-
if (typeof padding === 'number') return [padding, padding, padding, padding];
|
|
55
|
-
return padding;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function directionToFlexStyles(
|
|
59
|
-
direction: LayoutDirection,
|
|
60
|
-
maxWidth: number,
|
|
61
|
-
): Partial<LayoutStyles> {
|
|
45
|
+
function directionToFlex(direction: LayoutDirection): { direction: FlexDirection; wrap: boolean } {
|
|
62
46
|
switch (direction) {
|
|
63
|
-
case 'horizontal':
|
|
64
|
-
|
|
65
|
-
case '
|
|
66
|
-
|
|
67
|
-
case 'grid':
|
|
68
|
-
return { flexDirection: 'row', flexWrap: 'wrap' };
|
|
69
|
-
case 'wrap':
|
|
70
|
-
return {
|
|
71
|
-
flexDirection: 'row',
|
|
72
|
-
flexWrap: 'wrap',
|
|
73
|
-
...(maxWidth < Infinity ? { maxWidth } : {}),
|
|
74
|
-
};
|
|
47
|
+
case 'horizontal': return { direction: 'row', wrap: false };
|
|
48
|
+
case 'vertical': return { direction: 'column', wrap: false };
|
|
49
|
+
case 'grid': return { direction: 'row', wrap: true };
|
|
50
|
+
case 'wrap': return { direction: 'row', wrap: true };
|
|
75
51
|
}
|
|
76
52
|
}
|
|
77
53
|
|
|
78
|
-
function buildLayoutStyles(config: {
|
|
79
|
-
direction: LayoutDirection;
|
|
80
|
-
gap: number;
|
|
81
|
-
alignment: LayoutAlignment;
|
|
82
|
-
columns: number;
|
|
83
|
-
padding: [number, number, number, number];
|
|
84
|
-
maxWidth: number;
|
|
85
|
-
}): LayoutStyles {
|
|
86
|
-
const [pt, pr, pb, pl] = config.padding;
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
...directionToFlexStyles(config.direction, config.maxWidth),
|
|
90
|
-
gap: config.gap,
|
|
91
|
-
alignItems: ALIGNMENT_MAP[config.alignment],
|
|
92
|
-
paddingTop: pt,
|
|
93
|
-
paddingRight: pr,
|
|
94
|
-
paddingBottom: pb,
|
|
95
|
-
paddingLeft: pl,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
54
|
/**
|
|
100
|
-
* Responsive layout container powered by
|
|
55
|
+
* Responsive layout container powered by a lightweight built-in flex layout solver.
|
|
101
56
|
*
|
|
102
57
|
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
103
58
|
* alignment, padding, gap, and viewport-anchor positioning.
|
|
@@ -124,14 +79,17 @@ function buildLayoutStyles(config: {
|
|
|
124
79
|
* ```
|
|
125
80
|
*/
|
|
126
81
|
export class Layout extends Container {
|
|
82
|
+
readonly __uiComponent = true as const;
|
|
83
|
+
|
|
127
84
|
private _layoutConfig: Required<Pick<LayoutConfig, 'direction' | 'gap' | 'alignment' | 'autoLayout' | 'columns'>>;
|
|
128
|
-
private _padding: [number, number, number, number];
|
|
85
|
+
private _padding: number | [number, number, number, number];
|
|
129
86
|
private _anchor: LayoutAnchor;
|
|
130
87
|
private _maxWidth: number;
|
|
131
88
|
private _breakpoints: [number, Partial<LayoutConfig>][];
|
|
132
89
|
private _items: Container[] = [];
|
|
133
90
|
private _viewportWidth = 0;
|
|
134
91
|
private _viewportHeight = 0;
|
|
92
|
+
private _flex: FlexContainer;
|
|
135
93
|
|
|
136
94
|
constructor(config: LayoutConfig = {}) {
|
|
137
95
|
super();
|
|
@@ -144,7 +102,7 @@ export class Layout extends Container {
|
|
|
144
102
|
columns: config.columns ?? 2,
|
|
145
103
|
};
|
|
146
104
|
|
|
147
|
-
this._padding =
|
|
105
|
+
this._padding = config.padding ?? 0;
|
|
148
106
|
this._anchor = config.anchor ?? 'top-left';
|
|
149
107
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
150
108
|
|
|
@@ -154,16 +112,22 @@ export class Layout extends Container {
|
|
|
154
112
|
.sort((a, b) => a[0] - b[0])
|
|
155
113
|
: [];
|
|
156
114
|
|
|
115
|
+
// Create internal FlexContainer
|
|
116
|
+
this._flex = new FlexContainer();
|
|
117
|
+
super.addChild(this._flex);
|
|
118
|
+
|
|
157
119
|
this.applyLayoutStyles();
|
|
158
120
|
}
|
|
159
121
|
|
|
160
122
|
/** Add an item to the layout */
|
|
161
123
|
addItem(child: Container): this {
|
|
162
124
|
this._items.push(child);
|
|
163
|
-
this.addChild(child);
|
|
164
125
|
|
|
165
|
-
|
|
166
|
-
|
|
126
|
+
const flexConfig = this.buildFlexItemConfig(child);
|
|
127
|
+
this._flex.addFlexChild(child, flexConfig);
|
|
128
|
+
|
|
129
|
+
if (this._layoutConfig.autoLayout) {
|
|
130
|
+
this.applyLayoutStyles();
|
|
167
131
|
}
|
|
168
132
|
|
|
169
133
|
return this;
|
|
@@ -174,16 +138,14 @@ export class Layout extends Container {
|
|
|
174
138
|
const idx = this._items.indexOf(child);
|
|
175
139
|
if (idx !== -1) {
|
|
176
140
|
this._items.splice(idx, 1);
|
|
177
|
-
this.
|
|
141
|
+
this._flex.removeFlexChild(child);
|
|
178
142
|
}
|
|
179
143
|
return this;
|
|
180
144
|
}
|
|
181
145
|
|
|
182
146
|
/** Remove all items */
|
|
183
147
|
clearItems(): this {
|
|
184
|
-
|
|
185
|
-
this.removeChild(item);
|
|
186
|
-
}
|
|
148
|
+
this._flex.clearFlexChildren();
|
|
187
149
|
this._items.length = 0;
|
|
188
150
|
return this;
|
|
189
151
|
}
|
|
@@ -209,48 +171,68 @@ export class Layout extends Container {
|
|
|
209
171
|
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
210
172
|
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
211
173
|
const alignment = effective.alignment ?? this._layoutConfig.alignment;
|
|
212
|
-
const
|
|
213
|
-
const padding = effective.padding !== undefined
|
|
214
|
-
? normalizePadding(effective.padding)
|
|
215
|
-
: this._padding;
|
|
174
|
+
const padding = effective.padding ?? this._padding;
|
|
216
175
|
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
217
176
|
|
|
218
|
-
const
|
|
219
|
-
|
|
177
|
+
const { direction: flexDir, wrap } = directionToFlex(direction);
|
|
178
|
+
|
|
179
|
+
this._flex.setDirection(flexDir);
|
|
180
|
+
this._flex.setJustifyContent('start');
|
|
181
|
+
this._flex.setAlignItems(alignment as AlignItems);
|
|
182
|
+
this._flex.setGap(gap);
|
|
183
|
+
this._flex.setPadding(padding);
|
|
184
|
+
|
|
185
|
+
// Wrap and maxWidth
|
|
186
|
+
if (wrap) {
|
|
187
|
+
(this._flex as any)._config.flexWrap = true;
|
|
188
|
+
if (direction === 'grid' && maxWidth < Infinity) {
|
|
189
|
+
(this._flex as any)._maxWidth = maxWidth;
|
|
190
|
+
} else if (direction === 'grid') {
|
|
191
|
+
// For grid without explicit maxWidth, we don't constrain wrapping —
|
|
192
|
+
// each child gets a proportional width via flexConfig
|
|
193
|
+
}
|
|
194
|
+
if (maxWidth < Infinity) {
|
|
195
|
+
(this._flex as any)._maxWidth = maxWidth;
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
(this._flex as any)._config.flexWrap = false;
|
|
199
|
+
}
|
|
220
200
|
|
|
201
|
+
// Update grid child widths
|
|
221
202
|
if (direction === 'grid') {
|
|
222
203
|
for (const item of this._items) {
|
|
223
|
-
this.
|
|
204
|
+
const flexConfig = this.buildFlexItemConfig(item);
|
|
205
|
+
(item as any)._flexConfig = flexConfig;
|
|
224
206
|
}
|
|
225
207
|
}
|
|
208
|
+
|
|
209
|
+
// Set explicit size if we have viewport dimensions
|
|
210
|
+
if (this._viewportWidth > 0 && this._viewportHeight > 0) {
|
|
211
|
+
this._flex.resize(this._viewportWidth, this._viewportHeight);
|
|
212
|
+
} else {
|
|
213
|
+
this._flex.updateLayout();
|
|
214
|
+
}
|
|
226
215
|
}
|
|
227
216
|
|
|
228
|
-
private
|
|
217
|
+
private buildFlexItemConfig(_child: Container): FlexItemConfig | undefined {
|
|
229
218
|
const effective = this.resolveConfig();
|
|
219
|
+
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
230
220
|
const columns = effective.columns ?? this._layoutConfig.columns;
|
|
231
|
-
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
232
221
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
? { flexBasis: 0, flexGrow: 1, flexShrink: 1, maxWidth: `${(100 / columns).toFixed(2)}%` }
|
|
238
|
-
: { width: `${(100 / columns).toFixed(2)}%` };
|
|
239
|
-
|
|
240
|
-
if (child._layout) {
|
|
241
|
-
child._layout.setStyle(styles);
|
|
242
|
-
} else {
|
|
243
|
-
child.layout = styles;
|
|
222
|
+
if (direction === 'grid' && columns > 0) {
|
|
223
|
+
// For grid, give each item a proportional width
|
|
224
|
+
// The actual pixel width will be computed during layout
|
|
225
|
+
return { flexGrow: 1 };
|
|
244
226
|
}
|
|
227
|
+
|
|
228
|
+
return undefined;
|
|
245
229
|
}
|
|
246
230
|
|
|
247
231
|
private applyAnchor(): void {
|
|
248
232
|
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
249
233
|
if (this._viewportWidth === 0 || this._viewportHeight === 0) return;
|
|
250
234
|
|
|
251
|
-
const
|
|
252
|
-
const contentW = bounds.width * this.scale.x;
|
|
253
|
-
const contentH = bounds.height * this.scale.y;
|
|
235
|
+
const { width: contentW, height: contentH } = this._flex.getContentSize();
|
|
254
236
|
const vw = this._viewportWidth;
|
|
255
237
|
const vh = this._viewportHeight;
|
|
256
238
|
|
|
@@ -273,8 +255,8 @@ export class Layout extends Container {
|
|
|
273
255
|
anchorY = (vh - contentH) / 2;
|
|
274
256
|
}
|
|
275
257
|
|
|
276
|
-
this.x = anchorX
|
|
277
|
-
this.y = anchorY
|
|
258
|
+
this.x = anchorX;
|
|
259
|
+
this.y = anchorY;
|
|
278
260
|
}
|
|
279
261
|
|
|
280
262
|
private resolveConfig(): Partial<LayoutConfig> {
|
|
@@ -289,4 +271,21 @@ export class Layout extends Container {
|
|
|
289
271
|
}
|
|
290
272
|
return {};
|
|
291
273
|
}
|
|
274
|
+
|
|
275
|
+
/** React reconciler update hook */
|
|
276
|
+
updateConfig(changed: Record<string, any>): void {
|
|
277
|
+
if ('direction' in changed) this._layoutConfig.direction = changed.direction;
|
|
278
|
+
if ('gap' in changed) this._layoutConfig.gap = changed.gap;
|
|
279
|
+
if ('alignment' in changed) this._layoutConfig.alignment = changed.alignment;
|
|
280
|
+
if ('anchor' in changed) this._anchor = changed.anchor;
|
|
281
|
+
if ('padding' in changed) this._padding = changed.padding;
|
|
282
|
+
if ('columns' in changed) this._layoutConfig.columns = changed.columns;
|
|
283
|
+
this.applyLayoutStyles();
|
|
284
|
+
if (this._viewportWidth > 0) this.applyAnchor();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
|
|
288
|
+
this._items.length = 0;
|
|
289
|
+
super.destroy(options);
|
|
290
|
+
}
|
|
292
291
|
}
|
package/src/ui/Modal.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface ModalConfig {
|
|
|
17
17
|
* Modal overlay component.
|
|
18
18
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* Content is automatically centered via position calculations.
|
|
21
21
|
*
|
|
22
22
|
* @example
|
|
23
23
|
* ```ts
|
|
@@ -28,6 +28,8 @@ export interface ModalConfig {
|
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
export class Modal extends Container {
|
|
31
|
+
readonly __uiComponent = true as const;
|
|
32
|
+
|
|
31
33
|
private _overlay: Graphics;
|
|
32
34
|
private _contentContainer: Container;
|
|
33
35
|
private _config: Required<ModalConfig>;
|
|
@@ -132,4 +134,12 @@ export class Modal extends Container {
|
|
|
132
134
|
this._showing = false;
|
|
133
135
|
this.onClose?.();
|
|
134
136
|
}
|
|
137
|
+
|
|
138
|
+
/** React reconciler update hook */
|
|
139
|
+
updateConfig(changed: Record<string, any>): void {
|
|
140
|
+
if ('overlayAlpha' in changed) this._config.overlayAlpha = changed.overlayAlpha;
|
|
141
|
+
if ('closeOnOverlay' in changed) this._config.closeOnOverlay = changed.closeOnOverlay;
|
|
142
|
+
if ('animationDuration' in changed) this._config.animationDuration = changed.animationDuration;
|
|
143
|
+
if ('onClose' in changed) this.onClose = changed.onClose;
|
|
144
|
+
}
|
|
135
145
|
}
|
package/src/ui/Panel.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Container, NineSliceSprite, Texture } from 'pixi.js';
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
1
|
+
import { Container, Graphics, NineSliceSprite, Texture } from 'pixi.js';
|
|
2
|
+
import { FlexContainer } from './FlexContainer';
|
|
3
|
+
import type { FlexContainerConfig } from './FlexContainer';
|
|
4
4
|
|
|
5
5
|
export interface PanelConfig {
|
|
6
6
|
/** Width */
|
|
@@ -23,13 +23,15 @@ export interface PanelConfig {
|
|
|
23
23
|
nineSliceBorders?: [number, number, number, number];
|
|
24
24
|
/** Padding inside the panel */
|
|
25
25
|
padding?: number;
|
|
26
|
+
/** Flex layout config for content */
|
|
27
|
+
layout?: Partial<FlexContainerConfig>;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
|
-
* Background panel
|
|
31
|
+
* Background panel with optional flexbox content layout.
|
|
30
32
|
*
|
|
31
33
|
* Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
|
|
32
|
-
* Children added
|
|
34
|
+
* Children added via `addContent()` participate in flex layout automatically.
|
|
33
35
|
*
|
|
34
36
|
* @example
|
|
35
37
|
* ```ts
|
|
@@ -44,12 +46,19 @@ export interface PanelConfig {
|
|
|
44
46
|
* });
|
|
45
47
|
* ```
|
|
46
48
|
*/
|
|
47
|
-
export class Panel extends
|
|
49
|
+
export class Panel extends Container {
|
|
50
|
+
readonly __uiComponent = true as const;
|
|
51
|
+
|
|
52
|
+
private _bg: Container;
|
|
53
|
+
private _content: FlexContainer;
|
|
54
|
+
private _internalSetup = true;
|
|
48
55
|
private _panelConfig: Required<
|
|
49
56
|
Pick<PanelConfig, 'width' | 'height' | 'padding' | 'backgroundAlpha'>
|
|
50
57
|
> & PanelConfig;
|
|
51
58
|
|
|
52
59
|
constructor(config: PanelConfig = {}) {
|
|
60
|
+
super();
|
|
61
|
+
|
|
53
62
|
const resolvedConfig = {
|
|
54
63
|
width: config.width ?? 400,
|
|
55
64
|
height: config.height ?? 300,
|
|
@@ -58,8 +67,9 @@ export class Panel extends LayoutContainer {
|
|
|
58
67
|
...config,
|
|
59
68
|
};
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
this._panelConfig = resolvedConfig;
|
|
71
|
+
|
|
72
|
+
// Create background
|
|
63
73
|
if (config.nineSliceTexture) {
|
|
64
74
|
const texture =
|
|
65
75
|
typeof config.nineSliceTexture === 'string'
|
|
@@ -76,47 +86,109 @@ export class Panel extends LayoutContainer {
|
|
|
76
86
|
nineSlice.width = resolvedConfig.width;
|
|
77
87
|
nineSlice.height = resolvedConfig.height;
|
|
78
88
|
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
79
|
-
|
|
89
|
+
this._bg = nineSlice;
|
|
90
|
+
} else {
|
|
91
|
+
const g = new Graphics();
|
|
92
|
+
const bgColor = config.backgroundColor ?? 0x1a1a2e;
|
|
93
|
+
const radius = config.borderRadius ?? 0;
|
|
94
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius).fill(bgColor);
|
|
95
|
+
if (config.borderColor !== undefined && config.borderWidth) {
|
|
96
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius)
|
|
97
|
+
.stroke({ color: config.borderColor, width: config.borderWidth });
|
|
98
|
+
}
|
|
99
|
+
g.alpha = resolvedConfig.backgroundAlpha;
|
|
100
|
+
this._bg = g;
|
|
80
101
|
}
|
|
102
|
+
this.addChild(this._bg);
|
|
81
103
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
// Create content flex container
|
|
105
|
+
this._content = new FlexContainer({
|
|
106
|
+
...config.layout,
|
|
107
|
+
direction: config.layout?.direction ?? 'column',
|
|
108
|
+
justifyContent: config.layout?.justifyContent ?? 'start',
|
|
109
|
+
alignItems: config.layout?.alignItems ?? 'start',
|
|
110
|
+
gap: config.layout?.gap ?? 0,
|
|
111
|
+
padding: resolvedConfig.padding,
|
|
88
112
|
width: resolvedConfig.width,
|
|
89
113
|
height: resolvedConfig.height,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
});
|
|
115
|
+
this.addChild(this._content);
|
|
116
|
+
this._internalSetup = false;
|
|
117
|
+
}
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
/** Access the content flex container — add children here for layout */
|
|
120
|
+
get content(): FlexContainer {
|
|
121
|
+
return this._content;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Convenience: add a child to the content layout */
|
|
125
|
+
addContent(child: Container): this {
|
|
126
|
+
this._content.addFlexChild(child);
|
|
127
|
+
this._content.updateLayout();
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Resize the panel */
|
|
132
|
+
setSize(width: number, height: number): void {
|
|
133
|
+
this._panelConfig.width = width;
|
|
134
|
+
this._panelConfig.height = height;
|
|
135
|
+
|
|
136
|
+
// Resize background
|
|
137
|
+
if (this._bg instanceof NineSliceSprite) {
|
|
138
|
+
this._bg.width = width;
|
|
139
|
+
this._bg.height = height;
|
|
140
|
+
} else if (this._bg instanceof Graphics) {
|
|
141
|
+
const radius = this._panelConfig.borderRadius ?? 0;
|
|
142
|
+
const bgColor = this._panelConfig.backgroundColor ?? 0x1a1a2e;
|
|
143
|
+
this._bg.clear();
|
|
144
|
+
(this._bg as Graphics).roundRect(0, 0, width, height, radius).fill(bgColor);
|
|
145
|
+
if (this._panelConfig.borderColor !== undefined && this._panelConfig.borderWidth) {
|
|
146
|
+
(this._bg as Graphics).roundRect(0, 0, width, height, radius)
|
|
147
|
+
.stroke({ color: this._panelConfig.borderColor, width: this._panelConfig.borderWidth });
|
|
101
148
|
}
|
|
149
|
+
this._bg.alpha = this._panelConfig.backgroundAlpha;
|
|
102
150
|
}
|
|
103
151
|
|
|
104
|
-
this.
|
|
152
|
+
this._content.resize(width, height);
|
|
153
|
+
}
|
|
105
154
|
|
|
106
|
-
|
|
107
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Override addChild so external children are routed to content FlexContainer.
|
|
157
|
+
* Enables `<panel><label /><button /></panel>` in React JSX.
|
|
158
|
+
*/
|
|
159
|
+
override addChild<T extends Container>(...children: T[]): T {
|
|
160
|
+
if (this._internalSetup) {
|
|
161
|
+
return super.addChild(...children);
|
|
162
|
+
}
|
|
163
|
+
for (const child of children) {
|
|
164
|
+
this._content.addFlexChild(child as any);
|
|
108
165
|
}
|
|
166
|
+
this._content.updateLayout();
|
|
167
|
+
return children[0];
|
|
109
168
|
}
|
|
110
169
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
170
|
+
override removeChild<T extends Container>(...children: T[]): T {
|
|
171
|
+
if (this._internalSetup) {
|
|
172
|
+
return super.removeChild(...children);
|
|
173
|
+
}
|
|
174
|
+
for (const child of children) {
|
|
175
|
+
this._content.removeFlexChild(child as any);
|
|
176
|
+
}
|
|
177
|
+
return children[0];
|
|
114
178
|
}
|
|
115
179
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
180
|
+
/** React reconciler update hook */
|
|
181
|
+
updateConfig(changed: Record<string, any>): void {
|
|
182
|
+
if ('width' in changed || 'height' in changed) {
|
|
183
|
+
this.setSize(changed.width ?? this._panelConfig.width, changed.height ?? this._panelConfig.height);
|
|
184
|
+
}
|
|
185
|
+
if ('backgroundAlpha' in changed) {
|
|
186
|
+
this._panelConfig.backgroundAlpha = changed.backgroundAlpha;
|
|
187
|
+
this._bg.alpha = changed.backgroundAlpha;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
|
|
192
|
+
super.destroy(options);
|
|
121
193
|
}
|
|
122
194
|
}
|
package/src/ui/ProgressBar.ts
CHANGED
|
@@ -1,43 +1,63 @@
|
|
|
1
1
|
import { Container, Graphics } from 'pixi.js';
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
2
|
+
import { resolveView } from './view';
|
|
3
|
+
import type { ViewInput } from './view';
|
|
4
4
|
|
|
5
5
|
export interface ProgressBarConfig {
|
|
6
|
+
/** Width of the bar */
|
|
6
7
|
width?: number;
|
|
8
|
+
/** Height of the bar */
|
|
7
9
|
height?: number;
|
|
10
|
+
/** Corner radius (for Graphics-based bar) */
|
|
8
11
|
borderRadius?: number;
|
|
12
|
+
/** Fill color (for Graphics-based bar, ignored when fillView provided) */
|
|
9
13
|
fillColor?: number;
|
|
14
|
+
/** Track background color (for Graphics-based bar, ignored when trackView provided) */
|
|
10
15
|
trackColor?: number;
|
|
16
|
+
/** Border color */
|
|
11
17
|
borderColor?: number;
|
|
18
|
+
/** Border width */
|
|
12
19
|
borderWidth?: number;
|
|
13
20
|
/** Animated fill (smoothly interpolate) */
|
|
14
21
|
animated?: boolean;
|
|
15
22
|
/** Animation speed (0..1 per frame, default: 0.1) */
|
|
16
23
|
animationSpeed?: number;
|
|
17
|
-
}
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
|
|
25
|
+
/** Custom track background (string texture name, Texture, or Container) */
|
|
26
|
+
trackView?: ViewInput;
|
|
27
|
+
/** Custom fill bar (string texture name, Texture, or Container) */
|
|
28
|
+
fillView?: ViewInput;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
|
-
* Horizontal progress bar
|
|
32
|
+
* Horizontal progress bar with optional custom track/fill views.
|
|
27
33
|
*
|
|
28
|
-
*
|
|
34
|
+
* Supports asset-based skinning: provide `trackView` and/or `fillView`
|
|
35
|
+
* as texture names, Textures, or any Container (NineSliceSprite, custom artwork, etc).
|
|
36
|
+
* Falls back to colored Graphics when no custom views are provided.
|
|
29
37
|
*
|
|
30
38
|
* @example
|
|
31
39
|
* ```ts
|
|
40
|
+
* // Graphics-based (quick prototyping)
|
|
32
41
|
* const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
|
|
33
|
-
*
|
|
34
|
-
*
|
|
42
|
+
* bar.progress = 0.5;
|
|
43
|
+
*
|
|
44
|
+
* // Asset-based (production art)
|
|
45
|
+
* const bar = new ProgressBar({
|
|
46
|
+
* width: 300, height: 20,
|
|
47
|
+
* trackView: 'bar-track',
|
|
48
|
+
* fillView: new NineSliceSprite({ texture: 'bar-fill', ... }),
|
|
49
|
+
* });
|
|
50
|
+
* bar.progress = 0.75;
|
|
35
51
|
* ```
|
|
36
52
|
*/
|
|
37
53
|
export class ProgressBar extends Container {
|
|
38
|
-
|
|
54
|
+
readonly __uiComponent = true as const;
|
|
55
|
+
|
|
56
|
+
private _track: Container;
|
|
57
|
+
private _fill: Container;
|
|
58
|
+
private _fillMask: Graphics;
|
|
39
59
|
private _borderGfx: Graphics;
|
|
40
|
-
private _config: Required<ProgressBarConfig
|
|
60
|
+
private _config: Required<Pick<ProgressBarConfig, 'width' | 'height' | 'borderRadius' | 'fillColor' | 'trackColor' | 'borderColor' | 'borderWidth' | 'animated' | 'animationSpeed'>>;
|
|
41
61
|
private _progress = 0;
|
|
42
62
|
private _displayedProgress = 0;
|
|
43
63
|
|
|
@@ -58,23 +78,43 @@ export class ProgressBar extends Container {
|
|
|
58
78
|
|
|
59
79
|
const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
progress: 0,
|
|
74
|
-
};
|
|
81
|
+
// Track background — custom view or Graphics
|
|
82
|
+
const customTrack = resolveView(config.trackView);
|
|
83
|
+
if (customTrack) {
|
|
84
|
+
customTrack.width = width;
|
|
85
|
+
customTrack.height = height;
|
|
86
|
+
this._track = customTrack;
|
|
87
|
+
} else {
|
|
88
|
+
const g = new Graphics();
|
|
89
|
+
g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
90
|
+
this._track = g;
|
|
91
|
+
}
|
|
92
|
+
this.addChild(this._track);
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
// Fill bar — custom view or Graphics
|
|
95
|
+
const customFill = resolveView(config.fillView);
|
|
96
|
+
if (customFill) {
|
|
97
|
+
customFill.x = borderWidth;
|
|
98
|
+
customFill.y = borderWidth;
|
|
99
|
+
customFill.width = width - borderWidth * 2;
|
|
100
|
+
customFill.height = height - borderWidth * 2;
|
|
101
|
+
this._fill = customFill;
|
|
102
|
+
} else {
|
|
103
|
+
const g = new Graphics();
|
|
104
|
+
g.roundRect(
|
|
105
|
+
borderWidth, borderWidth,
|
|
106
|
+
width - borderWidth * 2, height - borderWidth * 2,
|
|
107
|
+
Math.max(0, borderRadius - 1),
|
|
108
|
+
).fill(fillColor);
|
|
109
|
+
this._fill = g;
|
|
110
|
+
}
|
|
111
|
+
this.addChild(this._fill);
|
|
112
|
+
|
|
113
|
+
// Mask for the fill (controls visible width)
|
|
114
|
+
this._fillMask = new Graphics();
|
|
115
|
+
this._fillMask.rect(0, 0, 0, height).fill(0xffffff);
|
|
116
|
+
this.addChild(this._fillMask);
|
|
117
|
+
this._fill.mask = this._fillMask;
|
|
78
118
|
|
|
79
119
|
// Border overlay
|
|
80
120
|
this._borderGfx = new Graphics();
|
|
@@ -95,7 +135,7 @@ export class ProgressBar extends Container {
|
|
|
95
135
|
this._progress = Math.max(0, Math.min(1, value));
|
|
96
136
|
if (!this._config.animated) {
|
|
97
137
|
this._displayedProgress = this._progress;
|
|
98
|
-
this.
|
|
138
|
+
this.updateMask();
|
|
99
139
|
}
|
|
100
140
|
}
|
|
101
141
|
|
|
@@ -106,11 +146,25 @@ export class ProgressBar extends Container {
|
|
|
106
146
|
if (!this._config.animated) return;
|
|
107
147
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
108
148
|
this._displayedProgress = this._progress;
|
|
149
|
+
this.updateMask();
|
|
109
150
|
return;
|
|
110
151
|
}
|
|
111
152
|
|
|
112
153
|
this._displayedProgress +=
|
|
113
154
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
114
|
-
this.
|
|
155
|
+
this.updateMask();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** React reconciler update hook */
|
|
159
|
+
updateConfig(changed: Record<string, any>): void {
|
|
160
|
+
if ('progress' in changed) this.progress = changed.progress;
|
|
161
|
+
if ('animated' in changed) this._config.animated = changed.animated;
|
|
162
|
+
if ('animationSpeed' in changed) this._config.animationSpeed = changed.animationSpeed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private updateMask(): void {
|
|
166
|
+
const w = this._config.width * this._displayedProgress;
|
|
167
|
+
this._fillMask.clear();
|
|
168
|
+
this._fillMask.rect(0, 0, w, this._config.height).fill(0xffffff);
|
|
115
169
|
}
|
|
116
170
|
}
|