ranger-compiler 3.5.0 → 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.
- package/CHANGELOG.md +1160 -19
- package/LICENSE +3 -1
- package/README.md +124 -29
- package/dist/Lang.rgr +1188 -290
- package/dist/api.d.ts +2343 -879
- package/dist/api.js +79924 -56922
- package/dist/lib/JSON.rgr +102 -91
- package/dist/lib/Shell.rgr +4 -4
- package/dist/lib/apple/AppleToolchain.rgr +4 -4
- package/dist/lib/apple/README.md +5 -4
- package/dist/lib/apple/apple_test.rgr +1 -1
- package/dist/lib/core/README.md +1 -1
- package/dist/lib/evg/EVG.rgr +12 -0
- package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
- package/dist/lib/evg/EVGA11yTree.rgr +894 -0
- package/dist/lib/evg/EVGBox.rgr +267 -0
- package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
- package/dist/lib/evg/EVGCodepoint.rgr +316 -0
- package/dist/lib/evg/EVGColor.rgr +700 -0
- package/dist/lib/evg/EVGCommands.rgr +177 -0
- package/dist/lib/evg/EVGComponent.rgr +331 -0
- package/dist/lib/evg/EVGComponentTest.rgr +387 -0
- package/dist/lib/evg/EVGConnector.rgr +541 -0
- package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
- package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
- package/dist/lib/evg/EVGEasing.rgr +370 -0
- package/dist/lib/evg/EVGEffectTest.rgr +253 -0
- package/dist/lib/evg/EVGElement.rgr +4914 -0
- package/dist/lib/evg/EVGFixedTest.rgr +289 -0
- package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
- package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
- package/dist/lib/evg/EVGFling.rgr +244 -0
- package/dist/lib/evg/EVGFocus.rgr +400 -0
- package/dist/lib/evg/EVGFocusTest.rgr +399 -0
- package/dist/lib/evg/EVGGradient.rgr +322 -0
- package/dist/lib/evg/EVGGrapheme.rgr +190 -0
- package/dist/lib/evg/EVGGrid.rgr +975 -0
- package/dist/lib/evg/EVGHitTest.rgr +208 -0
- package/dist/lib/evg/EVGHoles.rgr +294 -0
- package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
- package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
- package/dist/lib/evg/EVGHostTree.rgr +834 -0
- package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
- package/dist/lib/evg/EVGImageDecode.rgr +117 -0
- package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
- package/dist/lib/evg/EVGInspect.rgr +883 -0
- package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
- package/dist/lib/evg/EVGJsonTest.rgr +535 -0
- package/dist/lib/evg/EVGLayout.rgr +4220 -0
- package/dist/lib/evg/EVGMeasure.rgr +1267 -0
- package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
- package/dist/lib/evg/EVGPatch.rgr +1721 -0
- package/dist/lib/evg/EVGPatchTest.rgr +692 -0
- package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
- package/dist/lib/evg/EVGReconcile.rgr +226 -0
- package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
- package/dist/lib/evg/EVGReject.rgr +126 -0
- package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
- package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
- package/dist/lib/evg/EVGRuler.rgr +232 -0
- package/dist/lib/evg/EVGRulerTest.rgr +172 -0
- package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
- package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
- package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
- package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
- package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
- package/dist/lib/evg/EVGText.rgr +105 -0
- package/dist/lib/evg/EVGTextEngine.rgr +536 -0
- package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
- package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
- package/dist/lib/evg/EVGToolbar.rgr +1369 -0
- package/dist/lib/evg/EVGTransition.rgr +728 -0
- package/dist/lib/evg/EVGTreeJson.rgr +721 -0
- package/dist/lib/evg/EVGUnit.rgr +554 -0
- package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
- package/dist/lib/evg/EvgApp.rgr +189 -0
- package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
- package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
- package/dist/lib/evg/EvgHost.rgr +451 -0
- package/dist/lib/evg/EvgTest.rgr +91 -0
- package/dist/lib/evg/EvgTraceColor.rgr +236 -0
- package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
- package/dist/lib/evg/EvgTraceFit.rgr +767 -0
- package/dist/lib/evg/EvgTracePath.rgr +435 -0
- package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
- package/dist/lib/evg/EvgViewport.rgr +313 -0
- package/dist/lib/evg/FxDemoDoc.rgr +185 -0
- package/dist/lib/evg/HOSTS.md +227 -0
- package/dist/lib/evg/ISSUES.md +835 -0
- package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
- package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
- package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
- package/dist/lib/evg/PLAN_EVG.md +518 -0
- package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
- package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
- package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
- package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
- package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
- package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
- package/dist/lib/evg/PathBuilder.rgr +333 -0
- package/dist/lib/evg/README.md +1236 -0
- package/dist/lib/evg/SPEC.md +1252 -0
- package/dist/lib/evg/SVGPathParser.rgr +1199 -0
- package/dist/lib/evg/SvgParser.rgr +1736 -0
- package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
- package/dist/lib/evg/VectorShapes.rgr +331 -0
- package/dist/lib/evg/VectorStroke.rgr +107 -0
- package/dist/lib/evg/VectorViewBox.rgr +380 -0
- package/dist/lib/evg/agent/README.md +290 -0
- package/dist/lib/evg/agent/evg_agent.rgr +899 -0
- package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
- package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
- package/dist/lib/evg/agent/fixtures/connector.css +63 -0
- package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
- package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
- package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
- package/dist/lib/evg/agent/fixtures/popover.css +99 -0
- package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
- package/dist/lib/evg/agent/roundtrip.sh +74 -0
- package/dist/lib/evg/agent/smoke.sh +230 -0
- package/dist/lib/evg/android/README.md +97 -0
- package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
- package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
- package/dist/lib/evg/android/androidstubs/App.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
- package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
- package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
- package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
- package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
- package/dist/lib/evg/android/androidstubs/View.kt +113 -0
- package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
- package/dist/lib/evg/apple/README.md +128 -0
- package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
- package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
- package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
- package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
- package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
- package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
- package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
- package/dist/lib/evg/bench/README.md +64 -0
- package/dist/lib/evg/bench/layout-bench.mjs +316 -0
- package/dist/lib/evg/bench/layout-cases.mjs +510 -0
- package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
- package/dist/lib/evg/bin/.gitignore +17 -0
- package/dist/lib/evg/evg_test.rgr +313 -0
- package/dist/lib/evg/gl/README.md +198 -0
- package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
- package/dist/lib/evg/gl/blur-check.mjs +399 -0
- package/dist/lib/evg/gl/boxmodel.json +1 -0
- package/dist/lib/evg/gl/demo.html +46 -0
- package/dist/lib/evg/gl/effect-presets.css +285 -0
- package/dist/lib/evg/gl/effect-presets.js +63 -0
- package/dist/lib/evg/gl/effect-shots.mjs +135 -0
- package/dist/lib/evg/gl/evg-a11y.js +566 -0
- package/dist/lib/evg/gl/evg-binary.js +160 -0
- package/dist/lib/evg/gl/evg-engine.js +296 -0
- package/dist/lib/evg/gl/evg-fx.js +237 -0
- package/dist/lib/evg/gl/evg-gestures.js +209 -0
- package/dist/lib/evg/gl/evg-list.js +167 -0
- package/dist/lib/evg/gl/evg-measure.js +186 -0
- package/dist/lib/evg/gl/evg-textinput.js +303 -0
- package/dist/lib/evg/gl/evg-view.js +144 -0
- package/dist/lib/evg/gl/evg-webgl.js +3942 -0
- package/dist/lib/evg/gl/fx-check.mjs +998 -0
- package/dist/lib/evg/gl/fx-demo.html +166 -0
- package/dist/lib/evg/gl/fx-demo.js +2 -0
- package/dist/lib/evg/gl/fx-serve.mjs +47 -0
- package/dist/lib/evg/gl/gestures-check.mjs +189 -0
- package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
- package/dist/lib/evg/gl/measure-check.mjs +135 -0
- package/dist/lib/evg/gl/rotation-check.mjs +207 -0
- package/dist/lib/evg/gl/shift-check.mjs +113 -0
- package/dist/lib/evg/gl/stroke-check.mjs +168 -0
- package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
- package/dist/lib/evg/gl/view-check.mjs +343 -0
- package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
- package/dist/lib/evg/html/evg-dom.js +318 -0
- package/dist/lib/evg/html/evg-html.js +600 -0
- package/dist/lib/evg/inspect/README.md +415 -0
- package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
- package/dist/lib/evg/inspect/evg-inspect.js +947 -0
- package/dist/lib/evg/inspect/shots/css.png +0 -0
- package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
- package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
- package/dist/lib/evg/inspect/shots/state.png +0 -0
- package/dist/lib/evg/inspect/shots.mjs +226 -0
- package/dist/lib/evg/oracle/css-blur.json +576 -0
- package/dist/lib/evg/oracle/css-box.json +157 -0
- package/dist/lib/evg/oracle/css-timing.json +709 -0
- package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
- package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
- package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
- package/dist/lib/evg/original/EVGColor.clj +310 -0
- package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
- package/dist/lib/evg/original/SVGPath.rgr +627 -0
- package/dist/lib/evg/original/Vec2.crgr +103 -0
- package/dist/lib/evg/ranger.json +9 -0
- package/dist/lib/evg/showcase/README.md +246 -0
- package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
- package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
- package/dist/lib/evg/showcase/build.mjs +651 -0
- package/dist/lib/evg/showcase/pages/album.tsx +30 -0
- package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
- package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
- package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
- package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
- package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
- package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
- package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
- package/dist/lib/evg/showcase/pages/more.tsx +332 -0
- package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
- package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
- package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
- package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
- package/dist/lib/evg/showcase/pages/units.tsx +35 -0
- package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
- package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
- package/dist/lib/evg/showcase/pages/views.tsx +223 -0
- package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
- package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
- package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
- package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
- package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
- package/dist/lib/evg/showcase/themes/more-default.css +128 -0
- package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
- package/dist/lib/evg/showcase/themes/showcase.css +508 -0
- package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
- package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
- package/dist/lib/evg/showcase/themes/views-default.css +15 -0
- package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
- package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
- package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
- package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
- package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
- package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
- package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
- package/dist/lib/evg/web/responsive/README.md +97 -0
- package/dist/lib/evg/web/responsive/build.mjs +90 -0
- package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
- package/dist/lib/evg/web/responsive/index.html +177 -0
- package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
- package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
- package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
- package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
- package/dist/lib/evg/web/tools/minify.mjs +60 -0
- package/dist/lib/evg/web/tracer/build.mjs +87 -0
- package/dist/lib/evg/web/tracer/index.html +2805 -0
- package/dist/lib/evg/web/tracer/sample.png +0 -0
- package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
- package/dist/lib/evgr/Cargo.lock +21 -0
- package/dist/lib/evgr/Cargo.toml +19 -0
- package/dist/lib/evgr/README.md +140 -0
- package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
- package/dist/lib/evgr/bench/compare.mjs +252 -0
- package/dist/lib/evgr/bench/speed.mjs +277 -0
- package/dist/lib/evgr/bin/.gitignore +3 -0
- package/dist/lib/evgr/src/bin/bench.rs +106 -0
- package/dist/lib/evgr/src/bin/smoke.rs +13 -0
- package/dist/lib/evgr/src/grid.rs +730 -0
- package/dist/lib/evgr/src/lib.rs +1003 -0
- package/dist/lib/evgr/src/style.rs +468 -0
- package/dist/lib/evgr/src/text.rs +83 -0
- package/dist/lib/image/BitReader.rgr +171 -0
- package/dist/lib/image/Buffer.rgr +173 -0
- package/dist/lib/image/DCT.rgr +283 -0
- package/dist/lib/image/Deflate.rgr +339 -0
- package/dist/lib/image/HuffmanDecoder.rgr +181 -0
- package/dist/lib/image/ImageBuffer.rgr +706 -0
- package/dist/lib/image/JPEGDecoder.rgr +808 -0
- package/dist/lib/image/PNGDecoder.rgr +544 -0
- package/dist/lib/image/PNGEncoder.rgr +388 -0
- package/dist/lib/image/PPMImage.rgr +224 -0
- package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
- package/dist/lib/image/README.md +33 -0
- package/dist/lib/image/RasterBuffer.rgr +294 -0
- package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
- package/dist/lib/image/WebPDecoder.rgr +433 -0
- package/dist/lib/image/WebPLossless.rgr +1035 -0
- package/dist/lib/image/WebPLossy.rgr +1929 -0
- package/dist/lib/image/ranger.json +9 -0
- package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
- package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
- package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
- package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
- package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
- package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
- package/dist/lib/image/tests/run_webp_tests.sh +39 -0
- package/dist/lib/rust/RsJson.rgr +468 -0
- package/dist/lib/rust/RsPrelude.rgr +1663 -0
- package/dist/lib/shell_test.rgr +7 -7
- package/dist/lib/stdlib.rgr +21 -5
- package/dist/lib/zip/ranger.json +6 -0
- package/dist/rgrc.js +87318 -67610
- package/package.json +1010 -1040
- package/dist/README.md +0 -117
- 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.
|