@kurot/ui 2.1.4 → 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.
- package/CHANGELOG.md +62 -6
- package/README.md +69 -40
- package/dist/kurot/components/Label.d.ts +40 -0
- package/dist/kurot/components/Label.d.ts.map +1 -1
- package/dist/kurot/components/Label.js +182 -32
- package/dist/kurot/components/Label.js.map +1 -1
- package/dist/kurot/components/index.d.ts +1 -1
- package/dist/kurot/components/index.d.ts.map +1 -1
- package/dist/kurot/components/index.js.map +1 -1
- package/dist/kurot/text/fit-label-text.d.ts +23 -0
- package/dist/kurot/text/fit-label-text.d.ts.map +1 -0
- package/dist/kurot/text/fit-label-text.js +36 -0
- package/dist/kurot/text/fit-label-text.js.map +1 -0
- package/docs/ai-context.md +211 -0
- package/docs/architecture.md +478 -0
- package/docs/label-text-layout.md +94 -0
- package/package.json +4 -3
|
@@ -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.
|