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.
Files changed (338) hide show
  1. package/CHANGELOG.md +1160 -19
  2. package/LICENSE +3 -1
  3. package/README.md +124 -29
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79924 -56922
  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,1236 @@
1
+ # EVG — the layout engine
2
+
3
+ EVG lays a document out and hands the result to whatever draws it. It is a
4
+ CSS-shaped box model written in Ranger, with no browser under it and no browser
5
+ anywhere near it: flex, grid, the length units, a stylesheet with `@media` and
6
+ pseudo-classes, real font metrics with kerning, text wrapping, transitions, hit
7
+ testing and an accessibility tree — and then one flat list of draw commands that
8
+ a PDF writer, a rasteriser, an `<svg>`, a WebGL context, CoreGraphics, Android's
9
+ `Canvas` or an SDL window can each paint without knowing any of the above.
10
+
11
+ ```
12
+ tree ──► stylesheet ──► layout ──► display list ──► a painter
13
+ (names) (@media, vw, (boxes, (absolute px, (PDF, PNG,
14
+ themes, states) text runs) resolved colours) SVG, GL, …)
15
+ ```
16
+
17
+ The claim the repository makes about it everywhere: **the same tree, styled by
18
+ the same sheet, comes out the same on every target.** The showcase renders each
19
+ page to PDF, PNG and HTML from one source; the WebGL and SVG backends are
20
+ differenced pixel for pixel; and the display list is where all of them meet.
21
+
22
+ This file is the reference for the engine itself. The other documents here are:
23
+
24
+ | | |
25
+ | --- | --- |
26
+ | [`SPEC.md`](SPEC.md) | The document format, attribute by attribute, for someone implementing a reader or a painter |
27
+ | [`PLAN_CSS_LAYOUT_AND_FONTS.md`](PLAN_CSS_LAYOUT_AND_FONTS.md) | Why the CSS subset is the shape it is, and what is deliberately missing |
28
+ | [`PLAN_VECTOR_IR.md`](PLAN_VECTOR_IR.md) | The vector layer: paths, strokes, `viewBox`, SVG import |
29
+ | [`PLAN_ACCESSIBILITY.md`](PLAN_ACCESSIBILITY.md) | The second list a frame publishes — what it *means* |
30
+ | [`PLAN_NATIVE_HOSTS.md`](PLAN_NATIVE_HOSTS.md) | A spike: DOM, SwiftUI and Compose as hosts rather than painters — what the platform can do better than a canvas, where native layout stops, and the engine off the UI thread |
31
+ | [`bench/README.md`](bench/README.md) | The same CSS through this engine and through Chromium — where they disagree, and what layout costs at 100k boxes |
32
+ | [`ISSUES.md`](ISSUES.md) | Known defects, with the measurements that found them |
33
+ | [`showcase/README.md`](showcase/README.md) | The gallery, and how it is built |
34
+ | [`gl/README.md`](gl/README.md) | The display-list seam and the GPU backend |
35
+
36
+ ---
37
+
38
+ ## Contents
39
+
40
+ 1. [Quick start](#quick-start)
41
+ 2. [The pipeline](#the-pipeline)
42
+ 3. [Elements](#elements)
43
+ 4. [Property reference](#property-reference)
44
+ 5. [Units](#units)
45
+ 6. [The CSS subset](#the-css-subset)
46
+ 7. [Responsive layout](#responsive-layout)
47
+ 8. [Layout](#layout)
48
+ 9. [Text and fonts](#text-and-fonts)
49
+ 10. [The display list](#the-display-list)
50
+ 11. [Targets](#targets)
51
+ 12. [Interaction](#interaction)
52
+ 13. [Accessibility](#accessibility)
53
+ 14. [Retained trees](#retained-trees)
54
+ 15. [The files](#the-files)
55
+ 16. [Running things](#running-things)
56
+
57
+ ---
58
+
59
+ ## Quick start
60
+
61
+ Four objects and five calls. Everything else in this file is detail on one of
62
+ them.
63
+
64
+ ```ranger
65
+ Import "EVGElement.rgr"
66
+ Import "EVGStyleSheet.rgr"
67
+ Import "EVGLayout.rgr"
68
+ Import "EVGDisplayList.rgr"
69
+
70
+ ; 1. a tree of names
71
+ def page (EVGElement.createDiv())
72
+ page.className = "page"
73
+ def title (EVGElement.createSpan())
74
+ title.className = "title"
75
+ title.textContent = "Hello"
76
+ page.addChild(title)
77
+
78
+ ; 2. a stylesheet, and the surface it is being resolved against
79
+ def sheet (new EVGStyleSheet())
80
+ sheet.parse(".page { padding: 24px } .title { font-size: 28px; color: #10162b }")
81
+ sheet.setViewport(1200.0 800.0 false)
82
+ sheet.applyTree(page "")
83
+
84
+ ; 3. boxes
85
+ def lay (new EVGLayout())
86
+ lay.setPageSize(1200.0 800.0)
87
+ lay.layout(page)
88
+
89
+ ; 4. draw commands
90
+ def dl (new EVGDisplayList())
91
+ dl.setTextEngine((lay.getTextEngine()))
92
+ dl.build(page)
93
+
94
+ ; 5. whatever paints them
95
+ print (dl.toJson())
96
+ ```
97
+
98
+ Documents are usually written as JSX rather than built by hand —
99
+ `gallery/pdf_writer/src/jsx/JSXToEVG.rgr` reads a `.tsx` file into exactly this
100
+ tree — or with a `treefactory`, which is Ranger's tree literal. Both produce
101
+ `EVGElement`s and nothing downstream can tell which was used.
102
+
103
+ ---
104
+
105
+ ## The pipeline
106
+
107
+ Each stage has one job and hands on a strictly simpler thing than it received.
108
+
109
+ **1. The tree.** `EVGElement`s: a class name, a type, children, and any inline
110
+ attributes the author insisted on. In a well-written document this stage carries
111
+ no colours, sizes or spacing at all — the showcase's rule, and the reason one
112
+ tree can be a print page and a phone screen.
113
+
114
+ **2. The cascade.** `EVGStyleSheet.applyTree` resolves each element's classes,
115
+ theme, interaction state and the stated viewport into properties, and writes
116
+ them onto the element through the same `setAttribute` the authoring layer uses.
117
+ Inline attributes win; the sheet skips any property the author already set.
118
+
119
+ **3. Layout.** `EVGLayout.layout` resolves units, measures text, and computes
120
+ `calculatedX`, `calculatedY`, `calculatedWidth`, `calculatedHeight` for every
121
+ element. This is the flex/grid/flow engine and the largest file here.
122
+
123
+ **4. The display list.** `EVGDisplayList.build` walks the laid-out tree once and
124
+ emits flat commands — filled rect, border, image quad, text run, path, stroke,
125
+ push/pop clip. Absolute pixels, colours as 0–255 plus alpha, no tree and no
126
+ units left.
127
+
128
+ **5. A painter.** Which is small, because everything hard already happened.
129
+
130
+ The seam is between 4 and 5, and it is load-bearing: five painters used to walk
131
+ the tree themselves and each decided again what a box meant, which is how
132
+ `border-radius` came to work in PDF and silently not in PNG.
133
+
134
+ ---
135
+
136
+ ## Elements
137
+
138
+ `EVGElement` is the only node type. Its `elementType` says what it is:
139
+
140
+ | `elementType` | Kind | What it draws |
141
+ | --- | --- | --- |
142
+ | `0` | container | a box: background, border, radius, shadow, clip |
143
+ | `1` | text | a box plus its `textContent`, wrapped and measured |
144
+ | `2` | image | a box plus a decoded bitmap, fitted by `object-fit` |
145
+ | `3` | path | a box plus vector geometry from `d` / `svgPath` / `svgSource` |
146
+
147
+ Constructors: `EVGElement.createDiv()`, `createSpan()`, `createImg()` and
148
+ `createPath()`. A `treefactory` tag sets `elementType` in its props, as in
149
+ [`web/responsive/EvgResponsiveDemo.rgr`](web/responsive/EvgResponsiveDemo.rgr);
150
+ JSX sets it from the tag name.
151
+
152
+ Two tags mean more than their `elementType`. A **`popover`** is a container that
153
+ is also a [surface](#surfaces-popovers-anchors-and-presentation) — out of flow,
154
+ in the top layer, placed against an anchor — without saying `overlay: true`. A
155
+ **`connector`** is a path whose `d` the layout writes; see
156
+ [Connectors](#connectors).
157
+
158
+ Two names, deliberately different:
159
+
160
+ * **`id`** is global and outward-facing. The hit test reports it, the
161
+ accessibility tree carries it, a host addresses a control by it.
162
+ * **`key`** is sibling-scoped and inward-facing. It is what
163
+ [`EVGReconcile`](EVGReconcile.rgr) matches on so a rebuilt tree keeps the same
164
+ element objects — and therefore keeps scroll positions and running
165
+ transitions.
166
+
167
+ ---
168
+
169
+ ## Property reference
170
+
171
+ Every property is set through `EVGElement.setAttribute(name value)`, and every
172
+ name below is accepted in **both** spellings — `font-size` and `fontSize`. That
173
+ is not sugar: it is what stops the stylesheet and the inline attributes from
174
+ drifting apart, because the sheet hands its declarations to the same function.
175
+
176
+ ### Box
177
+
178
+ | Property | Notes |
179
+ | --- | --- |
180
+ | `width` `height` | any unit; unset = auto |
181
+ | `min-width` `max-width` `min-height` `max-height` | clamps, applied after the size is computed |
182
+ | `padding` | 1–4 values, CSS order (`all`, `v h`, `t h b`, `t r b l`) |
183
+ | `padding-top` `padding-right` `padding-bottom` `padding-left` | |
184
+ | `margin` and the four sides | same shorthand |
185
+ | `border` | `<width> solid <color>` |
186
+ | `border-width` `border-color` | |
187
+ | `border-radius` | one value, or four; percentages resolve against the box |
188
+ | `box-shadow` | `<dx> <dy> <blur> <color>` |
189
+ | `shadow-offset-x` `shadow-offset-y` `shadow-radius` `shadow-color` | the long form |
190
+ | `opacity` | `0`–`1`, multiplied through the subtree |
191
+ | `overflow` | `visible` is the default; every other value clips the subtree and makes the box scrollable |
192
+ | `scroll-top` `scroll-left` | where a scrollable box is scrolled to |
193
+
194
+ ### Layout
195
+
196
+ | Property | Values |
197
+ | --- | --- |
198
+ | `display` | `flex`, `grid` — anything else is block flow |
199
+ | `flex-direction` | `row`, `column`. The reversed directions parse and then warn: nothing lays them out |
200
+ | `flex-wrap` | `nowrap`, `wrap`, `wrap-reverse` |
201
+ | `justify-content` | `flex-start`, `center`, `flex-end`, `space-between`, `space-around`, `space-evenly` |
202
+ | `align-items` `align-content` | `flex-start`, `center`, `flex-end`, `stretch`, `baseline` |
203
+ | `align-self` | the same set, on the item, overriding its container's `align-items` |
204
+ | `flex` | the shorthand; `flex-basis` and `flex-shrink` separately |
205
+ | `gap` `row-gap` `column-gap` | the longhands override the shorthand, per axis — in a row `column-gap` is between the items and `row-gap` between the wrapped lines |
206
+ | `grid-template-columns` `grid-template-rows` | `120px 1fr 40%`, `repeat(3, 1fr)`, `minmax(40px, 1fr)`, `subgrid` |
207
+ | `grid-template-areas` | a picture of names; a repeated name is one rectangle |
208
+ | `grid-column` `grid-row` `grid-area` | placement and spans |
209
+ | `grid-auto-flow` | `row`, `column` |
210
+ | `direction` | `ltr`, `rtl` — set once on the root and the whole tree turns around |
211
+ | `top` `right` `bottom` `left` | absolute positioning, against the nearest box |
212
+ | `vertical-align` | `baseline` participation for inline-ish content |
213
+
214
+ ### Paint
215
+
216
+ | Property | Notes |
217
+ | --- | --- |
218
+ | `background-color` `color` | `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()`, `hsl()`, named colours, `transparent` |
219
+ | `background-gradient` | `linear-gradient(…)` / `radial-gradient(…)` |
220
+ | `gradient-from` `gradient-to` `gradient-dir` | the long form |
221
+ | `background-image` | a source the host can decode |
222
+ | `backdrop-filter` | `blur(Npx)` — softens what is already behind the box |
223
+ | `scrollbar-width` `scrollbar-color` | `auto`/`thin`/`none`; the thumb's colour then the track's, `auto` for ink on whatever the bar is over. The list draws the bar when an app asks it to (`EVGDisplayList.setScrollbars`); it is up while the container scrolls, lit under the pointer |
224
+ | `evg-scrollbar-label` | `percent` (default) or `none` — the "34 %" beside the thumb while the page moves |
225
+ | `fill` `stroke` `stroke-width` `stroke-dasharray` `stroke-dashoffset` `fill-rule` | vector paint |
226
+ | `clip-path` | |
227
+ | `transform` `transform-origin` `rotate` `scale` `translate-x` `translate-y` | |
228
+ | `object-fit` | `cover`, `contain`, `fill`, `none` |
229
+ | `image-offset-x` `image-offset-y` `image-quality` `maxImageSize` | how a bitmap is placed and resampled |
230
+ | `cursor` | inherited, so "what is the cursor here" is one lookup |
231
+ | `full-bleed` | the box ignores the page margins |
232
+
233
+ ### Type
234
+
235
+ | Property | Notes |
236
+ | --- | --- |
237
+ | `font-family` | inherited |
238
+ | `font-size` | inherited through `em`; `rem` stays with the root |
239
+ | `font-weight` | |
240
+ | `line-height` | a number (multiplier) or a length; `normal` is the face's own line box, which is **not** 1.2 |
241
+ | `text-align` | `left`, `center`, `right` |
242
+ | `line-break` | how lines may be broken |
243
+ | `emoji-color` | the colour fallback glyphs are painted in; defaults to `color` |
244
+
245
+ ### Surfaces: popovers, anchors and presentation
246
+
247
+ A **surface** is a box that is not in the flow and not in the page's stacking
248
+ order either: a dropdown, a tooltip, a menu panel, the toolbar that appears
249
+ around a selected element. Two things make it one, and they are separate:
250
+
251
+ * it takes no space in its parent — nothing moves for it;
252
+ * it is drawn **after the whole normal tree, outside every clip**. That is a
253
+ real top layer, the same thing the web's Popover API gives you: an ancestor's
254
+ `overflow: hidden` cannot cut it and no `z-index` is involved. See
255
+ `deferredOverlays` in [`EVGDisplayList`](EVGDisplayList.rgr).
256
+
257
+ It still lives where it belongs in the tree. A menu declared inside its button
258
+ is a child of that button for events, state and the accessibility tree, and is
259
+ only *drawn* somewhere else — which is what a portal is for in other toolkits,
260
+ here as a property of the layout.
261
+
262
+ Say so with the `popover` tag, or with `overlay: true` on any element:
263
+
264
+ ```json
265
+ {"tag": "div", "props": {"anchor-name": "--file"}, "text": "File"},
266
+ {"tag": "popover", "props": {
267
+ "position-anchor": "--file",
268
+ "position-area": "bottom start",
269
+ "position-try-fallbacks": "top start, right start",
270
+ "fit-viewport": "true", "overflow": "hidden",
271
+ "sheet-below": "600px"
272
+ }}
273
+ ```
274
+
275
+ #### Naming the anchor
276
+
277
+ | Property | Notes |
278
+ | --- | --- |
279
+ | `anchor-name` | names this element so something can point at it: `--file` |
280
+ | `position-anchor` | what this surface is positioned against: an `anchor-name` or an `#id`. Also marks the element as a surface |
281
+ | `overlay-anchor-role` | the older convention: the surface takes whichever **sibling** declares this |
282
+ | `overlay` / `isOverlay` | marks the element as a surface without naming an anchor |
283
+
284
+ Three ways to say it, in the order they win: an element set from code, then
285
+ `position-anchor`, then the sibling convention. `position-anchor` is the only
286
+ one that reaches out of the surface's own parent, so a menu no longer has to be
287
+ declared beside its trigger. It is the same name a connector points at — one
288
+ registry, collected once per layout.
289
+
290
+ #### Where it goes
291
+
292
+ | Property | Notes |
293
+ | --- | --- |
294
+ | `position-area` | `bottom start`, `top end`, `right center`, `bottom left`, `center`, `cover` — the side, then where along that side's cross axis |
295
+ | `overlay-side` / `overlay-align` | the same two things written separately |
296
+ | `overlay-gap` | distance from the anchor |
297
+ | `position-try-fallbacks` | `"top start, right start, left start"` — areas to try when the declared one does not fit |
298
+ | `position-try-order` | `most-space` takes the roomiest candidate instead of the first that fits |
299
+
300
+ `position-area`'s second word may be logical (`start`, `center`, `end`) or
301
+ physical (`left`, `right`, `top`, `bottom`); which axis a physical word means
302
+ depends on the side, exactly as in CSS. `center` and `cover` are sides of their
303
+ own: a modal centred on the page, and a backdrop covering it.
304
+
305
+ The candidates are tried in order — the declared area, then each fallback, then
306
+ the **opposite side**, which is always appended and is the flip this pass has
307
+ always done. The first candidate that is *wholly on the page* wins. With
308
+ `position-try-order: most-space` every fitting candidate is scored by the free
309
+ room left over and the roomiest wins. When none fits, the one that hangs off by
310
+ the least does, and it is then shifted back onto the page: a surface half off
311
+ the page is worse than one covering its anchor.
312
+
313
+ Where it actually went is written back onto the element as
314
+ `overlayPlacedSide` and `overlayPlacedAlign` — fields, not properties, because
315
+ they are the pass's output and an input the layout also writes would read last
316
+ frame's answer. An arrow that has to point the other way when a menu opens
317
+ upwards reads them, and so does every test here.
318
+
319
+ #### Placing one edge: `anchor()`
320
+
321
+ `position-area` puts a whole surface on a side of its anchor, which is what a
322
+ menu wants. A badge hanging off a card's corner is a different statement — one
323
+ edge of this box on one edge of that one — and no area can say it. That is
324
+ CSS's `anchor()`, in the inset properties:
325
+
326
+ ```json
327
+ {"tag": "div", "props": {"anchor-name": "--card"}},
328
+ {"tag": "div", "props": {
329
+ "position-anchor": "--card",
330
+ "left": "calc(anchor(right) - 12px)",
331
+ "top": "calc(anchor(top) - 10px)",
332
+ "width": "24px", "height": "24px"
333
+ }}
334
+ ```
335
+
336
+ | In | Written as | Means |
337
+ | --- | --- | --- |
338
+ | `left` / `right` | `anchor(left)`, `anchor(right)`, `anchor(center)` | that edge of the anchor |
339
+ | `top` / `bottom` | `anchor(top)`, `anchor(bottom)`, `anchor(center)` | that edge of the anchor |
340
+ | any of them | `calc(anchor(…) + 8px)`, `calc(anchor(…) - 8px)` | that edge, offset |
341
+
342
+ `left` and `top` place this box's near edges; `right` and `bottom` place its far
343
+ ones. Both insets on one axis **stretch** the box between them, the same rule
344
+ `left` with `right` already follows. An edge from the wrong axis — `left:
345
+ anchor(bottom)` — is reported and dropped rather than guessed at. One anchor
346
+ reference and at most one length: anything more would need a real `calc()`,
347
+ which this engine does not have.
348
+
349
+ One consequence worth knowing: `position-anchor` makes the element a surface,
350
+ so a badge placed this way is out of the flow and drawn in the top layer, above
351
+ the page and outside every clip. When you want it clipped with its container
352
+ and stacked with it, position it with `position: absolute` inside that
353
+ container instead — the geometry is yours to write, and nothing looks it up.
354
+
355
+ #### When it does not fit
356
+
357
+ | Property | Notes |
358
+ | --- | --- |
359
+ | `fit-viewport` | clamp the surface to the room on the side it landed on |
360
+ | `overflow` (`overflow-y`) | anything but `visible` clips, and then `scrollHeight` is how much more there is |
361
+
362
+ A menu of forty rows does not fit under anything. `fit-viewport: true` cuts it
363
+ to the room there is rather than letting it run off the page; with `overflow`
364
+ set it clips, and `scrollHeight` minus `clientHeight` is how far there is to
365
+ scroll — the ordinary scroll model, no second mechanism. The field
366
+ `overlayClamped` says it happened. Pair it with `position-try-order: most-space`, so the side chosen
367
+ is the roomier one before the clamp rather than the first that nearly fits.
368
+
369
+ This engine has one `overflow` per box, not one per axis; `overflow-y` and
370
+ `overflow-x` are accepted and set it, because `overflow-y: auto` on a menu is
371
+ how the web writes "and scroll if it is too tall" and dropping it silently was
372
+ worse than clipping both axes. Clipping both is safe for a submenu: a nested
373
+ surface is drawn in the top layer, outside every clip.
374
+
375
+ #### When it is the wrong widget
376
+
377
+ | Property | Notes |
378
+ | --- | --- |
379
+ | `presentation` | `anchored` (the default), `sheet`, `fullscreen` |
380
+ | `sheet-below` | become a sheet at or below this page width |
381
+
382
+ This is the one part with no CSS equivalent, and the reason it exists: at 390
383
+ wide an anchored menu is not badly placed, it is the wrong widget. No fallback
384
+ list fixes that. `presentation: sheet` drops the anchor entirely — the surface
385
+ becomes as wide as the page, as tall as its content needs, pinned to the bottom
386
+ edge, and its children are laid out again at that width. `fullscreen` takes the
387
+ page. `sheet-below: 600px` is the one line that covers the usual case; a
388
+ `@media` block setting `presentation` is the general route, and both arrive at
389
+ the same place. The field `overlayPlacedPresentation` says which it ended up
390
+ being.
391
+
392
+ #### The pass
393
+
394
+ ```
395
+ normal layout
396
+ ↓
397
+ surfaces, once every anchor has a rectangle
398
+ ↓
399
+ presentation anchored | sheet | fullscreen
400
+ ↓
401
+ anchor position-anchor → the name registry
402
+ ↓
403
+ insets anchor() in left/top/right/bottom, if any — done
404
+ ↓
405
+ placement position-area, then each fallback, then the flip
406
+ ↓ first that fits, or the roomiest, or the least bad
407
+ fit clamp to the room, measure the scroll extent
408
+ ↓
409
+ shift onto the page
410
+ ↓
411
+ top layer, drawn after everything, outside every clip
412
+ ```
413
+
414
+ What is **not** here: dismissal, focus policy, and the stack that keeps a
415
+ submenu open while its parent is. Those are interaction, not layout, and belong
416
+ with [`EVGFocus`](EVGFocus.rgr) and [`EVGCommands`](EVGCommands.rgr). A surface
417
+ with no anchor is reported by name rather than placed silently at the origin —
418
+ see `EVGLayout.getOverlayErrors()`.
419
+
420
+ Most of the vocabulary above is CSS's own — `anchor-name`, `position-anchor`,
421
+ `position-area`, `position-try-fallbacks`, `position-try-order` — so what you
422
+ know about anchor positioning transfers. `presentation`, `sheet-below` and
423
+ `fit-viewport` are EVG's, because the web builds those out of media queries and
424
+ a popover by hand.
425
+
426
+ `npm run evg:popover:test` and `npm run evg:overlay:test`.
427
+
428
+ ### Connectors
429
+
430
+ A `connector` is a line between two elements whose geometry the **layout**
431
+ writes. `path` is the other half of the pair: there the author owns `d` and the
432
+ layout owns nothing.
433
+
434
+ | Property | Notes |
435
+ | --- | --- |
436
+ | `anchor-name` | names this element so a connector can point at it, as in CSS Anchor Positioning: `--orders` |
437
+ | `from` / `to` | an `anchor-name` or an `#id` |
438
+ | `from-side` / `to-side` | `left`, `right`, `top`, `bottom`, `center`, the four corners — or `auto` |
439
+ | `routing` | `straight`, `orthogonal`, `bezier` |
440
+ | `from-offset` / `to-offset` | a gap between the box edge and the line's end |
441
+ | `arrow-start` / `arrow-end` | `none`, `open` (two strokes), `triangle` (filled) |
442
+ | `arrow-size` | the head's length |
443
+
444
+ It is drawn with the stroke vocabulary a path already has — `stroke`,
445
+ `stroke-width`, `stroke-linecap`, `stroke-linejoin`, `stroke-dasharray`. There
446
+ is no connector-specific styling and there should not be.
447
+
448
+ ```json
449
+ {"tag": "div", "props": {"anchor-name": "--orders"}},
450
+ {"tag": "div", "props": {"anchor-name": "--revenue"}},
451
+ {"tag": "connector", "props": {
452
+ "from": "--orders", "to": "--revenue",
453
+ "stroke": "rgb(250,204,21)", "stroke-width": "3px",
454
+ "arrow-end": "triangle", "arrow-size": "10px"
455
+ }}
456
+ ```
457
+
458
+ `auto` is the point of the tag. The connector above leaves the right edge of
459
+ one card and arrives at the left edge of the other while they sit side by side;
460
+ reflow the same document to one column and it leaves the bottom and arrives at
461
+ the top, because the sides are decided from where the boxes ended up rather
462
+ than from where they were when the document was written. A `path` at
463
+ `left: 171px` cannot do that, and that is what it was doing before.
464
+
465
+ Connectors are solved after layout and after the overlay pass, so a line can
466
+ point at anything, declared anywhere. What cannot be resolved is reported —
467
+ `connector: nothing is called "--revenue"` — and draws nothing rather than a
468
+ line to the origin. The heads are filled and the shaft is stroked, which is why
469
+ they are separate paths internally (`EVGElement.arrowPath`): a fill closes every
470
+ subpath it is given, and an orthogonal shaft closed is a triangle nobody asked
471
+ for.
472
+
473
+ See [`EVGConnector.rgr`](EVGConnector.rgr) and its test,
474
+ `npm run evg:connector:test`.
475
+
476
+ ### Interaction and meaning
477
+
478
+ `transition` (see [Interaction](#interaction)), the ARIA surface (see
479
+ [Accessibility](#accessibility)), and `evg-surface-effect` with `evg-effect-on`
480
+ and the `evg-fx-*` / `evg-ripple-*` parameters, which are GPU passes and are
481
+ dropped by the painters that have no render target.
482
+
483
+ #### Surface effects belong to an element
484
+
485
+ ```css
486
+ .hero-sky { evg-surface-effect: starfield; evg-effect-on: always; evg-fx-density: 1.6 }
487
+ .pool { evg-surface-effect: ripple; evg-effect-on: press drag }
488
+ ```
489
+
490
+ `evg-surface-effect` names WHAT runs, `evg-effect-on` says WHAT STARTS IT, and
491
+ `evg-fx-<name>: <number>` passes parameters the engine never reads — WHERE is
492
+ the element's own border box, because the layout already worked it out. So the
493
+ document decides which card ripples, and no application code holds a drop, a
494
+ clock or a coordinate.
495
+
496
+ The display list carries one instance per element that declared one, under the
497
+ element's `id`, and the painter looks the name up in a registry:
498
+
499
+ ```js
500
+ registerSurfaceEffect({ name, layer: "source" | "filter", params, frag })
501
+ ```
502
+
503
+ A **source** is drawn in paint order at the element's own background, so the
504
+ element's content is painted over it; a **filter** runs over the finished
505
+ surface, clipped to the box, which is how the ripple bends text it knows
506
+ nothing about; a **backdrop** is a filter in paint order — it reads what is
507
+ behind the element, writes it back changed, and then the element's own
508
+ background, border and children are drawn on top, sharp. Each is one GLSL
509
+ function and a parameter list, and the box mask is applied for them — a plugin
510
+ cannot paint outside its own element.
511
+
512
+ Seven ship with the painter:
513
+
514
+ | name | layer | what it is |
515
+ | --- | --- | --- |
516
+ | `ripple` | filter | rings through the finished surface, from a press or a drag |
517
+ | `starfield` | source | stars and dust on the element's own background |
518
+ | `liquid-glass` | backdrop | refraction at the rim, with a sweep that crosses it |
519
+ | `plasma-wave` | source | drifting ribbons of light |
520
+ | `raindrop` | backdrop | drops on the pane, each one a small lens over the page |
521
+ | `ambient-light` | source | a slow wash of colour with soft orbs in it |
522
+ | `smoke` | source | a bank of smoke rising through the box, or a cloud filling it |
523
+
524
+ `liquid-glass` is refraction rather than fog, and it composes with the CSS that
525
+ was already there:
526
+
527
+ ```css
528
+ .glass {
529
+ backdrop-filter: blur(9px); /* the frosting — already EVG's */
530
+ background-color: rgba(255,255,255,.07); /* the tint — an ordinary fill */
531
+ border-radius: 30px; /* the shape, said once */
532
+ evg-surface-effect: liquid-glass; /* and the lens at its rim */
533
+ evg-fx-strength: 40;
534
+ }
535
+ ```
536
+
537
+ The bend follows the element's own rounded box, so a pane is a lens at whatever
538
+ size the layout gave it — nothing in the plugin knows the shape in advance.
539
+
540
+ `lib/evg/gl/evg-fx.js` is the host's side: it hit-tests the boxes the list
541
+ carries and turns pointer events into the events the shaders read.
542
+ An instance the host marks `off` is skipped by the painter and the driver
543
+ both — no pass, no shader, and a press on it falls through to whatever is
544
+ under it. That is the hook a page's own switch hangs on, and the one
545
+ `prefers-reduced-motion` would; `fx-demo.html` has a switch per effect.
546
+
547
+ [`lib/evg/gl/effect-presets.css`](gl/effect-presets.css) is fourteen blocks
548
+ that are already a look — five skies, two plasma fields, two rains, two washes
549
+ and three of smoke —
550
+ paste-able into the editor under the gallery demo or into a stylesheet of your
551
+ own. `npm run evg:fx:shots` paints every one of them into a contact sheet, and
552
+ `fx-check` reads the same file through the engine's own cascade, so a preset
553
+ nobody can parse fails a check rather than quietly drawing the defaults.
554
+
555
+ The original whole-surface effect (`list.effect`, drops pushed in by the
556
+ application) is unchanged and still runs. [`PLAN_EFFECTS.md`](PLAN_EFFECTS.md)
557
+ has the design, what it does not do yet, and how to write a plugin;
558
+ `npm run evg:fx:test`, `npm run evg:fx:check`, `npm run evg:fx:demo` and
559
+ `npm run evg:fx:shots`.
560
+
561
+ ### Identity
562
+
563
+ `id`, `key`, `className`, `theme`, `role`, `pageWidth`, `pageHeight`.
564
+
565
+ ---
566
+
567
+ ## Units
568
+
569
+ A length is an `EVGUnit`: a number, a unit type, and the pixels it resolved to.
570
+
571
+ | Suffix | Resolves against |
572
+ | --- | --- |
573
+ | `px` | itself — the CSS reference pixel, which is what everything below is defined in terms of |
574
+ | `%` | the parent's **width** on a width property, the parent's **height** on a height property |
575
+ | `hp` | the parent's height, on any property |
576
+ | `em` | the element's own font size |
577
+ | `rem` | the **root** font size |
578
+ | `vw` `vh` | the layout's page size — see below |
579
+ | `fill` | whatever space is left |
580
+ | `pt` `pc` `in` `mm` `cm` | `96/72`, `16`, `96`, `96/25.4`, `96/2.54` px — exactly as CSS defines them |
581
+
582
+ `vw` and `vh` are the viewport, which is a different thing from the parent, and
583
+ that difference is invisible on a root element and load-bearing everywhere else.
584
+ `EVGLayout.setPageSize` is what they mean, and every host already calls it. On
585
+ paper that is the **page area** — the sheet less its margins — because that is
586
+ what CSS says viewport-percentage lengths mean in paged media, and what
587
+ `EVGPDFRenderer` sets. So `100vh` is the text column on A4 and the window in a
588
+ browser, with no print-specific code anywhere. [`EVGViewportUnitTest.rgr`](EVGViewportUnitTest.rgr)
589
+ exists to keep it that way.
590
+
591
+ An **unrecognised suffix leaves the unit unset** — i.e. auto — rather than
592
+ falling through to the bare-number path, because `to_double("10ch")` is `10`
593
+ and the length would silently become `10px`.
594
+
595
+ Not implemented: `calc()`, `ch`, `ex`, and the container-relative units.
596
+
597
+ ---
598
+
599
+ ## The CSS subset
600
+
601
+ [`EVGStyleSheet.rgr`](EVGStyleSheet.rgr) is deliberately not a browser cascade.
602
+ It supports what a document needs in order to change its look without editing
603
+ its tree:
604
+
605
+ ```css
606
+ .caption { … } /* class rule, every theme */
607
+ .theme-classic .caption { … } /* only under theme "classic" */
608
+ .a, .b { … } /* selector lists */
609
+ .btn:hover { … } /* interaction state */
610
+ /* comments */
611
+
612
+ @media (max-width: 640px) { … }
613
+ @media (min-width: 900px) and (orientation: landscape) { … }
614
+ @media (pointer: coarse) { … }
615
+
616
+ @vars { --ink: #09090b; } /* the palette, every theme */
617
+ @vars classic { --ink: #1b1a17; } /* …and what "classic" makes of it */
618
+ .caption { color: var(--ink); }
619
+ ```
620
+
621
+ **Resolution order**, with source order breaking ties inside each group:
622
+
623
+ ```
624
+ unscoped class rules < theme-scoped class rules < inline attributes
625
+ ```
626
+
627
+ Inline always wins: the applier skips any property the authoring layer already
628
+ set, which `EVGElement` records in `inlineProps`.
629
+
630
+ There are no IDs, no element selectors, no `!important`, no descendant
631
+ combinators beyond the theme scope, and no specificity arithmetic beyond
632
+ "theme-scoped beats unscoped".
633
+
634
+ ### Pseudo-classes
635
+
636
+ `:hover`, `:focus`, `:active` and `:disabled`, read off the element's own
637
+ `isHovered` / `isFocused` / `isPressed` / `a11yDisabled` flags. A controller
638
+ never writes a second class name for a state the sheet can ask about.
639
+
640
+ ### `@media`
641
+
642
+ A media block is a **condition on the rules inside it, not a new kind of
643
+ selector**: the rules keep the specificity they would have had outside, and a
644
+ block that does not match contributes nothing.
645
+
646
+ | Feature | Values |
647
+ | --- | --- |
648
+ | `min-width` `max-width` | CSS pixels |
649
+ | `min-height` `max-height` | CSS pixels |
650
+ | `orientation` | `portrait`, `landscape` |
651
+ | `pointer` | `coarse` (a finger), `fine` (a mouse) |
652
+
653
+ Combine with `and`; nested blocks mean both conditions hold, with the tighter
654
+ bound winning. A condition nobody could parse is *kept* rather than dropped, so
655
+ the rules inside it never apply — a misspelt query that styles everything is
656
+ worse than one that styles nothing, because it is invisible until it is not.
657
+
658
+ Conditions are evaluated against a viewport **the caller states**, because a
659
+ Ranger program has no window to ask:
660
+
661
+ ```ranger
662
+ sheet.setViewport(w h coarse) ; before applyTree
663
+ sheet.applyTreeIn(root theme w h coarse) ; or both in one call
664
+ ```
665
+
666
+ **With no viewport stated, a conditional rule does not apply at all.** A media
667
+ query that cannot be evaluated has no truth value, and guessing "yes" would
668
+ style a print page for a phone.
669
+
670
+ ### Custom properties
671
+
672
+ ```css
673
+ @vars { --ink: #09090b; --line: #e4e4e7; --brand: #14b8a6; }
674
+ @vars marine { --ink: #0a3344; --line: #b6d4e0; }
675
+ @media (max-width: 640px) { @vars { --gap: 8px; } }
676
+
677
+ .card { border: 1px solid var(--line); color: var(--ink); }
678
+ .badge { background-color: var(--brand, #333); } /* with a fallback */
679
+ ```
680
+
681
+ **The palette is sheet-level, and `@vars` says so.** In a browser `--x` is an
682
+ inherited property of an *element*, and `:root { --x }` is only the commonest
683
+ place to put it. This engine has no root element and no inheritance, so a
684
+ palette written on a class would look element-scoped and not be — `--x` inside
685
+ a class rule is therefore **an error**, not a thing that half works.
686
+
687
+ A declaration inside `@media` carries that condition, exactly as a rule does.
688
+ Precedence is the sheet's own, applied to the palette: theme-scoped beats
689
+ unscoped, later beats earlier among equals, and a block whose media condition
690
+ does not hold is not a candidate. A variable may be written in terms of
691
+ another, so a theme can move one name and everything defined from it follows.
692
+
693
+ `var(--x)` with **no definition and no fallback drops the declaration** and
694
+ reports it: half a shorthand is worse than the value that was already there,
695
+ and the error list says where. So does an unclosed `var(` and a definition
696
+ cycle.
697
+
698
+ **What it costs: nothing measurable.** `var()` is resolved where a rule's value
699
+ becomes an element's — when a *plan* is built, once per (class, theme, state)
700
+ — and never per element. Measured on the bench table with every colour in the
701
+ sheet turned into a variable, both the full pass and the skip pass are
702
+ unchanged within run-to-run noise: 22,403 elements are styled from **18**
703
+ plans, so the palette is read eighteen times over and not once per element per
704
+ frame. Substitution is textual and happens before the value reaches
705
+ `setAttribute`, which is what lets it work inside a shorthand
706
+ (`border: 1px solid var(--line)`) with no property having to know variables
707
+ exist.
708
+
709
+ [`EVGStyleVarTest.rgr`](EVGStyleVarTest.rgr) (`npm run evg:stylevar:test`)
710
+ covers the meaning and the cost; the cache suite's fixture uses variables too,
711
+ so the two places a value is resolved — the plan builder and the direct scan it
712
+ is checked against — cannot drift apart.
713
+
714
+ ### Transitions
715
+
716
+ ```css
717
+ .btn { transition: background-color 150ms ease 50ms }
718
+ ```
719
+
720
+ `transition` is a property like any other; [`EVGTransition`](EVGTransition.rgr)
721
+ is the clock. See [Interaction](#interaction).
722
+
723
+ ### The style cache
724
+
725
+ `applyTree` is not cheap and it runs on every frame, so the resolution is cached
726
+ on a key of class list, theme, interaction bits *and* the viewport — the last
727
+ one because `@media` makes the surface part of the answer. The pass also reports
728
+ whether anything it wrote can have moved a box (`layoutClean()`) or changed a
729
+ pixel at all (`nothingChanged()`), which lets a host skip layout on a hover.
730
+
731
+ ---
732
+
733
+ ## Responsive layout
734
+
735
+ Everything needed for a layout that answers the size it is given, and a live
736
+ page that does it:
737
+
738
+ * **`@media`** on width, height, orientation and pointer, above.
739
+ * **`vw` / `vh`**, which are the page rather than the parent.
740
+ * **Percentages**, which are the parent.
741
+ * **`flex-wrap` with `min-width`**, which needs no rule at all — the line simply
742
+ runs out and the next item goes below.
743
+ * **`grid-template-columns`** restated per breakpoint, which is how a card deck
744
+ goes from four across to one.
745
+ * **`min-width` / `max-width` clamps**, for the bounds a layout should never
746
+ cross whatever the window does.
747
+
748
+ Not implemented: `auto-fit` / `auto-fill` inside `repeat()` (the count must be a
749
+ number), container queries, and `calc()`.
750
+
751
+ ### The live demo
752
+
753
+ [`web/responsive/`](web/responsive/) is a page as wide as the browser window
754
+ that is laid out **again on every resize** — the compiled engine runs in the
755
+ browser, and the browser is handed finished pixels.
756
+
757
+ ```sh
758
+ npm run evg:responsive:web:serve # build + serve on http://localhost:8007/
759
+ npm run evg:responsive:check # the same layout, four widths, no browser
760
+ npm run evg:responsive:web:smoke # the built page, driven in Chromium
761
+ ```
762
+
763
+ Its tree carries no numbers at all; the three `@media` blocks in its stylesheet
764
+ are the entire difference between the wide layout and the phone one. Drag the
765
+ window edge and the whole pipeline — build, cascade, layout, display list,
766
+ paint — runs again, in about a millisecond of layout per frame at 1400px.
767
+
768
+ The checks assert what the breakpoints are *for* rather than what they say: the
769
+ card columns are counted by grouping the laid-out cards by their `y`, and the
770
+ sidebar's move is read off where its box ended up.
771
+
772
+ ---
773
+
774
+ ## Layout
775
+
776
+ [`EVGLayout.rgr`](EVGLayout.rgr) resolves units, measures text, and gives every
777
+ element a rectangle.
778
+
779
+ **The box model** is CSS's, with `padding` and `border` inside the declared
780
+ width. `EVGBox` holds the resolved pixels (`paddingLeftPx`, `borderWidthPx`, …)
781
+ after `resolveUnits` has run, and `resolveUnits` deliberately refuses to run
782
+ twice on the same element so a percentage is never resolved against an
783
+ already-resolved parent. Anything laid out more than once therefore has to start
784
+ from `resetLayoutState()`, which `layout()` calls.
785
+
786
+ **Flow** is a column of boxes. **Flex** is `display: flex` with direction, wrap,
787
+ justify, align, `align-self` and the two gaps; a text leaf shrink-wraps to its
788
+ measured content rather than claiming the parent's width. **Grid** is
789
+ `display: grid` with fixed, percentage and `fr` tracks, `repeat()`, `minmax()`,
790
+ named areas, spans and row `subgrid`.
791
+
792
+ **Sizing a flex line** is CSS's "resolve flexible lengths" in both directions:
793
+ a `flex-basis` is the item's starting main size whether or not it grows, the
794
+ free space is shared by `flex-grow` and the overflow by `flex-shrink` × base,
795
+ and an item that hits `min-width` or `max-width` is FROZEN at the limit and
796
+ what it did not take is offered to the rest. `max-width` is applied before
797
+ `min-width`, so when the two contradict each other the minimum wins.
798
+ [`EVGFlexRulesTest.rgr`](EVGFlexRulesTest.rgr) (`npm run evg:flexrules:test`)
799
+ is the statement of all five rules, and
800
+ [`bench/`](bench/) is where they were found — the same CSS through this engine
801
+ and through Chromium, box for box.
802
+
803
+ Three things worth knowing, all documented at their source:
804
+
805
+ * `align-items` defaults to **`flex-start`**, not `stretch`. An auto cross size
806
+ is therefore fit-content, so a column whose children should fill it says
807
+ `width: 100%` or `align-items: stretch`.
808
+ * There is no `position: fixed`. An absolutely positioned box belongs to the
809
+ content of the box it is positioned against, so scrolling a container moves
810
+ everything inside it — absolutes and overlays alike, which is what makes a
811
+ dropdown stay with its trigger.
812
+ * `flex-wrap` **initialises to `wrap`**, where CSS's initial value is `nowrap`.
813
+ A row that must stay on one line says `flex-wrap: nowrap`, which also enables
814
+ the row-axis shrink pass. The wrap test itself allows a hundredth of a pixel
815
+ of overflow, because a `flex: 1` child's width was computed out of the very
816
+ line it is then measured against and the parts do not always add back up to
817
+ the whole — see ISSUES #8, and `EVGFlexWrapTest.rgr`, which sweeps a sidebar
818
+ and a flexible panel across 5600 widths to keep it that way.
819
+
820
+ **The root** with no stated size becomes the page: `EVGLayout` gives it
821
+ `pageWidth` × `pageHeight`. That is right for paper and not for a window, where
822
+ the content is free to be taller and scroll — a host that wants the document's
823
+ own height measures the children's lowest edge, as
824
+ [`EvgResponsiveDemo.measuredHeight`](web/responsive/EvgResponsiveDemo.rgr) does.
825
+
826
+ **Layout warnings** are collected rather than printed: `warningCount()` /
827
+ `warningAt(i)`. The showcase build fails on them.
828
+
829
+ **And a declaration the engine cannot use is one of them.** `calc()`,
830
+ `width: min-content`, `aspect-ratio`, `align-self` before it existed,
831
+ `repeat(auto-fit, …)`, a reversed flex direction — each of these used to be
832
+ dropped in silence, and a dropped `width` is not neutral: in a row it fills the
833
+ parent, so `width: calc(100% - 40px)` did not fail to apply, it applied as
834
+ `width: 100%`. [`EVGReject`](EVGReject.rgr) collects every one and `layout()`
835
+ drains it into the same list. It de-duplicates and caps itself, so one bad rule
836
+ applied to 22,403 elements is one warning and not 22,403 — measured, because
837
+ the style cache replays a plan per element per frame.
838
+
839
+ ---
840
+
841
+ ## Text and fonts
842
+
843
+ Text is measured with an `EVGTextMeasurer`, and which one you give the layout
844
+ decides how honest the answer is:
845
+
846
+ | Measurer | Metrics from | For |
847
+ | --- | --- | --- |
848
+ | `EVGTextMeasurer` (base) | `fontSize * 0.55` per character | nothing; it is the floor |
849
+ | `SimpleTextMeasurer` | a measured advance table, one entry per printable character, taken from a browser's sans fallback | headless work, and the browser demos |
850
+ | `TTFTextMeasurer` | the TTF the output will embed, kerned from the face's own GPOS pairs | print |
851
+ | `EVGContextMeasurer` | the host renderer's own loaded faces | an interactive app that paints through `UIContext` |
852
+ | `EVGHostTextMeasurer` | **the platform that will paint** — canvas `measureText`, CoreText, Skia's `Paint`, Java2D — through one function the host hands over | every screen app; see below |
853
+
854
+ `isFontAccurate()` is how the engine knows the difference: a measurer that never
855
+ opens a font must not silently drive print layout, and `EVGTextEngine` asks
856
+ before letting a document that names custom faces through.
857
+
858
+ **The platform measures, EVG breaks the lines.** A screen app draws with a face
859
+ the platform chose, and the table is a snapshot of one browser's sans; where
860
+ the two differ a caret lands beside its glyphs and a label clips its box.
861
+ [`EVGHostTextMeasurer`](EVGHostTextMeasurer.rgr) closes that with **one
862
+ function** a host provides — `metric(kind text family size bold italic)`: a
863
+ run's width, or a face's ascent, descent and line gap — and keeps everything
864
+ else in Ranger: the per-face cache, the `-Bold` convention
865
+ `effectiveFontFamily` writes the weight in, the cache key, and the fallback to
866
+ the table until the platform attaches. The lines are still broken here, one
867
+ run per line, so a PDF and a screen that share a face still break in the same
868
+ place. The hosts are a page each:
869
+
870
+ | Platform | File | Installed by |
871
+ | --- | --- | --- |
872
+ | browser | [`gl/evg-measure.js`](gl/evg-measure.js) — canvas `measureText`, the painters' own `fontSpec`, the gap off a `line-height: normal` probe; works in a Worker | `gallery/ui/demo`, `gallery/realtrainer/web`, `gallery/ui/web`, `web/responsive` |
873
+ | Apple | [`apple/Sources/CoreTextMeasurer.swift`](apple/Sources/CoreTextMeasurer.swift) — the `CTFont` the surface draws with, `CTLineGetTypographicBounds` | `ui/ios`, its watch app, `realtrainer/ios` |
874
+ | Android | [`android/src/android/…/AndroidTextMeasurer.kt`](android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt) — the `FaceSet`'s `Typeface` through a `Paint`; [`AwtTextMeasurer.kt`](android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt) is the Java2D twin the desktop checks use | `ui/android`, `realtrainer/android`, both `CheckDashboard`-style checks |
875
+
876
+ They reach every layout through **`EVGDefaultMeasurer`** (in
877
+ `EVGTextMeasurer.rgr`): a process-wide default that `EVGLayout` and
878
+ `EVGTextEngine` read in their constructors, so the twenty-five `new
879
+ EVGLayout()`s in the gallery pick it up without being told. A host installs
880
+ before the app is constructed — the demos keep a layout from the moment they
881
+ exist — and `setMeasurer` still overrides it, which is how print keeps the TTF
882
+ one. `npm run evg:hostmeasurer:test` drives the Ranger half with a made-up
883
+ platform, and `npm run evg:measure:web` opens two built pages in Chromium and
884
+ asks whether a run measured through EVG is the width the painter's canvas
885
+ gives the same font shorthand; PLAN_NATIVE_HOSTS.md S0 is where it came from.
886
+
887
+ The vertical metrics are measured, not rounded: the sans fallback's ascent is
888
+ `0.905em` and its descent `0.212em`, summing to `1.117em` — no real face sums to
889
+ `1.00`, and an ascent a tenth of an em short draws every run of text that much
890
+ high. `line-height: normal` is `1.15em` for that face, not `1.2`.
891
+
892
+ [`EVGTextEngine`](EVGTextEngine.rgr) breaks paragraphs into lines, and the
893
+ display list is given the *same* engine so it breaks them in exactly the same
894
+ places. [`EVGGrapheme`](EVGGrapheme.rgr) and [`EVGCodepoint`](EVGCodepoint.rgr)
895
+ are what make "one character" mean what a reader means — 🇫🇮 is two codepoints,
896
+ 👨‍👩‍👧 is five, and each is one glyph, one advance, one caret stop.
897
+
898
+ ---
899
+
900
+ ## The display list
901
+
902
+ `EVGDisplayList.build(root)` flattens the laid-out tree into `EVGDrawCmd`s:
903
+
904
+ | Kind | | Carries |
905
+ | --- | --- | --- |
906
+ | `0` | `RECT` | x, y, w, h, colour, radii, gradient, shadow |
907
+ | `1` | `BORDER` | the same, plus thickness |
908
+ | `2` | `IMAGE` | source, quad, flips, rotation, the crop for `object-fit: cover` |
909
+ | `3` | `TEXT` | the run, x, y, size, colour, family, weight, italic, the line box |
910
+ | `4` `5` | `PUSH_CLIP` / `POP_CLIP` | a rectangle, and a stack |
911
+ | `6` | `PATH` | rings, fill rule |
912
+ | `7` | `STROKE` | a polyline and a thickness |
913
+
914
+ Three ways out:
915
+
916
+ * **`toJson()`** — what a browser gets. Gradients and shadows do not survive it,
917
+ so a JSON-fed backend cannot draw them.
918
+ * **`toBinary()`** — an `EVGSceneBinary` with an interned string pool, for a
919
+ native host, and for a browser host whose engine is in a Worker. Its record
920
+ width is published in the format rather than agreed in advance; see ISSUES
921
+ #4 for why that sentence is there. The record is 36 ints: since the list
922
+ started crossing a thread it also carries the other three corners, the
923
+ scroll layer a clip opens, and the shadow — which no JSON ever did.
924
+ [`gl/evg-binary.js`](gl/evg-binary.js) reads it back into the JSON's shape,
925
+ and `npm run evg:binary:check` holds it to the object reader.
926
+ * **the objects** — which is what a Kotlin or Swift host does, and why those
927
+ painters can draw gradients, shadows and multi-ring paths that a JSON one
928
+ cannot.
929
+
930
+ `offsetBy` and `appendFrom` compose lists, which is how a multi-page document is
931
+ assembled out of per-page layouts.
932
+
933
+ ---
934
+
935
+ ## Targets
936
+
937
+ Everything above the display list is one body of code. Everything below it is a
938
+ painter that knows about quads, glyph runs and scissor rectangles.
939
+
940
+ | Target | Where | Notes |
941
+ | --- | --- | --- |
942
+ | **PDF** | `gallery/pdf_writer/src/core/EVGPDFRenderer.rgr` | the print target: real vector operators, embedded subset fonts, UTF-8 and WinAnsi |
943
+ | **PNG / raster** | `gallery/pdf_writer/src/raster/EVGRasterRenderer.rgr` | anti-aliased scanline fill, the same one that paints the glyphs |
944
+ | **HTML** | `gallery/pdf_writer/src/core/EVGHTMLRenderer.rgr` | the debug view: absolutely positioned boxes and an inline `<svg>` |
945
+ | **SVG / DOM** | [`html/evg-html.js`](html/evg-html.js) | 500 lines, in the browser, from the display list |
946
+ | **Retained DOM** | [`html/evg-dom.js`](html/evg-dom.js) | one node per element, patched from the host tree's ops — the nodes survive a frame |
947
+ | **WebGL 2** | [`gl/evg-webgl.js`](gl/evg-webgl.js) | one instanced quad per command; rounded corners from a distance field |
948
+ | **SDL2 + OpenGL** | `lib/evg/gl/evg_gl_host.rgr` | the same list through the C++ target |
949
+ | **Android / AWT** | [`android/`](android/) | `EvgPainter.kt` walks the list once; `EvgSurface` is Canvas or Graphics2D |
950
+ | **Apple** | [`apple/`](apple/) | `EvgPainter.swift`, a transliteration of the Kotlin one, over CoreGraphics |
951
+
952
+ The SVG backend is the evidence that the seam is a seam: it shares no code with
953
+ the GL one, and the two are differenced pixel for pixel over the same frames —
954
+ 0.022% on a sheet built to exercise every command kind, 0.000% on every slide of
955
+ the `.pptx` deck.
956
+
957
+ The same document is also a `.pptx` slide, a `.docx` page and a printed book
958
+ elsewhere in `gallery/`. That is the point of the format.
959
+
960
+ ---
961
+
962
+ ## Interaction
963
+
964
+ **Hit testing.** [`EVGHitTest`](EVGHitTest.rgr) answers in **paint order,
965
+ backwards** — the same order the display list draws. A tree walk is nearly the
966
+ same answer and differs exactly where it matters: an open menu's panel is drawn
967
+ above the trigger beside it, and a tree walk would take the click through the
968
+ panel. It is also what makes a modal modal — the backdrop covers the page, so a
969
+ click outside the dialog lands on it.
970
+
971
+ **Transitions.** [`EVGTransition`](EVGTransition.rgr) holds a flight per
972
+ property: where it left from, where it is going, a clock, and an easing. The
973
+ host advances it (`advanceTree(root dtMs)`), then `reconcileTree(root)` leaves
974
+ on each element the value that is actually *showing* — which, for a property in
975
+ flight, is neither end. Reversals are handled the way CSS specifies: a hover
976
+ that leaves half way comes back in half the time, and one that lands on a third
977
+ colour gets the full duration.
978
+
979
+ **Easing.** [`EVGEasing`](EVGEasing.rgr): the named curves and `cubic-bezier()`.
980
+
981
+ **Components.** [`EVGComponent`](EVGComponent.rgr) is an instance that outlives
982
+ the tree it produces, so a control can keep state across a rebuild.
983
+ [`EVGWindow`](EVGWindow.rgr), [`EVGToolbar`](EVGToolbar.rgr),
984
+ [`EVGRuler`](EVGRuler.rgr) and [`EVGSelectChrome`](EVGSelectChrome.rgr) are
985
+ backend-agnostic pieces built on top of it, shared by the document apps in
986
+ `gallery/`.
987
+
988
+ ---
989
+
990
+ ## Accessibility
991
+
992
+ A canvas contributes one empty graphic to a browser's accessibility tree no
993
+ matter what was drawn into it, so an EVG frame publishes a **second list**
994
+ beside the display list: what it *means*.
995
+
996
+ [`EVGA11yTree`](EVGA11yTree.rgr) is that list;
997
+ [`EVGA11yFromTree`](EVGA11yFromTree.rgr) derives it from the element tree; and
998
+ [`gl/evg-a11y.js`](gl/evg-a11y.js) mirrors it into real DOM nodes over the
999
+ canvas, so a screen reader has something to read and a keyboard has something to
1000
+ focus.
1001
+
1002
+ The properties are the ARIA ones, in both spellings: `aria-label` / `a11yLabel`,
1003
+ `role` / `a11yRole`, and `a11yChecked`, `a11yCurrent`, `a11yDescription`,
1004
+ `a11yDisabled`, `a11yExpanded`, `a11yFocusable`, `a11yHasPopup`, `a11yHidden`,
1005
+ `a11yInvalid`, `a11yModal`, `a11yOrientation`, `a11yPressed`, `a11yReadOnly`, `a11yRequired`,
1006
+ `a11yRoleDescription`, `a11yRowCount`, `a11yRowIndex`, `a11ySelected`,
1007
+ `a11ySorted`, `a11yValue`.
1008
+
1009
+ Tri-state where ARIA is tri-state: `a11yExpanded` is not-applicable, no, yes or
1010
+ mixed, because an absent `aria-sort` and a present `aria-sort="none"` are
1011
+ different things and the DOM makes the distinction.
1012
+
1013
+ ---
1014
+
1015
+ ## The engine off the UI thread
1016
+
1017
+ Nothing above the display list needs a window, so on every platform the app
1018
+ can run on a thread of its own and hand the UI thread frames to paint
1019
+ (PLAN_NATIVE_HOSTS.md S1). The shape is the same three times: the host makes
1020
+ the app, then never touches it directly again — every call is posted to the
1021
+ engine in order, a call that changed the page produces a frame there, and the
1022
+ UI thread keeps the last frame and paints it. Three verbs: `post` (no
1023
+ answer), `ask` (an answer, later, on the UI thread), and `sync` for the one
1024
+ read a platform insists on at once (`canBecomeFirstResponder`,
1025
+ `onCheckIsTextEditor`).
1026
+
1027
+ | Platform | The harness | A host on it |
1028
+ | --- | --- | --- |
1029
+ | browser | [`gl/evg-engine.js`](gl/evg-engine.js): a Worker, frames as transferred `EVGSceneBinary`, input batched into the frame request | `gallery/realtrainer/web/main-worker.js` (`?engine=worker`) |
1030
+ | Apple | [`apple/Sources/EvgEngineQueue.swift`](apple/Sources/EvgEngineQueue.swift): a serial `DispatchQueue`, frames delivered to the main thread | `gallery/realtrainer/ios` |
1031
+ | Android / JVM | [`android/src/main/…/EvgEngineThread.kt`](android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt): a single-thread executor, `onMain` is `View.post` | `gallery/realtrainer/android` |
1032
+
1033
+ Frames are coalesced — a burst of posts makes one build after the last — and
1034
+ a kept list that only scrolled crosses as its layers' shifts, not as a list.
1035
+ The cost is that a host cannot read the app synchronously; the RealTrainer
1036
+ check measures what that costs a press (pointer-down to the frame that showed
1037
+ it) on both browser hosts, and prints it.
1038
+
1039
+ ## The host tree
1040
+
1041
+ The display list is deliberately dumb — no identity, so a painter is small
1042
+ — and that is exactly what a host that wants to KEEP nodes cannot use.
1043
+ [`EVGHostTree`](EVGHostTree.rgr) is the fifth list beside the four above,
1044
+ derived from the same laid-out tree by the same rule, and it says what
1045
+ changed rather than what to draw:
1046
+
1047
+ ```
1048
+ CREATE path parentPath index a node, with everything a host needs
1049
+ UPDATE path bits GEOMETRY | PAINT | TEXT | A11Y | SCROLL
1050
+ MOVE path parentPath index the same node, elsewhere
1051
+ REMOVE path
1052
+ ```
1053
+
1054
+ The identity is the inspector's path (`0/3/k:share`), so a keyed reorder is
1055
+ a MOVE and not a rebuild. Geometry is parent-relative at scroll 0, so a
1056
+ scroll is one SCROLL bit on the container and no op on its children — a
1057
+ compositor moves them. Text is the engine's lines, one run per line at the
1058
+ display list's offsets, so the host breaks nothing itself and print parity
1059
+ holds. [`html/evg-dom.js`](html/evg-dom.js) is the first host on it: a DOM
1060
+ node per element, patched; `npm run evg:dom:check` asks Chromium whether
1061
+ every node is where the engine put it and whether a resize updated the
1062
+ nodes rather than remaking them, and `npm run rt:dom` asks the same of
1063
+ RealTrainer, scene change and scroll included. Both are live on the site:
1064
+ [the responsive page](https://terotests.github.io/Ranger/evg/responsive/?painter=dom)
1065
+ and [RealTrainer](https://terotests.github.io/Ranger/realtrainer/?painter=dom)
1066
+ as `?painter=dom` (the SVG painter is the responsive page's default, and
1067
+ `?engine=worker` on RealTrainer runs the engine in a Worker).
1068
+ PLAN_NATIVE_HOSTS.md S2 and S3.
1069
+
1070
+ ## Retained trees
1071
+
1072
+ A document is laid out once. An application is laid out sixty times a second,
1073
+ and the difference is what these are for.
1074
+
1075
+ * [`EVGReconcile`](EVGReconcile.rgr) matches a rebuilt tree's children against
1076
+ the previous one **by `key`**, so the same element objects survive — and with
1077
+ them the scroll positions, the focus and the running transitions.
1078
+ * [`EVGComponent`](EVGComponent.rgr) does the same for the thing that *built*
1079
+ the tree.
1080
+ * The style cache and the `layoutClean()` / `nothingChanged()` signals let a
1081
+ frame that changed nothing skip layout, and one that changed only a colour
1082
+ skip it too.
1083
+ * `EVGElement.resetLayoutState()` is what makes a second pass over the same tree
1084
+ correct rather than a source of stale percentages.
1085
+
1086
+ `EVGInvalidateTest`, `EVGStyleCacheTest`, `EVGReconcileTest` and
1087
+ `EVGTimingTest` are the tests that keep all four honest.
1088
+
1089
+ ---
1090
+
1091
+ ## The files
1092
+
1093
+ **The engine**
1094
+
1095
+ | File | |
1096
+ | --- | --- |
1097
+ | `EVGElement.rgr` | the node: properties, `setAttribute`, inheritance, inline tracking |
1098
+ | `EVGLayout.rgr` | flow, flex, absolute positioning, overlays, scrolling, RTL |
1099
+ | `EVGGrid.rgr` | grid tracks, `repeat()`, `minmax()`, named areas, subgrid |
1100
+ | `EVGConnector.rgr` | connectors: a line between two elements, solved after layout |
1101
+ | `EVGBox.rgr` | the resolved box model |
1102
+ | `EVGUnit.rgr` | lengths and how they resolve |
1103
+ | `EVGStyleSheet.rgr` | the CSS subset, the cascade, `@media`, the style cache |
1104
+ | `EVGColor.rgr` | colour parsing, blending, interpolation |
1105
+ | `EVGGradient.rgr` | linear and radial gradient strings |
1106
+ | `EVGEasing.rgr` | timing functions |
1107
+ | `EVGTransition.rgr` | properties arriving at their values over time |
1108
+ | `EVGDisplayList.rgr` | the flat command list, JSON and binary |
1109
+ | `EVGCommands.rgr` | everything an application can do, by name |
1110
+ | `EVGHostTree.rgr` | the fifth list: what changed, for a host that keeps nodes |
1111
+
1112
+ **Text**
1113
+
1114
+ | File | |
1115
+ | --- | --- |
1116
+ | `EVGTextEngine.rgr` | line breaking, the engine layout and painting share |
1117
+ | `EVGTextMeasurer.rgr` | the measurer interface and the measured advance table |
1118
+ | `EVGContextMeasurer.rgr` | measurement through a host's own renderer |
1119
+ | `EVGHostTextMeasurer.rgr` | measurement through the platform that will paint, from one host function |
1120
+ | `EVGTextFit.rgr` | text that stays inside its box |
1121
+ | `EVGGrapheme.rgr` `EVGCodepoint.rgr` | what "one character" means |
1122
+
1123
+ **Vector**
1124
+
1125
+ | File | |
1126
+ | --- | --- |
1127
+ | `SVGPathParser.rgr` | the `d` attribute, every command including arcs |
1128
+ | `SvgParser.rgr` | whole SVG files: `<use>`, `<defs>`, baked transforms |
1129
+ | `PathBuilder.rgr` `VectorShapes.rgr` `VectorStroke.rgr` `VectorViewBox.rgr` | geometry, strokes, `viewBox` |
1130
+ | `EvgBitmapTracer.rgr` and `EvgTrace*.rgr` | the raster-to-vector tracer |
1131
+
1132
+ **Interaction, meaning, components**
1133
+
1134
+ `EVGHitTest.rgr`, `EVGA11yTree.rgr`, `EVGA11yFromTree.rgr`,
1135
+ `EVGReconcile.rgr`, `EVGComponent.rgr`, `EVGWindow.rgr`, `EVGToolbar.rgr`,
1136
+ `EVGToolbarView.rgr`, `EVGToolbarIcons.rgr`, `EVGRuler.rgr`,
1137
+ `EVGRulerView.rgr`, `EVGSelectChrome.rgr`, `EVGText.rgr`,
1138
+ `EVGImageDecode.rgr`, `EVGImageMeasurer.rgr`.
1139
+
1140
+ **Painters and pages**
1141
+
1142
+ `html/`, `gl/`, `android/`, `apple/`, `showcase/`, `web/tracer/`,
1143
+ `web/responsive/`, `tools/`.
1144
+
1145
+ The UI counterpart of the bitmap tracer, Erazer (a screenshot in, a nested
1146
+ EVG layout out), lives in its own repository:
1147
+ [terotests/Erazer](https://github.com/terotests/Erazer).
1148
+
1149
+ ---
1150
+
1151
+ ## Running things
1152
+
1153
+ ```sh
1154
+ # the engine's own tests
1155
+ npm run evg # evg_test: the layout basics
1156
+ npm run evg:box:test # the box-model shorthands
1157
+ npm run evg:flexwrap:test # a row must not wrap because of its own arithmetic
1158
+ npm run evg:style:test # pseudo-classes and transitions
1159
+ npm run evg:stylecache:test # the cache, viewport included
1160
+ npm run evg:viewport:test # vw / vh, on screen and on paper
1161
+ npm run evg:rtl:test # direction: rtl
1162
+ npm run evg:overlay:test # anchored overlays
1163
+ npm run evg:popover:test # named anchors, fallbacks, fit-viewport, sheets
1164
+ npm run evg:connector:test # a line between two elements, and absolute in a grid
1165
+ npm run evg:invalidate:test # what a frame is allowed to skip
1166
+ npm run evg:reconcile:test # keyed children
1167
+ npm run evg:component:test # instances that outlive the tree
1168
+ npm run evg:timing:test # easing and transition timing
1169
+ npm run evg:a11y:test # the accessibility tree
1170
+ npm run evg:json:test # the display list's JSON
1171
+ npm run evg:hostmeasurer:test # a platform's one function reaches every layout
1172
+ npm run evg:measure:web # ...and in Chromium the browser is the one measuring
1173
+ npm run evg:binary:check # the list reads the same off the object and off toBinary()
1174
+ npm run evg:hosttree:test # the host tree says only what changed
1175
+ npm run evg:dom:check # ...and the DOM painter puts the nodes where it said, and keeps them
1176
+ npm run evg:responsive:check # the responsive page at four widths
1177
+
1178
+ # oracles — the same question, asked of a browser
1179
+ npm run evg:box:oracle
1180
+ npm run evg:timing:oracle
1181
+ npm run evg:blur:oracle
1182
+
1183
+ # pages
1184
+ npm run showcase # the gallery -> showcase/dist/index.html
1185
+ npm run evg:responsive:web:serve # the live responsive page
1186
+ npm run evg:trace:web:serve # the live bitmap tracer
1187
+ # the live-build harness (an agent designs a screen, streamed as display lists)
1188
+ # moved to https://github.com/terotests/EvgHarness — `npm start` there.
1189
+
1190
+ # one document, three targets
1191
+ npm run evgpdf:test # -> PDF
1192
+ npm run evghtml:test # -> HTML
1193
+ npm run evg:displaylist -- page.tsx out.json -css sheet.css
1194
+ ```
1195
+
1196
+ Anything under `bin/` is generated; the Ranger compiler has to be built first
1197
+ (`npm run compile`).
1198
+
1199
+ ## As a package
1200
+
1201
+ EVG is the package `evg`. Inside this repository a gallery project names it
1202
+ by path; outside, `rgrc install` fetches it from Git by subdirectory:
1203
+
1204
+ ```json
1205
+ "dependencies": {
1206
+ "evg": { "path": "../../lib/evg" }
1207
+ }
1208
+ ```
1209
+
1210
+ ```json
1211
+ "dependencies": {
1212
+ "evg": { "git": "https://github.com/terotests/Ranger.git",
1213
+ "rev": "<commit>", "subdir": "lib/evg" }
1214
+ }
1215
+ ```
1216
+
1217
+ ```ranger
1218
+ Import "pkg:evg/EVGElement.rgr"
1219
+ Import "pkg:evg/EVGLayout.rgr"
1220
+ ```
1221
+
1222
+ Its own dependency is `image` (`lib/image`: the JPEG and PNG codecs behind
1223
+ `EVGImageDecode`), which depends on `zip` (`lib/zip`: DEFLATE). Both are
1224
+ sibling path dependencies, so a Git fetch of `lib/evg` brings them along at
1225
+ the same commit. Nothing under this directory imports `gallery/`.
1226
+
1227
+ The window layer that used to live here — `EVGWindow`, `EVGTextFit`,
1228
+ `EVGContextMeasurer`, `EVGRulerView`, `EVGToolbarView` — needs the gallery's
1229
+ software rasteriser and font engine and is the package
1230
+ [`gallery/evg_window`](../../gallery/evg_window/README.md).
1231
+
1232
+ ## License
1233
+
1234
+ **MIT**, like the compiler and the rest of `lib/`. EVG moved here from
1235
+ `gallery/evg`, and from AGPL-3.0-or-later to MIT, in September 2026; the
1236
+ reasoning is in [LICENSING.md](../../LICENSING.md).