react-x11 1.2.0 → 2.0.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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
package/src/styles.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // Style props → yoga setters (layout) and paint metadata. Flat, ink-style
2
2
  // props: <box flexDirection="row" padding={8} backgroundColor="#eee">.
3
3
  // Numbers are pixels; strings like '50%' / 'auto' pass through to yoga.
4
- // Yoga comes from ntk (>= 3.1.0) so renderer and ntk widgets share one
5
- // WASM instance and enum set.
6
- import { Yoga } from 'ntk';
4
+ // The engine is ours (`./yoga.js`) the enum tables below are built at
5
+ // module scope, which is what that module's synchronous half is for.
6
+ import { cssColorStraight } from 'ntk';
7
7
 
8
- export { Yoga };
8
+ import { parseBoxShadow, parseLinearGradient } from './decorations.js';
9
+ import { Yoga } from './yoga.js';
9
10
 
10
11
  const FLEX_DIRECTION = {
11
12
  row: Yoga.FLEX_DIRECTION_ROW,
@@ -51,12 +52,46 @@ const DISPLAY = {
51
52
  none: Yoga.DISPLAY_NONE,
52
53
  };
53
54
 
55
+ /**
56
+ * CSS's `flex` shorthand, as the three properties it sets. A number is
57
+ * `flex: <grow> 1 0` — "take this share of what is left, from a base size of
58
+ * nothing" — which is what `flex: 1` means everywhere it is written; the two
59
+ * keywords are CSS's own, `'auto'` for "grow and shrink from my content" and
60
+ * `'none'` for "do neither".
61
+ *
62
+ * It is here rather than in `LAYOUT_APPLIERS` because a shorthand is not a
63
+ * yoga property: it expands into three of them before the diff runs, so
64
+ * `{ flex: 1, flexBasis: 'auto' }` resolves the way CSS does — the longhand
65
+ * after the shorthand wins — and the applier for each still sees a plain
66
+ * value changing.
67
+ */
68
+ const FLEX_SHORTHAND = {
69
+ none: { flexGrow: 0, flexShrink: 0, flexBasis: 'auto' },
70
+ auto: { flexGrow: 1, flexShrink: 1, flexBasis: 'auto' },
71
+ };
72
+
73
+ const isFlexShorthand = (v) =>
74
+ (typeof v === 'number' && Number.isFinite(v) && v >= 0) ||
75
+ (typeof v === 'string' && v in FLEX_SHORTHAND);
76
+
54
77
  const OVERFLOW = {
55
78
  visible: Yoga.OVERFLOW_VISIBLE,
56
79
  hidden: Yoga.OVERFLOW_HIDDEN,
57
80
  scroll: Yoga.OVERFLOW_SCROLL,
58
81
  };
59
82
 
83
+ /**
84
+ * CSS's `direction`. `'inherit'` is yoga's own default and the value every
85
+ * node keeps having, so writing it is the same as leaving it out — it is
86
+ * spelled anyway because "take it from the box around me" is a thing a style
87
+ * has to be able to say back after saying `'rtl'`.
88
+ */
89
+ const DIRECTION = {
90
+ ltr: Yoga.DIRECTION_LTR,
91
+ rtl: Yoga.DIRECTION_RTL,
92
+ inherit: Yoga.DIRECTION_INHERIT,
93
+ };
94
+
60
95
  const pick = (map, value, name) => {
61
96
  if (value === undefined) return undefined;
62
97
  if (!(value in map)) {
@@ -98,6 +133,12 @@ const LAYOUT_APPLIERS = {
98
133
  n.setPositionType(
99
134
  pick(POSITION, v, 'position') ?? Yoga.POSITION_TYPE_RELATIVE,
100
135
  ),
136
+ // Which way the boxes under this one run. Everything else in this file is
137
+ // physical; this is the one property that decides what "start" means, and
138
+ // yoga inherits it down its own tree — so a `<box>` that sets it mirrors
139
+ // that subtree and nothing above it.
140
+ direction: (n, v) =>
141
+ n.setDirection(pick(DIRECTION, v, 'direction') ?? Yoga.DIRECTION_INHERIT),
101
142
  top: (n, v) => n.setPosition(Yoga.EDGE_TOP, v),
102
143
  right: (n, v) => n.setPosition(Yoga.EDGE_RIGHT, v),
103
144
  bottom: (n, v) => n.setPosition(Yoga.EDGE_BOTTOM, v),
@@ -112,6 +153,23 @@ const LAYOUT_APPLIERS = {
112
153
  paddingRight: (n, v) => n.setPadding(Yoga.EDGE_RIGHT, v),
113
154
  paddingBottom: (n, v) => n.setPadding(Yoga.EDGE_BOTTOM, v),
114
155
  paddingLeft: (n, v) => n.setPadding(Yoga.EDGE_LEFT, v),
156
+ // The **logical** edges — the side the text starts on and the side it ends
157
+ // on, whichever those turn out to be. A stylesheet written in these is the
158
+ // same stylesheet in both directions, which is the whole reason `direction`
159
+ // is worth having: a physical `paddingLeft` under `direction: 'rtl'` is a
160
+ // gutter on the wrong side of the text it was meant to indent.
161
+ //
162
+ // Yoga's edge precedence is start/end over the physical side over
163
+ // `EDGE_HORIZONTAL` over `EDGE_ALL` — so `paddingStart` beats `paddingLeft`
164
+ // even in LTR where the two name the same edge, the way CSS's
165
+ // `padding-inline-start` beats `padding-left`. Pinned in a test rather than
166
+ // trusted, since it is the opposite of what the vertical shorthands do.
167
+ start: (n, v) => n.setPosition(Yoga.EDGE_START, v),
168
+ end: (n, v) => n.setPosition(Yoga.EDGE_END, v),
169
+ marginStart: (n, v) => n.setMargin(Yoga.EDGE_START, v),
170
+ marginEnd: (n, v) => n.setMargin(Yoga.EDGE_END, v),
171
+ paddingStart: (n, v) => n.setPadding(Yoga.EDGE_START, v),
172
+ paddingEnd: (n, v) => n.setPadding(Yoga.EDGE_END, v),
115
173
  gap: (n, v) => n.setGap(Yoga.GUTTER_ALL, v ?? 0),
116
174
  rowGap: (n, v) => n.setGap(Yoga.GUTTER_ROW, v ?? 0),
117
175
  columnGap: (n, v) => n.setGap(Yoga.GUTTER_COLUMN, v ?? 0),
@@ -121,14 +179,46 @@ const LAYOUT_APPLIERS = {
121
179
  overflow: (n, v) =>
122
180
  n.setOverflow(pick(OVERFLOW, v, 'overflow') ?? Yoga.OVERFLOW_VISIBLE),
123
181
  borderWidth: (n, v) => n.setBorder(Yoga.EDGE_ALL, v ?? 0),
182
+ // per-side widths resolve the way padding does: the side overrides the
183
+ // shorthand, and yoga's own edge precedence (EDGE_TOP over EDGE_ALL) is
184
+ // what implements the override
185
+ borderTopWidth: (n, v) => n.setBorder(Yoga.EDGE_TOP, v),
186
+ borderRightWidth: (n, v) => n.setBorder(Yoga.EDGE_RIGHT, v),
187
+ borderBottomWidth: (n, v) => n.setBorder(Yoga.EDGE_BOTTOM, v),
188
+ borderLeftWidth: (n, v) => n.setBorder(Yoga.EDGE_LEFT, v),
189
+ borderStartWidth: (n, v) => n.setBorder(Yoga.EDGE_START, v),
190
+ borderEndWidth: (n, v) => n.setBorder(Yoga.EDGE_END, v),
124
191
  };
125
192
 
126
193
  // Props that only affect painting, not geometry.
194
+ //
195
+ // `outline*` is here rather than beside `border*` in LAYOUT_APPLIERS for the
196
+ // reason CSS grew a second property at all: a focus ring must not move the
197
+ // thing it is drawn around. It is painted outside the border box and takes
198
+ // no part in yoga, so switching it on is a repaint of one node and nothing
199
+ // under it reflows.
127
200
  const PAINT_PROPS = new Set([
128
201
  'backgroundColor',
202
+ // The two decorations that are not a colour (issue #345, src/decorations.js).
203
+ // Paint props like the rest of this set, which is what makes them legal in
204
+ // a state block — a card that lifts on `:hover` is the case they exist for
205
+ // — and what keeps them out of layout: a gradient is painted in the box the
206
+ // layout already decided on, and a shadow is drawn outside it and moves
207
+ // nothing.
208
+ 'backgroundImage',
209
+ 'boxShadow',
129
210
  'borderColor',
211
+ 'borderTopColor',
212
+ 'borderRightColor',
213
+ 'borderBottomColor',
214
+ 'borderLeftColor',
215
+ 'borderStartColor',
216
+ 'borderEndColor',
130
217
  'borderRadius',
131
218
  'zIndex',
219
+ 'outlineWidth',
220
+ 'outlineColor',
221
+ 'outlineOffset',
132
222
  ]);
133
223
 
134
224
  // Text style props. All affect measurement except color.
@@ -137,14 +227,1285 @@ export const TEXT_LAYOUT_PROPS = new Set([
137
227
  'fontSize',
138
228
  'fontWeight',
139
229
  'fontStyle',
230
+ // a variable font's axes, `{ wght: 460 }` — the `wght` axis is already
231
+ // driven by `fontWeight`, so this is for the rest. Compared by value, not
232
+ // identity, since it is written as an object literal in a render
233
+ 'fontVariationSettings',
140
234
  'textAlign',
141
235
  'lineHeight',
236
+ // read by TextNode rather than passed to ntk: it trims the box the layout
237
+ // produced, it does not change the layout
238
+ 'textBoxTrim',
239
+ // read by TextNode too, and it *is* the layout: `'nowrap'` measures at
240
+ // unbounded width, so the text is one line and whatever contains it decides
241
+ // what to do about the overflow
242
+ 'textWrap',
243
+ // The truncation pair. Both are handed straight to ntk's TextLayout, which
244
+ // does the careful version — the ellipsis in the cut run's own font, the
245
+ // cut on a grapheme boundary with the tail re-shaped, and on the visually
246
+ // last run rather than the logically last one. `textOverflow` changes what
247
+ // fits on a line, so it is a measurement input like the rest of this set.
248
+ 'textOverflow',
249
+ 'maxLines',
250
+ ]);
251
+
252
+ /**
253
+ * Text style props that change how the text is **drawn** and provably not
254
+ * where any of it lands. They still invalidate the cached layout — the value
255
+ * rides on the spans inside it — but never the box, so changing one repaints
256
+ * without reflowing.
257
+ *
258
+ * `textRendering` is CSS's, and picks the glyph path: `geometricPrecision`
259
+ * puts glyph origins exactly where shaping asked, `optimizeSpeed` keeps them
260
+ * on ntk's cached-bitmap path, `auto` lets size decide. Only rounding at
261
+ * draw time differs — ntk's layout answers byte-identically for all three,
262
+ * down to per-run offsets — which is what makes it safe to keep out of the
263
+ * measurement set.
264
+ */
265
+ export const TEXT_PAINT_PROPS = new Set(['textRendering']);
266
+
267
+ /**
268
+ * The text properties that **inherit** — the ones a node hands down to
269
+ * everything drawing text inside it, so `<box style={{ color: theme.textMuted }}>`
270
+ * dims the labels under it the way it would in CSS.
271
+ *
272
+ * This is CSS's inherited set narrowed to what a *descendant* can act on: the
273
+ * face, the size, the ink and the glyph rounding. `textAlign`, `lineHeight`,
274
+ * `textWrap`, `textOverflow`, `maxLines` and `textBoxTrim` stay out even
275
+ * though CSS inherits the first two — here they are read by the node that
276
+ * owns the **box** the text flows in, and a box is not something a descendant
277
+ * has. A `<box>` that wants its children aligned says so in the styles it
278
+ * gives them.
279
+ */
280
+ export const INHERITED_TEXT_PROPS = new Set([
281
+ 'fontFamily',
282
+ 'fontSize',
283
+ 'fontWeight',
284
+ 'fontStyle',
285
+ 'fontVariationSettings',
286
+ 'textRendering',
287
+ 'color',
142
288
  ]);
143
289
 
290
+ /**
291
+ * The text props that do **not** inherit — `TEXT_LAYOUT_PROPS` minus
292
+ * `INHERITED_TEXT_PROPS`. They shape the box a node's own text flows in, so
293
+ * no cascade can bring one in from above and the node that owns them is the
294
+ * only one that has to react.
295
+ */
296
+ export const LOCAL_TEXT_PROPS = new Set(
297
+ [...TEXT_LAYOUT_PROPS].filter((key) => !INHERITED_TEXT_PROPS.has(key)),
298
+ );
299
+
300
+ /** Did anything that shapes this node's own text box change? */
301
+ export function localTextStyleChanged(style, before) {
302
+ if (style === before) return false;
303
+ for (const key of LOCAL_TEXT_PROPS) {
304
+ if (style[key] !== before[key]) return true;
305
+ }
306
+ return false;
307
+ }
308
+
309
+ /** Did anything a descendant inherits change between two style bags? The
310
+ * gate on re-resolving a subtree, so a commit that moved `padding` walks
311
+ * nothing. */
312
+ export function inheritedTextChanged(style, before) {
313
+ if (style === before) return false;
314
+ for (const key of INHERITED_TEXT_PROPS) {
315
+ if (key === 'fontVariationSettings') {
316
+ if (!axesEqual(style[key], before[key])) return true;
317
+ } else if (style[key] !== before[key]) return true;
318
+ }
319
+ return false;
320
+ }
321
+
322
+ /** What a change in resolved text style costs the node that draws with it:
323
+ * a glyph may have moved. */
324
+ export const TEXT_REMEASURE = 2;
325
+ /** …or only the ink or the rounding did, so the box cannot have changed. */
326
+ export const TEXT_REPAINT = 1;
327
+
328
+ /**
329
+ * Compare two **resolved** text styles (`textStyleFrom`'s shape, which is
330
+ * ntk's) and price the difference.
331
+ *
332
+ * The split is what keeps a colour cascade off the layout path: `color` and
333
+ * `textRendering` ride on the spans inside a cached layout, so the layout
334
+ * still has to go — but neither moves a glyph, so nothing needs measuring
335
+ * again. Conflating the two is why `:hover { color }` used to be able to
336
+ * cost a full layout pass per pointer move.
337
+ */
338
+ export function resolvedTextDelta(a, b) {
339
+ if (a === b) return 0;
340
+ if (
341
+ a.family !== b.family ||
342
+ a.size !== b.size ||
343
+ a.weight !== b.weight ||
344
+ a.style !== b.style ||
345
+ !axesEqual(a.variations, b.variations)
346
+ ) {
347
+ return TEXT_REMEASURE;
348
+ }
349
+ if (a.color !== b.color || a.textRendering !== b.textRendering) {
350
+ return TEXT_REPAINT;
351
+ }
352
+ return 0;
353
+ }
354
+
355
+ /**
356
+ * Every text layout prop is a scalar and compares by value, except the one
357
+ * that is a bag of axis coordinates. `fontVariationSettings` is written as
358
+ * an object literal in a render, so a fresh one arrives on every commit and
359
+ * `!==` would call it a change every time — re-shaping the paragraph and
360
+ * re-rasterizing its glyphs to arrive at the same pixels. Small and flat, so
361
+ * comparing it is cheaper than believing it.
362
+ */
363
+ export function axesEqual(a, b) {
364
+ if (a === b) return true;
365
+ if (!a || !b) return false;
366
+ const keys = Object.keys(a);
367
+ if (keys.length !== Object.keys(b).length) return false;
368
+ for (const key of keys) {
369
+ if (a[key] !== b[key]) return false;
370
+ }
371
+ return true;
372
+ }
373
+
144
374
  export const isLayoutProp = (name) =>
145
375
  Object.prototype.hasOwnProperty.call(LAYOUT_APPLIERS, name);
146
376
  export const isPaintProp = (name) => PAINT_PROPS.has(name);
147
- export const isEventProp = (name) => /^on[A-Z]/.test(name);
377
+ /**
378
+ * A handler prop. `unstable_`-prefixed ones count: the prefix marks an API as
379
+ * provisional (React's own idiom), it does not stop the prop being a handler.
380
+ * Both callers care — a handler must not reach `CreateWindow` as a window
381
+ * attribute, and `paintChanged` must not claim a repaint every time a render
382
+ * passes a fresh inline arrow.
383
+ */
384
+ export const isEventProp = (name) => /^(?:unstable_)?on[A-Z]/.test(name);
385
+
386
+ /**
387
+ * State blocks, lowest precedence first. These are *node* states, not
388
+ * selectors: each one is something the node itself knows about, so
389
+ * resolving them needs no specificity and no matching. Anything relational
390
+ * — `:hover > child`, a sibling selector, `:nth-child` — stays in React,
391
+ * where composition already answers it.
392
+ *
393
+ * The two that read as relational are not. `:hover` and `:active` mark the
394
+ * whole ancestor chain because the node the pointer actually landed on is
395
+ * whatever the control happens to be built out of, and `:focus-within` is
396
+ * the same fact about the focus path — each of them is still "something
397
+ * true of this node", diffed over a path the event manager has already
398
+ * computed. What a child does about an ancestor's state is inheritance
399
+ * rather than a selector: a `:hover` block that sets `color` reaches the
400
+ * labels inside, the way it does in CSS.
401
+ */
402
+ export const STATE_KEYS = [
403
+ // The pointer is heading here but has not arrived (ntk#37). Lowest
404
+ // precedence of the lot, because it is the only *prediction* in the list
405
+ // and every other state is a fact: once the pointer actually lands,
406
+ // `:hover` is the truth and has to win.
407
+ ':attention',
408
+ ':hover',
409
+ // Focus is on this node or inside it — CSS's `:focus-within`. Below
410
+ // `:focus` on purpose: it is the broader fact, so a node that is itself
411
+ // focused should be able to say something narrower and win.
412
+ ':focus-within',
413
+ ':focus',
414
+ // Focus that came from the keyboard rather than from a press — CSS's
415
+ // `:focus-visible`, and for the same reason: a ring on every click is
416
+ // noise, a ring on Tab is the only way a keyboard user can tell where
417
+ // they are. `focus()` decides which it was; see EventManager.
418
+ ':focus-visible',
419
+ ':active',
420
+ ':disabled',
421
+ ':drag-over',
422
+ ':dragging',
423
+ ];
424
+
425
+ // What a state block may change. Deliberately paint-only: a state block that
426
+ // could set `padding` or `fontSize` would reflow the tree on pointer move,
427
+ // which is both a jitter bug and the end of the "hover is a repaint, not a
428
+ // React render" property that makes this worth having at all.
429
+ const STATE_PROPS = new Set([...PAINT_PROPS, 'color']);
430
+
431
+ const STYLE_PROPS = new Set([
432
+ ...Object.keys(LAYOUT_APPLIERS),
433
+ // the one layout property that is not a yoga property: a shorthand for
434
+ // three of them, expanded by `resolveComputedStyle`
435
+ 'flex',
436
+ ...PAINT_PROPS,
437
+ ...TEXT_LAYOUT_PROPS,
438
+ ...TEXT_PAINT_PROPS,
439
+ 'color',
440
+ 'borderStyle',
441
+ 'transition',
442
+ // a loop rather than a change with an end — see `animationsOf`
443
+ 'animation',
444
+ // CSS concepts even though they read as behaviour; React Native has been
445
+ // moving pointerEvents into style for the same reason
446
+ 'cursor',
447
+ 'pointerEvents',
448
+ // How far outside the box the pointer still counts as hitting it. Neither
449
+ // layout nor paint — the one thing it must never do is grow the visuals,
450
+ // since the whole point is a 24px target under a 16px control.
451
+ 'hitSlop',
452
+ ]);
453
+
454
+ export const isStyleProp = (name) => STYLE_PROPS.has(name);
455
+
456
+ /** Every style property, by name. DevTools' style editor takes this list as
457
+ * `nativeStyleEditorValidAttributes` — what it offers to add to an element
458
+ * — so it is the same set `isStyleProp` answers for rather than a second
459
+ * list that could drift from it. */
460
+ export const STYLE_PROP_NAMES = Object.freeze([...STYLE_PROPS].sort());
461
+
462
+ const isState = (key) => key.charCodeAt(0) === 58; /* ':' */
463
+
464
+ /**
465
+ * Window size queries: `'@width >= 600'`. The X11 analogue of `@media` —
466
+ * what a style can usefully ask about here is the window it is being laid
467
+ * out in, not the screen.
468
+ *
469
+ * Unlike a state block, a size query *may* set layout properties. That is
470
+ * not an inconsistency: pointer state changes must never reflow the tree,
471
+ * but a size query is only ever re-evaluated during a layout pass that a
472
+ * resize has already triggered, so it costs nothing extra.
473
+ */
474
+ const SIZE_QUERY = /^@(width|height)\s*(>=|<=|>|<)\s*(\d+(?:\.\d+)?)$/;
475
+
476
+ /**
477
+ * Capability queries: `'@supports transparency'`. Where a size query asks
478
+ * about the window, this asks about the *server* — what will actually be
479
+ * shown if the style asks for it.
480
+ *
481
+ * `transparency` is true only when the window really has an alpha channel
482
+ * (it was created on a 32-bit visual) *and* a compositor is running to
483
+ * blend it. Either half missing and a transparent corner is a black corner,
484
+ * so the honest answer is no. It is per window, not per display: a plain
485
+ * `<window>` never composites anything, so the same component nested in one
486
+ * gets the opaque design without being told twice.
487
+ */
488
+ const SUPPORTS_QUERY = /^@supports\s+(transparency)$/;
489
+ const SUPPORTS_FEATURES = ['transparency'];
490
+ const isQuery = (key) => key.charCodeAt(0) === 64; /* '@' */
491
+
492
+ const parsedQueries = new Map();
493
+ function parseQuery(key) {
494
+ let q = parsedQueries.get(key);
495
+ if (q === undefined) {
496
+ const size = SIZE_QUERY.exec(key);
497
+ const supports = size ? null : SUPPORTS_QUERY.exec(key);
498
+ q = size
499
+ ? { kind: 'size', axis: size[1], op: size[2], value: Number(size[3]) }
500
+ : supports
501
+ ? { kind: 'supports', feature: supports[1] }
502
+ : null;
503
+ parsedQueries.set(key, q);
504
+ }
505
+ return q;
506
+ }
507
+
508
+ function sizeMatches(q, size) {
509
+ const v = size?.[q.axis];
510
+ if (v == null) return false;
511
+ return q.op === '>='
512
+ ? v >= q.value
513
+ : q.op === '<='
514
+ ? v <= q.value
515
+ : q.op === '>'
516
+ ? v > q.value
517
+ : v < q.value;
518
+ }
519
+
520
+ const hasQueryOfKind = (style, kind) => {
521
+ for (const key of Object.keys(style)) {
522
+ if (isQuery(key) && parseQuery(key)?.kind === kind) return true;
523
+ }
524
+ return false;
525
+ };
526
+
527
+ /** Re-resolved when the window is laid out at a new size. */
528
+ export const styleHasSizeQueries = (style) => hasQueryOfKind(style, 'size');
529
+
530
+ /** Re-resolved when the server's answer changes — a compositor starting or
531
+ * stopping — rather than on every layout. The two registries are kept
532
+ * apart because the triggers are: a resize must not walk every node that
533
+ * only ever asked about transparency. */
534
+ export const styleHasSupportsQueries = (style) =>
535
+ hasQueryOfKind(style, 'supports');
536
+
537
+ /**
538
+ * Merge the query blocks that match, in declaration order, over the base.
539
+ * Size and capability blocks resolve in one pass so that ordering between
540
+ * them is the order they were written in. Returns the style itself when
541
+ * nothing matches, so the identity fast path survives the common case.
542
+ *
543
+ * `supports` is the map of capability answers, or null while they are still
544
+ * unknown — in which case a capability block does not apply, which is the
545
+ * safe way round: the fallback design is the one that works everywhere.
546
+ */
547
+ /**
548
+ * `resolveQueries` with only the size half, the shape this had before
549
+ * capability blocks existed. Kept because `react-x11/style` is a public
550
+ * entry and a registered element outside the package may be calling it.
551
+ */
552
+ export function resolveSizeQueries(style, size) {
553
+ return size ? resolveQueries(style, { size }) : style;
554
+ }
555
+
556
+ export function resolveQueries(style, { size = null, supports = null } = {}) {
557
+ let out = style;
558
+ for (const key of Object.keys(style)) {
559
+ if (!isQuery(key)) continue;
560
+ const q = parseQuery(key);
561
+ if (!q) continue;
562
+ const hit =
563
+ q.kind === 'size'
564
+ ? size && sizeMatches(q, size)
565
+ : Boolean(supports?.[q.feature]);
566
+ if (!hit) continue;
567
+ if (out === style) out = { ...style };
568
+ Object.assign(out, style[key]);
569
+ }
570
+ return out;
571
+ }
572
+
573
+ /**
574
+ * The two style values that are a small language rather than a number, and
575
+ * therefore the two that can be *wrong* rather than merely absent. Parsed in
576
+ * development wherever they are written — including inside a state block,
577
+ * which is the half of the surface a `continue` used to skip — so the error
578
+ * naming the property and the expected spelling arrives at the call site
579
+ * instead of as a blank panel three commits later.
580
+ *
581
+ * Tokens are still unresolved here (`$accent` is a colour as far as the
582
+ * grammar is concerned), so this checks the shape and never the colours.
583
+ */
584
+ function validateValue(key, value, where) {
585
+ if (key !== 'backgroundImage' && key !== 'boxShadow') return;
586
+ try {
587
+ if (key === 'backgroundImage') parseLinearGradient(value);
588
+ else parseBoxShadow(value);
589
+ } catch (err) {
590
+ // the parser names the property and the grammar; only the call site is
591
+ // missing, and it is what turns the message into a place to look
592
+ err.message += `\n in ${where}`;
593
+ throw err;
594
+ }
595
+ }
596
+
597
+ function validateStyle(style, where) {
598
+ for (const key of Object.keys(style)) {
599
+ if (isQuery(key)) {
600
+ if (!parseQuery(key)) {
601
+ throw new Error(
602
+ `react-x11: bad query "${key}" in ${where} (expected a size query ` +
603
+ 'like "@width >= 600", or a capability query like ' +
604
+ `"@supports ${SUPPORTS_FEATURES.join('" / "@supports ')}")`,
605
+ );
606
+ }
607
+ validateStyle(style[key] ?? {}, `${where} ${key}`);
608
+ continue;
609
+ }
610
+ if (isState(key)) {
611
+ if (!STATE_KEYS.includes(key)) {
612
+ throw new Error(
613
+ `react-x11: unknown style state "${key}" in ${where} ` +
614
+ `(expected one of ${STATE_KEYS.join(', ')})`,
615
+ );
616
+ }
617
+ for (const inner of Object.keys(style[key] ?? {})) {
618
+ if (STATE_PROPS.has(inner)) {
619
+ validateValue(inner, style[key][inner], `${where} ${key}`);
620
+ continue;
621
+ }
622
+ throw new Error(
623
+ `react-x11: "${inner}" is not allowed inside "${key}" in ${where}. ` +
624
+ 'State blocks may only change paint properties ' +
625
+ `(${[...STATE_PROPS].join(', ')}) — anything that reflows the ` +
626
+ 'tree on hover belongs in React state.',
627
+ );
628
+ }
629
+ continue;
630
+ }
631
+ if (!STYLE_PROPS.has(key)) {
632
+ throw new Error(`react-x11: unknown style property "${key}" in ${where}`);
633
+ }
634
+ // The one style value that describes motion rather than a state, so the
635
+ // one whose mistakes are invisible: a loop with a bad `to` does not draw
636
+ // anything wrong, it simply never moves.
637
+ if (key === 'animation') animationsOf(style, where);
638
+ validateValue(key, style[key], where);
639
+ if (key === 'flex' && !isFlexShorthand(style[key])) {
640
+ throw new Error(
641
+ `react-x11: invalid flex ${JSON.stringify(style[key])} in ${where} ` +
642
+ '(expected a number — flex: 1 is flexGrow: 1, flexShrink: 1, ' +
643
+ "flexBasis: 0 — or 'auto' / 'none')",
644
+ );
645
+ }
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Flatten a style prop — an object, or a nested array of them with falsy
651
+ * entries skipped, later entries winning:
652
+ *
653
+ * style={[styles.card, isWide && styles.wide, { padding: 4 }]}
654
+ *
655
+ * This is what replaces the cascade: precedence is written at the call
656
+ * site instead of being resolved by specificity.
657
+ */
658
+ export function flattenStyle(style, into) {
659
+ if (!style) return into ?? EMPTY_STYLE;
660
+ if (Array.isArray(style)) {
661
+ const acc = into ?? {};
662
+ for (const entry of style) flattenStyle(entry, acc);
663
+ return acc;
664
+ }
665
+ // a lone object is returned as-is: no copy, and `===` still identifies a
666
+ // hoisted style across renders
667
+ if (!into) return style;
668
+ for (const key of Object.keys(style)) {
669
+ // a state block merges with one already collected rather than replacing
670
+ // it, so [{':hover': {color}}, {':hover': {backgroundColor}}] keeps both
671
+ into[key] =
672
+ isState(key) && into[key] ? { ...into[key], ...style[key] } : style[key];
673
+ }
674
+ return into;
675
+ }
676
+
677
+ export const EMPTY_STYLE = Object.freeze({});
678
+
679
+ /**
680
+ * Declare styles once, outside render. Identity is the point: a hoisted
681
+ * style object lets the renderer skip an update with a `===` check, the
682
+ * same reason RN's StyleSheet.create exists now that its id registry is
683
+ * gone. It also validates keys, which a bare object literal cannot.
684
+ */
685
+ export function createStyles(sheet) {
686
+ for (const name of Object.keys(sheet)) {
687
+ validateStyle(sheet[name], `styles.${name}`);
688
+ Object.freeze(sheet[name]);
689
+ }
690
+ return Object.freeze(sheet);
691
+ }
692
+
693
+ /**
694
+ * Overlay the active state blocks on a flattened style, lowest precedence
695
+ * first (hover < focus < active < disabled — a disabled control must never
696
+ * look hovered). Returns the base object itself when no state is active,
697
+ * so the common case allocates nothing and stays `===`-comparable.
698
+ */
699
+ export function resolveStyleStates(style, states) {
700
+ let resolved = style;
701
+ for (const key of STATE_KEYS) {
702
+ const block = style[key];
703
+ if (!block || !states[key]) continue;
704
+ if (resolved === style) resolved = { ...style };
705
+ Object.assign(resolved, block);
706
+ }
707
+ return resolved;
708
+ }
709
+
710
+ /** Does this style react to node state at all? */
711
+ export function hasStateStyles(style) {
712
+ for (const key of STATE_KEYS) if (style[key]) return true;
713
+ return false;
714
+ }
715
+
716
+ /**
717
+ * Style properties a transition can animate: numbers and colours. Enums
718
+ * (`flexDirection`), `zIndex` (restacking every frame is not an animation)
719
+ * and `transition` itself are excluded — a change to those snaps.
720
+ */
721
+ const NOT_ANIMATABLE = new Set([
722
+ 'transition',
723
+ // the loop declaration itself: it is an object, and what it describes is
724
+ // already a value changing over time
725
+ 'animation',
726
+ // Both are strings that describe several numbers and a colour at once, and
727
+ // `interpolate` works on one value. They snap, which for a state change is
728
+ // what the shorter durations look like anyway; a card that wants to *rise*
729
+ // on hover transitions its `borderColor` or its background beside them.
730
+ 'backgroundImage',
731
+ 'boxShadow',
732
+ 'zIndex',
733
+ 'direction',
734
+ 'flexDirection',
735
+ 'justifyContent',
736
+ 'alignItems',
737
+ 'alignSelf',
738
+ 'alignContent',
739
+ 'flexWrap',
740
+ 'position',
741
+ 'display',
742
+ 'overflow',
743
+ 'cursor',
744
+ 'pointerEvents',
745
+ 'borderStyle',
746
+ 'fontFamily',
747
+ 'fontWeight',
748
+ 'fontStyle',
749
+ 'textAlign',
750
+ // nothing is drawn from it, so there is no frame in which a halfway value
751
+ // would be visible — and it may be an object, which does not lerp
752
+ 'hitSlop',
753
+ ]);
754
+
755
+ export const isAnimatableProp = (name) =>
756
+ STYLE_PROPS.has(name) && !NOT_ANIMATABLE.has(name);
757
+
758
+ /**
759
+ * `transition: 150` — every animatable property that changes, over 150ms.
760
+ * `transition: { backgroundColor: 150 }` — only these, with their own
761
+ * durations. Returns a lookup of prop -> ms, or null.
762
+ */
763
+ export function transitionFor(style, prop) {
764
+ const t = style.transition;
765
+ if (t == null || !isAnimatableProp(prop)) return 0;
766
+ if (typeof t === 'number') return t;
767
+ return t[prop] ?? 0;
768
+ }
769
+
770
+ /**
771
+ * The easing curves an `animation` can name. `transition` has one fixed
772
+ * ease-out and keeps it — a change that ends looks right slowing into its
773
+ * new value — but a loop is a different shape: ease-out on a cycle that
774
+ * restarts reads as a stutter at the wrap, so the default here is `linear`
775
+ * and the curve is the author's to pick.
776
+ */
777
+ const EASINGS = {
778
+ linear: (t) => t,
779
+ 'ease-in': (t) => t ** 3,
780
+ 'ease-out': (t) => 1 - (1 - t) ** 3,
781
+ 'ease-in-out': (t) => (t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2),
782
+ };
783
+
784
+ export const EASING_NAMES = Object.freeze(Object.keys(EASINGS));
785
+
786
+ /** A value that is still a `$token` reference, or mentions one. */
787
+ const unresolvedValue = (v) => isToken(v) || mentionsToken(v);
788
+
789
+ // Parsed `animation` objects, keyed by the object itself: a hoisted style
790
+ // parses once for the process, an inline one once per render. What is cached
791
+ // is the *declaration* — `from` may still be defaulting to the property's
792
+ // declared value, which belongs to the style rather than to this object.
793
+ const parsedAnimations = new WeakMap();
794
+
795
+ /**
796
+ * `animation` — a property that travels between two values and keeps doing
797
+ * it, as against `transition`, which is how long it takes to arrive
798
+ * somewhere and is over when it gets there.
799
+ *
800
+ * ```js
801
+ * animation: { left: { from: '-40%', to: '100%', duration: 1100 } }
802
+ * ```
803
+ *
804
+ * Per property rather than one config for the object, because the two things
805
+ * a loop needs — where it goes from and where it goes to — are per property
806
+ * already, and a shared `duration` would only be a shorthand for writing it
807
+ * twice.
808
+ *
809
+ * `from` defaults to what the style declares for that property, so a pulse
810
+ * reads as the resting value plus where it goes:
811
+ *
812
+ * ```js
813
+ * { backgroundColor: theme.track,
814
+ * animation: { backgroundColor: { to: theme.accent, duration: 900,
815
+ * alternate: true } } }
816
+ * ```
817
+ *
818
+ * Everything wrong with a declaration throws here rather than showing up as
819
+ * a thing that does not move: a loop nobody wrote a stop for is exactly the
820
+ * feature where silence is unreadable.
821
+ */
822
+ function parseAnimation(spec, where) {
823
+ if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {
824
+ throw new Error(
825
+ `react-x11: invalid animation ${JSON.stringify(spec)} in ${where} ` +
826
+ "(expected { property: { to, duration } }, e.g. { left: { from: '0%', " +
827
+ "to: '100%', duration: 900 } })",
828
+ );
829
+ }
830
+ const entries = [];
831
+ for (const prop of Object.keys(spec)) {
832
+ const at = `"${prop}" in ${where}`;
833
+ if (!isAnimatableProp(prop)) {
834
+ throw new Error(
835
+ `react-x11: ${at} cannot be animated — a loop interpolates, and ` +
836
+ 'this property has no midpoint between two values.',
837
+ );
838
+ }
839
+ const entry = spec[prop];
840
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
841
+ throw new Error(
842
+ `react-x11: invalid animation for ${at} — expected ` +
843
+ '{ from?, to, duration, easing?, alternate? }, got ' +
844
+ JSON.stringify(entry),
845
+ );
846
+ }
847
+ for (const key of Object.keys(entry)) {
848
+ if (!['from', 'to', 'duration', 'easing', 'alternate'].includes(key)) {
849
+ throw new Error(
850
+ `react-x11: unknown animation option "${key}" for ${at} ` +
851
+ '(expected from, to, duration, easing, alternate)',
852
+ );
853
+ }
854
+ }
855
+ if (entry.to === undefined) {
856
+ throw new Error(`react-x11: animation for ${at} has no "to" value`);
857
+ }
858
+ if (
859
+ typeof entry.duration !== 'number' ||
860
+ !Number.isFinite(entry.duration) ||
861
+ entry.duration <= 0
862
+ ) {
863
+ throw new Error(
864
+ `react-x11: animation for ${at} needs a positive "duration" in ms, ` +
865
+ `got ${JSON.stringify(entry.duration)}`,
866
+ );
867
+ }
868
+ const easing = entry.easing ?? 'linear';
869
+ if (!Object.hasOwn(EASINGS, easing)) {
870
+ throw new Error(
871
+ `react-x11: unknown animation easing ${JSON.stringify(easing)} for ` +
872
+ `${at} (expected one of ${EASING_NAMES.join(', ')})`,
873
+ );
874
+ }
875
+ entries.push({
876
+ prop,
877
+ from: entry.from,
878
+ to: entry.to,
879
+ duration: entry.duration,
880
+ easing,
881
+ ease: EASINGS[easing],
882
+ alternate: Boolean(entry.alternate),
883
+ });
884
+ }
885
+ return entries;
886
+ }
887
+
888
+ /**
889
+ * The loops a style declares, with `from` resolved against the style itself
890
+ * and both ends checked for a midpoint. Null when there are none, so the
891
+ * common case allocates nothing.
892
+ */
893
+ export function animationsOf(style, where = 'a style', scale = 1) {
894
+ const spec = style.animation;
895
+ if (spec == null) return null;
896
+ let parsed = parsedAnimations.get(spec);
897
+ if (!parsed) {
898
+ parsed = parseAnimation(spec, where);
899
+ parsedAnimations.set(spec, parsed);
900
+ }
901
+ if (parsed.length === 0) return null;
902
+ return parsed.map((entry) => {
903
+ // The display scale, only where the caller says the style is already in
904
+ // device pixels (`_syncLoops` passes the node's). The declared ends are
905
+ // logical like everything an app writes; a `from` *defaulted* from the
906
+ // style is a device value already and is left alone — which is why the
907
+ // scaling happens here, where the two can still be told apart.
908
+ if (scale !== 1 && SCALED_LENGTHS.has(entry.prop)) {
909
+ entry = {
910
+ ...entry,
911
+ ...(typeof entry.from === 'number' && { from: entry.from * scale }),
912
+ ...(typeof entry.to === 'number' && { to: entry.to * scale }),
913
+ };
914
+ }
915
+ const from = entry.from === undefined ? style[entry.prop] : entry.from;
916
+ if (from === undefined) {
917
+ throw new Error(
918
+ `react-x11: animation for "${entry.prop}" in ${where} has no "from" ` +
919
+ 'value and the style does not declare one to start from',
920
+ );
921
+ }
922
+ // A `$token` is not a colour yet — `validateStyle` runs where the style
923
+ // is written, which is before the node has an ancestry to resolve
924
+ // against. The check is not skipped, only deferred: `_syncStyle`
925
+ // resolves the tokens and comes back through here with real values.
926
+ const resolved = from === entry.from ? entry : { ...entry, from };
927
+ if (unresolvedValue(from) || unresolvedValue(entry.to)) return resolved;
928
+ if (interpolate(from, entry.to, 0.5) === null) {
929
+ throw new Error(
930
+ `react-x11: animation for "${entry.prop}" in ${where} has no ` +
931
+ `midpoint between ${JSON.stringify(from)} and ` +
932
+ `${JSON.stringify(entry.to)} — a loop interpolates, so both ends ` +
933
+ 'have to be numbers, colours, or percentages of the same unit.',
934
+ );
935
+ }
936
+ return resolved;
937
+ });
938
+ }
939
+
940
+ /** Whether two resolved loop declarations describe the same motion — what
941
+ * decides between letting a running loop keep its phase and starting it
942
+ * again from the top. A re-render that produces an equal declaration must
943
+ * not make the animation jump. */
944
+ export function sameAnimation(a, b) {
945
+ return (
946
+ a.from === b.from &&
947
+ a.to === b.to &&
948
+ a.duration === b.duration &&
949
+ a.easing === b.easing &&
950
+ a.alternate === b.alternate
951
+ );
952
+ }
953
+
954
+ /**
955
+ * Where a loop is at `elapsed` ms after it started. Pure, and the whole of
956
+ * the looping: the phase comes from a modulo of the elapsed time rather than
957
+ * from a per-cycle restart, so a bar that has been spinning for an hour is
958
+ * exactly where the clock says and no rounding has accumulated.
959
+ */
960
+ export function animationValueAt(spec, elapsed) {
961
+ const cycles = Math.max(0, elapsed) / spec.duration;
962
+ let t = cycles % 1;
963
+ if (spec.alternate && Math.floor(cycles) % 2 === 1) t = 1 - t;
964
+ return interpolate(spec.from, spec.to, spec.ease(t)) ?? spec.from;
965
+ }
966
+
967
+ const PERCENT = /^\s*(-?\d+(?:\.\d+)?)%\s*$/;
968
+
969
+ const rgba = (c) =>
970
+ c &&
971
+ `rgba(${Math.round(c[0] * 255)}, ${Math.round(c[1] * 255)}, ${Math.round(c[2] * 255)}, ${c[3]})`;
972
+
973
+ /**
974
+ * Interpolate one style value. Numbers lerp; two percentages of the same
975
+ * unit lerp as numbers and come back as a percentage, so a value expressed
976
+ * against its container animates without anyone having to measure the
977
+ * container; colours lerp per channel through ntk's own CSS colour parser,
978
+ * so anything the paint path accepts animates. Anything else — `auto`, an
979
+ * enum, a percentage against a pixel value — has no meaningful midpoint and
980
+ * returns null, which the caller treats as a snap.
981
+ *
982
+ * The colours are parsed **straight**, not premultiplied. The result is
983
+ * formatted back into an `rgba()` string, and that round trip only closes on
984
+ * unassociated components: premultiplied ones get scaled by alpha a second
985
+ * time when the paint path parses the string again, so a midpoint of a
986
+ * translucent colour would come out darker than either end.
987
+ */
988
+ export function interpolate(from, to, t) {
989
+ if (typeof from === 'number' && typeof to === 'number') {
990
+ return from + (to - from) * t;
991
+ }
992
+ if (typeof from === 'string' && typeof to === 'string') {
993
+ // Percentages first, and only percentage against percentage: `'-40%'` to
994
+ // `'100%'` is a number moving in the container's units, which is the one
995
+ // way a loop can travel the width of something it never measured. Mixing
996
+ // one with a pixel value is still a snap — the midpoint of `10` and
997
+ // `'50%'` depends on a layout that has not run yet.
998
+ const pa = PERCENT.exec(from);
999
+ if (pa) {
1000
+ const pb = PERCENT.exec(to);
1001
+ if (!pb) return null;
1002
+ const value = Number(pa[1]) + (Number(pb[1]) - Number(pa[1])) * t;
1003
+ return `${Math.round(value * 1000) / 1000}%`;
1004
+ }
1005
+ if (PERCENT.test(to)) return null;
1006
+ const a = cssColorStraight(from);
1007
+ const b = cssColorStraight(to);
1008
+ if (!a || !b) return null;
1009
+ // Interpolate *premultiplied*, then divide the alpha back out, which is
1010
+ // what CSS does and for the same reason. `transparent` is black at zero
1011
+ // alpha, so lerping straight channels drags the colour towards black on
1012
+ // the way: half way from `transparent` to a near-white hover fill lands on
1013
+ // mid grey — 0.736 against 0.973 — and the curve is not even monotonic,
1014
+ // it darkens and then lightens again. That is the rectangle that flashes
1015
+ // when hover crosses two adjacent tabs, one fading out as the other fades
1016
+ // in, both passing through grey together.
1017
+ const alpha = a[3] + (b[3] - a[3]) * t;
1018
+ if (alpha <= 0) return 'rgba(0, 0, 0, 0)';
1019
+ const channel = (i) => {
1020
+ const from0 = a[i] * a[3];
1021
+ return (from0 + (b[i] * b[3] - from0) * t) / alpha;
1022
+ };
1023
+ return rgba([channel(0), channel(1), channel(2), alpha]);
1024
+ }
1025
+ return null;
1026
+ }
1027
+
1028
+ /**
1029
+ * One more step in the direction `from` → `to`, clamped to the gamut.
1030
+ *
1031
+ * This is how a palette that named a hover and stopped there still gets a
1032
+ * pressed colour (`theme.js`): the press is the hover step taken twice. It
1033
+ * reads the *direction* rather than assuming one, so it darkens a light
1034
+ * theme and lightens a dark one without being told which it is — which is
1035
+ * the whole reason it is not a `darken(colour, 0.1)`.
1036
+ *
1037
+ * Straight components, like `interpolate`, and for the same round-trip
1038
+ * reason. Clamping is what extrapolation needs and interpolation does not:
1039
+ * two steps out of a near-white hover leaves the cube.
1040
+ */
1041
+ export function stepBeyond(from, to) {
1042
+ const a = cssColorStraight(from);
1043
+ const b = cssColorStraight(to);
1044
+ if (!a || !b) return to;
1045
+ const step = (i) => Math.min(1, Math.max(0, b[i] + (b[i] - a[i])));
1046
+ return rgba([step(0), step(1), step(2), step(3)]);
1047
+ }
1048
+
1049
+ /**
1050
+ * WCAG relative luminance — the perceptual lightness contrast is measured
1051
+ * in, which is not the mean of the channels: green carries most of it and
1052
+ * blue almost none, so `#0000ff` and `#00ff00` are worlds apart here and
1053
+ * three pixels apart in a channel average.
1054
+ */
1055
+ function luminance(c) {
1056
+ const linear = (v) =>
1057
+ v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
1058
+ return 0.2126 * linear(c[0]) + 0.7152 * linear(c[1]) + 0.0722 * linear(c[2]);
1059
+ }
1060
+
1061
+ /**
1062
+ * Which of `inks` can be read on `fill` — the one with the most contrast,
1063
+ * WCAG's ratio.
1064
+ *
1065
+ * This is what keeps a palette from having to name the ink on every fill it
1066
+ * names. The two candidates a palette always has are its own `text` and its
1067
+ * own `background`, and one of them is readable on any fill by construction:
1068
+ * a fill light enough to swallow the light one is dark enough to show the
1069
+ * dark one. So `resolveTheme` derives `accentText` and the status inks from
1070
+ * the family colour, and a theme that names a yellow warning gets dark
1071
+ * letters on it without having thought about it.
1072
+ *
1073
+ * Ratio rather than a lightness threshold because a threshold is exactly the
1074
+ * thing that fails on the mid-tones: an accent at L*55 is on whichever side
1075
+ * of 50% the theme's own ink is not, and only a comparison knows which.
1076
+ *
1077
+ * Returns the first ink where a colour will not parse, which is the same
1078
+ * "keep going with what you were given" the rest of this file does.
1079
+ */
1080
+ export function readableInk(fill, inks) {
1081
+ const bg = cssColorStraight(fill);
1082
+ if (!bg) return inks[0];
1083
+ const lb = luminance(bg);
1084
+ let best = inks[0];
1085
+ let bestRatio = -1;
1086
+ for (const ink of inks) {
1087
+ const c = cssColorStraight(ink);
1088
+ if (!c) continue;
1089
+ const li = luminance(c);
1090
+ const ratio = (Math.max(lb, li) + 0.05) / (Math.min(lb, li) + 0.05);
1091
+ if (ratio > bestRatio) {
1092
+ bestRatio = ratio;
1093
+ best = ink;
1094
+ }
1095
+ }
1096
+ return best;
1097
+ }
1098
+
1099
+ /**
1100
+ * A colour at a given opacity — `tint('#2980b9', 0.3)`.
1101
+ *
1102
+ * For fills that are drawn *under* text whose colour they do not control: a
1103
+ * selection highlight is the case, and an opaque one has to be chosen to
1104
+ * contrast with the ink on top of it, which cannot be done once for both a
1105
+ * light and a dark palette. A translucent one is chosen against the surface
1106
+ * instead, and the ink keeps whatever contrast it already had.
1107
+ */
1108
+ export function tint(color, alpha) {
1109
+ const c = cssColorStraight(color);
1110
+ if (!c) return color;
1111
+ return rgba([c[0], c[1], c[2], c[3] * alpha]);
1112
+ }
1113
+
1114
+ // ease-out cubic: fast to start, settles gently — the shape almost every UI
1115
+ // toolkit defaults to for state changes
1116
+ export const ease = (t) => 1 - (1 - t) ** 3;
1117
+
1118
+ /**
1119
+ * Theme tokens. A style value of `'$name'` resolves against the nearest
1120
+ * `theme` prop above the node, so a style can be hoisted — declared once,
1121
+ * outside render, with no access to React context — and still follow the
1122
+ * theme. The sigil is what keeps it unambiguous: `'red'` is a CSS colour,
1123
+ * `'$red'` is a token.
1124
+ *
1125
+ * A token also resolves **inside** a value that is a small language of its
1126
+ * own — `linear-gradient($accent, $accentActive)`, `boxShadow: '0 2px 8px
1127
+ * $shadow'`. Those two are the whole reason: the point of the palette is
1128
+ * that a colour is named once, and a decoration that could not name one
1129
+ * would push every gradient in an app back into `useTheme()` and out of a
1130
+ * hoisted style. It is the same substitution either way, so the value that
1131
+ * *is* a token keeps its fast path and the value that *mentions* one goes
1132
+ * through the regexp.
1133
+ */
1134
+ const isToken = (v) => typeof v === 'string' && v.charCodeAt(0) === 36; /* $ */
1135
+ /** `$name`, anywhere in a string. Deliberately the same grammar as a whole
1136
+ * token, so `'$accent'` and `'linear-gradient($accent, #000)'` cannot
1137
+ * disagree about what a name is. */
1138
+ const TOKEN_IN_VALUE = /\$[A-Za-z_][A-Za-z0-9_-]*/g;
1139
+ const mentionsToken = (v) =>
1140
+ typeof v === 'string' && v.charCodeAt(0) !== 36 && v.includes('$');
1141
+
1142
+ /** The nested blocks a token can hide in, one level down: a state block,
1143
+ * whose values are style values, and `animation`, whose values are the
1144
+ * per-property declarations *whose* values are style values. Both are
1145
+ * walked by everything that looks for a `$name`, so a themed loop resolves
1146
+ * by the same rules as a themed hover. */
1147
+ const animationBlocks = (style) =>
1148
+ style.animation && typeof style.animation === 'object'
1149
+ ? Object.values(style.animation).filter((e) => e && typeof e === 'object')
1150
+ : [];
1151
+
1152
+ export function styleUsesTokens(style) {
1153
+ for (const key of Object.keys(style)) {
1154
+ const v = style[key];
1155
+ if (isToken(v) || mentionsToken(v)) return true;
1156
+ if (key.charCodeAt(0) === 58 && v && styleUsesTokens(v)) return true;
1157
+ }
1158
+ return animationBlocks(style).some(styleUsesTokens);
1159
+ }
1160
+
1161
+ /** Every `$name` a style mentions. Not used internally any more — a token
1162
+ * that does not resolve now throws, naming itself — but it is part of the
1163
+ * style surface `src/style.js` publishes for tooling. */
1164
+ export function tokenNames(style, out = new Set()) {
1165
+ for (const key of Object.keys(style)) {
1166
+ const v = style[key];
1167
+ if (isToken(v)) out.add(v);
1168
+ else if (mentionsToken(v))
1169
+ for (const m of v.match(TOKEN_IN_VALUE) ?? []) out.add(m);
1170
+ else if (key.charCodeAt(0) === 58 && v) tokenNames(v, out);
1171
+ }
1172
+ for (const block of animationBlocks(style)) tokenNames(block, out);
1173
+ return out;
1174
+ }
1175
+
1176
+ // (style object, theme object) -> resolved style. Two hoisted styles under
1177
+ // one theme therefore keep their identity across renders, which is what the
1178
+ // `===` fast path in applyProps relies on.
1179
+ const resolvedCache = new WeakMap();
1180
+
1181
+ /**
1182
+ * Before a node is attached it has no ancestors and so no theme yet — one
1183
+ * commit tick, but long enough for yoga to be handed a `'$gutter'`. Drop
1184
+ * the token-valued properties until the real value is known; the node
1185
+ * restyles on attach.
1186
+ */
1187
+ export function stripTokens(style) {
1188
+ const out = {};
1189
+ for (const key of Object.keys(style)) {
1190
+ const v = style[key];
1191
+ // a value that *mentions* a token goes too, and whole: half a gradient
1192
+ // is not a gradient, and the node restyles on attach either way
1193
+ if (isToken(v) || mentionsToken(v)) continue;
1194
+ // …and so does a loop with a token at either end, whole: a declaration
1195
+ // missing one of its two values is not a shorter animation, it is one
1196
+ // that would throw for having no midpoint.
1197
+ if (key === 'animation' && animationBlocks(style).some(styleUsesTokens)) {
1198
+ continue;
1199
+ }
1200
+ out[key] = key.charCodeAt(0) === 58 && v ? stripTokens(v) : v;
1201
+ }
1202
+ return out;
1203
+ }
1204
+
1205
+ /**
1206
+ * `strict` says the node's ancestry is complete, so a token that does not
1207
+ * resolve is a mistake. While a subtree is still being built its nodes can
1208
+ * see only part of their ancestry — the theme two levels up does not exist
1209
+ * for them yet — so resolution there is provisional: unknown tokens are
1210
+ * dropped and the node restyles when it attaches.
1211
+ */
1212
+ export function resolveTokens(style, theme, where = 'style', strict = true) {
1213
+ if (!theme) return stripTokens(style);
1214
+ let byTheme = strict ? resolvedCache.get(style) : null;
1215
+ if (strict && !byTheme) resolvedCache.set(style, (byTheme = new WeakMap()));
1216
+ const hit = byTheme?.get(theme);
1217
+ if (hit) return hit;
1218
+
1219
+ const out = {};
1220
+ for (const key of Object.keys(style)) {
1221
+ const v = style[key];
1222
+ if (isToken(v)) {
1223
+ const name = v.slice(1);
1224
+ if (name in theme) {
1225
+ out[key] = theme[name];
1226
+ continue;
1227
+ }
1228
+ if (!strict) continue;
1229
+ throw new Error(
1230
+ `react-x11: unknown theme token "${v}" in ${where} ` +
1231
+ `(theme has ${Object.keys(theme).join(', ') || 'nothing'})`,
1232
+ );
1233
+ } else if (mentionsToken(v)) {
1234
+ let unknown = null;
1235
+ const substituted = v.replace(TOKEN_IN_VALUE, (token) => {
1236
+ const name = token.slice(1);
1237
+ if (name in theme) return theme[name];
1238
+ unknown ??= token;
1239
+ return token;
1240
+ });
1241
+ // Same rule as a whole-value token, one level down: unknown is a
1242
+ // mistake once the ancestry is complete, and provisional before that —
1243
+ // and a half-substituted gradient is dropped rather than painted,
1244
+ // since `$accent` is not a colour and the parse would fail at the
1245
+ // frame instead of at the style.
1246
+ if (!unknown) out[key] = substituted;
1247
+ else if (strict) {
1248
+ throw new Error(
1249
+ `react-x11: unknown theme token "${unknown}" in ${where} ${key} ` +
1250
+ `(theme has ${Object.keys(theme).join(', ') || 'nothing'})`,
1251
+ );
1252
+ }
1253
+ } else if (key.charCodeAt(0) === 58 && v) {
1254
+ out[key] = resolveTokens(v, theme, `${where} ${key}`, strict);
1255
+ } else if (key === 'animation' && v && typeof v === 'object') {
1256
+ const loops = {};
1257
+ let incomplete = false;
1258
+ for (const prop of Object.keys(v)) {
1259
+ const entry = v[prop];
1260
+ if (!entry || typeof entry !== 'object') {
1261
+ loops[prop] = entry;
1262
+ continue;
1263
+ }
1264
+ const resolved = resolveTokens(
1265
+ entry,
1266
+ theme,
1267
+ `${where} animation ${prop}`,
1268
+ strict,
1269
+ );
1270
+ // A provisional resolution drops what it cannot resolve, which for
1271
+ // an ordinary property means "not styled yet". A loop with one end
1272
+ // missing is not a shorter loop, so the whole declaration waits for
1273
+ // the ancestry to complete rather than throwing at a half of one.
1274
+ if (Object.keys(resolved).length !== Object.keys(entry).length) {
1275
+ incomplete = true;
1276
+ }
1277
+ loops[prop] = resolved;
1278
+ }
1279
+ if (!incomplete) out[key] = loops;
1280
+ } else {
1281
+ out[key] = v;
1282
+ }
1283
+ }
1284
+ byTheme?.set(theme, out);
1285
+ return out;
1286
+ }
1287
+
1288
+ export { validateStyle };
1289
+
1290
+ /**
1291
+ * The style the layout is actually run from: the `flex` shorthand expanded,
1292
+ * and the defaults `overflow: 'scroll'` implies folded in. Both are things a
1293
+ * style *means* rather than things it says, and doing them here — once, on
1294
+ * the resolved object — is what lets everything downstream stay a flat diff
1295
+ * of yoga properties.
1296
+ *
1297
+ * Returns `style` itself when there is nothing to add, so identity comparisons
1298
+ * upstream keep meaning "the style did not change".
1299
+ *
1300
+ * The scroll-container half, property by property:
1301
+ *
1302
+ * - **`min-width/height: 0`** is the spec's own rule, not an invention: CSS
1303
+ * computes `min-*: auto` to `0` on a flex item whose overflow is not
1304
+ * `visible`. It is the one place the automatic minimum size (`Node`'s
1305
+ * content floors) does not apply, and the reason a viewport can be smaller
1306
+ * than what is inside it — which is what scrolling is.
1307
+ * - **`flexBasis: 0`** — what CSS's `flex: 1` means — only when the author
1308
+ * asked it to grow and gave it no size of its own. A flex item's base size
1309
+ * is its content, and a window whose scrolling pane holds more rows than
1310
+ * fit grew *past* the window, pushing the footer out of view however small
1311
+ * the window got. Zeroing the basis fixes the whole ancestor chain at once,
1312
+ * since the content stops counting towards any of their heights.
1313
+ */
1314
+ export function resolveComputedStyle(style) {
1315
+ const scrolls = style.overflow === 'scroll';
1316
+ const loops = style.animation == null ? null : animationsOf(style);
1317
+ if (style.flex === undefined && !scrolls && !loops) return style;
1318
+ const out = {};
1319
+ if (style.flex !== undefined) {
1320
+ const expansion =
1321
+ typeof style.flex === 'number'
1322
+ ? { flexGrow: style.flex, flexShrink: 1, flexBasis: 0 }
1323
+ : FLEX_SHORTHAND[style.flex];
1324
+ if (!expansion) {
1325
+ throw new Error(
1326
+ `react-x11: invalid flex ${JSON.stringify(style.flex)} ` +
1327
+ "(expected a number, 'auto' or 'none')",
1328
+ );
1329
+ }
1330
+ Object.assign(out, expansion);
1331
+ }
1332
+ // after the expansion, so a longhand written beside the shorthand wins
1333
+ for (const key of Object.keys(style)) {
1334
+ if (key !== 'flex') out[key] = style[key];
1335
+ }
1336
+ if (scrolls) {
1337
+ if (out.minWidth === undefined) out.minWidth = 0;
1338
+ if (out.minHeight === undefined) out.minHeight = 0;
1339
+ if (
1340
+ out.flexBasis === undefined &&
1341
+ out.width === undefined &&
1342
+ out.height === undefined &&
1343
+ (out.flexGrow ?? 0) > 0
1344
+ ) {
1345
+ out.flexBasis = 0;
1346
+ }
1347
+ }
1348
+ // A loop owns the property it animates, so `from` is what the property
1349
+ // *is* when nothing is running it — before the first frame, while the
1350
+ // window is off screen, and on a desktop that asked for less motion. Doing
1351
+ // it here rather than at the first tick is what makes the resting frame
1352
+ // and the animated one the same layout: yoga is set up from this style.
1353
+ if (loops) {
1354
+ for (const loop of loops) out[loop.prop] = loop.from;
1355
+ }
1356
+ return out;
1357
+ }
1358
+
1359
+ /**
1360
+ * The style lengths that are *distances on the screen*, and therefore the
1361
+ * complete set the display scale multiplies (src/scale.js). Everything a
1362
+ * style can say that is not here is deliberately not here: `lineHeight` is
1363
+ * a multiplier over the font's own height, `aspectRatio` and the flex
1364
+ * factors are ratios, `opacity` and `zIndex` are not lengths, and
1365
+ * `boxShadow` is a string whose lengths are scaled where it is parsed
1366
+ * (src/decorations.js), because its parse is memoized on the raw string.
1367
+ */
1368
+ export const SCALED_LENGTH_PROPS = [
1369
+ 'width',
1370
+ 'height',
1371
+ 'minWidth',
1372
+ 'minHeight',
1373
+ 'maxWidth',
1374
+ 'maxHeight',
1375
+ 'flexBasis',
1376
+ 'top',
1377
+ 'right',
1378
+ 'bottom',
1379
+ 'left',
1380
+ 'start',
1381
+ 'end',
1382
+ 'margin',
1383
+ 'marginTop',
1384
+ 'marginRight',
1385
+ 'marginBottom',
1386
+ 'marginLeft',
1387
+ 'marginStart',
1388
+ 'marginEnd',
1389
+ 'padding',
1390
+ 'paddingTop',
1391
+ 'paddingRight',
1392
+ 'paddingBottom',
1393
+ 'paddingLeft',
1394
+ 'paddingStart',
1395
+ 'paddingEnd',
1396
+ 'gap',
1397
+ 'rowGap',
1398
+ 'columnGap',
1399
+ 'borderWidth',
1400
+ 'borderTopWidth',
1401
+ 'borderRightWidth',
1402
+ 'borderBottomWidth',
1403
+ 'borderLeftWidth',
1404
+ 'borderStartWidth',
1405
+ 'borderEndWidth',
1406
+ 'borderRadius',
1407
+ 'outlineWidth',
1408
+ 'outlineOffset',
1409
+ 'fontSize',
1410
+ ];
1411
+
1412
+ const SCALED_LENGTHS = new Set(SCALED_LENGTH_PROPS);
1413
+
1414
+ /**
1415
+ * A resolved style in logical pixels → the same style in device pixels.
1416
+ *
1417
+ * This is the whole mechanism by which the display scale reaches layout,
1418
+ * paint and text: it runs once, at the end of the style funnel
1419
+ * (`_syncStyle` → `resolveComputedStyle` → here → `_retarget`), so yoga,
1420
+ * every `this.style.borderRadius ?? 0` at a paint site, and the font size
1421
+ * the text stack shapes at are all *already* device pixels and none of them
1422
+ * ever multiplies again. Numbers scale; `'50%'`, `'auto'` and every other
1423
+ * string mean the same thing at any density and pass through; `hitSlop`
1424
+ * scales inside its number-or-per-side shape.
1425
+ *
1426
+ * Never mutates: `flattenStyle` hands back the app's own hoisted object
1427
+ * when it can, and scaling it in place would corrupt the next render.
1428
+ * Identity is preserved at scale 1 — the everyday case costs one compare.
1429
+ */
1430
+ export function scaleResolvedStyle(style, scale) {
1431
+ if (!style || !scale || scale === 1) return style;
1432
+ let out = style;
1433
+ for (const key of SCALED_LENGTH_PROPS) {
1434
+ const value = style[key];
1435
+ if (typeof value !== 'number' || value === 0) continue;
1436
+ if (out === style) out = { ...style };
1437
+ out[key] = value * scale;
1438
+ }
1439
+ const slop = style.hitSlop;
1440
+ if (typeof slop === 'number') {
1441
+ if (out === style) out = { ...style };
1442
+ out.hitSlop = slop * scale;
1443
+ } else if (slop && typeof slop === 'object') {
1444
+ if (out === style) out = { ...style };
1445
+ out.hitSlop = {
1446
+ ...slop,
1447
+ ...(typeof slop.top === 'number' && { top: slop.top * scale }),
1448
+ ...(typeof slop.right === 'number' && { right: slop.right * scale }),
1449
+ ...(typeof slop.bottom === 'number' && { bottom: slop.bottom * scale }),
1450
+ ...(typeof slop.left === 'number' && { left: slop.left * scale }),
1451
+ };
1452
+ }
1453
+ return out;
1454
+ }
1455
+
1456
+ /**
1457
+ * The renderer's own yoga config — one for the process, shared by every node
1458
+ * — which exists so that a **measurement** can be taken off the pixel grid.
1459
+ *
1460
+ * Yoga rounds a finished layout to whole pixels, and does it on absolute
1461
+ * positions, so a box 36.4 tall reads back as 36 or 37 depending on where it
1462
+ * landed. Summing those with exact paddings is how a min-content measurement
1463
+ * of 36.4 comes out as 37 — and a floor of 37 does not hold a box at the size
1464
+ * it already was, it *grows* it, one pixel per nesting level, all the way up
1465
+ * the tree. Measuring with the grid switched off is what keeps a floor a
1466
+ * promise not to shrink rather than an instruction to grow.
1467
+ *
1468
+ * The final layout — the one that decides where anything is actually drawn —
1469
+ * always runs rounded, which is what keeps a border on a whole pixel.
1470
+ */
1471
+ let config = null;
1472
+ const layoutConfig = () => (config ??= Yoga.Config.create());
1473
+
1474
+ /** A yoga node in the renderer's config. */
1475
+ export const createLayoutNode = () => Yoga.Node.create(layoutConfig());
1476
+
1477
+ /**
1478
+ * Run `measure` with the pixel grid switched off, and put it back however
1479
+ * that goes: a throw here would otherwise leave every later layout in the
1480
+ * process unrounded, which is a class of blurry-by-a-half-pixel bug nothing
1481
+ * would connect back to this.
1482
+ */
1483
+ export function measuringExactly(measure) {
1484
+ const cfg = layoutConfig();
1485
+ cfg.setPointScaleFactor(0);
1486
+ try {
1487
+ return measure();
1488
+ } finally {
1489
+ cfg.setPointScaleFactor(1);
1490
+ }
1491
+ }
1492
+
1493
+ /**
1494
+ * The yoga defaults that are not CSS's, written once per node.
1495
+ *
1496
+ * `applyLayoutStyle` only calls a setter for a property that **changed**, so
1497
+ * a style that never mentions `flexShrink` never reaches the `?? 1` in its
1498
+ * applier and yoga's own `0` would stand. A default that only exists in the
1499
+ * reset path is not a default; this is where it actually happens.
1500
+ *
1501
+ * The pair to it is the automatic minimum size — a flex item that may shrink
1502
+ * still cannot shrink below its content unless it says so. Yoga has no such
1503
+ * rule, so the renderer measures the floors itself; see `Node`'s content
1504
+ * floors in nodes.js.
1505
+ */
1506
+ export function applyLayoutDefaults(yogaNode) {
1507
+ yogaNode.setFlexShrink(1);
1508
+ }
148
1509
 
149
1510
  /**
150
1511
  * Apply changed layout props to a yoga node.
@@ -166,7 +1527,12 @@ export function paintPropsChanged(props, oldProps = {}) {
166
1527
  for (const key of PAINT_PROPS) {
167
1528
  if (props[key] !== oldProps[key]) return true;
168
1529
  }
169
- return props.color !== oldProps.color;
1530
+ // `color` and `borderStyle` paint but are deliberately not in PAINT_PROPS:
1531
+ // that set also decides what a state block is allowed to set, and widening
1532
+ // it would change validation rather than just this comparison
1533
+ return (
1534
+ props.color !== oldProps.color || props.borderStyle !== oldProps.borderStyle
1535
+ );
170
1536
  }
171
1537
 
172
1538
  /** Resolved text style (TextLayout base style) from props + inherited. */
@@ -176,14 +1542,124 @@ export function textStyleFrom(props, inherited) {
176
1542
  size: props.fontSize ?? inherited.size,
177
1543
  weight: props.fontWeight ?? inherited.weight,
178
1544
  style: props.fontStyle ?? inherited.style,
1545
+ // ntk's name for it is `variations`; the prop is spelled after the CSS
1546
+ // property, like every other name in this vocabulary
1547
+ variations: props.fontVariationSettings ?? inherited.variations,
1548
+ textRendering: props.textRendering ?? inherited.textRendering,
179
1549
  color: props.color ?? inherited.color,
180
1550
  };
181
1551
  }
182
1552
 
1553
+ /**
1554
+ * The floor under `Node.inheritedTextStyle`, which is what text actually
1555
+ * reads: the ink, the face and the size all come from the palette in force,
1556
+ * and these are only what is left when there is no palette at all — a node
1557
+ * that has not been attached yet, or a bare `theme` prop naming neither.
1558
+ *
1559
+ * `family`, `size` and `color` therefore mirror `DefaultTheme.fontFamily`,
1560
+ * `.fontSize` and `.text`; the rest have no token because no theme has ever
1561
+ * wanted to set the weight of every label in an app at once.
1562
+ */
183
1563
  export const DEFAULT_TEXT_STYLE = {
184
1564
  family: 'sans-serif',
185
1565
  size: 14,
186
1566
  weight: 'normal',
187
1567
  style: 'normal',
1568
+ variations: undefined,
1569
+ textRendering: undefined,
188
1570
  color: 'black',
189
1571
  };
1572
+
1573
+ /**
1574
+ * The focus ring a focusable node draws when nothing asked it to.
1575
+ *
1576
+ * WCAG 2.4.7 is not something an application should have to opt into, and
1577
+ * the vocabulary alone would not have delivered it: `outlineWidth` in a
1578
+ * `:focus-visible` block is a thing every widget author would then have to
1579
+ * remember, on every focusable, forever. So this is the default and
1580
+ * `outlineWidth: 0` is the opt-out. A theme overrides the three values with
1581
+ * `focusRing`, `focusRingWidth` and `focusRingOffset`.
1582
+ *
1583
+ * The offset is what keeps it legible against a control whose own border is
1584
+ * already coloured — the ring is outside the box with a gap, not a second
1585
+ * border on it.
1586
+ */
1587
+ export const DEFAULT_FOCUS_RING = {
1588
+ color: '#2980b9',
1589
+ width: 2,
1590
+ offset: 1,
1591
+ };
1592
+
1593
+ /**
1594
+ * Per-side hit slop from `hitSlop: 4` or `hitSlop: { top: 4, bottom: 4 }`,
1595
+ * or null when there is none. Sides left out are 0, so the object form only
1596
+ * has to name what it grows.
1597
+ */
1598
+ /**
1599
+ * Which physical side each logical edge lands on. The one function that knows
1600
+ * what `start` means, so a widget or a paint path never has to spell the
1601
+ * conditional out again.
1602
+ */
1603
+ export const physicalSides = (direction) =>
1604
+ direction === 'rtl'
1605
+ ? { start: 'right', end: 'left' }
1606
+ : { start: 'left', end: 'right' };
1607
+
1608
+ /**
1609
+ * Per-side border widths, resolved the way padding resolves: the side
1610
+ * property overrides the `borderWidth` shorthand, and a **logical** side
1611
+ * overrides the physical one — `borderStartWidth` beats `borderLeftWidth` in
1612
+ * LTR, the way `border-inline-start-width` beats `border-left-width` in CSS.
1613
+ * This is the paint-side reading of the rule yoga applies on the layout side
1614
+ * (EDGE_START over EDGE_LEFT over EDGE_ALL), kept in one place so the two
1615
+ * cannot disagree — a border that lays out one width and paints another is a
1616
+ * gap along the edge of the box.
1617
+ *
1618
+ * `direction` is the resolved direction of the node being painted, so this is
1619
+ * also where a `borderStartWidth` crosses to the other side of the box.
1620
+ */
1621
+ export function resolveBorderWidths(style, direction) {
1622
+ const all = style.borderWidth ?? 0;
1623
+ const { start, end } = physicalSides(direction);
1624
+ const sides = {
1625
+ top: style.borderTopWidth ?? all,
1626
+ right: style.borderRightWidth ?? all,
1627
+ bottom: style.borderBottomWidth ?? all,
1628
+ left: style.borderLeftWidth ?? all,
1629
+ };
1630
+ if (style.borderStartWidth !== undefined)
1631
+ sides[start] = style.borderStartWidth;
1632
+ if (style.borderEndWidth !== undefined) sides[end] = style.borderEndWidth;
1633
+ return sides;
1634
+ }
1635
+
1636
+ /** …and the same rule for the colours those widths are stroked in. */
1637
+ export function resolveBorderColors(style, direction) {
1638
+ const all = style.borderColor;
1639
+ const { start, end } = physicalSides(direction);
1640
+ const sides = {
1641
+ top: style.borderTopColor ?? all,
1642
+ right: style.borderRightColor ?? all,
1643
+ bottom: style.borderBottomColor ?? all,
1644
+ left: style.borderLeftColor ?? all,
1645
+ };
1646
+ if (style.borderStartColor !== undefined)
1647
+ sides[start] = style.borderStartColor;
1648
+ if (style.borderEndColor !== undefined) sides[end] = style.borderEndColor;
1649
+ return sides;
1650
+ }
1651
+
1652
+ export function resolveHitSlop(value) {
1653
+ if (value == null) return null;
1654
+ if (typeof value === 'number') {
1655
+ if (!(value > 0)) return null;
1656
+ return { top: value, right: value, bottom: value, left: value };
1657
+ }
1658
+ const slop = {
1659
+ top: value.top ?? 0,
1660
+ right: value.right ?? 0,
1661
+ bottom: value.bottom ?? 0,
1662
+ left: value.left ?? 0,
1663
+ };
1664
+ return slop.top || slop.right || slop.bottom || slop.left ? slop : null;
1665
+ }