@react-x11/components 0.3.0 → 0.5.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 (314) hide show
  1. package/README.md +131 -4
  2. package/dist/charts/components.d.ts +3 -1
  3. package/dist/charts/components.d.ts.map +1 -1
  4. package/dist/charts/components.js +8 -4
  5. package/dist/charts/components.js.map +1 -1
  6. package/dist/charts/node.d.ts +13 -6
  7. package/dist/charts/node.d.ts.map +1 -1
  8. package/dist/charts/node.js +75 -24
  9. package/dist/charts/node.js.map +1 -1
  10. package/dist/charts/render.d.ts +4 -0
  11. package/dist/charts/render.d.ts.map +1 -1
  12. package/dist/charts/render.js +11 -6
  13. package/dist/charts/render.js.map +1 -1
  14. package/dist/code-editor/node.d.ts +47 -16
  15. package/dist/code-editor/node.d.ts.map +1 -1
  16. package/dist/code-editor/node.js +121 -51
  17. package/dist/code-editor/node.js.map +1 -1
  18. package/dist/flow/draw.d.ts +19 -1
  19. package/dist/flow/draw.d.ts.map +1 -1
  20. package/dist/flow/draw.js +64 -35
  21. package/dist/flow/draw.js.map +1 -1
  22. package/dist/flow/index.d.ts.map +1 -1
  23. package/dist/flow/index.js +24 -8
  24. package/dist/flow/index.js.map +1 -1
  25. package/dist/flow/model.d.ts +4 -2
  26. package/dist/flow/model.d.ts.map +1 -1
  27. package/dist/flow/model.js +4 -2
  28. package/dist/flow/model.js.map +1 -1
  29. package/dist/flow/node.d.ts +40 -9
  30. package/dist/flow/node.d.ts.map +1 -1
  31. package/dist/flow/node.js +142 -53
  32. package/dist/flow/node.js.map +1 -1
  33. package/dist/flow/types.d.ts +46 -21
  34. package/dist/flow/types.d.ts.map +1 -1
  35. package/dist/formula/index.d.ts +4 -2
  36. package/dist/formula/index.d.ts.map +1 -1
  37. package/dist/formula/index.js.map +1 -1
  38. package/dist/formula/node.d.ts +5 -1
  39. package/dist/formula/node.d.ts.map +1 -1
  40. package/dist/formula/node.js +19 -3
  41. package/dist/formula/node.js.map +1 -1
  42. package/dist/html/controls.d.ts +4 -1
  43. package/dist/html/controls.d.ts.map +1 -1
  44. package/dist/html/controls.js.map +1 -1
  45. package/dist/html/css/cascade.d.ts +11 -5
  46. package/dist/html/css/cascade.d.ts.map +1 -1
  47. package/dist/html/css/cascade.js +18 -8
  48. package/dist/html/css/cascade.js.map +1 -1
  49. package/dist/html/css/parse.d.ts +15 -10
  50. package/dist/html/css/parse.d.ts.map +1 -1
  51. package/dist/html/css/parse.js +41 -16
  52. package/dist/html/css/parse.js.map +1 -1
  53. package/dist/html/css/style.d.ts +3 -0
  54. package/dist/html/css/style.d.ts.map +1 -1
  55. package/dist/html/css/style.js +3 -1
  56. package/dist/html/css/style.js.map +1 -1
  57. package/dist/html/css/values.d.ts +14 -1
  58. package/dist/html/css/values.d.ts.map +1 -1
  59. package/dist/html/css/values.js +11 -8
  60. package/dist/html/css/values.js.map +1 -1
  61. package/dist/html/index.d.ts.map +1 -1
  62. package/dist/html/index.js +1 -0
  63. package/dist/html/index.js.map +1 -1
  64. package/dist/html/layout/boxes.d.ts +12 -5
  65. package/dist/html/layout/boxes.d.ts.map +1 -1
  66. package/dist/html/layout/boxes.js +7 -4
  67. package/dist/html/layout/boxes.js.map +1 -1
  68. package/dist/html/layout/inline.d.ts.map +1 -1
  69. package/dist/html/layout/inline.js +22 -1
  70. package/dist/html/layout/inline.js.map +1 -1
  71. package/dist/html/node.d.ts +22 -8
  72. package/dist/html/node.d.ts.map +1 -1
  73. package/dist/html/node.js +97 -15
  74. package/dist/html/node.js.map +1 -1
  75. package/dist/html/paint.d.ts +3 -0
  76. package/dist/html/paint.d.ts.map +1 -1
  77. package/dist/html/paint.js +2 -2
  78. package/dist/html/paint.js.map +1 -1
  79. package/dist/index.d.ts +11 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +26 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/maps/clip.d.ts +43 -0
  84. package/dist/maps/clip.d.ts.map +1 -0
  85. package/dist/maps/clip.js +171 -0
  86. package/dist/maps/clip.js.map +1 -0
  87. package/dist/maps/gzip.d.ts +15 -0
  88. package/dist/maps/gzip.d.ts.map +1 -0
  89. package/dist/maps/gzip.js +56 -0
  90. package/dist/maps/gzip.js.map +1 -0
  91. package/dist/maps/index.d.ts +55 -0
  92. package/dist/maps/index.d.ts.map +1 -0
  93. package/dist/maps/index.js +141 -0
  94. package/dist/maps/index.js.map +1 -0
  95. package/dist/maps/labels.d.ts +123 -0
  96. package/dist/maps/labels.d.ts.map +1 -0
  97. package/dist/maps/labels.js +323 -0
  98. package/dist/maps/labels.js.map +1 -0
  99. package/dist/maps/mvt.d.ts +244 -0
  100. package/dist/maps/mvt.d.ts.map +1 -0
  101. package/dist/maps/mvt.js +820 -0
  102. package/dist/maps/mvt.js.map +1 -0
  103. package/dist/maps/node.d.ts +278 -0
  104. package/dist/maps/node.d.ts.map +1 -0
  105. package/dist/maps/node.js +1459 -0
  106. package/dist/maps/node.js.map +1 -0
  107. package/dist/maps/overlay.d.ts +157 -0
  108. package/dist/maps/overlay.d.ts.map +1 -0
  109. package/dist/maps/overlay.js +472 -0
  110. package/dist/maps/overlay.js.map +1 -0
  111. package/dist/maps/paint.d.ts +226 -0
  112. package/dist/maps/paint.d.ts.map +1 -0
  113. package/dist/maps/paint.js +705 -0
  114. package/dist/maps/paint.js.map +1 -0
  115. package/dist/maps/proj.d.ts +281 -0
  116. package/dist/maps/proj.d.ts.map +1 -0
  117. package/dist/maps/proj.js +451 -0
  118. package/dist/maps/proj.js.map +1 -0
  119. package/dist/maps/sources.d.ts +256 -0
  120. package/dist/maps/sources.d.ts.map +1 -0
  121. package/dist/maps/sources.js +210 -0
  122. package/dist/maps/sources.js.map +1 -0
  123. package/dist/maps/style.d.ts +141 -0
  124. package/dist/maps/style.d.ts.map +1 -0
  125. package/dist/maps/style.js +147 -0
  126. package/dist/maps/style.js.map +1 -0
  127. package/dist/maps/styles.d.ts +82 -0
  128. package/dist/maps/styles.d.ts.map +1 -0
  129. package/dist/maps/styles.js +898 -0
  130. package/dist/maps/styles.js.map +1 -0
  131. package/dist/maps/tiles.d.ts +283 -0
  132. package/dist/maps/tiles.d.ts.map +1 -0
  133. package/dist/maps/tiles.js +469 -0
  134. package/dist/maps/tiles.js.map +1 -0
  135. package/dist/maps/types.d.ts +244 -0
  136. package/dist/maps/types.d.ts.map +1 -0
  137. package/dist/maps/types.js +2 -0
  138. package/dist/maps/types.js.map +1 -0
  139. package/dist/qml/containers.d.ts +3 -0
  140. package/dist/qml/containers.d.ts.map +1 -0
  141. package/dist/qml/containers.js +331 -0
  142. package/dist/qml/containers.js.map +1 -0
  143. package/dist/qml/controls.d.ts +12 -0
  144. package/dist/qml/controls.d.ts.map +1 -0
  145. package/dist/qml/controls.js +21 -0
  146. package/dist/qml/controls.js.map +1 -0
  147. package/dist/qml/globals.d.ts +6 -0
  148. package/dist/qml/globals.d.ts.map +1 -0
  149. package/dist/qml/globals.js +25 -0
  150. package/dist/qml/globals.js.map +1 -0
  151. package/dist/qml/index.d.ts +15 -0
  152. package/dist/qml/index.d.ts.map +1 -0
  153. package/dist/qml/index.js +25 -0
  154. package/dist/qml/index.js.map +1 -0
  155. package/dist/qml/interaction.d.ts +12 -0
  156. package/dist/qml/interaction.d.ts.map +1 -0
  157. package/dist/qml/interaction.js +246 -0
  158. package/dist/qml/interaction.js.map +1 -0
  159. package/dist/qml/ir.d.ts +106 -0
  160. package/dist/qml/ir.d.ts.map +1 -0
  161. package/dist/qml/ir.js +7 -0
  162. package/dist/qml/ir.js.map +1 -0
  163. package/dist/qml/layouts.d.ts +3 -0
  164. package/dist/qml/layouts.d.ts.map +1 -0
  165. package/dist/qml/layouts.js +122 -0
  166. package/dist/qml/layouts.js.map +1 -0
  167. package/dist/qml/models.d.ts +14 -0
  168. package/dist/qml/models.d.ts.map +1 -0
  169. package/dist/qml/models.js +80 -0
  170. package/dist/qml/models.js.map +1 -0
  171. package/dist/qml/objects.d.ts +228 -0
  172. package/dist/qml/objects.d.ts.map +1 -0
  173. package/dist/qml/objects.js +1103 -0
  174. package/dist/qml/objects.js.map +1 -0
  175. package/dist/qml/parse.d.ts +10 -0
  176. package/dist/qml/parse.d.ts.map +1 -0
  177. package/dist/qml/parse.js +607 -0
  178. package/dist/qml/parse.js.map +1 -0
  179. package/dist/qml/qtquick.d.ts +7 -0
  180. package/dist/qml/qtquick.d.ts.map +1 -0
  181. package/dist/qml/qtquick.js +347 -0
  182. package/dist/qml/qtquick.js.map +1 -0
  183. package/dist/qml/react.d.ts +83 -0
  184. package/dist/qml/react.d.ts.map +1 -0
  185. package/dist/qml/react.js +323 -0
  186. package/dist/qml/react.js.map +1 -0
  187. package/dist/qml/resolver.d.ts +38 -0
  188. package/dist/qml/resolver.d.ts.map +1 -0
  189. package/dist/qml/resolver.js +64 -0
  190. package/dist/qml/resolver.js.map +1 -0
  191. package/dist/qml/slots.d.ts +65 -0
  192. package/dist/qml/slots.d.ts.map +1 -0
  193. package/dist/qml/slots.js +230 -0
  194. package/dist/qml/slots.js.map +1 -0
  195. package/dist/qml/states.d.ts +5 -0
  196. package/dist/qml/states.d.ts.map +1 -0
  197. package/dist/qml/states.js +276 -0
  198. package/dist/qml/states.js.map +1 -0
  199. package/dist/qml/view-utils.d.ts +42 -0
  200. package/dist/qml/view-utils.d.ts.map +1 -0
  201. package/dist/qml/view-utils.js +121 -0
  202. package/dist/qml/view-utils.js.map +1 -0
  203. package/dist/reorder/hx.d.ts +18 -0
  204. package/dist/reorder/hx.d.ts.map +1 -0
  205. package/dist/reorder/hx.js +10 -0
  206. package/dist/reorder/hx.js.map +1 -0
  207. package/dist/reorder/index.d.ts +314 -0
  208. package/dist/reorder/index.d.ts.map +1 -0
  209. package/dist/reorder/index.js +1539 -0
  210. package/dist/reorder/index.js.map +1 -0
  211. package/dist/reorder/model.d.ts +107 -0
  212. package/dist/reorder/model.d.ts.map +1 -0
  213. package/dist/reorder/model.js +162 -0
  214. package/dist/reorder/model.js.map +1 -0
  215. package/dist/richtext/node.d.ts +15 -5
  216. package/dist/richtext/node.d.ts.map +1 -1
  217. package/dist/richtext/node.js +40 -11
  218. package/dist/richtext/node.js.map +1 -1
  219. package/dist/richtext/runs.d.ts +35 -13
  220. package/dist/richtext/runs.d.ts.map +1 -1
  221. package/dist/richtext/runs.js +83 -27
  222. package/dist/richtext/runs.js.map +1 -1
  223. package/dist/tabs/hx.d.ts +18 -0
  224. package/dist/tabs/hx.d.ts.map +1 -0
  225. package/dist/tabs/hx.js +10 -0
  226. package/dist/tabs/hx.js.map +1 -0
  227. package/dist/tabs/index.d.ts +160 -0
  228. package/dist/tabs/index.d.ts.map +1 -0
  229. package/dist/tabs/index.js +1265 -0
  230. package/dist/tabs/index.js.map +1 -0
  231. package/dist/terminal/vt/fonts.d.ts +45 -15
  232. package/dist/terminal/vt/fonts.d.ts.map +1 -1
  233. package/dist/terminal/vt/fonts.js +89 -19
  234. package/dist/terminal/vt/fonts.js.map +1 -1
  235. package/dist/terminal/vt/index.d.ts +1 -1
  236. package/dist/terminal/vt/index.d.ts.map +1 -1
  237. package/dist/terminal/vt/index.js +3 -3
  238. package/dist/terminal/vt/index.js.map +1 -1
  239. package/dist/terminal/vt/node.d.ts +42 -0
  240. package/dist/terminal/vt/node.d.ts.map +1 -1
  241. package/dist/terminal/vt/node.js +95 -11
  242. package/dist/terminal/vt/node.js.map +1 -1
  243. package/dist/terminal/vt/pty.d.ts +16 -0
  244. package/dist/terminal/vt/pty.d.ts.map +1 -1
  245. package/dist/terminal/vt/pty.js +186 -0
  246. package/dist/terminal/vt/pty.js.map +1 -1
  247. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  248. package/dist/terminal/vt/renderer.js +13 -3
  249. package/dist/terminal/vt/renderer.js.map +1 -1
  250. package/package.json +23 -3
  251. package/src/charts/components.ts +14 -7
  252. package/src/charts/node.ts +81 -26
  253. package/src/charts/render.ts +16 -6
  254. package/src/code-editor/node.ts +147 -53
  255. package/src/flow/draw.ts +104 -35
  256. package/src/flow/index.ts +33 -13
  257. package/src/flow/model.ts +4 -2
  258. package/src/flow/node.ts +158 -59
  259. package/src/flow/types.ts +46 -21
  260. package/src/formula/index.ts +4 -2
  261. package/src/formula/node.ts +24 -5
  262. package/src/html/controls.ts +4 -1
  263. package/src/html/css/cascade.ts +18 -7
  264. package/src/html/css/parse.ts +46 -21
  265. package/src/html/css/style.ts +6 -1
  266. package/src/html/css/values.ts +25 -9
  267. package/src/html/index.ts +1 -0
  268. package/src/html/layout/boxes.ts +19 -9
  269. package/src/html/layout/inline.ts +23 -1
  270. package/src/html/node.ts +110 -17
  271. package/src/html/paint.ts +17 -2
  272. package/src/index.ts +142 -0
  273. package/src/maps/clip.ts +184 -0
  274. package/src/maps/gzip.ts +66 -0
  275. package/src/maps/index.ts +316 -0
  276. package/src/maps/labels.ts +442 -0
  277. package/src/maps/mvt.ts +857 -0
  278. package/src/maps/node.ts +1819 -0
  279. package/src/maps/overlay.ts +639 -0
  280. package/src/maps/paint.ts +978 -0
  281. package/src/maps/proj.ts +641 -0
  282. package/src/maps/sources.ts +425 -0
  283. package/src/maps/style.ts +309 -0
  284. package/src/maps/styles.ts +1040 -0
  285. package/src/maps/tiles.ts +676 -0
  286. package/src/maps/types.ts +260 -0
  287. package/src/qml/containers.tsx +380 -0
  288. package/src/qml/controls.tsx +47 -0
  289. package/src/qml/globals.ts +33 -0
  290. package/src/qml/index.ts +67 -0
  291. package/src/qml/interaction.tsx +317 -0
  292. package/src/qml/ir.ts +80 -0
  293. package/src/qml/layouts.tsx +141 -0
  294. package/src/qml/models.ts +101 -0
  295. package/src/qml/objects.ts +1431 -0
  296. package/src/qml/parse.ts +624 -0
  297. package/src/qml/qtquick.tsx +413 -0
  298. package/src/qml/react.tsx +454 -0
  299. package/src/qml/resolver.ts +99 -0
  300. package/src/qml/slots.ts +249 -0
  301. package/src/qml/states.ts +318 -0
  302. package/src/qml/view-utils.ts +140 -0
  303. package/src/reorder/hx.ts +31 -0
  304. package/src/reorder/index.ts +2223 -0
  305. package/src/reorder/model.ts +232 -0
  306. package/src/richtext/node.ts +49 -14
  307. package/src/richtext/runs.ts +115 -30
  308. package/src/tabs/hx.ts +31 -0
  309. package/src/tabs/index.ts +1686 -0
  310. package/src/terminal/vt/fonts.ts +128 -36
  311. package/src/terminal/vt/index.ts +4 -2
  312. package/src/terminal/vt/node.ts +105 -12
  313. package/src/terminal/vt/pty.ts +243 -0
  314. package/src/terminal/vt/renderer.ts +13 -3
@@ -35,7 +35,10 @@ export interface ControlChrome {
35
35
  borderColor: string;
36
36
  }
37
37
 
38
- /** Where a control goes, in the element's own coordinate space. */
38
+ /** Where a control goes, in the element's own coordinate space. Inside the
39
+ * engine (`controlRectsOf`) these are device pixels like every box; what
40
+ * `onControls` reports is the same rect in logical pixels, because it
41
+ * becomes the style of a widget. */
39
42
  export interface ControlRect {
40
43
  element: Element;
41
44
  kind: ReplacedKind;
@@ -219,10 +219,15 @@ export class Cascade {
219
219
  private _pointer: PointerState = NO_POINTER;
220
220
  readonly initial: ComputedStyle;
221
221
  readonly look: RootLook;
222
- /** Viewport width the media queries were evaluated at. */
222
+ /** Viewport width the media queries were evaluated at, in device pixels
223
+ * like every other length here. */
223
224
  viewportWidth: number;
224
225
  viewportHeight: number;
225
- /** Every width at which some `@media` rule changes its mind. */
226
+ /** Device pixels per CSS pixel. Every computed length is device; a
227
+ * `@media` width is the one CSS-pixel comparison left, and it divides. */
228
+ readonly scale: number;
229
+ /** Every width at which some `@media` rule changes its mind, in CSS
230
+ * pixels — the unit the author wrote them in. */
226
231
  readonly breakpoints: number[];
227
232
 
228
233
  constructor(
@@ -230,11 +235,13 @@ export class Cascade {
230
235
  look: RootLook,
231
236
  viewportWidth: number,
232
237
  viewportHeight: number,
238
+ scale = 1,
233
239
  ) {
234
240
  this.look = look;
235
241
  this.initial = initialStyle(look);
236
242
  this.viewportWidth = viewportWidth;
237
243
  this.viewportHeight = viewportHeight;
244
+ this.scale = scale;
238
245
  const breakpoints = new Set<number>();
239
246
  for (const sheet of sheets) {
240
247
  for (const rule of sheet.rules) this._index.add(rule);
@@ -266,12 +273,14 @@ export class Cascade {
266
273
  this._pointer = pointer;
267
274
  }
268
275
 
269
- /** Which media band a width falls in. Two widths in the same band produce
270
- * identical styles, which is what lets a resize skip restyling. */
276
+ /** Which media band a device-pixel width falls in. Two widths in the same
277
+ * band produce identical styles, which is what lets a resize skip
278
+ * restyling. */
271
279
  mediaBand(width: number): number {
280
+ const cssWidth = width / this.scale;
272
281
  let band = 0;
273
282
  for (const bp of this.breakpoints) {
274
- if (width >= bp) band += 1;
283
+ if (cssWidth >= bp) band += 1;
275
284
  else break;
276
285
  }
277
286
  return band;
@@ -299,6 +308,7 @@ export class Cascade {
299
308
  rem: this.initial.fontSize,
300
309
  vw: this.viewportWidth,
301
310
  vh: this.viewportHeight,
311
+ scale: this.scale,
302
312
  };
303
313
  for (const c of candidates) {
304
314
  for (const d of pick(c)) {
@@ -345,13 +355,14 @@ export class Cascade {
345
355
 
346
356
  private _candidates(el: Element): Candidate[] {
347
357
  const out: Candidate[] = [];
348
- const width = this.viewportWidth;
358
+ // A media query's width is CSS pixels; the viewport is kept in device.
359
+ const width = this.viewportWidth / this.scale;
349
360
 
350
361
  const consider = (bucket: IndexedRule[] | undefined): void => {
351
362
  if (!bucket) return;
352
363
  for (const indexed of bucket) {
353
364
  const rule = indexed.rule;
354
- if (!mediaMatches(rule.media, width)) continue;
365
+ if (!mediaMatches(rule.media, width, this.look.colorScheme)) continue;
355
366
  if (!indexed.compiled) {
356
367
  indexed.compiled = true;
357
368
  try {
@@ -51,13 +51,17 @@ export interface Stylesheet {
51
51
  breakpoints: number[];
52
52
  }
53
53
 
54
- /** The width tests this evaluates. Anything else `prefers-color-scheme`,
55
- * `orientation`, `print` — is decided once, at parse time, by `staticPass`. */
54
+ /** The tests this evaluates live: a width, a colour scheme, or both.
55
+ * Anything else — `orientation`, `print`, `prefers-reduced-motion` — is
56
+ * decided once, at parse time, by `staticPass`. */
56
57
  export interface MediaCondition {
57
58
  min?: number;
58
59
  max?: number;
59
- /** Set when the query could not be evaluated as a width test: `true` keeps
60
- * the rule, `false` drops it, and neither depends on the viewport. */
60
+ /** `prefers-color-scheme`, answered from the palette in force. */
61
+ scheme?: 'light' | 'dark';
62
+ /** Set when the query could not be evaluated as a width or scheme test:
63
+ * `true` keeps the rule, `false` drops it, and neither depends on the
64
+ * viewport or the theme. */
61
65
  staticPass?: boolean;
62
66
  }
63
67
 
@@ -403,10 +407,10 @@ function importUrl(prelude: string): string | null {
403
407
  }
404
408
 
405
409
  /**
406
- * A `@media` prelude, reduced to the width tests this can honour. Each comma
407
- * group is one condition and they are OR-ed; within a group, `and` means the
408
- * tests intersect, so a group with a feature this does not understand is
409
- * decided statically by that feature alone.
410
+ * A `@media` prelude, reduced to the width and colour-scheme tests this can
411
+ * honour. Each comma group is one condition and they are OR-ed; within a
412
+ * group, `and` means the tests intersect, so a group with a feature this
413
+ * does not understand is decided statically by that feature alone.
410
414
  */
411
415
  export function parseMediaQuery(prelude: string): MediaCondition[] {
412
416
  const out: MediaCondition[] = [];
@@ -432,14 +436,23 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
432
436
  } else if (key === 'max-width' && px !== null) {
433
437
  condition.max = Math.min(condition.max ?? Infinity, px);
434
438
  sawWidth = true;
435
- } else if (
436
- key === 'prefers-color-scheme' ||
437
- key === 'prefers-reduced-motion'
438
- ) {
439
- // The host decides the palette, so a document's own preference
440
- // query is not this renderer's to answer: the light branch is the
441
- // one whose declarations are least likely to fight the theme.
442
- pass = feature[2].trim().toLowerCase() === 'light';
439
+ } else if (key === 'prefers-color-scheme') {
440
+ // Answered live, from the palette in force: a document dropped
441
+ // into a dark application takes its dark branch, and follows the
442
+ // desktop when that changes. The two schemes are the whole
443
+ // vocabulary; anything else never matches.
444
+ const scheme = feature[2].trim().toLowerCase();
445
+ if (scheme === 'light' || scheme === 'dark') {
446
+ if (condition.scheme && condition.scheme !== scheme) pass = false;
447
+ condition.scheme = scheme;
448
+ } else {
449
+ pass = false;
450
+ }
451
+ } else if (key === 'prefers-reduced-motion') {
452
+ // Nothing here moves, so there is nothing to reduce: the branch an
453
+ // animated page keeps for this preference is not one this
454
+ // renderer needs.
455
+ pass = false;
443
456
  } else if (key === 'orientation') {
444
457
  pass = feature[2].trim().toLowerCase() === 'landscape';
445
458
  }
@@ -450,8 +463,13 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
450
463
  if (type === 'print' || type === 'speech') pass = false;
451
464
  }
452
465
  if (negated) {
453
- // `not` over a width range is not expressible as one range; the honest
454
- // reduction is to decide it statically rather than invert it wrongly.
466
+ // `not` over a scheme is the other scheme. `not` over a width range
467
+ // is not expressible as one range; the honest reduction is to decide
468
+ // it statically rather than invert it wrongly.
469
+ if (!sawWidth && pass && condition.scheme) {
470
+ out.push({ scheme: condition.scheme === 'dark' ? 'light' : 'dark' });
471
+ continue;
472
+ }
455
473
  out.push({ staticPass: !sawWidth && pass ? false : !pass });
456
474
  continue;
457
475
  }
@@ -461,6 +479,7 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
461
479
  }
462
480
  if (
463
481
  !sawWidth &&
482
+ condition.scheme === undefined &&
464
483
  condition.min === undefined &&
465
484
  condition.max === undefined
466
485
  ) {
@@ -472,12 +491,17 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
472
491
  return out.length ? out : [{ staticPass: true }];
473
492
  }
474
493
 
475
- const ZERO_UNITS = { em: 16, rem: 16, vw: 0, vh: 0 };
494
+ /** A `@media` width is compared with the viewport in CSS pixels — the
495
+ * cascade divides the device width by the scale before asking — so the
496
+ * thresholds parse at scale 1 whatever panel the document lands on. */
497
+ const ZERO_UNITS = { em: 16, rem: 16, vw: 0, vh: 0, scale: 1 };
476
498
 
477
- /** Whether a rule's `@media` blocks all hold at this viewport width. */
499
+ /** Whether a rule's `@media` blocks all hold at this viewport width, under
500
+ * this colour scheme. */
478
501
  export function mediaMatches(
479
502
  media: MediaCondition[][] | null,
480
503
  width: number,
504
+ scheme: 'light' | 'dark' = 'light',
481
505
  ): boolean {
482
506
  if (!media) return true;
483
507
  for (const block of media) {
@@ -489,7 +513,8 @@ export function mediaMatches(
489
513
  }
490
514
  if (
491
515
  (c.min === undefined || width >= c.min) &&
492
- (c.max === undefined || width <= c.max)
516
+ (c.max === undefined || width <= c.max) &&
517
+ (c.scheme === undefined || c.scheme === scheme)
493
518
  ) {
494
519
  any = true;
495
520
  }
@@ -214,6 +214,9 @@ export interface RootLook {
214
214
  borderColor: string;
215
215
  mutedColor: string;
216
216
  background: string;
217
+ /** Which scheme the palette is — what `@media (prefers-color-scheme)`
218
+ * is answered from. */
219
+ colorScheme: 'light' | 'dark';
217
220
  /**
218
221
  * The palette's own control chrome, carried here because the **box in the
219
222
  * flow has to be the size the widget will be** and layout runs long before
@@ -1063,7 +1066,9 @@ function alignKeyword(value: string): string | null {
1063
1066
 
1064
1067
  function borderWidth(value: string, ctx: UnitContext): number | null {
1065
1068
  const kw = BORDER_WIDTH_KEYWORDS[value.trim().toLowerCase()];
1066
- if (kw !== undefined) return kw;
1069
+ // The keywords are CSS pixels that never pass through `parseLength`, so
1070
+ // they take the display scale here.
1071
+ if (kw !== undefined) return kw * ctx.scale;
1067
1072
  const len = parseLength(value, ctx);
1068
1073
  return typeof len === 'number' ? Math.max(0, len) : null;
1069
1074
  }
@@ -54,7 +54,18 @@ export function resolveOrNull(len: Len, base: number): number | null {
54
54
  return Number.isFinite(base) ? (len.pct / 100) * base : null;
55
55
  }
56
56
 
57
- /** The font-relative context a length parses against. */
57
+ /**
58
+ * The context a length parses against.
59
+ *
60
+ * Every length that comes out of `parseLength` is in **device pixels** —
61
+ * the unit the box tree, the paint and the hit tests all share. `em`, `rem`,
62
+ * `vw` and `vh` are already device (the root font size and the viewport are
63
+ * handed over that way); the absolute units are CSS pixels the author wrote,
64
+ * and `scale` is how many device pixels one of those is (core's
65
+ * `docs/scale.md`). At 1x it is the identity, which is how a renderer that
66
+ * never multiplied passed every test and drew a `16px` heading eight logical
67
+ * pixels tall on a retina panel.
68
+ */
58
69
  export interface UnitContext {
59
70
  /** The element's own `font-size`, already computed. */
60
71
  em: number;
@@ -63,6 +74,8 @@ export interface UnitContext {
63
74
  /** Viewport, for `vw`/`vh`/`vmin`/`vmax`. */
64
75
  vw: number;
65
76
  vh: number;
77
+ /** Device pixels per CSS pixel — the display scale. */
78
+ scale: number;
66
79
  }
67
80
 
68
81
  const LENGTH_RE =
@@ -89,31 +102,34 @@ export function parseLength(
89
102
  if (!m) return null;
90
103
  const n = Number(m[1]);
91
104
  const unit = m[2];
92
- if (!unit) return bareIsPx ? n : null;
105
+ if (!unit) return bareIsPx ? n * ctx.scale : null;
93
106
  if (unit === '%') return { pct: n };
94
107
  return n * unitScale(unit, ctx);
95
108
  }
96
109
 
110
+ /** Device pixels per one of `unit`. The absolute units are CSS pixels and
111
+ * multiples of them, so they carry the display scale; the relative ones
112
+ * resolve against values that are device already. */
97
113
  function unitScale(unit: string, ctx: UnitContext): number {
98
114
  switch (unit) {
99
115
  case 'px':
100
- return 1;
116
+ return ctx.scale;
101
117
  case 'em':
102
118
  return ctx.em;
103
119
  case 'rem':
104
120
  return ctx.rem;
105
121
  case 'pt':
106
- return 96 / 72;
122
+ return (96 / 72) * ctx.scale;
107
123
  case 'pc':
108
- return 16;
124
+ return 16 * ctx.scale;
109
125
  case 'in':
110
- return 96;
126
+ return 96 * ctx.scale;
111
127
  case 'cm':
112
- return 96 / 2.54;
128
+ return (96 / 2.54) * ctx.scale;
113
129
  case 'mm':
114
- return 96 / 25.4;
130
+ return (96 / 25.4) * ctx.scale;
115
131
  case 'q':
116
- return 96 / 101.6;
132
+ return (96 / 101.6) * ctx.scale;
117
133
  // Approximations rather than font queries: both are within a few percent
118
134
  // for every text face, and asking the font manager here would make the
119
135
  // cascade depend on font loading.
package/src/html/index.ts CHANGED
@@ -181,6 +181,7 @@ function deriveLook(
181
181
  borderColor: String(theme.border ?? '#b2bec3'),
182
182
  mutedColor: String(theme.textMuted ?? '#7f8c8d'),
183
183
  background: String(theme.background ?? 'white'),
184
+ colorScheme: theme.scheme === 'dark' ? 'dark' : 'light',
184
185
  surface: String(theme.surface ?? theme.background ?? 'white'),
185
186
  controlPadY: Number(theme.paddingY ?? 6),
186
187
  controlBorder: Number(theme.borderWidth ?? 1),
@@ -124,9 +124,11 @@ export interface TextLayoutLike {
124
124
  end: number;
125
125
  /** The span as handed in — a richtext `TextRun`, which is how the
126
126
  * decoration a `<span>` carried reaches the paint pass on the same
127
- * object the glyphs did. */
128
- span: TextRun;
129
- run: {
127
+ * object the glyphs did. Optional, with `run`, for the reason
128
+ * `src/richtext/runs.ts` gives: react-x11's Cocoa engine hands back
129
+ * a run's geometry and nothing else. */
130
+ span?: TextRun;
131
+ run?: {
130
132
  font: { metrics(size: number): { ascent: number; descent: number } };
131
133
  size: number;
132
134
  direction?: 'ltr' | 'rtl';
@@ -339,8 +341,13 @@ export interface BoxTree {
339
341
 
340
342
  export interface BuildOptions {
341
343
  cascade: Cascade;
342
- /** Intrinsic size for an image the host has already loaded. `null` when it
343
- * has not: the box takes the attribute size, or a placeholder. */
344
+ /** Device pixels per CSS pixel. An image's pixels and a `width="600"`
345
+ * attribute are CSS pixels; every box is device, so both are multiplied
346
+ * on the way in. Default 1. */
347
+ scale?: number;
348
+ /** Intrinsic size for an image the host has already loaded, in the
349
+ * image's own pixels. `null` when it has not: the box takes the attribute
350
+ * size, or a placeholder. */
344
351
  imageSize(el: Element): { width: number; height: number } | null;
345
352
  /** The size a real widget wants, so the box in the flow is the size the
346
353
  * control will be drawn at. */
@@ -495,17 +502,20 @@ class Builder {
495
502
  else if (style.float !== 'none') box.isFloat = true;
496
503
 
497
504
  if (replaced === 'image') {
505
+ // Both sources are CSS pixels — an image pixel is one, and so is an
506
+ // attribute — and the box is device.
507
+ const scale = this._options.scale ?? 1;
498
508
  const loaded = this._options.imageSize(el);
499
509
  if (loaded) {
500
- box.intrinsicWidth = loaded.width;
501
- box.intrinsicHeight = loaded.height;
510
+ box.intrinsicWidth = loaded.width * scale;
511
+ box.intrinsicHeight = loaded.height * scale;
502
512
  } else {
503
513
  // An image that has not arrived still needs a box, or the document
504
514
  // reflows under the reader when it does. The attributes are the
505
515
  // author telling us the size in advance; without them the box is a
506
516
  // small placeholder rather than nothing.
507
- box.intrinsicWidth = numberAttr(el, 'width') ?? 0;
508
- box.intrinsicHeight = numberAttr(el, 'height') ?? 0;
517
+ box.intrinsicWidth = (numberAttr(el, 'width') ?? 0) * scale;
518
+ box.intrinsicHeight = (numberAttr(el, 'height') ?? 0) * scale;
509
519
  }
510
520
  // The alt text joins the document text, so a document read with the
511
521
  // images blocked still copies as prose.
@@ -317,7 +317,14 @@ export function layoutInline(block: Box, options: InlineOptions): InlineResult {
317
317
  open.x += first.width;
318
318
  open.left = band.left;
319
319
 
320
- if (!fragment.truncated) {
320
+ // Did the segment wrap? ntk answers with `truncated`. A layout that does
321
+ // not carry the flag — react-x11's Cocoa engine reports none — is asked
322
+ // the same of its line ends instead: it wrapped if anything but
323
+ // whitespace follows the first line. Read as "fitted", a cut fragment
324
+ // advanced past the whole segment, and a paragraph beside a float lost
325
+ // every line after its first.
326
+ const wrapped = fragment.truncated ?? inkBeyond(segment.runs, first.end);
327
+ if (!wrapped) {
321
328
  // It fitted: the cursor stays on this line for whatever comes next.
322
329
  //
323
330
  // With one correction first. ntk strips a line's trailing whitespace —
@@ -805,6 +812,21 @@ function spaceAdvance(fonts: FontsLike, run: TextRun): number {
805
812
  return advance;
806
813
  }
807
814
 
815
+ /** Whether anything but whitespace lies past a code-unit offset into the
816
+ * runs' joined text — "did `maxLines` drop content", for a layout that
817
+ * does not say. */
818
+ function inkBeyond(runs: TextRun[], offset: number): boolean {
819
+ let at = 0;
820
+ for (const run of runs) {
821
+ const next = at + run.text.length;
822
+ if (next > offset && /\S/.test(run.text.slice(Math.max(0, offset - at)))) {
823
+ return true;
824
+ }
825
+ at = next;
826
+ }
827
+ return false;
828
+ }
829
+
808
830
  // --- style questions --------------------------------------------------------
809
831
 
810
832
  function wraps(style: ComputedStyle): boolean {
package/src/html/node.ts CHANGED
@@ -101,7 +101,8 @@ export interface HtmlViewProps {
101
101
  request: ResourceRequest,
102
102
  ) => Promise<ResourceResult | null> | ResourceResult | null;
103
103
  onScript?: (script: ScriptRequest) => void;
104
- /** Where the real widgets go, in the element's own coordinates. */
104
+ /** Where the real widgets go, in the element's own coordinates and in
105
+ * logical pixels — the unit the style that mounts each one is in. */
105
106
  onControls?: (rects: ControlRect[]) => void;
106
107
  /** The parsed document, once per parse — the DOM handle. */
107
108
  onDocument?: (document: Document) => void;
@@ -168,6 +169,70 @@ export class HtmlViewNode extends Node {
168
169
  return this.props as unknown as HtmlViewProps;
169
170
  }
170
171
 
172
+ // --- units ----------------------------------------------------------------
173
+ //
174
+ // The whole pipeline — the cascade's lengths, the box tree, the paint, the
175
+ // walks the accessors run — works in **device pixels**, the unit `abs`,
176
+ // `contentBox()`, the paint context and `paintDamage()` arrive in (core's
177
+ // `docs/scale.md`). Three things cross into that unit and each converts
178
+ // once, here:
179
+ //
180
+ // - a CSS pixel: every `px` the author wrote, the UA sheet's, an image's
181
+ // own pixels, a `width="600"` attribute, the theme's font size and
182
+ // control chrome in `look`. `UnitContext.scale` and `_deviceLook()`
183
+ // multiply them on the way in;
184
+ // - a synthetic event's `x`/`y`, and so the public point queries
185
+ // (`elementAtPoint`, `hrefAtPoint`, `setHover`): logical, multiplied in
186
+ // `_toDocument`. The selection seam (`textIndexAt` and the two rect
187
+ // accessors) is core's device-pixel contract and stays device;
188
+ // - a control rect on its way out to `onControls`: divided, because it
189
+ // becomes a style.
190
+ //
191
+ // At 1x every conversion is the identity, which is how a view that
192
+ // compared `ev.x` with `abs` and laid `16px` out as sixteen device pixels
193
+ // passed every test and then hovered the wrong element and drew the
194
+ // document at half size on a retina panel.
195
+
196
+ /** Device pixels per logical pixel — the display scale this element's
197
+ * window resolved to, constant for the node's life. */
198
+ private get _scale(): number {
199
+ return this.scale > 0 ? this.scale : 1;
200
+ }
201
+
202
+ private _deviceLookFor: RootLook | null = null;
203
+ private _deviceLookAt = 0;
204
+ private _deviceLookValue: RootLook | null = null;
205
+
206
+ /** The look with its lengths on the device grid: the theme's font size and
207
+ * the control chrome are logical pixels, and the cascade's initial style
208
+ * and `measureControl` want device ones. Memoized on the look's identity
209
+ * so the cascade and the control measurer see one object. */
210
+ private _deviceLook(): RootLook {
211
+ const look = this._props().look;
212
+ const s = this._scale;
213
+ if (
214
+ this._deviceLookValue &&
215
+ this._deviceLookFor === look &&
216
+ this._deviceLookAt === s
217
+ ) {
218
+ return this._deviceLookValue;
219
+ }
220
+ const scaled =
221
+ s === 1
222
+ ? look
223
+ : {
224
+ ...look,
225
+ fontSize: look.fontSize * s,
226
+ controlPadY: look.controlPadY * s,
227
+ controlBorder: look.controlBorder * s,
228
+ controlRadius: look.controlRadius * s,
229
+ };
230
+ this._deviceLookFor = look;
231
+ this._deviceLookAt = s;
232
+ this._deviceLookValue = scaled;
233
+ return scaled;
234
+ }
235
+
171
236
  // --- the pipeline ---------------------------------------------------------
172
237
 
173
238
  private _invalidate(stale: Stale): void {
@@ -232,7 +297,8 @@ export class HtmlViewNode extends Node {
232
297
  /** Rebuild the cascade — the document's sheets plus the host's. */
233
298
  private _restyle(width: number): void {
234
299
  const props = this._props();
235
- const sheets: Stylesheet[] = [uaStylesheet(props.look)];
300
+ const look = this._deviceLook();
301
+ const sheets: Stylesheet[] = [uaStylesheet(look)];
236
302
  let order = 0;
237
303
  for (const ref of this._source.facts().sheets) {
238
304
  const text =
@@ -267,9 +333,10 @@ export class HtmlViewNode extends Node {
267
333
  }
268
334
  this._cascade = new Cascade(
269
335
  sheets,
270
- props.look,
336
+ look,
271
337
  width,
272
338
  this._viewportHeight(),
339
+ this._scale,
273
340
  );
274
341
  this._cascade.setPointer({
275
342
  hovered: new Set(this._hovered),
@@ -321,12 +388,13 @@ export class HtmlViewNode extends Node {
321
388
  cascade.viewportHeight = this._viewportHeight();
322
389
 
323
390
  if (this._stale >= Stale.Boxes || !this._tree) {
324
- const props = this._props();
391
+ const look = this._deviceLook();
325
392
  this._tree = buildBoxes(this._source.document, {
326
393
  cascade,
394
+ scale: this._scale,
327
395
  imageSize: (el) => this._resources.imageSize(attr(el, 'src') ?? ''),
328
396
  controlSize: (el, kind, style) =>
329
- measureControl(el, kind, style, this._fonts(), props.look),
397
+ measureControl(el, kind, style, this._fonts(), look),
330
398
  });
331
399
  this._textPoints = null;
332
400
  this._pointsAreUnits = null;
@@ -355,7 +423,20 @@ export class HtmlViewNode extends Node {
355
423
  const tree = this._tree;
356
424
  const report = this._props().onControls;
357
425
  if (!tree || !report) return;
358
- const rects = controlRectsOf(tree);
426
+ // The boxes are device pixels; each rect becomes the style of a widget
427
+ // mounted beside this element, and a style is logical.
428
+ const s = this._scale;
429
+ const rects = controlRectsOf(tree).map((r) =>
430
+ s === 1
431
+ ? r
432
+ : {
433
+ ...r,
434
+ x: r.x / s,
435
+ y: r.y / s,
436
+ width: r.width / s,
437
+ height: r.height / s,
438
+ },
439
+ );
359
440
  if (sameRects(rects, this._controls)) return;
360
441
  this._controls = rects;
361
442
  report(rects);
@@ -463,7 +544,9 @@ export class HtmlViewNode extends Node {
463
544
  override textIndexAt(x: number, y: number): number {
464
545
  const tree = this._tree;
465
546
  if (!tree) return 0;
466
- const local = this._toDocument(x, y);
547
+ // Core's contract: a device-pixel point, the same space the two rect
548
+ // accessors below answer in.
549
+ const local = { x: x - this.abs.x, y: y - this.abs.y };
467
550
  const hit = nearestText(tree.root, local.x, local.y);
468
551
  if (!hit) return 0;
469
552
  return this._toPoints(hit);
@@ -495,10 +578,15 @@ export class HtmlViewNode extends Node {
495
578
  }
496
579
 
497
580
  // --- pointer --------------------------------------------------------------
498
-
499
- /** The link under a point, if any. Not part of the selection seam: core
500
- * deliberately left hover and `cursorAt` out of #291, so following a link
501
- * stays this package's. */
581
+ //
582
+ // The three point queries take **logical** window pixels what a synthetic
583
+ // event's `x`/`y` carry and what `useLinkClicks` and an application's
584
+ // handler hand over — and are the one place the two units meet on the way
585
+ // in. `_toDocument` multiplies.
586
+
587
+ /** The link under a logical window point, if any. Not part of the
588
+ * selection seam: core deliberately left hover and `cursorAt` out of
589
+ * #291, so following a link stays this package's. */
502
590
  hrefAtPoint(x: number, y: number): string | null {
503
591
  const el = this.elementAtPoint(x, y);
504
592
  let node: Element | null = el;
@@ -510,7 +598,7 @@ export class HtmlViewNode extends Node {
510
598
  return null;
511
599
  }
512
600
 
513
- /** The deepest element whose box contains a window-space point. */
601
+ /** The deepest element whose box contains a logical window point. */
514
602
  elementAtPoint(x: number, y: number): Element | null {
515
603
  const tree = this._tree;
516
604
  if (!tree) return null;
@@ -519,9 +607,10 @@ export class HtmlViewNode extends Node {
519
607
  }
520
608
 
521
609
  /**
522
- * The pointer moved. Returns true when the cascade's answer could have
523
- * changed, so the caller knows whether to invalidate — which it only ever
524
- * does for a document that actually contains a `:hover` rule.
610
+ * The pointer moved, to a logical window point. Returns true when the
611
+ * cascade's answer could have changed, so the caller knows whether to
612
+ * invalidate — which it only ever does for a document that actually
613
+ * contains a `:hover` rule.
525
614
  */
526
615
  setHover(x: number, y: number): boolean {
527
616
  const cascade = this._cascade;
@@ -570,8 +659,10 @@ export class HtmlViewNode extends Node {
570
659
  return this._source.facts().title;
571
660
  }
572
661
 
662
+ /** A logical window point in document (device) coordinates. */
573
663
  private _toDocument(x: number, y: number): { x: number; y: number } {
574
- return { x: x - this.abs.x, y: y - this.abs.y };
664
+ const s = this._scale;
665
+ return { x: x * s - this.abs.x, y: y * s - this.abs.y };
575
666
  }
576
667
 
577
668
  // --- pointer defaults -----------------------------------------------------
@@ -618,6 +709,7 @@ export class HtmlViewNode extends Node {
618
709
  paintDocument(ctx as PaintContext, tree, {
619
710
  originX: this.abs.x,
620
711
  originY: this.abs.y,
712
+ scale: this._scale,
621
713
  damage,
622
714
  selection: range
623
715
  ? { start: this._toUnits(range.start), end: this._toUnits(range.end) }
@@ -951,7 +1043,8 @@ function deepestAt(box: Box, x: number, y: number): Element | null {
951
1043
  for (const run of natural.runs) {
952
1044
  const left = text.drawX + natural.x + run.x;
953
1045
  if (x >= left && x < left + run.width) {
954
- const owner = (run.span as { element?: Element }).element;
1046
+ const owner = (run.span as { element?: Element } | undefined)
1047
+ ?.element;
955
1048
  if (owner) found = owner;
956
1049
  }
957
1050
  }