@kurot/ui 3.1.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 +29 -0
- package/README.md +107 -47
- 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/index.d.ts +1 -0
- 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/docs/ai-context.md +19 -6
- package/docs/architecture.md +397 -408
- package/docs/bitmap-label.md +41 -0
- package/docs/label-text-layout.md +8 -3
- package/package.json +4 -4
package/docs/architecture.md
CHANGED
|
@@ -1,478 +1,467 @@
|
|
|
1
|
-
# @kurot/ui
|
|
1
|
+
# @kurot/ui architecture
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
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.
|
|
8
11
|
|
|
9
12
|
---
|
|
10
13
|
|
|
11
|
-
##
|
|
14
|
+
## 1. Overview
|
|
12
15
|
|
|
13
|
-
`@kurot/ui`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
`implementUIComponent()`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
28
31
|
|
|
29
32
|
---
|
|
30
33
|
|
|
31
|
-
##
|
|
34
|
+
## 2. Validation cycle
|
|
32
35
|
|
|
33
|
-
### 2.1
|
|
36
|
+
### 2.1 Three phases and three queues
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
`_propsQueue
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
The phases execute in a fixed order with different traversal directions:
|
|
46
49
|
|
|
47
|
-
```
|
|
48
|
-
validateProperties()
|
|
49
|
-
validateSize()
|
|
50
|
-
validateDisplayList()
|
|
50
|
+
```text
|
|
51
|
+
validateProperties() shallow → deep commitProperties()
|
|
52
|
+
validateSize() deep → shallow measure()
|
|
53
|
+
validateDisplayList() shallow → deep updateDisplayList()
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
### 2.3 与渲染循环的顺序保证
|
|
110
|
-
|
|
111
|
-
`_schedule()` 通过 core 的 `ticker.callLater()` 安排 `_flush()`。Core 在
|
|
112
|
-
`Player.render()` 之前先执行 `flushCallLaters()`,因此动态挂载的组件会先完成
|
|
113
|
-
属性提交、测量和布局,再产生首帧画面。这个顺序等价于 Egret Validator 通过
|
|
114
|
-
`Event.RENDER` 建立的渲染前校验契约,同时避免 UI 和渲染器各自维护独立 RAF
|
|
115
|
-
所产生的先后竞争。
|
|
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.
|
|
116
112
|
|
|
117
113
|
---
|
|
118
114
|
|
|
119
|
-
##
|
|
115
|
+
## 3. Skin system
|
|
120
116
|
|
|
121
|
-
### 3.1 Skin
|
|
117
|
+
### 3.1 Skin is a data object
|
|
122
118
|
|
|
123
|
-
`
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
声明 `skinParts`(字符串数组)、`states`(State 列表)、`elementsContent`
|
|
127
|
-
(要挂载的显示对象数组),实际挂载动作由宿主 `Component` 的
|
|
128
|
-
`_setSkin()` 完成——`elementsContent` 里的每个显示对象被直接
|
|
129
|
-
`addChildAt` 到*宿主组件*上,不是挂到 `Skin` 对象上。
|
|
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.
|
|
130
122
|
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
133
128
|
|
|
134
|
-
### 3.2
|
|
129
|
+
### 3.2 Theme loading
|
|
135
130
|
|
|
136
|
-
```
|
|
137
|
-
Theme(url)
|
|
138
|
-
→ setTheme(this)
|
|
139
|
-
→ _load(url)
|
|
140
|
-
→ _onConfigLoaded(raw)
|
|
141
|
-
→
|
|
142
|
-
→ _loadSkinsModule()
|
|
143
|
-
→ _onLoaded()
|
|
144
|
-
→
|
|
145
|
-
|
|
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
|
|
146
141
|
```
|
|
147
142
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
清理的重复实现,两个 adapter 分别用 XHR 和 fetch,逻辑没有共享,将来
|
|
155
|
-
修 bug 容易只改一边漏另一边。
|
|
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.
|
|
156
149
|
|
|
157
|
-
### 3.3
|
|
150
|
+
### 3.3 Compiled skin factories and global registration
|
|
158
151
|
|
|
159
|
-
|
|
152
|
+
When CLI compiles KUI XML skins, its theme index module performs registrations
|
|
153
|
+
equivalent to:
|
|
160
154
|
|
|
161
155
|
```js
|
|
162
156
|
import { createButtonSkin as s0 } from './skin0.js';
|
|
163
157
|
globalThis['skins.ButtonSkin'] = s0;
|
|
164
158
|
```
|
|
165
159
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
皮肤 factory 已经把自己挂到 `globalThis` 上了。
|
|
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.
|
|
170
163
|
|
|
171
|
-
|
|
172
|
-
`
|
|
173
|
-
所以完整链路是:
|
|
164
|
+
Component._parseSkinName() resolves a string skinName through
|
|
165
|
+
`(globalThis as Record<string, unknown>)[skinName]`. The complete flow is:
|
|
174
166
|
|
|
175
|
-
```
|
|
176
|
-
CLI
|
|
177
|
-
Theme
|
|
178
|
-
Component
|
|
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
|
|
179
171
|
```
|
|
180
172
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
会直接覆盖前一个在 `globalThis` 上的槏位。
|
|
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.
|
|
185
176
|
|
|
186
|
-
### 3.4
|
|
177
|
+
### 3.4 Components created before the theme is ready
|
|
187
178
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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:
|
|
191
182
|
|
|
192
|
-
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
198
189
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
UI
|
|
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.
|
|
202
193
|
|
|
203
|
-
### 3.5
|
|
194
|
+
### 3.5 Replacing a skin
|
|
204
195
|
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
207
199
|
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
210
203
|
|
|
211
|
-
|
|
204
|
+
_setSkin(skin) follows this lifecycle:
|
|
212
205
|
|
|
213
|
-
1.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
3. 收尾 `invalidateSize()` + `invalidateDisplayList()` + 在*组件*(不是
|
|
223
|
-
皮肤)上派发 `Event.COMPLETE`。
|
|
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.
|
|
224
215
|
|
|
225
216
|
---
|
|
226
217
|
|
|
227
|
-
##
|
|
218
|
+
## 4. View states and overrides
|
|
228
219
|
|
|
229
|
-
### 4.1
|
|
220
|
+
### 4.1 State transitions remove and reapply overrides
|
|
230
221
|
|
|
231
|
-
|
|
232
|
-
|
|
222
|
+
IOverride defines apply(host, skin) and remove(host, skin).
|
|
223
|
+
Skin._applyState(fromState, toState) follows this algorithm:
|
|
233
224
|
|
|
234
|
-
```
|
|
225
|
+
```text
|
|
235
226
|
oldState = states.find(s => s.name === fromState)
|
|
236
|
-
|
|
227
|
+
if present: call override.remove(host, this) for every oldState override
|
|
237
228
|
|
|
238
229
|
newState = states.find(s => s.name === toState)
|
|
239
|
-
|
|
230
|
+
if present: call override.apply(host, this) for every newState override
|
|
240
231
|
```
|
|
241
232
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
这个签名假设的是一对独立的 `Component` + `Skin`,但 `Group` 只有自己
|
|
274
|
-
一个对象,只能同时假扮两个角色(这能工作是因为 `Group` 状态实际用到的
|
|
275
|
-
override 通常是 `SetStateProperty`,从不真正调用 `skin.getPart()`)。
|
|
276
|
-
|
|
277
|
-
这是一处已知的架构重复——两套结构相同的状态机分别长在 `Skin` 和 `Group`
|
|
278
|
-
上,如果抽出一个共享的 `applyStateTransition(states, from, to, host, skin)`
|
|
279
|
-
辅助函数,能去掉大约 30 行重复代码和那处不安全的双重类型转换。目前
|
|
280
|
-
未做这个抽取,属于已知但尚未处理的技术债,不是刻意的设计。
|
|
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.
|
|
281
264
|
|
|
282
265
|
---
|
|
283
266
|
|
|
284
|
-
##
|
|
285
|
-
|
|
286
|
-
### 5.1
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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.
|
|
372
356
|
|
|
373
357
|
---
|
|
374
358
|
|
|
375
|
-
##
|
|
376
|
-
|
|
377
|
-
###
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
|
384
|
-
|
|
|
385
|
-
| `
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
390
|
-
| `
|
|
391
|
-
| `
|
|
392
|
-
| `
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
400
|
-
| `
|
|
401
|
-
| `
|
|
402
|
-
| `
|
|
403
|
-
| `
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
|
408
|
-
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
|
420
|
-
|
|
|
421
|
-
| `
|
|
422
|
-
| `
|
|
423
|
-
| `
|
|
424
|
-
|
|
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 |
|
|
425
417
|
|
|
426
418
|
---
|
|
427
419
|
|
|
428
|
-
##
|
|
429
|
-
|
|
430
|
-
| Egret EUI
|
|
431
|
-
|
|
|
432
|
-
| `eui.Component`
|
|
433
|
-
| `eui.Skin`
|
|
434
|
-
|
|
|
435
|
-
|
|
|
436
|
-
|
|
|
437
|
-
|
|
|
438
|
-
|
|
|
439
|
-
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
DisplayObject 子类存在的状态机对象,被 `Group`/`Component` 各自持有一份
|
|
447
|
-
(`this.ui`),而不是把校验状态直接塞进一个公共基类——这正是
|
|
448
|
-
`Group`/`Component` 能在只有 1 层继承的情况下都实现 `ILayoutTarget`
|
|
449
|
-
的原因。
|
|
450
|
-
- `Skin` 作为非视觉数据持有者的设计(前面已详述)——这是相对 Egret
|
|
451
|
-
"皮肤即容器"模型的刻意偏离。
|
|
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.
|
|
452
438
|
|
|
453
439
|
---
|
|
454
440
|
|
|
455
|
-
##
|
|
456
|
-
|
|
457
|
-
`test
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
|
461
|
-
|
|
|
462
|
-
|
|
|
463
|
-
|
|
|
464
|
-
|
|
|
465
|
-
|
|
|
466
|
-
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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.
|