uxml-preview 0.1.0 → 0.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 CHANGED
@@ -1,6 +1,102 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 — unreleased
3
+ ## 0.2.0 — 2026-08-06
4
+
5
+ Five controls instead of three, a representative screen measured end to end
6
+ against Unity, and two cascade defects that a synthetic case set could not have
7
+ found.
8
+
9
+ ### Changed — this affects what you see on screen
10
+
11
+ - **Controls with no renderer of their own are now drawn as plain boxes**
12
+ instead of being dropped along with everything inside them. One
13
+ `<ui:ScrollView>` used to empty a panel. Each still reports a warning, and
14
+ what is missing is only whatever makes that control look like itself.
15
+ - **`Button` now carries Unity's default `margin: 1px 3px`.** Existing layouts
16
+ containing buttons will shift by a few pixels — toward Unity, not away from
17
+ it. Unity supplies this through a theme stylesheet and this library supplied
18
+ nothing, which is why the first `Button` ever compared missed on 18 of 48
19
+ values. Overridable from your own USS exactly as in Unity, and the values are
20
+ in `src/controls/theme.ts`.
21
+ - **`Button` labels are now centred**, which is what Unity does. Found by the eye
22
+ check rather than by measurement: where a glyph sits inside a box does not move
23
+ the box, so all 548 compared coordinates agreed while the two screenshots
24
+ plainly did not.
25
+ - **Control defaults now lose to your USS regardless of specificity.** They are a
26
+ lower origin, like a browser's user-agent sheet — measured, and the opposite of
27
+ what this library previously did and documented. `Button { margin: 0 }` in your
28
+ stylesheet now wins against the built-in `.unity-button` rule, as in Unity.
29
+ - **`enabled="false"` puts an element and its subtree into `:disabled`**, without
30
+ anyone passing `states`. Both the state and its inheritance are measured.
31
+ - **Disabled elements render at half opacity**, as Unity draws them. The value
32
+ came from sampling the screenshots rather than judging them: Unity paints the
33
+ disabled button rgb(41,50,67), which is its base colour composited at exactly
34
+ one half over the footer on all three channels.
35
+
36
+ ### Added
37
+
38
+ - **`ScrollView`.** One tag becomes four elements in Unity, and the three it adds
39
+ decide where everything inside lands — reproducing the box alone put every
40
+ descendant in the wrong place and squeezed children that should have overflowed.
41
+ The hierarchy was observed from layout dumps rather than read from a source
42
+ mirror; the middle level, `unity-content-and-vertical-scroll-container`, appears
43
+ in no documentation. Scrollbar width is reserved but nothing is drawn: dragging,
44
+ wheeling and scroll position are outside a static render.
45
+ - **`Image`**, and `-unity-background-scale-mode` with its three values. The
46
+ default is `stretch-to-fill`, confirmed by the eye check.
47
+ - **Author USS reaches control parts.** `#unity-content-container { flex-direction: row }`
48
+ — how a real project lays out a scroll region's contents — previously reached
49
+ nothing at all, because parts were built after the cascade had run. A selector
50
+ naming any `unity-` element that matches nothing now warns and says why.
51
+ - **`<Style src="…">` is read**, through the same `resolveImport` hook as
52
+ `@import`. UI Builder writes that element into a file whenever a stylesheet is
53
+ attached, so it is the ordinary shape of a real document — and ignoring it meant
54
+ a real project's UXML rendered unstyled with nothing said about why.
55
+
56
+ - **Pseudo-class states, per element.** `render(doc, el, { states: { '#UseButton':
57
+ ['hover'], '#DropButton': ['disabled'] } })`. Keys are ordinary USS selectors.
58
+ States are explicit input rather than mouse events, so the same call always
59
+ draws the same picture — a screen you can compare against Unity, or against
60
+ yesterday. Per element rather than per document because real screens mix one
61
+ hovered button with a disabled one.
62
+ Unity's *own* state defaults are deliberately not included: a layout dump
63
+ measures geometry and a hover colour is not geometry, so there is no way to
64
+ prove them yet.
65
+ - Style provenance now records which states a rule needed (`origin.states`), so
66
+ an editor can tell "this is the hover colour" from "this is the colour".
67
+
68
+ ### Verified
69
+
70
+ **532 of 548 element coordinates identical** against Unity 6000.0.40f1 across 31
71
+ layout cases, one of which is a complete inventory screen.
72
+
73
+ The ratio is lower than 0.1.0's 242/244, and the reason is worth stating: that
74
+ figure covered 18 cases of which 17 were `VisualElement` alone, and no `Button`
75
+ had ever been compared. This one includes a text-heavy working screen, and **ten
76
+ of the sixteen differences are font metrics rather than layout** — both engines
77
+ shrink the same box for the same reason, and only the ruler differs. Three more
78
+ are 1px, two are the `yoga-layout` version difference already known in 0.1.0, and
79
+ **one is genuinely unresolved**. All sixteen are named in
80
+ [`docs/accuracy.en.md`](docs/accuracy.en.md).
81
+
82
+ Colour, borders and corners are held by a CSS-declaration regression baseline
83
+ (`tests/render/visual.test.ts`), which detects our own drift and is not evidence
84
+ of agreement with Unity. That comparison is a one-time eye check, recorded with
85
+ both screenshots in [`docs/visual-check.md`](docs/visual-check.md).
86
+
87
+ ### Known limitations
88
+
89
+ - **Text-driven layout will not match exactly.** A browser cannot measure Unity's
90
+ font asset, so any box sized by its text differs. This is the same fact that
91
+ makes screenshot diffing useless, and it is not fixable here.
92
+ - **A wrapped ScrollView's content height diverges.** Unity clamps it to the
93
+ viewport; Yoga grows it to the content. Unity's rule is not yet identified —
94
+ the fix that matches one case breaks another, so nothing was guessed.
95
+ - Scrollbars reserve space but are not drawn, and there is no scrolling.
96
+ - `-unity-background-scale-mode`'s two non-default values are implemented from
97
+ Unity's documented semantics but have not been seen side by side.
98
+
99
+ ## 0.1.0 — 2026-08-03
4
100
 
5
101
  First release. Parses Unity UI Toolkit `.uxml` and `.uss`, lays them out through
6
102
  the same Yoga engine UI Toolkit uses, paints to the DOM, and writes the files
package/README.md CHANGED
@@ -21,8 +21,17 @@ stylesheet comes out wrong.</sub>
21
21
 
22
22
  [English](#english) · [한국어](#한국어)
23
23
 
24
- > ⚠️ **Status: pre-alpha.** Under active development. Not yet published to npm.
25
- > 현재 개발 초기 단계입니다. 아직 npm에 배포되지 않았습니다.
24
+ ```bash
25
+ npm install uxml-preview
26
+ ```
27
+
28
+ > **v0.1.0 — early release.** Three controls (`VisualElement`, `Label`,
29
+ > `Button`) and a support matrix that says plainly what has not been measured.
30
+ > Read [what is not verified yet](#what-is-not-verified-yet) before relying on it.
31
+ >
32
+ > **v0.1.0 — 초기 릴리스입니다.** 컨트롤 3종(`VisualElement`, `Label`, `Button`)이고,
33
+ > 검증되지 않은 부분을 지원 범위표에 그대로 적어뒀습니다. 쓰시기 전에
34
+ > [아직 검증되지 않은 것](#아직-검증되지-않은-것)을 먼저 봐주세요.
26
35
 
27
36
  ---
28
37
 
@@ -36,8 +45,10 @@ directly in a web browser. It uses **Yoga** — the same layout engine Unity UI
36
45
  itself uses — compiled to WebAssembly, so results match the Unity Editor rather than
37
46
  approximating it.
38
47
 
39
- Measured against Unity 6000.0.40f1: **242 of 244 element coordinates identical**
40
- across 18 layout cases. The two that differ are named and explained in
48
+ Measured against Unity 6000.0.40f1: **532 of 548 element coordinates identical**
49
+ across 31 layout cases, including a full working screen. Ten of the sixteen
50
+ differences are font metrics rather than layout — a browser cannot measure
51
+ Unity's font asset. All sixteen are named in
41
52
  [`docs/accuracy.en.md`](docs/accuracy.en.md).
42
53
 
43
54
  ```
@@ -94,16 +105,25 @@ Full plan: [`docs/ROADMAP.md`](docs/ROADMAP.md)
94
105
  The support matrix is deliberately conservative — see
95
106
  [`docs/supported.en.md`](docs/supported.en.md) for the full table.
96
107
 
97
- - **Only geometry was compared, and only as far as Yoga.** The figure above is
98
- measured at the coordinates the layout engine produces. Whether the painted
99
- DOM reproduces them is checked by an invariant of our own rather than against
100
- Unity. Colours, borders, corner radii and fonts are implemented but were not
101
- measured at all; coordinates say nothing about them.
108
+ - **Only geometry is compared automatically, and only as far as Yoga.** The
109
+ figure above is measured at the coordinates the layout engine produces.
110
+ Whether the painted DOM reproduces them is checked by an invariant of our own
111
+ rather than against Unity.
112
+ - **Colour, borders and corners rest on a single eye comparison.** One screen was
113
+ put side by side with Unity and checked item by item
114
+ ([`docs/visual-check.md`](docs/visual-check.md)); it found two real defects, so
115
+ it was worth doing, but it is one screen and not an automated diff. Everything
116
+ it did not contain is still unmeasured.
102
117
  - **Text measurement was not compared.** Unity renders text with its own font
103
- asset. Layout that is driven by measured text will not match exactly, and the
104
- line-height factor is still an estimate.
105
- - **`Button` has no golden case.** It renders through the same path as `Label`,
106
- but "same path" is not a measurement.
118
+ asset, so any box sized by its text differs — this accounts for ten of the
119
+ sixteen coordinate divergences. The line-height factor is still an estimate.
120
+ - **A wrapped `ScrollView`'s content height diverges.** Unity clamps it to the
121
+ viewport; Yoga grows it to the content. Unity's rule is not yet identified.
122
+ - **Nested disabled elements dim twice.** `enabled="false"` disables a subtree
123
+ and each level takes the half opacity, so a disabled control inside a disabled
124
+ panel comes out at a quarter. Unity almost certainly dims once; this is not
125
+ measured yet.
126
+ - Scrollbars reserve their width but are not drawn, and nothing scrolls.
107
127
  - `-unity-slice-*`, `-unity-background-image-tint-color` and `transition` are
108
128
  parsed and preserved, but not drawn.
109
129
 
@@ -121,6 +141,10 @@ const doc = parse(uxmlText, ussText);
121
141
  // model → screen
122
142
  const view = render(doc, document.getElementById('preview'), {
123
143
  resolveAsset: (path) => myAssetUrlFor(path),
144
+
145
+ // Pseudo-class states are explicit input, keyed by USS selector, so the same
146
+ // call always draws the same picture. Per element, because real screens mix.
147
+ states: { '#UseButton': ['hover'], '#DropButton': ['disabled'] },
124
148
  });
125
149
  console.log(view.warnings);
126
150
 
@@ -187,14 +211,17 @@ Measured against Unity on 2026-08-02, over 18 cases and 61 elements:
187
211
 
188
212
  | | |
189
213
  |---|---|
190
- | Cases matching exactly | **17 / 18** |
191
- | Values within 0.5px | **242 / 244 (99.2%)** |
192
-
193
- The one divergence is a percentage sized against a parent that has no explicit
194
- size, where `yoga-layout` resolves the main axis and the Yoga inside UI Toolkit
195
- does not. It is recorded rather than smoothed over — see
196
- [`docs/accuracy.en.md`](docs/accuracy.en.md) for the numbers, the failed attempts to
197
- configure around it, and how to reproduce the measurement.
214
+ | Cases matching exactly | **29 / 31** |
215
+ | Values within 0.5px | **532 / 548 (97.1%)** |
216
+
217
+ The sixteen divergences are four different things, and the distinction matters
218
+ more than the ratio: **ten are text metrics**, three are 1px, two are a
219
+ `yoga-layout` version difference, and **one is unresolved**. The ten are not a
220
+ layout defect — both engines shrink the same box for the same reason and only
221
+ the ruler differs, which is the same fact that makes pixel diffing useless.
222
+ Every one is named in [`docs/accuracy.en.md`](docs/accuracy.en.md), along with
223
+ the failed attempts to configure around the version difference and how to
224
+ reproduce the measurement.
198
225
 
199
226
  ### Questions this answers
200
227
 
@@ -216,10 +243,11 @@ as nested flex, and computed values have to be resolved to fixed numbers or
216
243
  calculated in C#.
217
244
 
218
245
  **How close is the output to Unity?**
219
- 242 of 244 element coordinates identical against Unity 6000.0.40f1, across 18
220
- layout cases. Geometry is compared rather than screenshots, because Unity draws
221
- text with its own font asset. The single divergence and the numbers behind this
222
- are in [`docs/accuracy.en.md`](docs/accuracy.en.md).
246
+ 532 of 548 element coordinates identical against Unity 6000.0.40f1, across 31
247
+ layout cases including a complete inventory screen. Geometry is compared rather
248
+ than screenshots, because Unity draws text with its own font asset — which is
249
+ also why ten of the sixteen differences are font metrics rather than layout.
250
+ Every divergence is named in [`docs/accuracy.en.md`](docs/accuracy.en.md).
223
251
 
224
252
  **Will it damage my files if I edit them?**
225
253
  Editing goes through a model that keeps the original text. An unedited document
@@ -228,9 +256,10 @@ CRLF endings included — and an edit rewrites only the region it touched. The
228
256
  playground shows this live as `round-trip: exact`.
229
257
 
230
258
  **What happens to controls it does not support?**
231
- They are kept in the tree, reported as a warning, excluded from rendering, and
232
- written back out unchanged. One unsupported control never takes down the rest of
233
- the screen, and nothing is silently dropped.
259
+ They are drawn as a plain `VisualElement`, reported as a warning, and written
260
+ back out unchanged. Their own styles and their children come out normally; only
261
+ what makes that control look like itself is missing. One unfamiliar tag never
262
+ takes down the rest of the screen, and nothing is silently dropped.
234
263
 
235
264
  ### License
236
265
 
@@ -248,9 +277,10 @@ Apache-2.0
248
277
  **Yoga**를 WebAssembly로 그대로 사용하기 때문에, 결과를 "비슷하게 흉내내는" 것이 아니라
249
278
  유니티 에디터와 일치시킵니다.
250
279
 
251
- Unity 6000.0.40f1과 대조한 결과, 레이아웃 케이스 18개에서
252
- **요소 좌표 244개 중 242개가 완전히 일치**합니다. 어긋난 2개는
253
- [`docs/accuracy.md`](docs/accuracy.md)에 사유까지 적어뒀습니다.
280
+ Unity 6000.0.40f1과 대조한 결과, 실무형 화면을 포함한 레이아웃 케이스 31개에서
281
+ **요소 좌표 548개 중 532개가 일치**합니다. 어긋난 16개 중 **10개는 레이아웃이 아니라
282
+ 폰트 메트릭** 차이입니다 — 브라우저는 유니티 폰트 에셋을 잴 수 없습니다.
283
+ 16개 전부 [`docs/accuracy.md`](docs/accuracy.md)에 이름과 사유를 적어뒀습니다.
254
284
 
255
285
  ```
256
286
  .uxml + .uss → 파싱 → 스타일 계산 → Yoga 레이아웃 → DOM 페인팅
@@ -305,15 +335,22 @@ Unity 6000.0.40f1과 대조한 결과, 레이아웃 케이스 18개에서
305
335
  지원 범위는 일부러 보수적으로 적었습니다. 전체 표는
306
336
  [`docs/supported.md`](docs/supported.md)에 있습니다.
307
337
 
308
- - **좌표만, 그것도 Yoga 층까지만 비교했습니다.** 위 수치는 레이아웃 엔진이 낸
338
+ - **자동 대조는 좌표만, 그것도 Yoga 층까지입니다.** 위 수치는 레이아웃 엔진이 낸
309
339
  좌표를 잰 것입니다. 그려진 DOM이 그 좌표를 재현하는지는 유니티가 아니라 자체
310
- 불변식 검사로 봅니다. 색·테두리·모서리·폰트는 구현돼 있지만 아예 측정하지
311
- 않았습니다 — 좌표로는 알 수 없는 것들입니다
340
+ 불변식 검사로 봅니다
341
+ - **색·테두리·모서리는 육안 대조 1회에 기대고 있습니다.** 화면 하나를 유니티와
342
+ 나란히 놓고 항목별로 확인했고([`docs/visual-check.md`](docs/visual-check.md)),
343
+ 거기서 실제 결함 두 개가 나왔으니 할 값어치는 있었습니다. 다만 **화면 하나이고
344
+ 자동 diff가 아닙니다.** 그 화면에 없던 것은 여전히 미측정입니다
312
345
  - **텍스트 측정은 대조하지 않았습니다.** 유니티는 자기 폰트 에셋으로 글자를
313
- 그립니다. 측정된 텍스트가 좌우하는 레이아웃은 정확히 맞지 않고, 행 높이
314
- 계수도 아직 추정치입니다
315
- - **`Button`은 골든 케이스가 없습니다.** `Label`과 같은 경로로 그려지지만,
316
- "같은 경로"는 측정이 아닙니다
346
+ 그리므로 텍스트가 크기를 좌우하는 박스는 어긋납니다 — 좌표 불일치 16건 중
347
+ 10건이 이것입니다. 행 높이 계수도 아직 추정치입니다
348
+ - **wrap된 `ScrollView`의 콘텐츠 높이가 어긋납니다.** 유니티는 뷰포트에 묶고
349
+ Yoga는 콘텐츠로 키웁니다. 유니티의 규칙을 아직 특정하지 못했습니다
350
+ - **중첩된 비활성 요소가 두 번 흐려집니다.** `enabled="false"`가 하위 트리를
351
+ 비활성으로 만들고 각 층이 0.5 불투명도를 받아 1/4이 됩니다. 유니티는 한 번만
352
+ 흐리게 할 것이 거의 확실하지만 아직 측정하지 않았습니다
353
+ - 스크롤바는 폭만 예약하고 그리지 않으며, 스크롤되지 않습니다
317
354
  - `-unity-slice-*`, `-unity-background-image-tint-color`, `transition`은
318
355
  파싱·보존은 되지만 그려지지 않습니다
319
356
 
@@ -340,7 +377,7 @@ pnpm dev
340
377
 
341
378
  예제는 예쁜 것보다 **함정을 보여주는 것**으로 골랐습니다 — `flex-direction`의 기본값이
342
379
  `column`이라는 것, 겹침 순서가 `z-index`가 아니라 마크업 순서로 정해진다는 것,
343
- 미지원 컨트롤이 그려지지는 않지만 경고와 함께 왕복에서 살아남는다는 것.
380
+ 전용 렌더러가 없는 컨트롤도 일반 박스로 그려지고 경고와 함께 왕복에서 살아남는다는 것.
344
381
 
345
382
  ### 개발
346
383
 
@@ -373,10 +410,11 @@ pnpm build
373
410
  계산이 필요한 값은 고정값으로 풀거나 C#에서 계산해야 합니다.
374
411
 
375
412
  **유니티와 얼마나 같나요?**
376
- Unity 6000.0.40f1 기준, 레이아웃 케이스 18개에서 **요소 좌표 244개 중 242개 일치**
377
- 입니다. 스크린샷이 아니라 좌표를 비교하는데, 유니티가 자기 폰트 에셋으로 글자를
378
- 그리기 때문입니다. 유일한 불일치와 근거 수치는
379
- [`docs/accuracy.md`](docs/accuracy.md)에 있습니다.
413
+ Unity 6000.0.40f1 기준, 인벤토리 화면 전체를 포함한 레이아웃 케이스 31개에서
414
+ **요소 좌표 548개 중 532개 일치**입니다. 스크린샷이 아니라 좌표를 비교하는데,
415
+ 유니티가 자기 폰트 에셋으로 글자를 그리기 때문입니다 — 그리고 그 사실 때문에
416
+ 불일치 16개 중 **10개가 레이아웃이 아니라 폰트 메트릭**입니다. 전부
417
+ [`docs/accuracy.md`](docs/accuracy.md)에 이름이 적혀 있습니다.
380
418
 
381
419
  **편집하면 파일이 망가지지 않나요?**
382
420
  편집은 원본 텍스트를 그대로 들고 있는 모델을 거칩니다. 고치지 않은 문서는 주석·속성
@@ -384,9 +422,9 @@ Unity 6000.0.40f1 기준, 레이아웃 케이스 18개에서 **요소 좌표 244
384
422
  영역만 바뀝니다. 놀이터의 `round-trip: exact` 표시가 이걸 실시간으로 보여줍니다.
385
423
 
386
424
  **지원하지 않는 컨트롤은 어떻게 되나요?**
387
- 트리에 유지되고, 경고로 보고되고, 렌더링에서만 빠지고, 저장하면 그대로 다시
388
- 나옵니다. 미지원 컨트롤 하나 때문에 화면 전체가 죽지 않고, 조용히 사라지는 것도
389
- 없습니다.
425
+ 일반 `VisualElement`처럼 그려지고, 경고로 보고되고, 저장하면 그대로 다시 나옵니다.
426
+ 자기 스타일과 자식은 정상적으로 나오며 그 컨트롤 고유의 모양만 빠집니다. 모르는 태그
427
+ 하나 때문에 화면 전체가 죽지 않고, 조용히 사라지는 것도 없습니다.
390
428
 
391
429
  ### 라이선스
392
430
 
@@ -1,10 +1,67 @@
1
1
  import { ElementNode } from '../model/types';
2
+ /**
3
+ * An element a control builds for itself, which the file never mentions.
4
+ *
5
+ * Unity's ScrollView is one tag that becomes four elements, and the three it
6
+ * adds are what decide where everything inside lands — a child of a ScrollView
7
+ * is not a child of the ScrollView. Reproducing the box without them puts every
8
+ * descendant in the wrong place, so they are modelled rather than approximated.
9
+ *
10
+ * `name` is Unity's own, and it is the key the layout dump joins on: these are
11
+ * observed in `tests/golden/unity/scrollview-*.json`, not invented.
12
+ */
13
+ export interface ControlPart {
14
+ /** Unity's name, e.g. `unity-content-viewport`. */
15
+ name: string;
16
+ /**
17
+ * USS this part always carries, because the control fixes it.
18
+ *
19
+ * Written as USS rather than as CSS or Yoga calls so it goes through the same
20
+ * cascade output, Yoga mapping and CSS mapping as everything else. A second
21
+ * styling path for parts would be a second set of USS bugs.
22
+ */
23
+ style: Readonly<Record<string, string>>;
24
+ }
2
25
  export interface ControlSpec {
3
- /** Renders the `text` attribute as its content. */
26
+ /** Renders the `text` attribute as its own content. */
4
27
  hasText: boolean;
28
+ /**
29
+ * Elements the control inserts between itself and the file's children,
30
+ * outermost first. The file's children go inside the last one.
31
+ */
32
+ parts: readonly ControlPart[];
33
+ /**
34
+ * USS classes Unity's own constructor puts on this control, which the theme
35
+ * stylesheet then targets. Not in the `class` attribute and not in the model:
36
+ * the file says `<ui:Button />` and Unity adds the rest.
37
+ */
38
+ classes: readonly string[];
39
+ }
40
+ export interface ResolvedControl {
41
+ spec: ControlSpec;
42
+ /**
43
+ * No renderer of its own; drawn as a plain `VisualElement`. Callers report
44
+ * this once per element so the preview says what it did (CLAUDE.md rule 6).
45
+ */
46
+ fallback: boolean;
5
47
  }
6
48
  /** The document container. Not a control, but it is the panel's root box. */
7
49
  export declare const ROOT_LOCAL_NAME = "UXML";
8
- export declare function controlFor(node: ElementNode): ControlSpec | null;
50
+ export declare function isNonVisual(node: ElementNode): boolean;
51
+ /**
52
+ * Purpose: the spec to draw `node` with, always.
53
+ * Ensures: never null. `fallback` is true only for elements a caller
54
+ * should report; the root is excluded because it is the panel
55
+ * box rather than a control that failed to resolve.
56
+ */
57
+ export declare function resolveControl(node: ElementNode): ResolvedControl;
9
58
  export declare function isRoot(node: ElementNode): boolean;
59
+ /**
60
+ * Purpose: the USS classes Unity would have added to this element.
61
+ * Ensures: empty for anything with no renderer of its own — a fallback box
62
+ * must not pick up theme styling meant for a control we do not
63
+ * actually draw.
64
+ */
65
+ export declare function implicitClassesOf(node: ElementNode): readonly string[];
66
+ /** Controls with a renderer of their own. Everything else still draws. */
10
67
  export declare function supportedControlNames(): string[];
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Unity's control defaults, as far as we have measured them.
3
+ *
4
+ * Unity ships a theme stylesheet that gives controls their own margin, padding
5
+ * and border before any author USS runs. This renderer supplied none of it, so
6
+ * every Button sat 3px left and 1px high of where Unity puts it — invisible on
7
+ * one button and compounding across a row.
8
+ *
9
+ * **Only measured values live here.** Every declaration below was derived from
10
+ * a Unity layout dump in tests/golden/unity/, and adding `Button`'s margin
11
+ * moved all five Button cases from 18 mismatched values to zero. Values read
12
+ * from Unity's source instead would look identical in this file while being
13
+ * wrong in a way no test could see: they would not appear in the unmeasured
14
+ * list, so they would quietly pass for ground truth.
15
+ *
16
+ * Two kinds of measurement feed this file, and the second was a long time
17
+ * coming. Geometry comes from the coordinate dumps. `-unity-text-align` cannot:
18
+ * where a glyph sits inside a box does not move the box, so no dump can ever
19
+ * see it. It came from the Step 6 eye check — Unity centres a Button's label
20
+ * and this renderer had it at the top left, which is visible the moment the two
21
+ * screenshots sit side by side and invisible in 368 compared numbers.
22
+ *
23
+ * `opacity` on `:disabled` came from the same screenshots, but by sampling
24
+ * pixels rather than looking. Unity's disabled Drop button is rgb(41,50,67);
25
+ * the base colour rgb(58,74,104) composited at half over the footer's
26
+ * rgb(24,26,31) is (41, 50, 67.5) — all three channels exact. That arithmetic
27
+ * is the evidence, and it is why the value is 0.5 and not a number that merely
28
+ * looked about right.
29
+ *
30
+ * Getting there took two wrong turns worth recording. First the author's
31
+ * `Button:disabled` rule was blamed for not applying — it genuinely does not,
32
+ * because `#DropButton` outranks it, in both engines. Then Unity was accused of
33
+ * ranking state rules above specificity, and `state-vs-id` and `state-vs-inline`
34
+ * refuted that: Unity answers 40 to both, exactly as this cascade already did.
35
+ * The dimming was never about the cascade at all.
36
+ *
37
+ * What is deliberately absent, and why:
38
+ *
39
+ * - `Label` — measured, and it has no default margin. `inherit-vs-direct`
40
+ * puts an explicitly-sized Label at the origin of a sized parent, and Unity
41
+ * reports x=0 y=0. Nothing to add is a result, not an omission.
42
+ * - padding, border, font — invisible to the cases we have. Every compared
43
+ * element is explicitly sized, and USS is border-box, so padding and border
44
+ * do not move the outer rectangle. They are unmeasured, so they are not here.
45
+ * - `Label`'s text alignment — the eye check could not separate it from the
46
+ * box at this size. Unmeasured, so absent.
47
+ *
48
+ * The class names were a separate claim from the values, and are now measured
49
+ * too. `unity-button` came from Unity's documented `Button.ussClassName`, which
50
+ * the margin dumps said nothing about — they proved the spacing, not the
51
+ * selector delivering it. The golden case `theme-class-hook` was written to
52
+ * settle exactly that, and Unity answered on 2026-08-05: a `.unity-button` rule
53
+ * in author USS reaches a bare `<ui:Button>`, sizing it 120×40 in both engines.
54
+ * The selector below is therefore measured, not assumed.
55
+ */
56
+ /** Where every value in this file was measured. Theme values move between versions. */
57
+ export declare const THEME_UNITY_VERSION = "6000.0.40f1";
58
+ /**
59
+ * Parsed once and spliced in ahead of author sheets, so an author rule of equal
60
+ * specificity wins on order — which is how a theme stylesheet behaves in Unity.
61
+ *
62
+ * Class selectors rather than type selectors, also for Unity's reasons: a theme
63
+ * rule at `.unity-button` (0,1,0) beats an author's `Button { }` (0,0,1), and
64
+ * that is a real Unity gotcha worth reproducing rather than smoothing over.
65
+ */
66
+ export declare const THEME_USS = ":disabled {\n opacity: 0.5;\n}\n\n.unity-button {\n margin: 1px 3px;\n -unity-text-align: middle-center;\n}\n";
67
+ /**
68
+ * **Unmeasured, and reachable: nested disabled elements.**
69
+ *
70
+ * `enabled="false"` puts a whole subtree into `:disabled` — measured, by
71
+ * `disabled-inherits`. The rule above therefore hands every level its own
72
+ * `opacity: 0.5`, and CSS multiplies them, so a disabled button inside a
73
+ * disabled panel comes out at a quarter. Unity almost certainly dims once.
74
+ *
75
+ * Not guessed at, because the fix has a choice inside it — dim only where
76
+ * `enabled="false"` was written, or suppress the inherited one — and picking
77
+ * without evidence is how the `.unity-button` selector became a hypothesis
78
+ * nobody noticed for a day. Opacity leaves no trace in a coordinate dump, so
79
+ * settling it needs a screenshot of a nested case. Recorded in
80
+ * `docs/backlog.md` and visible in `visual-nested-disabled`.
81
+ */
82
+ /**
83
+ * Purpose: how much width a vertical scrollbar claims, given whether it shows.
84
+ * Ensures: 0 when the content fits — Unity hides the scroller rather than
85
+ * shrinking the viewport for a bar nobody can see.
86
+ */
87
+ export declare function verticalScrollbarWidth(overflows: boolean): number;
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export type { Span, SourceRef, NodeId, ElementName, Attribute, ElementSpans, Ele
11
11
  export { loadLayoutEngine, isLayoutEngineReady, liveNodeCount } from './layout/yoga';
12
12
  export type { LayoutBox, MeasureText, TextContext, TextMetrics } from './layout/yoga';
13
13
  export { createDefaultMeasureText } from './render/measure';
14
- export { NODE_ATTRIBUTE } from './render/paint';
14
+ export { NODE_ATTRIBUTE, PART_ATTRIBUTE, PART_OWNER_ATTRIBUTE } from './render/paint';
15
15
  export { supportedControlNames } from './controls/registry';
16
16
  export { resolveStyles, explainProperty } from './style/resolve';
17
17
  export type { Candidate, ComputedStyle, ComputedValue, ResolveOptions, ResolveResult, } from './style/resolve';
@@ -26,12 +26,15 @@ export { isInherited } from './style/properties';
26
26
  */
27
27
  export interface ParseOptions {
28
28
  /**
29
- * Loads the text of an `@import`ed stylesheet. Return `null` when the path
30
- * cannot be resolved; the import is then reported as a warning and its rules
29
+ * Loads the text of a stylesheet the document refers to. Return `null` when
30
+ * the path cannot be resolved; it is then reported as a warning and its rules
31
31
  * simply do not participate.
32
32
  *
33
- * USS import paths look like `project://database/Assets/UI/base.uss`, which
34
- * only the host application knows how to turn into file contents.
33
+ * Used for both ways a document can name a stylesheet — `@import` inside USS,
34
+ * and `<Style src="…">` inside UXML. Both quote paths like
35
+ * `project://database/Assets/UI/base.uss`, which only the host application
36
+ * knows how to turn into file contents, and there is no reason for a host to
37
+ * implement that lookup twice.
35
38
  */
36
39
  resolveImport?: (url: string) => string | null;
37
40
  }
@@ -69,8 +72,23 @@ export interface RenderOptions {
69
72
  * above all — should supply its own.
70
73
  */
71
74
  measureText?: MeasureText;
72
- /** Pseudo-classes to render as active, e.g. `new Set(['hover'])`. */
75
+ /** Pseudo-classes to render as active on every element, e.g. `new Set(['hover'])`. */
73
76
  activeStates?: ReadonlySet<string>;
77
+ /**
78
+ * Pseudo-classes to render as active per element, keyed by USS selector.
79
+ *
80
+ * ```ts
81
+ * render(doc, el, { states: { '#UseButton': ['hover'], '#DropButton': ['disabled'] } });
82
+ * ```
83
+ *
84
+ * States are explicit input rather than real mouse events, so the same call
85
+ * always draws the same picture — which is what makes a rendered screen
86
+ * something you can compare against Unity, or against yesterday.
87
+ *
88
+ * Per element and not per document because real screens mix: one button
89
+ * hovered while another is disabled is the normal case, not an edge one.
90
+ */
91
+ states?: Readonly<Record<string, readonly string[]>>;
74
92
  }
75
93
  export interface RenderResult {
76
94
  /**