@kurot/ui 3.0.0 → 3.1.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,211 @@
1
+ # @kurot/ui — AI context map
2
+
3
+ Read this before exploring `src/`. It is a compressed map so an agent
4
+ unfamiliar with Kurot does not need to re-derive the architecture from scratch
5
+ each session. Treat the package source and its `src/index.ts` barrel as the
6
+ authority for current behavior and exports; this file provides the compressed
7
+ map, runtime contracts and task→file lookup.
8
+
9
+ Package identity: `@kurot/ui@3.1.0`, EUI-compatible UI framework on top of
10
+ `@kurot/core`. Peer-depends on `@kurot/core@^2.1.0`. Rewritten with standard class
11
+ inheritance and delegation — no namespace mixins, no prototype copying.
12
+
13
+ Source root: `src/kurot/`. Public API: `src/index.ts` is a flat re-export of
14
+ 7 barrels — `core`, `layouts`, `components`, `events`, `states`, `collections`,
15
+ `binding` — see §4.
16
+
17
+ ## 1. Directory map
18
+
19
+ ```
20
+ src/kurot/
21
+ ├── core/ Layout contract + state machine + theming.
22
+ │ IUIComponent (interface every UI component implements),
23
+ │ UIState (the actual layout state machine — see §3),
24
+ │ Validator/validator (render-preparation validation scheduler),
25
+ │ Theme/getTheme()/setTheme(), IViewport, IAssetAdapter/IThemeAdapter.
26
+ ├── components/ Group, Component (delegation core — see §2), Skin, and
27
+ │ concrete widgets: Button, Label, CheckBox, RadioButton,
28
+ │ ToggleButton/ToggleSwitch, ProgressBar, HSlider/VSlider,
29
+ │ Rect, Image, EditableText, TextInput, Panel, ViewStack,
30
+ │ Scroller, UILayer, DataGroup, List, TabBar, ComboBox,
31
+ │ ItemRenderer, HScrollBar/VScrollBar, Animation, Range.
32
+ ├── text/ Internal text measurement and font-fitting algorithms.
33
+ │ Used by components; not part of the public export barrels.
34
+ ├── states/ View States: State, IOverride, SetProperty,
35
+ │ SetStateProperty, AddItems. Declarative skin state-diffing.
36
+ ├── binding/ Watcher (property-chain observer), Binding (static helpers:
37
+ │ bindProperty/bindHandler/bindProperties). Available to programmatic skins and runtime integration code.
38
+ ├── collections/ ICollection, ArrayCollection. Observable data source for
39
+ │ DataGroup/List/TabBar/ComboBox.
40
+ ├── layouts/ LayoutBase (abstract), BasicLayout, VerticalLayout,
41
+ │ HorizontalLayout (both extend LinearLayoutBase), TileLayout,
42
+ │ ILayoutTarget.
43
+ └── events/ UIEvent, ItemTapEvent, CollectionEvent, PropertyEvent,
44
+ ScrollerThrowEvent.
45
+ ```
46
+
47
+ ## 2. Non-obvious current behavior
48
+
49
+ - **`Skin` is NOT a `Group`/DisplayObject subclass.** `components/Skin.ts`
50
+ defines `class Skin extends EventDispatcher<SkinEvents>` — a plain data
51
+ holder, not a visual container. A skin's
52
+ `elementsContent: DisplayObject[]` are physically parented onto the _host_
53
+ `Component` by `Component._setSkin()`, not onto the skin itself. The skin
54
+ object itself never appears in the display tree — its only job is to
55
+ declare `skinParts`/`states`/bindings.
56
+ - `Skin.setPart(name, value)` is the intentional write-side counterpart to
57
+ `getPart(name)`: programmatic materializers use it to install authored part
58
+ names as real Skin properties. This preserves the existing EUI Skin model;
59
+ it is not an alternate Map-backed namespace. Callers must reject names that
60
+ collide with `Skin` or inherited runtime members. For Agent-generated UI,
61
+ treat such a collision as invalid authored data and surface a rename
62
+ diagnostic rather than silently rewriting or accepting the name.
63
+ - **Two independent, near-duplicate state machines exist.** `Group` implements
64
+ `states`/`currentState`/`_commitCurrentState()` itself (containers have no
65
+ skin). `Component` delegates state application to its attached
66
+ `Skin.currentState` setter. Both call the same `IOverride.apply/remove(host,
67
+ skin)` signature, but `Group._commitCurrentState()` passes `this` cast
68
+ `as unknown as Skin` — a type-unsafe workaround since Group has neither a
69
+ real Component nor a real Skin.
70
+ - `Component._invokeSkinFactory()` distinguishes KUI-compiled factory
71
+ functions (called with `.call(this)` to supply the host) from real
72
+ `class extends Skin` constructors (detected from their function source and
73
+ invoked with `new`).
74
+ - `Component.touchEnabled`/`touchChildren` diverge from the raw `Sprite`
75
+ value while `enabled === false`: the setter stores intent in
76
+ `_explicitTouchEnabled`/`_explicitTouchChildren` but forces the live value
77
+ to `false`. Reading `.touchEnabled` always returns the live (possibly
78
+ forced) value, not the cached intent — easy to miss.
79
+ - `UIState.setWidth()`/`setHeight()` write to `this._owner.$explicitWidth`, a
80
+ **core** `DisplayObject` field — not a UIState-local field. Only the
81
+ _measured/actual_ size lives in `UIState`. Setting `.width` is not a simple
82
+ property write: it triggers `invalidateProperties()` +
83
+ `invalidateDisplayList()` + parent invalidation as side effects.
84
+ - **Reading `.width`/`.height` can force a synchronous layout pass.**
85
+ `getWidth()/getHeight()` call `_validateSizeNow()` before returning — this
86
+ therefore a getter may perform layout work before returning.
87
+ - `Component.measure()` reads `UIState.getUnscaledPreferredBounds()` so skin
88
+ measurement stays in component-local units. Its own scale/rotation/skew must
89
+ affect parent layout bounds only; using transformed preferred bounds here
90
+ applies the transform twice to implicitly sized skins.
91
+ - Anchor/percent constraints (`left`/`right`/`percentWidth`, etc.) have zero
92
+ effect until the component is added to a `Group` that has a `layout`
93
+ assigned. Setting them on a standalone/unparented component is a no-op.
94
+ - `isUIComponent()` is duck-typed (`'ui' in obj`) — any object with a `.ui`
95
+ property passes, not necessarily a real `Group`/`Component`.
96
+ - **Virtual layout defaults to OFF.**
97
+ Must opt in explicitly: `list.useVirtualLayout = true`.
98
+ - `ArrayCollection.filterFunction()` **mutates `_source` in place** — filtered-
99
+ out items are permanently removed from the backing array, not just hidden
100
+ from the view. To "unfilter," reassign `source`.
101
+ Also: `source =` setter dispatches `RESET` (resets scroll position
102
+ downstream in `DataGroup`); `replaceAll()` deliberately avoids that to
103
+ preserve scroll position — called out in its own doc comment.
104
+ - `Watcher`/`Binding` are **not automatically reactive**. They depend on the
105
+ host manually dispatching `PropertyEvent.PROPERTY_CHANGE` in its setters
106
+ (`PropertyEvent.dispatchPropertyEvent(this, 'propName')`). A property whose
107
+ setter doesn't dispatch this event will never be observed by a binding.
108
+ - `Skin.$watchers` (populated by compiled skin factories) must be drained via
109
+ `Skin.unwatchAll()` — done automatically by `Component._setSkin()` when
110
+ detaching, but a hand-rolled skin creating `Watcher`s outside `$watchers`
111
+ will leak listeners.
112
+ - `UIEvent.dispatchUIEvent()` is a no-op guard: it returns early
113
+ (`if (!target.hasEventListener(eventType)) return true;`) without
114
+ constructing the event object at all if nobody's listening — used
115
+ pervasively to avoid allocation overhead on every property/position change.
116
+ - `childrenCreated()` and `UIEvent.CREATION_COMPLETE` both fire exactly **once
117
+ per component instance**, synchronously, right after `createChildren()` on
118
+ first add to stage. Re-adding a removed component does **not** re-fire
119
+ either — it instead force-validates synchronously via
120
+ `validator.validateClient()`.
121
+
122
+ ## 3. Domain-specific terminology
123
+
124
+ | Term | Definition | Where defined |
125
+ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
126
+ | `UIState` | The actual layout state machine every `Group`/`Component` delegates to via `this.ui`. Owns constraint fields (left/right/top/bottom/center/percent) packed into a `Record<K, number\|boolean>` keyed by a numeric `const enum K`. Talks back to its host only through the narrow `IUIOwner` interface, decoupling it from any specific DisplayObject subclass. | `core/UIState.ts` |
127
+ | `IUIOwner` | The callback interface `UIState` uses to talk back to its host component (`createChildren()`, `commitProperties()`, `measure()`, `updateDisplayList()`, `childrenCreated()`). Implemented by `Group` and `Component`. | `core/UIState.ts` |
128
+ | Validation cycle | Three deferred phases run in order before core renders: **validateProperties** (shallow→deep, `commitProperties()`), **validateSize** (deep→shallow, `measure()`), **validateDisplayList** (shallow→deep, `updateDisplayList()`). Scheduled through the core ticker's `callLater` queue by the global `Validator`/`validator` singleton and sorted by `$nestLevel` (a **core** `DisplayObject` field, not a UI concept — see `packages/core/src/kurot/display/DisplayObject.ts`). | `core/Validator.ts` |
129
+ | `Validator.validateClient(target)` | Forces synchronous validation of everything at or below `target`'s depth. Used by `validateNow()` and by the re-add-to-stage path in `UIState`. | `core/Validator.ts` |
130
+ | `Theme` | Maps a component's class name (`hostComponentKey`) to a default skin class name, loaded via `IThemeAdapter` (network fetch by default). Components created before the theme finishes loading queue into a `_delayList` and get skinned retroactively. | `core/Theme.ts` |
131
+ | `skinParts` | `Skin.skinParts: string[]` lists the complete named part set exposed by a skin. `Component` collects the values into one internal read-only map, exposes it through the public typed `skinParts` accessor, then invokes `onSkinReady()`. Before replacement or removal, `onSkinRemoved()` runs while the old complete map remains available. No dynamic properties are written to the component instance. | `components/Skin.ts`, `components/Component.ts` |
132
+ | Skin/State/SetProperty pattern | Declare `skinParts` + `states: State[]` in a `Skin` subclass. `Component.currentState` flows into `Skin.currentState`, which diffs old vs new `State.overrides` and calls `apply()`/`remove()` on each `IOverride`. | `states/State.ts`, `states/IOverride.ts`, `states/SetProperty.ts`, `components/Skin.ts` |
133
+ | `IViewport` | Extends `IUIComponent` with `contentWidth`/`contentHeight` (readonly) and `scrollH`/`scrollV`/`scrollEnabled`. Implemented by `Group` (and thus `DataGroup`/`List`), consumed by `Scroller`. | `core/IViewport.ts` |
134
+ | Virtual layout | Layout mode where only currently-visible-index item renderers are instantiated; off-screen renderers recycle into `DataGroup._freeRenderers` (a `Map<class, ItemRenderer[]>` free-list pool keyed by renderer class). Toggled via `layout.useVirtualLayout`. Off by default. | `layouts/LayoutBase.ts`, `components/DataGroup.ts` |
135
+ | `childrenCreated()` vs `UIEvent.CREATION_COMPLETE` | `childrenCreated()` is the imperative override hook, called synchronously once right after `createChildren()`. `CREATION_COMPLETE` is the event fired immediately after, for external listeners. Both fire at the same instant, exactly once per instance. | `core/UIState.ts` (`$onAddToStage`), `events/UIEvent.ts` |
136
+
137
+ ## 4. Public API surface (`src/index.ts`)
138
+
139
+ Flat re-export of 7 barrels, in this order: `core`, `layouts`, `components`,
140
+ `events`, `states`, `collections`, `binding`.
141
+
142
+ - **core**: `IUIComponent`, `IUIOwner`, `UIState`, `isUIComponent`, `Validator`/`validator`, `Theme`/`getTheme`/`setTheme`, `IViewport`, `IAssetAdapter`/`DefaultAssetAdapter`, `setAssetAdapter`/`getAssetAdapter`, `IThemeAdapter`/`DefaultThemeAdapter`, `Direction`, `ScrollPolicy`, `IDisplayText`, `IItemRenderer`.
143
+ - **layouts**: `ILayoutTarget`, `LayoutBase`, `BasicLayout`, `LinearLayoutBase`, `VerticalLayout`, `HorizontalLayout`, `TileLayout`, `ColumnAlign`, `RowAlign`, `JustifyAlign`, `TileOrientation`.
144
+ - **components**: `Group`/`GroupEvents`, `Component`/`ComponentEvents`, `Skin`/`SkinEvents`; `Button`, `Label`/`TextFitMode` (type), `CheckBox`, `RadioButton`/`RadioButtonGroup`, `ToggleButton`, `ToggleSwitch`, `ProgressBar`, `HSlider`/`VSlider`/`SliderBase`, `Rect`, `Image`, `EditableText`, `TextInput`, `Panel`, `ViewStack`, `Scroller`, `TouchScroll`, `UILayer`, `DataGroup`, `List`/`ListBase`, `TabBar`, `ComboBox`, `ItemRenderer`, `HScrollBar`/`VScrollBar`/`ScrollBarBase`, `Animation`, `Range`.
145
+ - **events**: `UIEvent`, `ItemTapEvent`, `CollectionEvent`/`CollectionEventKind`, `PropertyEvent`, `ScrollerThrowEvent`.
146
+ - **states**: `State`, `IOverride`, `SetProperty`, `SetStateProperty`, `AddItems`.
147
+ - **collections**: `ICollection`, `ArrayCollection`.
148
+ - **binding**: `Watcher`, `Binding`.
149
+
150
+ ## 5. Dependency on `@kurot/core` — and the dirty-flag interaction
151
+
152
+ Builds directly on core's `DisplayObject`/`DisplayObjectContainer`/`Sprite`
153
+ (`Group` and `Component` both `extend Sprite`), `EventDispatcher`/`Event`,
154
+ `Rectangle`/`Matrix`/`Point`, and `TextField` (wrapped by `Label`/
155
+ `EditableText`).
156
+
157
+ **UIState deliberately hooks into core's render-dirty system.** In
158
+ `UIState._setActualSize()`, after updating width/height it explicitly calls
159
+ `this._owner.$markDirty()` — the source comment explains why: a layout size
160
+ change affects rendered bounds, and since UI layout is deferred to the core
161
+ ticker's render-preparation queue (while core's dirty propagation runs
162
+ synchronously on the mutation's own tick), a `cacheAsBitmap`-flagged ancestor could otherwise
163
+ render a stale bitmap after deferred measurement completes. Similarly,
164
+ `Skin`'s re-add-to-stage path forces `validateNow()` synchronously to avoid
165
+ `structureDirty` firing before the `Validator` has filled in graphics
166
+ commands. **Takeaway: UI validation is deferred but runs through core's ticker
167
+ before the corresponding render; add-to-stage and size changes still explicitly
168
+ bridge layout invalidation into core's render-dirty system.**
169
+
170
+ Also: `Group`/`Component` override several `$`-prefixed **internal** core
171
+ methods (`$updateUseTransform`, `$setMatrix`, `$setAnchorOffsetX/Y`,
172
+ `$setX/Y`) purely to hook `ui.$invalidateParentLayout()`. Code that mutates
173
+ transform directly at the core level bypassing these overrides risks not
174
+ triggering a parent layout re-validation — a coupling point worth knowing
175
+ before patching core's transform code.
176
+
177
+ ## 6. Task → file map
178
+
179
+ | I want to... | Look at |
180
+ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181
+ | Add a new skinnable widget | `components/Component.ts` for the base pattern, model on `components/Button.ts` |
182
+ | Add a new container-only widget (no skin) | `components/Group.ts` |
183
+ | Change Label font fitting | `text/fit-label-text.ts` for metric calculation; `components/Label.ts` for layout and lifecycle integration |
184
+ | Add a new layout algorithm | `layouts/LayoutBase.ts`, follow `layouts/VerticalLayout.ts` |
185
+ | Debug a state not applying | `components/Skin.ts` (`currentState` setter, `_applyState`), `states/State.ts`, `states/IOverride.ts` |
186
+ | Debug a binding not firing | Check the source property's setter actually calls `PropertyEvent.dispatchPropertyEvent` — see `binding/Watcher.ts` |
187
+ | Add virtual-layout support to a new layout | `layouts/LayoutBase.ts` (`elementAdded`/`elementRemoved`/`getElementIndicesInView`/`clearVirtualLayoutCache`), reference `layouts/VerticalLayout.ts`'s `elementSizeTable` |
188
+ | Understand validation/render scheduling order | `core/Validator.ts`, core `player/SystemTicker.ts` |
189
+ | Measure UI validation/rendering performance | `examples/benchmark/` for the visual runner, scenarios, Playwright automation, and local JSON/Markdown reports |
190
+
191
+ ## Label text layout contract in 3.1.0
192
+
193
+ Label defaults to multiline; explicit false draws only the first hard-separated
194
+ line without wrapping. EditableText defaults to single-line and rejects
195
+ shrinking. `wordWrap` chooses Unicode versus character boundaries only while
196
+ multiline is enabled.
197
+
198
+ `TextFitMode` is exported with Label. `textFit` is `none` by default or `shrink`
199
+ for single-line Labels. `minFontSize` defaults to 12 and must be finite and >= 1.
200
+ `size` stays authored; `renderedSize` and `textFitOverflow` are read-only results
201
+ after validation. Fitting uses fixed axes, parent layout constraints or explicit
202
+ maximums, reserves outline thickness and caches unchanged metrics. Measured
203
+ automatic bounds are not fed back as fit constraints.
204
+
205
+ `label.invalidateSize()` also invalidates Core line metrics. Call it after
206
+ late font loading, even if the family name did not change. This requires
207
+ Core >= 2.1.0; there is no global font-readiness listener.
208
+
209
+ See [Label text layout](label-text-layout.md) for fit bounds, state restoration,
210
+ font readiness and shared-schema boundaries. Native UI still depends only on
211
+ Core; KUI authoring uses ui-document 0.9.x and matching CLI/runtime releases.