ranger-compiler 3.5.1 → 3.5.2

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 (338) hide show
  1. package/CHANGELOG.md +1064 -20
  2. package/LICENSE +3 -1
  3. package/README.md +123 -28
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79923 -56921
  7. package/dist/lib/JSON.rgr +102 -91
  8. package/dist/lib/Shell.rgr +4 -4
  9. package/dist/lib/apple/AppleToolchain.rgr +4 -4
  10. package/dist/lib/apple/README.md +5 -4
  11. package/dist/lib/apple/apple_test.rgr +1 -1
  12. package/dist/lib/core/README.md +1 -1
  13. package/dist/lib/evg/EVG.rgr +12 -0
  14. package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
  15. package/dist/lib/evg/EVGA11yTree.rgr +894 -0
  16. package/dist/lib/evg/EVGBox.rgr +267 -0
  17. package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
  18. package/dist/lib/evg/EVGCodepoint.rgr +316 -0
  19. package/dist/lib/evg/EVGColor.rgr +700 -0
  20. package/dist/lib/evg/EVGCommands.rgr +177 -0
  21. package/dist/lib/evg/EVGComponent.rgr +331 -0
  22. package/dist/lib/evg/EVGComponentTest.rgr +387 -0
  23. package/dist/lib/evg/EVGConnector.rgr +541 -0
  24. package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
  25. package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
  26. package/dist/lib/evg/EVGEasing.rgr +370 -0
  27. package/dist/lib/evg/EVGEffectTest.rgr +253 -0
  28. package/dist/lib/evg/EVGElement.rgr +4914 -0
  29. package/dist/lib/evg/EVGFixedTest.rgr +289 -0
  30. package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
  31. package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
  32. package/dist/lib/evg/EVGFling.rgr +244 -0
  33. package/dist/lib/evg/EVGFocus.rgr +400 -0
  34. package/dist/lib/evg/EVGFocusTest.rgr +399 -0
  35. package/dist/lib/evg/EVGGradient.rgr +322 -0
  36. package/dist/lib/evg/EVGGrapheme.rgr +190 -0
  37. package/dist/lib/evg/EVGGrid.rgr +975 -0
  38. package/dist/lib/evg/EVGHitTest.rgr +208 -0
  39. package/dist/lib/evg/EVGHoles.rgr +294 -0
  40. package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
  41. package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
  42. package/dist/lib/evg/EVGHostTree.rgr +834 -0
  43. package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
  44. package/dist/lib/evg/EVGImageDecode.rgr +117 -0
  45. package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
  46. package/dist/lib/evg/EVGInspect.rgr +883 -0
  47. package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
  48. package/dist/lib/evg/EVGJsonTest.rgr +535 -0
  49. package/dist/lib/evg/EVGLayout.rgr +4220 -0
  50. package/dist/lib/evg/EVGMeasure.rgr +1267 -0
  51. package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
  52. package/dist/lib/evg/EVGPatch.rgr +1721 -0
  53. package/dist/lib/evg/EVGPatchTest.rgr +692 -0
  54. package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
  55. package/dist/lib/evg/EVGReconcile.rgr +226 -0
  56. package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
  57. package/dist/lib/evg/EVGReject.rgr +126 -0
  58. package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
  59. package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
  60. package/dist/lib/evg/EVGRuler.rgr +232 -0
  61. package/dist/lib/evg/EVGRulerTest.rgr +172 -0
  62. package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
  63. package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
  64. package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
  65. package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
  66. package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
  67. package/dist/lib/evg/EVGText.rgr +105 -0
  68. package/dist/lib/evg/EVGTextEngine.rgr +536 -0
  69. package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
  70. package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
  71. package/dist/lib/evg/EVGToolbar.rgr +1369 -0
  72. package/dist/lib/evg/EVGTransition.rgr +728 -0
  73. package/dist/lib/evg/EVGTreeJson.rgr +721 -0
  74. package/dist/lib/evg/EVGUnit.rgr +554 -0
  75. package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
  76. package/dist/lib/evg/EvgApp.rgr +189 -0
  77. package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
  78. package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
  79. package/dist/lib/evg/EvgHost.rgr +451 -0
  80. package/dist/lib/evg/EvgTest.rgr +91 -0
  81. package/dist/lib/evg/EvgTraceColor.rgr +236 -0
  82. package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
  83. package/dist/lib/evg/EvgTraceFit.rgr +767 -0
  84. package/dist/lib/evg/EvgTracePath.rgr +435 -0
  85. package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
  86. package/dist/lib/evg/EvgViewport.rgr +313 -0
  87. package/dist/lib/evg/FxDemoDoc.rgr +185 -0
  88. package/dist/lib/evg/HOSTS.md +227 -0
  89. package/dist/lib/evg/ISSUES.md +835 -0
  90. package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
  91. package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
  92. package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
  93. package/dist/lib/evg/PLAN_EVG.md +518 -0
  94. package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
  95. package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
  96. package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
  97. package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
  98. package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
  99. package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
  100. package/dist/lib/evg/PathBuilder.rgr +333 -0
  101. package/dist/lib/evg/README.md +1236 -0
  102. package/dist/lib/evg/SPEC.md +1252 -0
  103. package/dist/lib/evg/SVGPathParser.rgr +1199 -0
  104. package/dist/lib/evg/SvgParser.rgr +1736 -0
  105. package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
  106. package/dist/lib/evg/VectorShapes.rgr +331 -0
  107. package/dist/lib/evg/VectorStroke.rgr +107 -0
  108. package/dist/lib/evg/VectorViewBox.rgr +380 -0
  109. package/dist/lib/evg/agent/README.md +290 -0
  110. package/dist/lib/evg/agent/evg_agent.rgr +899 -0
  111. package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
  112. package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
  113. package/dist/lib/evg/agent/fixtures/connector.css +63 -0
  114. package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
  115. package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
  116. package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
  117. package/dist/lib/evg/agent/fixtures/popover.css +99 -0
  118. package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
  119. package/dist/lib/evg/agent/roundtrip.sh +74 -0
  120. package/dist/lib/evg/agent/smoke.sh +230 -0
  121. package/dist/lib/evg/android/README.md +97 -0
  122. package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
  123. package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
  124. package/dist/lib/evg/android/androidstubs/App.kt +30 -0
  125. package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
  126. package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
  127. package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
  128. package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
  129. package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
  130. package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
  131. package/dist/lib/evg/android/androidstubs/View.kt +113 -0
  132. package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
  133. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
  134. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
  135. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
  136. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
  137. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
  138. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
  139. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
  140. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
  141. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
  142. package/dist/lib/evg/apple/README.md +128 -0
  143. package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
  144. package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
  145. package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
  146. package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
  147. package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
  148. package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
  149. package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
  150. package/dist/lib/evg/bench/README.md +64 -0
  151. package/dist/lib/evg/bench/layout-bench.mjs +316 -0
  152. package/dist/lib/evg/bench/layout-cases.mjs +510 -0
  153. package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
  154. package/dist/lib/evg/bin/.gitignore +17 -0
  155. package/dist/lib/evg/evg_test.rgr +313 -0
  156. package/dist/lib/evg/gl/README.md +198 -0
  157. package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
  158. package/dist/lib/evg/gl/blur-check.mjs +399 -0
  159. package/dist/lib/evg/gl/boxmodel.json +1 -0
  160. package/dist/lib/evg/gl/demo.html +46 -0
  161. package/dist/lib/evg/gl/effect-presets.css +285 -0
  162. package/dist/lib/evg/gl/effect-presets.js +63 -0
  163. package/dist/lib/evg/gl/effect-shots.mjs +135 -0
  164. package/dist/lib/evg/gl/evg-a11y.js +566 -0
  165. package/dist/lib/evg/gl/evg-binary.js +160 -0
  166. package/dist/lib/evg/gl/evg-engine.js +296 -0
  167. package/dist/lib/evg/gl/evg-fx.js +237 -0
  168. package/dist/lib/evg/gl/evg-gestures.js +209 -0
  169. package/dist/lib/evg/gl/evg-list.js +167 -0
  170. package/dist/lib/evg/gl/evg-measure.js +186 -0
  171. package/dist/lib/evg/gl/evg-textinput.js +303 -0
  172. package/dist/lib/evg/gl/evg-view.js +144 -0
  173. package/dist/lib/evg/gl/evg-webgl.js +3942 -0
  174. package/dist/lib/evg/gl/fx-check.mjs +998 -0
  175. package/dist/lib/evg/gl/fx-demo.html +166 -0
  176. package/dist/lib/evg/gl/fx-demo.js +2 -0
  177. package/dist/lib/evg/gl/fx-serve.mjs +47 -0
  178. package/dist/lib/evg/gl/gestures-check.mjs +189 -0
  179. package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
  180. package/dist/lib/evg/gl/measure-check.mjs +135 -0
  181. package/dist/lib/evg/gl/rotation-check.mjs +207 -0
  182. package/dist/lib/evg/gl/shift-check.mjs +113 -0
  183. package/dist/lib/evg/gl/stroke-check.mjs +168 -0
  184. package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
  185. package/dist/lib/evg/gl/view-check.mjs +343 -0
  186. package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
  187. package/dist/lib/evg/html/evg-dom.js +318 -0
  188. package/dist/lib/evg/html/evg-html.js +600 -0
  189. package/dist/lib/evg/inspect/README.md +415 -0
  190. package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
  191. package/dist/lib/evg/inspect/evg-inspect.js +947 -0
  192. package/dist/lib/evg/inspect/shots/css.png +0 -0
  193. package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
  194. package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
  195. package/dist/lib/evg/inspect/shots/state.png +0 -0
  196. package/dist/lib/evg/inspect/shots.mjs +226 -0
  197. package/dist/lib/evg/oracle/css-blur.json +576 -0
  198. package/dist/lib/evg/oracle/css-box.json +157 -0
  199. package/dist/lib/evg/oracle/css-timing.json +709 -0
  200. package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
  201. package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
  202. package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
  203. package/dist/lib/evg/original/EVGColor.clj +310 -0
  204. package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
  205. package/dist/lib/evg/original/SVGPath.rgr +627 -0
  206. package/dist/lib/evg/original/Vec2.crgr +103 -0
  207. package/dist/lib/evg/ranger.json +9 -0
  208. package/dist/lib/evg/showcase/README.md +246 -0
  209. package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
  210. package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
  211. package/dist/lib/evg/showcase/build.mjs +651 -0
  212. package/dist/lib/evg/showcase/pages/album.tsx +30 -0
  213. package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
  214. package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
  215. package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
  216. package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
  217. package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
  218. package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
  219. package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
  220. package/dist/lib/evg/showcase/pages/more.tsx +332 -0
  221. package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
  222. package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
  223. package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
  224. package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
  225. package/dist/lib/evg/showcase/pages/units.tsx +35 -0
  226. package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
  227. package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
  228. package/dist/lib/evg/showcase/pages/views.tsx +223 -0
  229. package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
  230. package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
  231. package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
  232. package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
  233. package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
  234. package/dist/lib/evg/showcase/themes/more-default.css +128 -0
  235. package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
  236. package/dist/lib/evg/showcase/themes/showcase.css +508 -0
  237. package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
  238. package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
  239. package/dist/lib/evg/showcase/themes/views-default.css +15 -0
  240. package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
  241. package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
  242. package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
  243. package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
  244. package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
  245. package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
  246. package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
  247. package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
  248. package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
  249. package/dist/lib/evg/web/responsive/README.md +97 -0
  250. package/dist/lib/evg/web/responsive/build.mjs +90 -0
  251. package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
  252. package/dist/lib/evg/web/responsive/index.html +177 -0
  253. package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
  254. package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
  255. package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
  256. package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
  257. package/dist/lib/evg/web/tools/minify.mjs +60 -0
  258. package/dist/lib/evg/web/tracer/build.mjs +87 -0
  259. package/dist/lib/evg/web/tracer/index.html +2805 -0
  260. package/dist/lib/evg/web/tracer/sample.png +0 -0
  261. package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
  262. package/dist/lib/evgr/Cargo.lock +21 -0
  263. package/dist/lib/evgr/Cargo.toml +19 -0
  264. package/dist/lib/evgr/README.md +140 -0
  265. package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
  266. package/dist/lib/evgr/bench/compare.mjs +252 -0
  267. package/dist/lib/evgr/bench/speed.mjs +277 -0
  268. package/dist/lib/evgr/bin/.gitignore +3 -0
  269. package/dist/lib/evgr/src/bin/bench.rs +106 -0
  270. package/dist/lib/evgr/src/bin/smoke.rs +13 -0
  271. package/dist/lib/evgr/src/grid.rs +730 -0
  272. package/dist/lib/evgr/src/lib.rs +1003 -0
  273. package/dist/lib/evgr/src/style.rs +468 -0
  274. package/dist/lib/evgr/src/text.rs +83 -0
  275. package/dist/lib/image/BitReader.rgr +171 -0
  276. package/dist/lib/image/Buffer.rgr +173 -0
  277. package/dist/lib/image/DCT.rgr +283 -0
  278. package/dist/lib/image/Deflate.rgr +339 -0
  279. package/dist/lib/image/HuffmanDecoder.rgr +181 -0
  280. package/dist/lib/image/ImageBuffer.rgr +706 -0
  281. package/dist/lib/image/JPEGDecoder.rgr +808 -0
  282. package/dist/lib/image/PNGDecoder.rgr +544 -0
  283. package/dist/lib/image/PNGEncoder.rgr +388 -0
  284. package/dist/lib/image/PPMImage.rgr +224 -0
  285. package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
  286. package/dist/lib/image/README.md +33 -0
  287. package/dist/lib/image/RasterBuffer.rgr +294 -0
  288. package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
  289. package/dist/lib/image/WebPDecoder.rgr +433 -0
  290. package/dist/lib/image/WebPLossless.rgr +1035 -0
  291. package/dist/lib/image/WebPLossy.rgr +1929 -0
  292. package/dist/lib/image/ranger.json +9 -0
  293. package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
  294. package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
  295. package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
  296. package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
  297. package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
  298. package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
  299. package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
  300. package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
  301. package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
  302. package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
  303. package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
  304. package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
  305. package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
  306. package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
  307. package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
  308. package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
  309. package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
  310. package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
  311. package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
  312. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
  313. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
  314. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
  315. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
  316. package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
  317. package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
  318. package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
  319. package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
  320. package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
  321. package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
  322. package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
  323. package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
  324. package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
  325. package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
  326. package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
  327. package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
  328. package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
  329. package/dist/lib/image/tests/run_webp_tests.sh +39 -0
  330. package/dist/lib/rust/RsJson.rgr +468 -0
  331. package/dist/lib/rust/RsPrelude.rgr +1663 -0
  332. package/dist/lib/shell_test.rgr +7 -7
  333. package/dist/lib/stdlib.rgr +21 -5
  334. package/dist/lib/zip/ranger.json +6 -0
  335. package/dist/rgrc.js +87318 -67610
  336. package/package.json +1010 -1040
  337. package/dist/README.md +0 -117
  338. package/dist/package.json +0 -47
@@ -0,0 +1,650 @@
1
+ # Screen readers for an EVG program on the GPU
2
+
3
+ How a WebGL or SDL2+OpenGL program built out of EVG could be made usable by
4
+ NVDA, JAWS, VoiceOver and Orca — and why the answer is a second list beside the
5
+ display list rather than anything in the renderer.
6
+
7
+ Status: **phases 1–3 are built and tested** in the browser
8
+ ([§12](#12-what-is-built)); the **macOS native bridge is written but has not run
9
+ on a Mac** ([§13](#13-the-native-host-macos)). The declarative side (§4) and the
10
+ other two desktop platforms are still design.
11
+
12
+ ---
13
+
14
+ ## 1. Why a GPU frame is invisible
15
+
16
+ A screen reader does not look at pixels. It asks the platform for a *tree* —
17
+ nodes with a role, a name, a value, a state and a rectangle — and it walks that
18
+ tree with the arrow keys, drives it with the Tab key, mirrors it onto a braille
19
+ display and reads out what changed. Windows calls that tree UI Automation, macOS
20
+ calls it NSAccessibility, Linux calls it AT-SPI2, and a browser builds it out of
21
+ the DOM.
22
+
23
+ Everything under `lib/evg/gl/` deliberately throws that away. `EVGDisplayList`
24
+ emits, in its own words, commands that are "absolute pixels, resolved colours,
25
+ no tree, no units", and `evg-webgl.js` turns them into instanced quads with a
26
+ signed distance function. By the time a button exists on screen it is a rounded
27
+ rect and a glyph run, and `sdRoundedBox` has no opinion about whether that is a
28
+ button, a cell, a tab or a decorative divider.
29
+
30
+ So a `<canvas>` running the DataGrid is, to NVDA, one empty graphic. The SDL2
31
+ host is worse: an OpenGL window with no accessible children at all. This is the
32
+ same wall Google Sheets, Figma, Flutter-on-canvas and every game UI hit. Nobody
33
+ has solved it by making the renderer smarter. Everybody has solved it — when
34
+ they solved it — by publishing a parallel semantic tree.
35
+
36
+ ## 2. Two dead ends, named so they are not tried
37
+
38
+ **Inferring semantics from the display list.** Tempting, because the list is
39
+ already flat, already positioned, and already crosses the seam as JSON. It is
40
+ OCR against your own program: a rect with a border and a centred run of text is
41
+ a button, unless it is a tab, or a cell, or a chart legend swatch. The list is
42
+ lossy *on purpose* — that is what made it portable — and guessing back the part
43
+ that was discarded produces a tree that is wrong in exactly the cases that
44
+ matter (state, grouping, order, and what is decoration).
45
+
46
+ **`role="application"` plus an `aria-live` region.** Cheap: one div, one string,
47
+ announce what changed. It is also a lecture, not a user interface. The reader
48
+ gets no exploration, no braille cursor, no touch, no "what is next to this", no
49
+ list of headings — and every AT user's first move, reading the screen
50
+ top-to-bottom with the review cursor, returns nothing. Live regions are a
51
+ supplement for events that have no anchor ("Recalculated 1,200 cells"), not a
52
+ substitute for a tree.
53
+
54
+ ## 3. The seam, restated: walk once, emit two lists
55
+
56
+ `EVGDisplayList` exists because five painters each walked the laid-out tree
57
+ themselves, and that is how border-radius came to work in PDF and silently not
58
+ in PNG — one painter read `box.borderRadius`, another read a stale
59
+ `el.borderRadius` that nothing wrote. An accessibility tree written as a *second*
60
+ independent walk would repeat that mistake with worse symptoms: the drift would
61
+ be silent to everyone who can see the screen, and only a blind user would ever
62
+ hit it.
63
+
64
+ So the same rule applies. One pass over the same state produces two outputs:
65
+
66
+ ```
67
+ ┌─► EVGDisplayList (geometry) ─► WebGL / SDL2+GL / PDF / PNG
68
+ app state ──► walk ──┤
69
+ └─► EVGA11yTree (meaning) ─► DOM mirror / UIA / AT-SPI / AX
70
+ ```
71
+
72
+ Same coordinates, same build call, same generation number. If a node has no
73
+ matching geometry, or geometry has no node and is not marked decoration, that
74
+ is a bug the builder can *detect* — which is the second reason to keep them
75
+ together.
76
+
77
+ ## 4. Where the meaning actually lives today
78
+
79
+ EVG has two distinct program shapes, and they need different answers. This is
80
+ the part that decides the whole design.
81
+
82
+ ### Declarative EVG — there is a tree
83
+
84
+ `EVGDisplayList.build(root:EVGElement)` walks a laid-out `EVGElement` tree: the
85
+ showcase pages, the PDF documents, anything coming through
86
+ `jsx/JSXToEVG.rgr`. The tree already carries `tagName`, `id`, `className`,
87
+ `textContent`, and `alt` on images (`JSXToEVG.rgr:593`). It carries almost no
88
+ *intent*: nothing says focusable, nothing says button, nothing says this
89
+ `<div>` is a group of radio buttons and that one is a shadow.
90
+
91
+ What is missing is a handful of fields on `EVGElement` and their pass-through in
92
+ `JSXToEVG`:
93
+
94
+ ```
95
+ role aria-label aria-describedby aria-hidden
96
+ tabIndex aria-live aria-level aria-expanded / -checked / -selected
97
+ ```
98
+
99
+ Plus sane defaults, so an unannotated page is not silent: `<span>` with text is
100
+ static text, `<img alt="…">` is an image with that name, `<img>` with no `alt`
101
+ is decoration, a `<Button>` component is a button. Defaults are what make the
102
+ first 80% arrive for free; explicit attributes are what make the last 20%
103
+ correct.
104
+
105
+ ### Immediate-mode EVG — there is no tree, and that is fine
106
+
107
+ `GridView.buildDisplayList` (`gallery/datagrid/src/GridView.rgr:1204`) and
108
+ `EVGWindow.paint` build a display list *directly* from a domain model. There is
109
+ no element tree to annotate and there should not be one — the model is richer
110
+ than a tree of divs ever was.
111
+
112
+ And the model already knows nearly everything an AT wants:
113
+
114
+ | Already in the repo | What it is, in accessibility terms |
115
+ | --- | --- |
116
+ | `EVGControlKind` — label, button, radio, checkbox, input, separator, swatch, tool, content | a role enum, already written |
117
+ | `EVGControl.text` / `.value` | accessible name / value |
118
+ | `EVGControl.checked` / `.enabled` / `.isDefault` | states |
119
+ | `EVGControl.x/y/w/h`, `contains()` | bounds and hit test |
120
+ | `EVGWindow.focusedId`, `focusNext()` | a focus model with a single owner |
121
+ | `EVGWindow.controls` order | reading order and Tab order |
122
+ | `SpreadsheetModel.cellLabel(row col)` | "B7" — the name of a gridcell |
123
+ | `GridSelection.active` / `liveRange()` | focused cell, selected range |
124
+ | `DataGrid` visible window + model row/col counts | virtualization indices |
125
+ | `@process` `markStateDirty()` / scene generation (`?seen=N`, 204) | the change signal an AT event needs |
126
+
127
+ The immediate-mode side is *further along* than the declarative side. Its
128
+ problem is not that the information is missing; it is that nothing publishes it.
129
+ `EVGWindow` could emit an accessibility node per control in about the same
130
+ number of lines it takes to paint one.
131
+
132
+ ## 5. The model: `EVGA11yTree` in Ranger
133
+
134
+ A new pure module beside `EVGDisplayList.rgr` — no device resources, no host
135
+ calls — so it compiles to ES6, C++, Rust and Go exactly like the display list
136
+ does. That is the specifically *Ranger* part of this: the accessibility model is
137
+ written once and is then available to the browser host, the SDL2 host, and any
138
+ future host, instead of being re-implemented per platform the way it normally is.
139
+
140
+ The field set is chosen as the intersection of ARIA, UIA, AT-SPI,
141
+ NSAccessibility and AccessKit — everything below exists in all five:
142
+
143
+ ```ranger
144
+ class EVGA11yRole {
145
+ sfn none:int () { return 0 } ; decoration; never surfaced
146
+ sfn group:int () { return 1 }
147
+ sfn text:int () { return 2 }
148
+ sfn button:int () { return 3 }
149
+ sfn checkbox:int () { return 4 }
150
+ sfn radio:int () { return 5 }
151
+ sfn textField:int () { return 6 }
152
+ sfn image:int () { return 7 }
153
+ sfn grid:int () { return 8 }
154
+ sfn row:int () { return 9 }
155
+ sfn cell:int () { return 10 }
156
+ sfn tab:int () { return 11 }
157
+ sfn menuItem:int () { return 12 }
158
+ sfn dialog:int () { return 13 }
159
+ sfn status:int () { return 14 }
160
+ }
161
+
162
+ class EVGA11yNode {
163
+ def id:string "" ; STABLE across frames — see §8
164
+ def parentId:string ""
165
+ def role:int 0
166
+ def name:string "" ; what gets read
167
+ def description:string "" ; read after a pause, on request
168
+ def value:string "" ; a field's text, a slider's number
169
+ def x:double 0.0 def y:double 0.0 def w:double 0.0 def h:double 0.0
170
+ def focusable:boolean false
171
+ def focused:boolean false
172
+ def disabled:boolean false
173
+ def checked:int 0 ; 0 no, 1 yes, 2 mixed, 3 not applicable
174
+ def selected:boolean false
175
+ def expanded:int 3
176
+ def readOnly:boolean false
177
+ ; Position in a set the tree does not fully contain — the whole point of
178
+ ; virtualization. "Row 4,120 of 10,000" is this and nothing else.
179
+ def rowIndex:int 0 def colIndex:int 0
180
+ def rowCount:int 0 def colCount:int 0
181
+ def posInSet:int 0 def setSize:int 0
182
+ ; Text fields: caret and selection, in UTF-16 offsets into `value`.
183
+ def caret:int 0 def selStart:int 0 def selEnd:int 0
184
+ def actions:int 0 ; bitmask: focus | activate | setValue | expand | scrollTo
185
+ def live:int 0 ; 0 off, 1 polite, 2 assertive
186
+ }
187
+
188
+ class EVGA11yTree {
189
+ def nodes:[EVGA11yNode]
190
+ def rootId:string ""
191
+ def focusId:string "" ; the app's focus, not the host's
192
+ def generation:int 0 ; same counter the scene uses
193
+ fn addNode:EVGA11yNode (id:string parentId:string role:int)
194
+ fn toJson:string ()
195
+ fn diff:EVGA11yUpdate (prev:EVGA11yTree) ; §8
196
+ fn lint:[string] () ; §9
197
+ }
198
+ ```
199
+
200
+ Nothing in there is browser-specific. `role`, `name`, `bounds`, `focus`, plus a
201
+ tree of stable ids and an update packet, is precisely the shape of an AccessKit
202
+ `TreeUpdate`, and AccessKit is what egui, Bevy and Slint use to reach all three
203
+ desktop platforms. Matching its shape deliberately makes the native adapter in
204
+ §7 nearly a transcription.
205
+
206
+ ## 6. The browser host: a DOM mirror over the canvas
207
+
208
+ For WebGL there is only one approach that actually works with real screen
209
+ readers, and it is what Google Sheets does: keep a small, real, focusable DOM
210
+ tree positioned over the canvas, and let the browser build the accessibility
211
+ tree from it as usual.
212
+
213
+ Where it would go: `lib/evg/gl/evg-a11y.js`, beside `evg-webgl.js`, wired
214
+ from `gallery/datagrid/web/standalone/standalone.mjs` where the scene is already
215
+ pulled each frame.
216
+
217
+ ```
218
+ GridApp ─┬─ sceneJson() ─► evg-webgl.js ─► pixels (what a sighted user gets)
219
+ └─ a11yJson() ─► evg-a11y.js ─► DOM mirror (what NVDA reads)
220
+ ```
221
+
222
+ The mechanics that matter:
223
+
224
+ - **The canvas gets `aria-hidden="true"`** and loses `tabindex`. It is now
225
+ scenery. Today it is the focus target (`web/standalone/index.html:168`), so
226
+ this is a real change to the input path, not an addition.
227
+ - **The mirror is the focus and keyboard target.** The existing
228
+ `canvas.addEventListener("keydown", …)` moves onto the mirror container, so a
229
+ screen reader in forms/focus mode passes keys straight into `GridApp.handleKey`
230
+ as it does today.
231
+ - **Nodes are positioned at their real bounds** (`position:absolute`, CSS
232
+ pixels, divided by DPR). This is not cosmetic: touch exploration on iOS/Android,
233
+ screen magnifiers, and the "route mouse to focus" command all use those
234
+ rectangles. Nodes are invisible via `opacity:0` / `color:transparent`, never
235
+ `display:none` or `visibility:hidden` — those remove the node from the
236
+ accessibility tree, which is the one thing being built here.
237
+ - **Focus has exactly one owner: the app.** When `GridSelection.active` or
238
+ `EVGWindow.focusedId` moves, the host calls `.focus()` on the mirror node;
239
+ when a `focusin` arrives from the browser (Tab from the address bar), the host
240
+ tells the app. A re-entrancy guard around both directions, or the two chase
241
+ each other forever.
242
+ - **The cell editor becomes a real `<input>`.** IME, dictation, Android/iOS
243
+ keyboards, braille input and caret announcements are things browsers only give
244
+ to real text controls. The app stays the source of truth; the input is a
245
+ puppet, synced from `editBuf` and forwarding every change back.
246
+ - **The grid is `role="grid"` with honest virtualization.** Emit the ~40 visible
247
+ rows, each with `aria-rowindex`, and put `aria-rowcount="10000"` /
248
+ `aria-colcount` on the grid. A 10,000-row DOM mirror is not an option, and
249
+ lying about the counts makes the reader announce nonsense positions.
250
+ - **One `aria-live="polite"` status region** for what has no anchor: sort
251
+ applied, recalculation finished, file loaded, "3 cells copied". Assertive is
252
+ for errors only. Chatty live regions are the most common way a technically
253
+ correct implementation becomes unusable.
254
+
255
+ The browse-mode trap deserves naming: NVDA and JAWS intercept arrow keys in
256
+ browse mode, so a grid the user must arrow around needs
257
+ `role="application"`/focus mode on the container — at which point the app owes
258
+ the user *complete* keyboard navigation, because the reader's own navigation is
259
+ now switched off. That is a promise, not a flag.
260
+
261
+ ## 7. The native host: SDL2 + OpenGL
262
+
263
+ There is no cross-platform accessibility API. There are three, and SDL2 exposes
264
+ none of them (SDL3 has only the beginnings). What SDL does give is the native
265
+ window handle, which is the anchor every platform adapter needs.
266
+
267
+ | Platform | API | Anchor |
268
+ | --- | --- | --- |
269
+ | Windows | UI Automation (`IRawElementProviderSimple/Fragment`) | HWND |
270
+ | macOS | NSAccessibility protocol on the `NSView` | NSWindow/NSView |
271
+ | Linux | AT-SPI2 over D-Bus (via ATK) | window + bus name |
272
+
273
+ Writing three adapters is months of work and is why almost no GPU app has this.
274
+ The realistic route is **AccessKit**: one Rust crate with a C API that
275
+ implements all three behind a single node/tree/update model, already shipping in
276
+ egui, Bevy and Slint. The work then is:
277
+
278
+ 1. `EVGA11yTree` → AccessKit `TreeUpdate` (a field-for-field transcription; it
279
+ can live in Ranger-generated C++ or Rust, since both are targets).
280
+ 2. Create the adapter from the SDL window handle
281
+ (`SDL_GetWindowWMInfo` → HWND / NSView / X11 window).
282
+ 3. Push an update whenever the generation changes; route the actions AccessKit
283
+ reports back (activate, focus, set value) into the same `GridApp` entry points
284
+ the pointer path uses.
285
+
286
+ `gallery/datagrid/platform/sdl/evg_gl_native.cpp` is where that shim would sit,
287
+ next to the GL upload it already does. Note the platform work is bounded and
288
+ one-time, while the *content* — roles, names, states, order — is the Ranger
289
+ module both hosts share.
290
+
291
+ ## 8. The three problems that decide whether it works
292
+
293
+ **Stable ids.** If node ids are assigned by emission order, then every frame
294
+ produces a "new" tree, the AT loses its cursor, focus resets, and the braille
295
+ display flickers. Ids must be derived from identity, not from paint order:
296
+ `win:7/ctrl:3`, `sheet:Q3/cell:B7`, `el:#total`. This is the single most
297
+ common way a first implementation fails, and it fails in a way that looks fine
298
+ on screen.
299
+
300
+ **Diffing, not republishing.** Platform APIs want *events* — "this node's value
301
+ changed", "focus moved" — not a fresh tree at 60 Hz. The repo already has the
302
+ signal: `markStateDirty()` and the scene generation that lets an idle page
303
+ answer 204 instead of re-sending. `EVGA11yTree.diff()` should produce the same
304
+ kind of packet: added, removed, changed, focus moved. Rebuilding a DOM mirror
305
+ every frame will melt the tab, and rebuilding a UIA tree every frame will hang
306
+ the reader.
307
+
308
+ **Virtualization honesty.** Only what the model has may be claimed. The visible
309
+ window is what gets nodes; `rowCount`/`colCount`/`setSize` carry the truth about
310
+ the rest; and scrolling in response to an AT `scrollTo` action must actually
311
+ move the viewport, or the reader will ask for row 4,120 and be told it does not
312
+ exist.
313
+
314
+ ## 9. Testing it without owning a screen reader
315
+
316
+ This repo's habit is dumps and oracles, and accessibility suits that unusually
317
+ well — the tree is text.
318
+
319
+ - **A golden a11y dump** next to the display-list dumps (`npm run
320
+ datagrid:artifacts` style): every node, indented, `role · name · state ·
321
+ bounds`. A refactor that silently drops a name shows up as a diff. This
322
+ catches most real regressions and needs no host at all.
323
+ - **Lints in the builder**, failing the test suite: a focusable node with no
324
+ accessible name; a duplicate id; a node whose bounds fall outside its parent;
325
+ `focusId` pointing at a node that is not in the tree; a live region with no
326
+ text; a text run in the display list with no node covering it and no
327
+ decoration marker (the geometry/meaning cross-check from §3).
328
+ - **Keyboard-only reachability**, as a pure Ranger test over `GridApp` /
329
+ `EVGWindow`: from a cold start, is every action reachable with Tab and the
330
+ arrow keys? A screen reader over a mouse-only app is a facade; this test is
331
+ what stops that from shipping.
332
+ - **The real browser tree.** The `?selftest=1` harness already drives the page in
333
+ headless Chrome. Chrome DevTools Protocol's `Accessibility.getFullAXTree`
334
+ returns the accessibility tree the browser actually computed — that is a
335
+ genuine end-to-end assertion (roles, names, focus) with no AT installed.
336
+ - **What none of it proves.** Whether the thing is *usable* is decided by a pass
337
+ with NVDA on Windows and VoiceOver on macOS, by someone who uses them. No CI
338
+ check substitutes for that, and this container has neither a GPU nor a screen
339
+ reader, so nothing above has been run here.
340
+
341
+ ## 10. Two things that come along for free
342
+
343
+ - **Tagged PDF / PDF-UA.** `EVGPDFRenderer` is right there, and a tagged PDF is
344
+ the same information under another name: a structure tree of headings,
345
+ paragraphs, figures with `/Alt`, tables with real cells, and decoration marked
346
+ `/Artifact`. Today the PDF output is a picture of a document. The a11y tree
347
+ is exactly what would make it a document.
348
+ - **`EVGHTMLRenderer`.** It emits absolutely positioned `<div>`s — the same
349
+ semantic void as the canvas. Given the tree, it can emit real roles and names
350
+ instead, and the HTML target becomes the cheapest place to check the semantics
351
+ against a browser.
352
+
353
+ ## 11. Order of work
354
+
355
+ | Phase | Work | State |
356
+ | --- | --- | --- |
357
+ | 1 | `EVGA11yTree.rgr` + emission from `EVGWindow` controls + text dump + lints | **done** — `npm run evg:a11y:test` |
358
+ | 2 | `lib/evg/gl/evg-a11y.js` DOM mirror, wired into the standalone host; canvas `aria-hidden`; focus routing; status live region | **done** — `npm run datagrid:web:test` |
359
+ | 3 | Grid semantics: `role=grid`, row/col indices, virtualization counts, headers, sheet tabs, toolbar | **done** — `npm run datagrid:a11y:test` |
360
+ | 4 | The cell editor as a real `<input>`, for IME, dictation and braille entry | not done — see §12 |
361
+ | 5 | Keyboard completeness and a visible focus ring, audited rather than assumed | not done, and it is what phase 6 should wait for |
362
+ | 6 | Declarative side: `role` / `aria-*` on `EVGElement` + `JSXToEVG`, defaults per tag, showcase pages, `EVGHTMLRenderer` parity | not done |
363
+ | 7 | Native macOS: `dgfx_a11y.mm`, NSAccessibility elements over the SDL2 window | **written, unverified** — see §13 |
364
+ | 8 | Windows (UI Automation) and Linux (AT-SPI2), most likely via AccessKit | not done — analysed in §14 |
365
+ | 9 | Tagged PDF from the same tree | not done |
366
+
367
+ The honest summary: the renderer needs no changes at all, the browser host needs
368
+ a new file and a change to who owns focus, and the native host needs a bounded
369
+ platform shim. The real work — and the part that is easy to underestimate — is
370
+ that every widget must say what it *is*, which is a change spread thinly across
371
+ `EVGWindow`, `GridView` and every page, and a discipline (name it, or the lint
372
+ fails) rather than a feature.
373
+
374
+ ---
375
+
376
+ ## 12. What is built
377
+
378
+ Three files carry it, and the split is the one §3 argues for: meaning is emitted
379
+ in Ranger beside the geometry, and each host translates it into whatever its
380
+ platform speaks.
381
+
382
+ | File | What it is |
383
+ | --- | --- |
384
+ | [`EVGA11yTree.rgr`](EVGA11yTree.rgr) | The model: roles, names, states, bounds, virtualization indices, focus, a text dump and the lints. Pure Ranger — no host, no device — so it compiles to ES6, C++, Rust and Go like the display list does. |
385
+ | [`EVGWindow.rgr`](EVGWindow.rgr) · [`EVGToolbar.rgr`](EVGToolbar.rgr) | `a11y()` on both. `EVGControlKind` was already a role enumeration and `focusedId` already a focus model; publishing them was most of the work. |
386
+ | [`GridView.rgr`](../../gallery/datagrid/src/GridView.rgr) | `a11yTree()` — the sheet as a `grid` with column and row headers, the visible cells, the formula bar, the sheet tabs, the toolbar, the dialogs and a status live region. `GridApp.a11yJson()` / `a11yDump()` are the entry points. |
387
+ | [`gl/evg-a11y.js`](gl/evg-a11y.js) | The browser half: real DOM over the canvas, positioned at the rectangles that were painted, reusing elements by node id. |
388
+
389
+ What a reader gets today, in the standalone DataGrid page:
390
+
391
+ - The canvas is `aria-hidden`; the mirror is what the browser sees.
392
+ - The sheet is a `role="grid"` claiming every row it has (`aria-rowcount`) while
393
+ emitting only the ones on screen, each with `aria-rowindex` / `aria-colindex`,
394
+ so "row 7 of 1,000" is true rather than invented.
395
+ - Column letters and row numbers are `columnheader` / `rowheader`, which is what
396
+ makes a cell announce as "B, 7, 1204" instead of "1204".
397
+ - The caret cell is the one tab stop in the whole application — a roving
398
+ tabindex — and moving the selection moves the reader with it.
399
+ - The toolbar is named buttons with `aria-pressed` on the toggles; the sheet
400
+ tabs are a `tablist`; a dialog is a `dialog` with `aria-modal`, and while one
401
+ is open the sheet behind it is hidden from the reader.
402
+ - Activating anything through the reader presses the app *where the thing is*,
403
+ so there is no map from node ids to commands to keep in step.
404
+ - `?a11y=0` turns the mirror off, which is how to tell a mirror bug from an app
405
+ bug.
406
+
407
+ ### Trying it with a screen reader
408
+
409
+ ```bash
410
+ npm run datagrid:web:serve # builds, then serves it on :8000
411
+ ```
412
+
413
+ On macOS, VoiceOver is already installed — **⌘F5** turns it on and off. Safari
414
+ pairs with it best; Chrome works.
415
+
416
+ - **VO+→ / VO+←** (Control+Option+arrow) walks the tree: toolbar buttons by
417
+ name, the formula bar, the grid, the sheet tabs.
418
+ - **VO+Shift+↓** interacts with the grid; plain **arrow keys** then move the
419
+ spreadsheet's own caret and each cell is announced with its column and row.
420
+ - If arrows do nothing, VoiceOver's Quick Nav is on — press **← and → together**
421
+ to turn it off.
422
+ - **VO+Space** presses whatever is focused.
423
+
424
+ Orca on Linux and NVDA on Windows read the same DOM; nothing in the mirror is
425
+ macOS-specific.
426
+
427
+ ### Tested
428
+
429
+ | Check | Where |
430
+ | --- | --- |
431
+ | The model, the lints, dialog emission, reading order, id stability | `npm run evg:a11y:test` (36 checks) |
432
+ | The sheet's tree: virtualization, headers, caret focus, editing, modal focus, live region | `npm run datagrid:a11y:test` (46 checks) |
433
+ | The real DOM in a real browser: mirror present, canvas hidden, honest counts, one tab stop, activation moves the caret, a modal hides the sheet | `npm run datagrid:web:test` (34 checks, headless Chrome) |
434
+
435
+ ### What is honestly not done
436
+
437
+ - **The cell editor is not a real `<input>` yet.** It is a focusable
438
+ `role="textbox"` carrying the edit buffer, and keys reach the app exactly as
439
+ they did before — so typing works and the field announces itself, but IME,
440
+ dictation and braille *entry* need a real input, with the app still owning the
441
+ buffer. That is phase 4 and it is the largest remaining browser-side gap.
442
+ - **No screen reader has run against it here.** This container has no GPU and no
443
+ assistive technology; everything above was verified through the DOM the
444
+ browser built. Whether it is *usable* is decided by someone using it.
445
+ - **Keyboard completeness is assumed, not audited.** The mirror faithfully
446
+ exposes whatever the app can do; anything the app can only be told with a
447
+ mouse is still unreachable, and no test currently asserts otherwise.
448
+ - **The hosted page** (`web/client.mjs`, the Node-server variant) has no mirror.
449
+ Only the standalone build does.
450
+ - **Nothing is emitted for charts and images** beyond the panel they sit in, and
451
+ a chart is a picture with no alternative text.
452
+
453
+ ---
454
+
455
+ ## 13. The native host (macOS)
456
+
457
+ The browser mirror proved the tree; the SDL2 + OpenGL build is the second
458
+ consumer of it, and the one that shows whether the seam was worth having. It
459
+ was: the app side did not change at all.
460
+
461
+ ```text
462
+ GridApp ─┬─ EVGDisplayList ─► EvgGlPainter ─► OpenGL the picture
463
+ └─ a11yJson() ─► dgfx_a11y.mm ─► NSAccessibility what it means
464
+ ```
465
+
466
+ Three files, mirroring the existing `dgfx_menu` pattern exactly:
467
+
468
+ | File | What |
469
+ | --- | --- |
470
+ | [`dgfx_a11y.h`](../../gallery/datagrid/platform/sdl/dgfx_a11y.h) | Four C functions: is anything listening, publish a tree, take a press, reset |
471
+ | [`dgfx_a11y.mm`](../../gallery/datagrid/platform/sdl/dgfx_a11y.mm) | macOS: `NSJSONSerialization` → one `NSAccessibilityElement` per node under the window's content view |
472
+ | [`dgfx_a11y_stub.cpp`](../../gallery/datagrid/platform/sdl/dgfx_a11y_stub.cpp) | Everywhere else: says nobody is listening, so nothing is built |
473
+
474
+ Why macOS first, other than the machine being to hand: the build **already
475
+ links AppKit**, for the real `NSMenu` in `dgfx_menu.mm`. NSAccessibility is in
476
+ that same framework, so the platform half needed no new dependency — the file
477
+ next to it and one more line in `build.sh`.
478
+
479
+ Decisions worth naming, because each is a way this normally goes wrong:
480
+
481
+ - **The JSON is the interface.** The bridge takes the same string the browser
482
+ page parses. That is one serialization for both hosts, and it means the
483
+ native side has no opinion at all about what a spreadsheet is.
484
+ - **Elements are reused by node id**, as in the browser. This is what the stable
485
+ ids buy: rebuilding the element VoiceOver is sitting on throws its cursor back
486
+ to the top of the window, and nothing looks wrong on screen.
487
+ - **Nothing is built when nothing is listening.** `dgfx_a11y_active()` reads
488
+ VoiceOver's own state (`NSWorkspace.isVoiceOverEnabled`), so an ordinary run
489
+ pays nothing; `DGFX_A11Y=1` forces it on for Accessibility Inspector.
490
+ - **A press comes back as a point**, not a command, and the host presses the app
491
+ there. Same decision as the browser, for the same reason: no second table of
492
+ what each thing does, and a button that moved is still pressed correctly.
493
+ - **Focus is posted only when the app's focus moves.** Posting
494
+ `NSAccessibilityFocusedUIElementChangedNotification` every frame interrupts
495
+ the reader mid-sentence, over and over.
496
+ - **Coordinates are converted once**, in `screenRect`: the tree is in window
497
+ points with y down, NSAccessibility wants screen points with y up. This is the
498
+ line most likely to need adjusting on a multi-display setup.
499
+
500
+ ### What is verified, and what is not
501
+
502
+ Running here (Linux container, no GPU, no macOS):
503
+
504
+ ```bash
505
+ npm run datagrid:sdl # Ranger → C++ → SDL2 + OpenGL binary
506
+ npm run datagrid:sdl:a11y # …and print the tree it produces
507
+ ```
508
+
509
+ - The whole app **compiles to C++ and links**, with the a11y model, the emission
510
+ and the operators in it.
511
+ - The binary **runs** (`SDL_VIDEODRIVER=dummy`) and prints the same tree the
512
+ browser gets — 541 lines of roles, names and rectangles — which is the claim
513
+ "the model is portable" being checked rather than asserted.
514
+ - The **Linux stub path** builds and is what that run used.
515
+
516
+ Not verified: `dgfx_a11y.mm` itself. It has never been compiled — there is no
517
+ AppKit here — so expect a round of compiler errors on a Mac before it works,
518
+ and treat the VoiceOver behaviour as unproven until someone hears it.
519
+
520
+ ### One thing this found
521
+
522
+ The native build was **broken before any of this**: `evggl_clip` and
523
+ `evggl_clip_off` were added to `evg_gl_native.h` but not to the mirrored
524
+ `extern "C"` block in `gfx_datagrid_sdl.rgr` that the generated C++ actually
525
+ sees, so the link failed on two symbols. Nobody noticed because the container
526
+ has no SDL2 and the build was never run here. Two declarations fixed it. It is
527
+ the same failure mode the display list exists to prevent, one layer down: two
528
+ copies of a list, and only one of them was updated.
529
+
530
+ ---
531
+
532
+ ## 14. Would Windows work?
533
+
534
+ The question worth asking after two hosts is not "can it be done" — it can — but
535
+ whether the abstraction is the right shape, or whether macOS quietly bent it.
536
+
537
+ **The tree is portable. The action channel is not.** That is the whole answer;
538
+ the rest is why.
539
+
540
+ ### The tree maps to UI Automation better than it maps to ARIA
541
+
542
+ UIA is not a poorer relation of the web's accessibility model, it is a richer
543
+ one, and a spreadsheet is the case it was designed around. Every field
544
+ `EVGA11yNode` carries has a UIA property or pattern waiting for it:
545
+
546
+ | `EVGA11yNode` | UI Automation |
547
+ | --- | --- |
548
+ | `role` | `ControlType` — Button, CheckBox, RadioButton, Edit, DataGrid, DataItem, HeaderItem, ToolBar, Group, Text, Image, TabItem |
549
+ | `name` / `description` | `Name` / `HelpText` |
550
+ | `value` | `IValueProvider::Value` |
551
+ | `b` (x, y, w, h) | `BoundingRectangle` — screen pixels, **y down**, so simpler than the macOS flip |
552
+ | `focusable` / `focused` | `IsKeyboardFocusable` / `HasKeyboardFocus` + `UIA_AutomationFocusChangedEventId` |
553
+ | `disabled` / `readOnly` | `IsEnabled` / `IValueProvider::IsReadOnly` |
554
+ | `checked` (tri-state) | `IToggleProvider::ToggleState` — Off / On / Indeterminate, the same three |
555
+ | `selected` | `ISelectionItemProvider::IsSelected` |
556
+ | `modal` | `IsModal` |
557
+ | `rowIndex` / `colIndex` | `IGridItemProvider::Row` / `Column` |
558
+ | `rowCount` / `colCount` | `IGridProvider::RowCount` / `ColumnCount` |
559
+ | `posInSet` / `setSize` | `PositionInSet` / `SizeOfSet` |
560
+ | `live` | `LiveSetting` + `UIA_LiveRegionChangedEventId` |
561
+ | `actActivate` / `actSetValue` / `actScrollTo` | `IInvokeProvider` / `IValueProvider::SetValue` / `IScrollItemProvider::ScrollIntoView` |
562
+
563
+ Nothing in that column had to be invented for it and nothing in the left column
564
+ is left over. The virtualization story is better than the web's, too:
565
+ `IGridProvider::GetItem(row, column)` is a call the app can answer for a row it
566
+ has never emitted, which is exactly the shape `DataGrid` already has.
567
+
568
+ ### Three things the bridge would have to grow
569
+
570
+ **1. It needs the window handle.** `WM_GETOBJECT` is how Windows asks a window
571
+ for its provider, and answering means returning
572
+ `UiaReturnRawElementProvider(...)` as the message's `LRESULT`. SDL2's own hook
573
+ cannot do that — checked against the header in this container:
574
+
575
+ ```c
576
+ typedef void (SDLCALL * SDL_WindowsMessageHook)(void *userdata, void *hWnd,
577
+ unsigned int message, Uint64 wParam, Sint64 lParam); /* SDL_system.h:46 */
578
+ ```
579
+
580
+ It returns `void`, so it can observe `WM_GETOBJECT` and not answer it. The
581
+ window procedure has to be subclassed instead — `SDL_GetWindowWMInfo` for the
582
+ `HWND` (`info.info.win.window`), `SetWindowLongPtr(GWLP_WNDPROC)`, chain to the
583
+ original. So `dgfx_a11y.h` grows one entry point, `dgfx_a11y_attach(SDL_Window*)`,
584
+ called once beside `dgfx_install_menus`. macOS did not need it only because
585
+ `NSApp` can find its own window; that was macOS being convenient, not the
586
+ abstraction being right.
587
+
588
+ **2. The contract has to say which thread.** This is the one that would bite.
589
+ NSAccessibility asks its questions on the main thread, so `dgfx_a11y.mm` can
590
+ hold plain mutable state and never think about it. UIA clients — NVDA, JAWS,
591
+ Narrator — call provider methods from **RPC threads**, concurrently with the
592
+ app's frame loop calling `publish`. The four functions do not change shape, but
593
+ the contract behind them does: `publish` must swap in an **immutable snapshot**
594
+ under a lock (or an atomic pointer), and every provider answers from the
595
+ snapshot it was handed. Get that wrong and it is a crash under a screen reader
596
+ and nowhere else.
597
+
598
+ **3. The action channel is the part that is actually wrong.** Today a press
599
+ comes back as a *point*, and the host presses the app there. That was a good
600
+ trade for two hosts: no map from node ids to commands, and a button that moved
601
+ is still pressed correctly. It cannot express what UIA will ask for:
602
+
603
+ | UIA asks | Point suffices? |
604
+ | --- | --- |
605
+ | `IInvokeProvider::Invoke` | yes |
606
+ | `ISelectionItemProvider::Select` | yes |
607
+ | `IToggleProvider::Toggle` | yes |
608
+ | `IValueProvider::SetValue(BSTR)` | **no** — a string has to arrive, and at a named node |
609
+ | `IScrollItemProvider::ScrollIntoView` | **no** — the point is off screen, which is the problem |
610
+ | `IExpandCollapseProvider` | no |
611
+
612
+ So `dgfx_a11y_take_press` would become something like
613
+ `dgfx_a11y_take_action() -> "kind\tnodeId\tx,y\tpayload"`, and the app grows
614
+ one entry point that takes a node id rather than a coordinate. Notably this is
615
+ the **same gap** as the browser's missing real `<input>` (§12): both are the
616
+ write direction, and both are unfinished for the same reason — the tree was
617
+ built first because it is what a reader reads.
618
+
619
+ ### Hand-written or AccessKit
620
+
621
+ | | Hand-written UIA | AccessKit |
622
+ | --- | --- | --- |
623
+ | Size | ~800–1200 lines of COM C++: a provider class per node, `IRawElementProviderSimple` / `Fragment` / `FragmentRoot`, six pattern interfaces, refcounting, `UiaDisconnectProvider` on removal, the threading above | roughly the transcription `dgfx_a11y.mm` already is |
624
+ | Risk | COM lifetime and cross-thread bugs that only appear with a screen reader attached | someone else already made them |
625
+ | Cost | none | a Rust toolchain in a C++ build, and a static library to link |
626
+ | Covers | Windows | Windows **and** Linux AT-SPI2 **and** macOS |
627
+
628
+ `EVGA11yNode` was deliberately shaped as the intersection of these five APIs,
629
+ and AccessKit's `TreeUpdate` is the closest of the five. If Windows is wanted,
630
+ AccessKit is the answer for it and for Linux, and `dgfx_a11y.mm` then becomes
631
+ the odd one out — worth keeping while it is the only tested path, worth
632
+ retiring once it is not.
633
+
634
+ ### Verdict
635
+
636
+ Nothing on the **Ranger side** would change: not `EVGA11yTree`, not
637
+ `GridView.a11yTree`, not `GridApp.a11yJson`. That is the actual test of whether
638
+ the seam was in the right place, and it passes. What changes is the bridge —
639
+ one more entry point, one documented threading rule, and a typed action channel
640
+ replacing the point. Those are amendments to a 60-line header, and the reason
641
+ to make them is Windows; there is no reason to make them speculatively before
642
+ somebody writes the other side.
643
+
644
+ One Windows-specific trap to write down before it is discovered the hard way:
645
+ `BoundingRectangle` is in **physical** screen pixels. A process that is not
646
+ per-monitor DPI aware gets its rectangles virtualized by the system, and on a
647
+ scaled display every node would be reported in the wrong place — the same class
648
+ of bug as the Retina pointer offset documented in
649
+ [`platform/sdl/README.md`](../../gallery/datagrid/platform/sdl/README.md), arriving from
650
+ the other end.