@kurot/ui 3.0.0 → 3.2.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/CHANGELOG.md +78 -6
- package/README.md +154 -69
- package/dist/kurot/components/BitmapLabel.d.ts +46 -0
- package/dist/kurot/components/BitmapLabel.d.ts.map +1 -0
- package/dist/kurot/components/BitmapLabel.js +180 -0
- package/dist/kurot/components/BitmapLabel.js.map +1 -0
- package/dist/kurot/components/Label.d.ts +40 -0
- package/dist/kurot/components/Label.d.ts.map +1 -1
- package/dist/kurot/components/Label.js +182 -32
- package/dist/kurot/components/Label.js.map +1 -1
- package/dist/kurot/components/index.d.ts +2 -1
- package/dist/kurot/components/index.d.ts.map +1 -1
- package/dist/kurot/components/index.js +1 -0
- package/dist/kurot/components/index.js.map +1 -1
- package/dist/kurot/text/fit-label-text.d.ts +23 -0
- package/dist/kurot/text/fit-label-text.d.ts.map +1 -0
- package/dist/kurot/text/fit-label-text.js +36 -0
- package/dist/kurot/text/fit-label-text.js.map +1 -0
- package/docs/ai-context.md +224 -0
- package/docs/architecture.md +467 -0
- package/docs/bitmap-label.md +41 -0
- package/docs/label-text-layout.md +99 -0
- package/package.json +5 -4
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# @kurot/ui architecture
|
|
2
|
+
|
|
3
|
+
> Package version: 3.2.0, with peerDependency `@kurot/core: ^2.2.0`.
|
|
4
|
+
> This release is prepared against published Core 2.2.0; UI publication is not yet confirmed.
|
|
5
|
+
> See [CHANGELOG.md](../CHANGELOG.md) for release-by-release changes.
|
|
6
|
+
> The [AI context map](./ai-context.md) provides directory, behavior, terminology
|
|
7
|
+
> and task-to-file references. This document explains design decisions and internal
|
|
8
|
+
> mechanisms for human readers; the two documents complement each other.
|
|
9
|
+
> Version 3.2.0 exports BitmapLabel for native code and programmatic skins.
|
|
10
|
+
> See [bitmap-label.md](./bitmap-label.md) for its resource and ownership contract.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Overview
|
|
15
|
+
|
|
16
|
+
`@kurot/ui` is an EUI-compatible UI framework rewritten on top of
|
|
17
|
+
[`@kurot/core`](../../core/docs/architecture.md). Core is a `peerDependency`,
|
|
18
|
+
excluded from the output bundle. Consumers explicitly install it so packages
|
|
19
|
+
using UI share one Core instance, display tree infrastructure and global state.
|
|
20
|
+
|
|
21
|
+
The central architectural choice is delegation in place of prototype mixins.
|
|
22
|
+
Egret EUI uses `implementUIComponent()` to copy prototype members and combine
|
|
23
|
+
DisplayObject and UIComponent behavior. Such copied members are difficult to
|
|
24
|
+
represent in strict TypeScript. Kurot uses composition instead: Group and
|
|
25
|
+
Component both directly extend Sprite and hold a UIState instance as `this.ui`.
|
|
26
|
+
Their IUIComponent methods delegate layout state and validation to UIState.
|
|
27
|
+
UIState communicates with its host through the narrow IUIOwner callback interface,
|
|
28
|
+
without depending on the host's concrete DisplayObject subclass. This separation
|
|
29
|
+
allows skin and validation systems to evolve independently and follows the
|
|
30
|
+
repository preference for composition over deeper application inheritance.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. Validation cycle
|
|
35
|
+
|
|
36
|
+
### 2.1 Three phases and three queues
|
|
37
|
+
|
|
38
|
+
Layout updates are deferred and batched. Property changes enqueue components
|
|
39
|
+
for Core's render-preparation `callLater` queue instead of immediately repeating
|
|
40
|
+
layout work for each change within a frame.
|
|
41
|
+
|
|
42
|
+
The Validator singleton (`validator`) maintains three DepthQueues:
|
|
43
|
+
`_propsQueue`, `_sizeQueue` and `_displayQueue`. Each groups clients by
|
|
44
|
+
`$nestLevel`, a Core DisplayObject field, using `Map<number, DepthBin>`.
|
|
45
|
+
Each DepthBin combines an array for ordered removal with a Set for constant-time
|
|
46
|
+
duplicate checks.
|
|
47
|
+
|
|
48
|
+
The phases execute in a fixed order with different traversal directions:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
validateProperties() shallow → deep commitProperties()
|
|
52
|
+
validateSize() deep → shallow measure()
|
|
53
|
+
validateDisplayList() shallow → deep updateDisplayList()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
These directions follow the data dependencies:
|
|
57
|
+
|
|
58
|
+
- Properties commit from parent to child. For example, List assigns data to a
|
|
59
|
+
renderer before the renderer measures its text. Children added or removed
|
|
60
|
+
during a parent commit can participate in the same queue processing.
|
|
61
|
+
- Size measurement runs from child to parent. Container measurements depend
|
|
62
|
+
on child dimensions, whether VerticalLayout sums them or BasicLayout unions bounds.
|
|
63
|
+
- Display-list updates run from parent to child. A parent first assigns child
|
|
64
|
+
layout bounds through setLayoutBoundsSize/setLayoutBoundsPosition; the child
|
|
65
|
+
can then finalize its display using the allocated bounds.
|
|
66
|
+
|
|
67
|
+
DepthQueue.shift() tracks the shallowest bucket with `_min`; pop() tracks the
|
|
68
|
+
deepest with `_max`. Removing clients advances these cursors rather than scanning
|
|
69
|
+
from the beginning on each call. New insertions can adjust the bounds again.
|
|
70
|
+
|
|
71
|
+
### 2.2 Restarting after a later invalidation
|
|
72
|
+
|
|
73
|
+
Separate phases create a dependency problem: processing one client can invalidate
|
|
74
|
+
another client whose earlier phase has already run. A single pass may leave such
|
|
75
|
+
an invalidation pending even though it belongs to the same logical update.
|
|
76
|
+
|
|
77
|
+
`validateClient(target)` handles this for forced synchronous validation, such as
|
|
78
|
+
re-adding a removed component to the stage or calling component.validateNow():
|
|
79
|
+
|
|
80
|
+
1. Set `_targetLevel` to target.$nestLevel when it is currently Infinity. Nested
|
|
81
|
+
calls retain the existing level and restore it afterward.
|
|
82
|
+
2. Run an outer `while (!done)` loop:
|
|
83
|
+
- Remove clients from the target subtree's properties queue from shallow to
|
|
84
|
+
deep and call validateProperties. Check obj.stage before each call to skip
|
|
85
|
+
objects removed from the stage during validation.
|
|
86
|
+
- Process the subtree's size queue from deep to shallow. If measurement sets
|
|
87
|
+
`_clientPropsFlag` and a pending property client exists in the subtree,
|
|
88
|
+
reinsert that client, set done=false and break. The outer loop restarts at
|
|
89
|
+
properties before proceeding to display-list updates.
|
|
90
|
+
- Apply the same restart logic in the display-list phase for both
|
|
91
|
+
`_clientPropsFlag` and `_clientSizeFlag`.
|
|
92
|
+
3. In finally, restore `_targetLevel`, synchronize empty-queue flags and clear
|
|
93
|
+
client flags. Validator.test.ts covers consistency after a client throws.
|
|
94
|
+
4. Client flags are raised when an invalidated client's depth is at least
|
|
95
|
+
`_targetLevel`. Queue removal then checks actual ancestry to select clients
|
|
96
|
+
in the target subtree. Outside validateClient, `_targetLevel` is Infinity,
|
|
97
|
+
so ordinary clients do not raise these flags.
|
|
98
|
+
|
|
99
|
+
The deferred path, `_schedule()` → `_flush()`, is simpler: it runs properties,
|
|
100
|
+
size and display-list queues once each across the tree, using shift/pop, without
|
|
101
|
+
the synchronous restart loop. If flags remain afterward, it schedules another
|
|
102
|
+
callLater. Cascading invalidations can therefore require later deferred flushes;
|
|
103
|
+
validateClient instead converges synchronously for its target subtree.
|
|
104
|
+
|
|
105
|
+
### 2.3 Ordering before rendering
|
|
106
|
+
|
|
107
|
+
`_schedule()` uses Core's ticker.callLater() to schedule `_flush()`. Core executes
|
|
108
|
+
flushCallLaters() before Player.render(), so initial property commits, measurement
|
|
109
|
+
and layout run before newly mounted components are drawn. This provides the
|
|
110
|
+
render-preparation ordering that Egret's Validator establishes through
|
|
111
|
+
Event.RENDER, without independent UI and renderer RAF loops competing for order.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 3. Skin system
|
|
116
|
+
|
|
117
|
+
### 3.1 Skin is a data object
|
|
118
|
+
|
|
119
|
+
`components/Skin.ts` defines Skin as an EventDispatcher<SkinEvents>, rather than
|
|
120
|
+
a display object. Egret EUI's Skin also extends EventDispatcher; its nonvisual
|
|
121
|
+
role is shared with Kurot.
|
|
122
|
+
|
|
123
|
+
Skin declares skinParts (names), states (State entries) and elementsContent
|
|
124
|
+
(display objects to attach). Component._setSkin() performs the attachment:
|
|
125
|
+
elementsContent children are added directly to the host Component, and Skin
|
|
126
|
+
itself never appears in the display tree. This keeps the skin's configuration
|
|
127
|
+
and state role separate from the Component's visual-host role.
|
|
128
|
+
|
|
129
|
+
### 3.2 Theme loading
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
new Theme(url)
|
|
133
|
+
→ setTheme(this) synchronously register before loading completes
|
|
134
|
+
→ _load(url) adapter.getTheme(url, onSuccess, onError)
|
|
135
|
+
→ _onConfigLoaded(raw) parse JSON and merge skins/styles
|
|
136
|
+
→ if skinsJs is present
|
|
137
|
+
→ _loadSkinsModule() dynamically import compiled skins
|
|
138
|
+
→ _onLoaded() mark initialized, handle _delayList, emit COMPLETE
|
|
139
|
+
→ failure → _onLoadFailed() mark initialized and handle _delayList,
|
|
140
|
+
then emit IOErrorEvent.IO_ERROR
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Theme.ts uses an inline `_defaultThemeAdapter` backed by XMLHttpRequest unless
|
|
144
|
+
a custom adapter is passed to the constructor. The separately exported
|
|
145
|
+
DefaultThemeAdapter in `core/DefaultThemeAdapter.ts` uses fetch and is activated
|
|
146
|
+
only when the caller creates and supplies it. These two implementations do not
|
|
147
|
+
share loading logic, so changes to default-adapter behavior must account for
|
|
148
|
+
both paths.
|
|
149
|
+
|
|
150
|
+
### 3.3 Compiled skin factories and global registration
|
|
151
|
+
|
|
152
|
+
When CLI compiles KUI XML skins, its theme index module performs registrations
|
|
153
|
+
equivalent to:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import { createButtonSkin as s0 } from './skin0.js';
|
|
157
|
+
globalThis['skins.ButtonSkin'] = s0;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Theme._loadSkinsModule() resolves skinsJs relative to the theme configuration URL
|
|
161
|
+
and dynamically imports it. By the time that import promise resolves, the module
|
|
162
|
+
has registered its skin factories on globalThis.
|
|
163
|
+
|
|
164
|
+
Component._parseSkinName() resolves a string skinName through
|
|
165
|
+
`(globalThis as Record<string, unknown>)[skinName]`. The complete flow is:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
CLI-generated module globalThis["skins.X"] = factory import side effect
|
|
169
|
+
Theme configuration hostComponentKey → "skins.X" string mapping
|
|
170
|
+
Component skin setup globalThis["skins.X"] factory lookup
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
There is no separate registry object. Skin names occupy a global namespace:
|
|
174
|
+
if two themes use the same name, such as skins.ButtonSkin, the later-loaded
|
|
175
|
+
factory overwrites the earlier registration.
|
|
176
|
+
|
|
177
|
+
### 3.4 Components created before the theme is ready
|
|
178
|
+
|
|
179
|
+
Game code can construct the UI tree before the theme request finishes.
|
|
180
|
+
If Component.createChildren() has no explicit skinName, it asks the global
|
|
181
|
+
Theme for a default:
|
|
182
|
+
|
|
183
|
+
- While the theme is uninitialized, getSkinName() puts the component into the
|
|
184
|
+
deduplicated `_delayList` and returns an empty string. The component starts
|
|
185
|
+
without a skin.
|
|
186
|
+
- On loading completion or failure, `_handleDelayList()` revisits components
|
|
187
|
+
whose skinName has not been set explicitly. It resolves available skin names
|
|
188
|
+
and calls component._applySkinName(), causing validation of the applied skin.
|
|
189
|
+
|
|
190
|
+
An early-created component can render without its skin until theme loading,
|
|
191
|
+
including the skinsJs import, finishes. This follows EUI's asynchronous-theme
|
|
192
|
+
pattern and decouples UI-tree construction from theme loading.
|
|
193
|
+
|
|
194
|
+
### 3.5 Replacing a skin
|
|
195
|
+
|
|
196
|
+
_parseSkinName() dispatches by skinName type: a function is invoked as a factory
|
|
197
|
+
or constructor; a string is resolved through globalThis; an object is used as
|
|
198
|
+
a Skin instance.
|
|
199
|
+
|
|
200
|
+
_invokeSkinFactory() tests `/^class\s/.test(Function.prototype.toString.call(fn))`
|
|
201
|
+
to distinguish ES class constructors, invoked with new, from compiled KUI
|
|
202
|
+
factories, invoked with fn.call(this) to supply the host context.
|
|
203
|
+
|
|
204
|
+
_setSkin(skin) follows this lifecycle:
|
|
205
|
+
|
|
206
|
+
1. Detach the old skin: call onSkinRemoved() while its complete skinParts map
|
|
207
|
+
remains available, mark it unready and clear the map, set
|
|
208
|
+
oldSkin.hostComponent=undefined to exit its state, call oldSkin.unwatchAll()
|
|
209
|
+
to remove Watchers, and detach elementsContent from the host.
|
|
210
|
+
2. Attach the new skin: collect declared parts through skin.getPart(name) into
|
|
211
|
+
the internal map, add elementsContent to the host in declaration order,
|
|
212
|
+
set currentState and hostComponent, mark the skin ready and call onSkinReady().
|
|
213
|
+
The Component instance does not acquire dynamic properties for those part names.
|
|
214
|
+
3. Invalidate size and display list, then dispatch Event.COMPLETE on the Component.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 4. View states and overrides
|
|
219
|
+
|
|
220
|
+
### 4.1 State transitions remove and reapply overrides
|
|
221
|
+
|
|
222
|
+
IOverride defines apply(host, skin) and remove(host, skin).
|
|
223
|
+
Skin._applyState(fromState, toState) follows this algorithm:
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
oldState = states.find(s => s.name === fromState)
|
|
227
|
+
if present: call override.remove(host, this) for every oldState override
|
|
228
|
+
|
|
229
|
+
newState = states.find(s => s.name === toState)
|
|
230
|
+
if present: call override.apply(host, this) for every newState override
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
It does not compute the difference between override lists. Even a shared override
|
|
234
|
+
instance is removed and reapplied. SetProperty/SetStateProperty may restore a
|
|
235
|
+
value and immediately set it again. AddItems can remove and reinsert an object,
|
|
236
|
+
with possible z-order changes unless its placement is fully specified.
|
|
237
|
+
Developers expecting only changed overrides to execute must account for this.
|
|
238
|
+
|
|
239
|
+
### 4.2 Override behavior
|
|
240
|
+
|
|
241
|
+
- SetProperty targets a skin part, or Skin itself for an empty target. apply()
|
|
242
|
+
saves `_oldValue` and assigns the new value. remove() restores it only when
|
|
243
|
+
`_applied` is true.
|
|
244
|
+
- SetStateProperty uses the same save/restore pattern on the host Component,
|
|
245
|
+
through host[name]=value. It changes public host behavior, such as enabled,
|
|
246
|
+
rather than a named skin part.
|
|
247
|
+
- AddItems moves a display object into a destination container, defaulting to the
|
|
248
|
+
host, at the specified position. remove() detaches it only if item.parent===dest,
|
|
249
|
+
preserving an object that other logic has since moved elsewhere.
|
|
250
|
+
|
|
251
|
+
### 4.3 Separate Skin and Group state machines
|
|
252
|
+
|
|
253
|
+
Group has no attached Skin, so `_commitCurrentState()` implements similar
|
|
254
|
+
apply/remove logic using `_states`, `_statesMap` and `_currentState`.
|
|
255
|
+
It passes Group as both roles using
|
|
256
|
+
`override.apply(this as unknown as Component, this as unknown as Skin)`.
|
|
257
|
+
The signature assumes separate Component and Skin objects, while Group has only
|
|
258
|
+
itself. Host-property overrides can use this arrangement; code that requires
|
|
259
|
+
real Skin APIs such as getPart() must not assume that Group provides them.
|
|
260
|
+
|
|
261
|
+
The duplicated transition logic and casts are a known coupling point. A shared
|
|
262
|
+
transition helper could reduce the duplication, but no such extraction is
|
|
263
|
+
currently implemented.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 5. Layout system
|
|
268
|
+
|
|
269
|
+
### 5.1 Measurement and display-list contracts
|
|
270
|
+
|
|
271
|
+
LayoutBase defines the two operations implemented by concrete layouts:
|
|
272
|
+
|
|
273
|
+
- measure() runs from deep to shallow. It uses each child's getPreferredBounds()
|
|
274
|
+
to determine the container's preferred dimensions and reports them through
|
|
275
|
+
target.setMeasuredSize(w, h).
|
|
276
|
+
- updateDisplayList(width, height) runs from shallow to deep, receiving allocated
|
|
277
|
+
dimensions that can differ from measured dimensions because of parent
|
|
278
|
+
constraints. It assigns participating children through setLayoutBoundsSize()
|
|
279
|
+
and setLayoutBoundsPosition(), then reports occupied dimensions with
|
|
280
|
+
target.setContentSize(w, h) for scrolling/viewport calculations.
|
|
281
|
+
|
|
282
|
+
ILayoutTarget is the minimal container interface used by layout algorithms:
|
|
283
|
+
numChildren, getChildAt, scrollH, setMeasuredSize and related operations.
|
|
284
|
+
Group and Component both implement it. Layouts such as BasicLayout and
|
|
285
|
+
VerticalLayout can be assigned to Group.layout; Component internally reuses a
|
|
286
|
+
shared BasicLayout for measurement and display-list updates by temporarily
|
|
287
|
+
setting its target. Component's skin measurement uses unscaled preferred bounds,
|
|
288
|
+
so the host's transform is applied by its parent rather than counted twice.
|
|
289
|
+
|
|
290
|
+
Layouts skip children that do not implement the UI layout contract or have
|
|
291
|
+
includeInLayout=false. This excludes an element from layout without requiring
|
|
292
|
+
its removal from the display tree. State membership that changes display-tree
|
|
293
|
+
attachment is handled separately by AddItems.
|
|
294
|
+
|
|
295
|
+
### 5.2 Percentage dimensions
|
|
296
|
+
|
|
297
|
+
BasicLayout resolves left/right/top/bottom, horizontalCenter/verticalCenter and
|
|
298
|
+
percentWidth/percentHeight. When both edge constraints on an axis are set, they
|
|
299
|
+
fix that dimension: width becomes unscaledWidth-right-left, taking precedence
|
|
300
|
+
over percentWidth. Percentage sizing applies when the two edge constraints do
|
|
301
|
+
not both determine the dimension.
|
|
302
|
+
|
|
303
|
+
The fmt() helper parses string constraints such as `left="10%"` in KUI XML.
|
|
304
|
+
A trailing percent sign selects percentage calculation; otherwise the value is
|
|
305
|
+
converted to a number. There is no separate runtime type tag for percentages
|
|
306
|
+
versus pixels.
|
|
307
|
+
|
|
308
|
+
LinearLayoutBase.flexChildrenProportionally(), shared by VerticalLayout and
|
|
309
|
+
HorizontalLayout, iteratively allocates percentages with min/max constraints.
|
|
310
|
+
A do...while loop identifies children whose proportional allocation exceeds a
|
|
311
|
+
bound, clamps them and removes them from the remaining allocation pool using
|
|
312
|
+
in-place partition swaps. It subtracts their percentage shares and redistributes
|
|
313
|
+
the remainder. One division is insufficient because clamping a child changes
|
|
314
|
+
the effective shares of the others. This follows the Flex-style percentage
|
|
315
|
+
allocation algorithm.
|
|
316
|
+
|
|
317
|
+
### 5.3 Gaps and padding
|
|
318
|
+
|
|
319
|
+
LinearLayoutBase owns gap and paddingLeft/Right/Top/Bottom; setters invalidate
|
|
320
|
+
the target layout. VerticalLayout/HorizontalLayout subtract
|
|
321
|
+
(numElements-1)*gap before allocation, then add gaps during positioning.
|
|
322
|
+
TileLayout maintains separate horizontalGap/verticalGap and padding fields.
|
|
323
|
+
It extends LayoutBase directly, so these accessors are implemented independently
|
|
324
|
+
of LinearLayoutBase.
|
|
325
|
+
|
|
326
|
+
### 5.4 Virtual layout
|
|
327
|
+
|
|
328
|
+
Virtual layout is disabled by default. Enable it explicitly through
|
|
329
|
+
layout.useVirtualLayout or the corresponding DataGroup/List property.
|
|
330
|
+
It lets List/DataGroup instantiate renderers only for the visible range when
|
|
331
|
+
handling thousands of records:
|
|
332
|
+
|
|
333
|
+
- LinearLayoutBase's protected elementSizeTable stores the last known axis size
|
|
334
|
+
by data index, rather than display-child index. NaN means unknown, estimated
|
|
335
|
+
using typicalWidth/typicalHeight, initially 71 × 22.
|
|
336
|
+
- elementAdded(index)/elementRemoved(index) keep the size table synchronized with
|
|
337
|
+
ICollection changes without requiring full remeasurement.
|
|
338
|
+
- getStartPosition(index) accumulates preceding sizes to obtain an offset.
|
|
339
|
+
findIndexAt uses recursive binary search to map scroll positions to visible indices.
|
|
340
|
+
- updateDisplayListVirtual() visits only startIndex..endIndex. It calls
|
|
341
|
+
target.setVirtualElementIndicesInView(startIndex, endIndex), which lets
|
|
342
|
+
DataGroup recycle and instantiate renderers for that range.
|
|
343
|
+
- measureVirtual() estimates total scrollable size from getElementTotalSize()
|
|
344
|
+
and a correction based on visible elements, without instantiating every
|
|
345
|
+
off-screen record.
|
|
346
|
+
- TileLayout uses scrollH/scrollV and fixed column/row strides, including gaps,
|
|
347
|
+
to determine the visible range through _getIndexInView(). Uniform cells make
|
|
348
|
+
this simpler than variable-size linear virtualization.
|
|
349
|
+
|
|
350
|
+
LayoutBase.getElementIndicesInView() returns an empty array by default.
|
|
351
|
+
TileLayout overrides it to expose the currently calculated range.
|
|
352
|
+
VerticalLayout/HorizontalLayout use protected getIndexInView() methods for their
|
|
353
|
+
internal calculations and do not override that public query. New layouts must
|
|
354
|
+
explicitly connect range calculation, viewport invalidation and renderer
|
|
355
|
+
materialization; overriding the public query alone does not drive virtual layout.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 6. Component hierarchy
|
|
360
|
+
|
|
361
|
+
### Component subclasses
|
|
362
|
+
|
|
363
|
+
Component is the visual host for skinnable controls, delegating skin states to
|
|
364
|
+
Skin and layout/validation state to UIState. The table lists its descendants,
|
|
365
|
+
including intermediate base classes.
|
|
366
|
+
|
|
367
|
+
| Class | Responsibility |
|
|
368
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
369
|
+
| `Component` | Base for skinnable components: skin lifecycle, view states, constraints and validation delegation |
|
|
370
|
+
| `Rect` | Rectangle graphics with fill and stroke |
|
|
371
|
+
| `Image` | Displays bitmap data resolved through IAssetAdapter |
|
|
372
|
+
| `Label` | Text display wrapping one TextField |
|
|
373
|
+
| `BitmapLabel` | Bitmap-font label wrapping one BitmapText; requires Core 2.2.0 or later |
|
|
374
|
+
| `EditableText extends Label` | Editable text with prompt support |
|
|
375
|
+
| `Button` | Clickable up/down/disabled states; toggle enables automatic selected changes |
|
|
376
|
+
| `ToggleButton extends Button` | Defaults toggle to true and toggles selection on click |
|
|
377
|
+
| `CheckBox extends ToggleButton` | Toggle behavior whose appearance is defined by its skin |
|
|
378
|
+
| `RadioButton extends ToggleButton` | Mutually exclusive selection through RadioButtonGroup |
|
|
379
|
+
| `ToggleSwitch extends ToggleButton` | Skin variant presenting a sliding switch |
|
|
380
|
+
| `Range` | Bounds values to minimum/maximum with optional snapInterval; base for sliders |
|
|
381
|
+
| `SliderBase extends Range` | Abstract dragging behavior for thumb/track skin parts |
|
|
382
|
+
| `HSlider extends SliderBase` | Horizontal slider, increasing left to right |
|
|
383
|
+
| `VSlider extends SliderBase` | Vertical slider, increasing bottom to top |
|
|
384
|
+
| `ScrollBarBase` | Binds thumb movement to a viewport's scroll position |
|
|
385
|
+
| `HScrollBar extends ScrollBarBase` | Horizontal scrolling through scrollH |
|
|
386
|
+
| `VScrollBar extends ScrollBarBase` | Vertical scrolling through scrollV |
|
|
387
|
+
| `ProgressBar` | Displays progress through a clipped thumb using scrollRect, with label formatting |
|
|
388
|
+
| `TextInput` | Text input with prompts and password masking |
|
|
389
|
+
| `Panel` | Skinnable container with optional title bar, close button and drag area |
|
|
390
|
+
| `Scroller` | Wraps an IViewport, usually Group, and manages touch scrolling and scrollbars |
|
|
391
|
+
| `ComboBox` | Dropdown selection with a trigger button and popup list |
|
|
392
|
+
| `ItemRenderer` | Data-driven item renderer base for DataGroup/List/TabBar |
|
|
393
|
+
|
|
394
|
+
### Group subclasses
|
|
395
|
+
|
|
396
|
+
Group is a container without a skin. It owns a lightweight state machine and a
|
|
397
|
+
pluggable LayoutBase.
|
|
398
|
+
|
|
399
|
+
| Class | Responsibility |
|
|
400
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
401
|
+
| `Group` | Participates in validation and delegates child positioning to LayoutBase; maintains its own state machine, as described in section 4.3 |
|
|
402
|
+
| `DataGroup extends Group` | Converts ICollection records into renderers and maintains a reusable pool for virtual layout |
|
|
403
|
+
| `ListBase extends DataGroup` | Adds selectedIndex/selectedItem and requireSelection semantics |
|
|
404
|
+
| `List extends ListBase` | Adds touch selection by clicking renderers |
|
|
405
|
+
| `TabBar extends ListBase` | Selectable tabs with requireSelection=true by default |
|
|
406
|
+
| `ViewStack` | Stacked navigation container showing one child at a time; implements ICollection |
|
|
407
|
+
| `UILayer` | Top-level container following stage dimensions through RESIZE events |
|
|
408
|
+
|
|
409
|
+
### Supporting classes
|
|
410
|
+
|
|
411
|
+
| Class | Responsibility |
|
|
412
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------- |
|
|
413
|
+
| `Skin` | Nonvisual EventDispatcher declaring skinParts/states/elementsContent; see section 3 |
|
|
414
|
+
| `RadioButtonGroup` | EventDispatcher tracking mutually exclusive RadioButtons under a groupName and emitting PropertyEvent |
|
|
415
|
+
| `TouchScroll` | Inertial touch-scroll physics used by Scroller |
|
|
416
|
+
| `Animation` | Numeric easing used by TouchScroll, driven by Core's ticker |
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 7. Relationship to Egret EUI
|
|
421
|
+
|
|
422
|
+
| Egret EUI concept | Kurot UI counterpart | Notes |
|
|
423
|
+
| ------------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
424
|
+
| `eui.Component` | `Component` | Same role; UI behavior uses standard Sprite inheritance and explicit delegation rather than prototype mixins |
|
|
425
|
+
| `eui.Skin` | `Skin` | Both are nonvisual EventDispatcher objects; their content is attached to the host component |
|
|
426
|
+
| Three-phase UIComponent validation | `UIState` + `Validator` | Depth-ordered properties, measurement and display-list phases; Kurot uses Core's callLater queue before rendering where Egret uses Event.RENDER |
|
|
427
|
+
| Theme adapters and theme registration | `IThemeAdapter` + `Theme`/`getTheme()`/`setTheme()` | Both support theme adapters; Kurot injects the adapter through the Theme constructor, while the Egret reference resolves eui.IThemeAdapter through its implementation registry |
|
|
428
|
+
| Global registration of compiled EXML skins | `globalThis["skins.X"] = factory` from ESM imports | Both use global skin names; Kurot's compiled output registers factories invoked with .call(this) to provide host context |
|
|
429
|
+
| State/SetProperty/SetStateProperty/AddItems | Same class names and IOverride interface | Direct adaptation of declarative state overrides |
|
|
430
|
+
| BasicLayout/VerticalLayout/HorizontalLayout/TileLayout | Same class names | Similar percentage allocation, gaps, padding and virtual-layout algorithms, including iterative min/max clamping |
|
|
431
|
+
| Prototype-copy UIComponent implementation | TypeScript inheritance + UIState/ILayoutTarget delegation | The central architectural change described in section 1 |
|
|
432
|
+
|
|
433
|
+
Kurot-specific choices include the IUIOwner/UIState composition boundary,
|
|
434
|
+
constructor-based Theme adapter injection and ESM skin factories with explicit
|
|
435
|
+
host context. UIState can exist independently of a specific DisplayObject
|
|
436
|
+
subclass; Group and Component each hold an instance rather than requiring a
|
|
437
|
+
shared visual base class for all validation state.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## 8. Test coverage
|
|
442
|
+
|
|
443
|
+
Tests are grouped under `test/`. Run `pnpm --dir packages/ui test` for current
|
|
444
|
+
file and case counts.
|
|
445
|
+
|
|
446
|
+
| Area | Test files |
|
|
447
|
+
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
448
|
+
| Validation / theme infrastructure | sanity, Theme, Validator |
|
|
449
|
+
| Skins and states | Skin, SkinAlignment (real CLI template skins), SkinMeasurement, AddItems |
|
|
450
|
+
| Data binding | Binding, Watcher |
|
|
451
|
+
| Collections and data-driven components | ArrayCollection, DataGroup, ListBase, ItemRenderer, ComboBox, TabBar, ViewStack |
|
|
452
|
+
| Individual components | RadioButton, Range, Slider, ProgressBar, Scroller, Label, BitmapLabel, TextInput |
|
|
453
|
+
| Text fitting | ButtonLabelTextFit, LabelTextFit, LabelTextFitBounds |
|
|
454
|
+
| Cross-component behavior | Enabled (EUI-aligned enabled/touchEnabled/touchChildren behavior), EventMapOverride, GestureLifecycle, TransformLayout, ScrollAndVirtualLayout |
|
|
455
|
+
|
|
456
|
+
There is no dedicated Group.test.ts; Group state behavior is covered indirectly
|
|
457
|
+
by container/component tests. There are also no separate VerticalLayout.test.ts
|
|
458
|
+
or TileLayout.test.ts files. Layout behavior is exercised by component and
|
|
459
|
+
integration tests such as TransformLayout, Scroller, SkinAlignment,
|
|
460
|
+
SkinMeasurement and ScrollAndVirtualLayout. Dedicated state-machine and layout
|
|
461
|
+
algorithm tests remain useful areas for additional coverage.
|
|
462
|
+
|
|
463
|
+
`examples/benchmark/` provides real Chromium performance checks for a 400-node
|
|
464
|
+
static UI, 240-node transform/alpha animation and a virtual list with 10,000 records.
|
|
465
|
+
It records frame/render time, draw calls, validation-phase calls and ItemRenderer
|
|
466
|
+
creation/reuse. It is a regression baseline for this UI implementation, rather
|
|
467
|
+
than a ranking across UI frameworks.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# BitmapLabel
|
|
2
|
+
|
|
3
|
+
Available in UI 3.2.0 with `@kurot/core ^2.2.0`. Core resolves its published
|
|
4
|
+
@kurot/bitmap-font dependency from npm. UI 3.2.0 is prepared for publication;
|
|
5
|
+
published UI 3.1.0 does not contain this component. Core 2.1.1 does not contain
|
|
6
|
+
its required BitmapText rendering.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { BitmapLabel } from '@kurot/ui';
|
|
10
|
+
|
|
11
|
+
const label = new BitmapLabel('1,234.56');
|
|
12
|
+
label.font = 'number_font_fnt';
|
|
13
|
+
label.multiline = false;
|
|
14
|
+
label.height = 100;
|
|
15
|
+
group.addChild(label);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`font` accepts a BitmapFont, a configured resource name, or undefined to clear.
|
|
19
|
+
Names resolve through the Core resource singleton. Preloaded names resolve
|
|
20
|
+
synchronously during commitProperties; unloaded names start resource.load.
|
|
21
|
+
Successful name resolution dispatches Event.COMPLETE; failed or incorrectly
|
|
22
|
+
typed resources dispatch IOErrorEvent.IO_ERROR with source/error data.
|
|
23
|
+
Superseded asynchronous requests cannot overwrite a newer font assignment,
|
|
24
|
+
including A-to-B-to-A changes. The resource cache or caller retains ownership.
|
|
25
|
+
|
|
26
|
+
Text assignments dispatch PropertyEvent.PROPERTY_CHANGE for binding. The
|
|
27
|
+
component participates in normal layout, measurement and invalidation and owns
|
|
28
|
+
one BitmapText child. Parent width constraints affect wrapping; measurements do
|
|
29
|
+
not leave temporary width/height constraints on that child. textWidth/textHeight
|
|
30
|
+
report content under the width constraint before height clipping.
|
|
31
|
+
|
|
32
|
+
Properties: text, font, textAlign, verticalAlign, letterSpacing, lineSpacing,
|
|
33
|
+
multiline and smoothing, plus ordinary Component properties. Tint is the ordinary
|
|
34
|
+
DisplayObject tint. Alignments position glyphs without scaling them. Set scaleX
|
|
35
|
+
and scaleY for decorative number sizing. Default multiline=true; explicit false
|
|
36
|
+
keeps the first hard-separated line unwrapped.
|
|
37
|
+
|
|
38
|
+
This initial change exposes a native UI component and supports programmatic
|
|
39
|
+
skins. The ui-document built-in catalog, CLI KUI tag registry, Editor and
|
|
40
|
+
ui-runtime materializers do not yet adopt BitmapLabel. Those require coordinated
|
|
41
|
+
consumer releases; do not assume an installed compiler accepts the XML tag.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Label text layout and fitting
|
|
2
|
+
|
|
3
|
+
Introduced in UI 3.1.0 with Core >= 2.1.0, this contract is unchanged in UI 3.2.0,
|
|
4
|
+
which requires Core ^2.2.0. The public
|
|
5
|
+
`TextFitMode` type is `none | shrink`. Native UI depends only on Core. KUI
|
|
6
|
+
authoring introduced fitting metadata in ui-document 0.9.0 and retains it in
|
|
7
|
+
0.11.0. Published CLI 3.3.0 and ui-runtime 0.8.2 use ui-document `^0.11.0`.
|
|
8
|
+
Runtime 0.8.0's `^0.9.0` and runtime 0.8.1's `^0.10.0` peer ranges exclude
|
|
9
|
+
the current kernel. Label presets are expanded into native properties before
|
|
10
|
+
rendering; their precedence does not change fitting behavior. See the
|
|
11
|
+
[project style contract](../../ui-document/docs/project-styles.md).
|
|
12
|
+
|
|
13
|
+
## Lines and bounds
|
|
14
|
+
|
|
15
|
+
- `Label.multiline` defaults to `true`. Text wraps at a finite available width;
|
|
16
|
+
hard separators also create lines. Automatic height measures all those lines.
|
|
17
|
+
- `multiline = false` displays only the first hard-separated line, without
|
|
18
|
+
automatic wrapping. Overflow is clipped by the available bounds. Original
|
|
19
|
+
`text` is retained, including any hidden later lines.
|
|
20
|
+
- `wordWrap = true` chooses Unicode line-break opportunities, with the Core
|
|
21
|
+
dictionary and overflow tailoring. `false` chooses character boundaries.
|
|
22
|
+
This property has no effect while `multiline` is false.
|
|
23
|
+
- A fixed height limits the visible area. It does not change the line count or
|
|
24
|
+
switch a multiline label into single-line mode.
|
|
25
|
+
- `EditableText` defaults to single-line input. Core `TextField` defaults to
|
|
26
|
+
multiline in dynamic mode and single-line in input mode until `multiline` is
|
|
27
|
+
explicitly assigned. An assignment persists across later type changes.
|
|
28
|
+
- CRLF, CR, LF, VT, FF, NEL, line separator and paragraph separator are hard
|
|
29
|
+
separators. In single-line mode, only the content before the first is drawn.
|
|
30
|
+
A literal backslash followed by `n` remains literal text in KUI strings.
|
|
31
|
+
|
|
32
|
+
Explicit `multiline="false"` in older skins now means a real single line.
|
|
33
|
+
Remove it or set it to `true` for text intended to wrap. No files are migrated
|
|
34
|
+
automatically; omitted Label flags continue to allow wrapping.
|
|
35
|
+
|
|
36
|
+
## Shrink to fit
|
|
37
|
+
|
|
38
|
+
```xml
|
|
39
|
+
<Label id="labelDisplay" text="KZT 10 000,00" width="140" size="24"
|
|
40
|
+
multiline="false" textFit="shrink" minFontSize="16" />
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`textFit` accepts `none` (default) and `shrink`. Shrinking applies to ordinary
|
|
44
|
+
single-line Labels. Multiline Labels retain their authored size; EditableText
|
|
45
|
+
rejects `shrink`, including through the shared authoring schema. Put the policy
|
|
46
|
+
on a Button's `labelDisplay` skin part rather than on the Button itself.
|
|
47
|
+
|
|
48
|
+
- `size` is always the authored/base size. `renderedSize` is the derived drawing
|
|
49
|
+
size after layout validation. State overrides capture and restore `size`.
|
|
50
|
+
- Fit starts from that base size on every metric change. Shorter text or a
|
|
51
|
+
larger region restores the base size; text never grows beyond it.
|
|
52
|
+
- `minFontSize` defaults to 12 logical pixels, must be finite and at least 1,
|
|
53
|
+
and acts as a lower bound. A minimum above `size` never enlarges the text.
|
|
54
|
+
- Measurement uses the displayed text, font family, bold and italic settings,
|
|
55
|
+
and reserves twice the outline thickness on each constrained axis. The
|
|
56
|
+
ordinary Core clipping and outline rendering rules remain in effect.
|
|
57
|
+
- Available bounds come from fixed dimensions, parent layout constraints or
|
|
58
|
+
explicit maximum dimensions. Icon spacing belongs to the skin's Label
|
|
59
|
+
region. Automatic measured dimensions are not fed back as fit constraints.
|
|
60
|
+
- A finite zero-sized region is constrained. An automatic axis is unconstrained.
|
|
61
|
+
- Search performs at most 16 metric comparisons after the endpoint checks.
|
|
62
|
+
Intermediate results are rounded down to tenths of a logical pixel while
|
|
63
|
+
retaining the minimum. Base sizes and minimums may be fractional.
|
|
64
|
+
- If text still cannot fit at the minimum, `textFitOverflow` is true after
|
|
65
|
+
validation and ordinary clipping remains. The project can shorten its
|
|
66
|
+
wording, change formatting or enlarge the region. The engine never changes
|
|
67
|
+
amounts, translations, currency symbols or the authored text.
|
|
68
|
+
- The fit result is cached for unchanged metrics and bounds. Recalculation is
|
|
69
|
+
part of UI validation rather than a per-frame or project-wide scan.
|
|
70
|
+
|
|
71
|
+
These two read-only results are runtime observations, not authored KUI fields.
|
|
72
|
+
CLI emits the policy and base size; ui-runtime uses the same native Label. Neither
|
|
73
|
+
writes the derived size into XML, styles, semantic documents or history.
|
|
74
|
+
|
|
75
|
+
Load fonts before constructing or measuring text where possible. If a font
|
|
76
|
+
becomes ready later, call `label.invalidateSize()` before the next validation;
|
|
77
|
+
this clears both fitting and Core line metrics even when the family name stays
|
|
78
|
+
the same. Direct Core users call `textField.invalidateTextMetrics()`.
|
|
79
|
+
|
|
80
|
+
## Verification
|
|
81
|
+
|
|
82
|
+
Core tests cover dynamic/input defaults, explicit mode changes, every hard
|
|
83
|
+
separator, width wrapping and fixed-height clipping. UI tests cover multilingual
|
|
84
|
+
and currency strings, minimum overflow, outlines, font readiness, state size
|
|
85
|
+
restoration, password display, automatic dimensions and parent resize stability.
|
|
86
|
+
Native Button tests cover the label's actual available region, changing content,
|
|
87
|
+
disabled-state restoration and replacing the active skin without updating the
|
|
88
|
+
detached label.
|
|
89
|
+
Shared-schema, CLI and ui-runtime tests cover XML round trips, invalid policies,
|
|
90
|
+
read-only boundaries and materialized preview without authored-document changes.
|
|
91
|
+
Tests use deterministic Canvas measurement doubles; actual platform fonts can
|
|
92
|
+
produce different fitted sizes.
|
|
93
|
+
|
|
94
|
+
Browser verification also covers WebGL and Canvas with Chakra Petch regular and
|
|
95
|
+
bold fonts from the KUI example, plus platform fallback for Chinese. It checks
|
|
96
|
+
single-line fitting, Unicode wrapping without leading separator spaces, outline
|
|
97
|
+
allowance, minimum overflow, native Button state restoration and invalidation
|
|
98
|
+
after a font finishes loading. Fitted sizes are platform-dependent; these checks
|
|
99
|
+
assert the layout contract rather than a universal numeric result.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kurot/ui",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "EUI-compatible UI component framework for the Kurot game engine
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "EUI-compatible UI component framework for the Kurot game engine \u2014 skin-based components, layouts, data binding, view states",
|
|
5
5
|
"author": "Kelvin <kyia.x52@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist/**/*",
|
|
19
|
+
"docs/**/*",
|
|
19
20
|
"CHANGELOG.md",
|
|
20
21
|
"LICENSE"
|
|
21
22
|
],
|
|
@@ -32,10 +33,10 @@
|
|
|
32
33
|
"prepublishOnly": "npm run clean && npm run build"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
|
-
"@kurot/core": "^2.
|
|
36
|
+
"@kurot/core": "^2.2.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@kurot/core": "^2.
|
|
39
|
+
"@kurot/core": "^2.2.0",
|
|
39
40
|
"@playwright/test": "1.62.1",
|
|
40
41
|
"@types/node": "^22.0.0",
|
|
41
42
|
"happy-dom": "^20.8.9",
|