@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.
@@ -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.0.0",
4
- "description": "EUI-compatible UI component framework for the Kurot game engine — skin-based components, layouts, data binding, view states",
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.0.0"
36
+ "@kurot/core": "^2.2.0"
36
37
  },
37
38
  "devDependencies": {
38
- "@kurot/core": "^2.0.0",
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",