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,4914 @@
1
+ ; SPDX-License-Identifier: MIT
2
+
3
+ ; EVGElement.rgr - Base element class for EVG layout
4
+
5
+ Import "EVGUnit.rgr"
6
+ Import "EVGReject.rgr"
7
+ Import "EVGColor.rgr"
8
+ Import "EVGEasing.rgr"
9
+ Import "EVGBox.rgr"
10
+ Import "EVGGradient.rgr"
11
+ Import "SvgParser.rgr"
12
+ Import "SVGPathParser.rgr"
13
+
14
+ ; Element type constants
15
+ ; 0 = CONTAINER (div)
16
+ ; 1 = TEXT (span)
17
+ ; 2 = IMAGE (img)
18
+ ; 3 = PATH (svg path)
19
+
20
+ ; One property on its way from one value to another.
21
+ ;
22
+ ; Colours and numbers are both here rather than in two classes: a flight is a
23
+ ; start, an end and a clock, and the only difference is how the two ends are
24
+ ; read back out. Splitting them would double the bookkeeping to save one field.
25
+ class EVGFlight {
26
+ def property:string ""
27
+ ; The duration this flight is actually running for. Not the one the sheet
28
+ ; declared — a reversal shortens it, and `reversingFactor` is the record of
29
+ ; by how much.
30
+ def durationMs:double 0.0
31
+ def delayMs:double 0.0
32
+ def elapsedMs:double 0.0
33
+ def easing:EVGEasing (new EVGEasing())
34
+ ; Colours, when the property is one.
35
+ def fromColor:EVGColor
36
+ def toColor:EVGColor
37
+ ; A plain number, when it is not.
38
+ def fromNumber:double 0.0
39
+ def toNumber:double 0.0
40
+ def isColor:boolean false
41
+ ; The EVGUnit type this number is expressed in, for the flights where that
42
+ ; can differ between the two ends. 25 per cent and 25 pixels are the same
43
+ ; number and not the same place, so a flight whose unit has changed is not
44
+ ; a flight at all.
45
+ def unitCode:int 0
46
+
47
+ ; --- what a reversal needs to remember ------------------------------------
48
+ ;
49
+ ; CSS calls this the "reversing-adjusted start value": the value that, if
50
+ ; the property is asked to go back to it, means this is a REVERSAL rather
51
+ ; than a new journey somewhere else. For a fresh transition it is where the
52
+ ; flight left from; after a reversal it is the target the flight was
53
+ ; heading for before it turned round.
54
+ ;
55
+ ; The distinction matters. A hover that leaves half way is a reversal and
56
+ ; must come back in half the time; a hover that leaves and lands on a THIRD
57
+ ; colour is not, and gets the full duration. Comparing against the start
58
+ ; value alone gets the second case right and the first case wrong as soon
59
+ ; as the pointer changes its mind twice.
60
+ def reversingStartColor:EVGColor
61
+ def reversingStartNumber:double 0.0
62
+ def reversingFactor:double 1.0
63
+
64
+ ; --- what this flight last put on the element -----------------------------
65
+ ;
66
+ ; `reconcile` reads the TARGET off the element and `writeBack` writes the
67
+ ; SHOWING value to the same field, so the two can be told apart only if the
68
+ ; flight remembers what it wrote. Without that, a second `reconcile` in the
69
+ ; same frame reads its own output back, decides the target has moved to
70
+ ; wherever the animation has got to, and retargets there — which collapses
71
+ ; the flight to a standstill.
72
+ ;
73
+ ; That is not hypothetical. `laidOut()` runs once per question a host asks
74
+ ; — what to draw, what is under the pointer, what it means — so three
75
+ ; reconciles a frame is ordinary. It stayed hidden because the stylesheet
76
+ ; rewrites its own values at the top of every one of them, which re-declares
77
+ ; the target each time. Anything the stylesheet does NOT write — an inline
78
+ ; attribute set by the author — had no such protection, and simply refused
79
+ ; to animate.
80
+ def wroteNumber:double 0.0
81
+ def wroteColor:EVGColor
82
+ def hasWrote:boolean false
83
+
84
+ Constructor () {
85
+ }
86
+
87
+ ; Where the clock is, 0 to 1, before the timing function has its say.
88
+ ; The delay is part of the clock — a flight in its delay has not started
89
+ ; moving and reads 0 — which is what the browser does and what makes
90
+ ; `transition: opacity 200ms 100ms` hold still for its first tenth of a
91
+ ; second instead of easing very slowly through it.
92
+ fn progress:double () {
93
+ if (durationMs <= 0.0) {
94
+ return 1.0
95
+ }
96
+ def t:double ((elapsedMs - delayMs) / durationMs)
97
+ if (t < 0.0) {
98
+ return 0.0
99
+ }
100
+ if (t > 1.0) {
101
+ return 1.0
102
+ }
103
+ return t
104
+ }
105
+
106
+ ; What the value is actually interpolated by. Every reader wants this one;
107
+ ; `progress()` is only the clock.
108
+ fn eased:double () {
109
+ return (easing.ease((this.progress())))
110
+ }
111
+
112
+ fn done:boolean () {
113
+ return (elapsedMs >= (delayMs + durationMs))
114
+ }
115
+ }
116
+
117
+ class EVGElement {
118
+ def id:string ""
119
+ ; The RECONCILER's name for this element, and not the application's.
120
+ ;
121
+ ; `id` is global and outward-facing: the hit test reports it, the
122
+ ; accessibility tree carries it, and a page addresses a control by it.
123
+ ; `key` is sibling-scoped and inward-facing: it says "this new child is the
124
+ ; same logical child as that old one", so a rebuild can keep the element
125
+ ; instead of replacing it. Two lists on one page may both use the key
126
+ ; "video"; two elements may not share an id.
127
+ ;
128
+ ; "" means unkeyed, and an unkeyed child is matched by POSITION among its
129
+ ; unkeyed siblings — the same rule every library that has this uses, and
130
+ ; the reason a reordered list needs keys to animate.
131
+ def key:string ""
132
+ ; Where this box goes when it is followed: a URL, or `#name` for a place
133
+ ; in this same document.
134
+ ;
135
+ ; A destination is not application logic — PDF has `/Annots`, SVG has
136
+ ; `<a>`, HTML has `<a>` — and a renderer that cannot say it is the one
137
+ ; missing something. What it deliberately does NOT say is what following
138
+ ; it means: the PDF writes an annotation and stops, and an interactive
139
+ ; host asks the display list what is under the pointer and decides for
140
+ ; itself. Nothing here navigates.
141
+ ;
142
+ ; It applies to the subtree, the way `<a>` does, so a link may be a word,
143
+ ; a row or a card without the thing inside it knowing.
144
+ def href:string ""
145
+ def tagName:string "div"
146
+ def elementType:int 0
147
+
148
+ ; Print/Book format properties
149
+ def format:string "" ; Book format: "a4", "trade-5x8", "large-square", etc.
150
+ def orientation:string "" ; "portrait" or "landscape"
151
+ def pageWidth:double 0.0 ; Resolved page width in points
152
+ def pageHeight:double 0.0 ; Resolved page height in points
153
+
154
+ ; Tree structure
155
+ def parent@(optional):EVGElement
156
+ def children:[EVGElement]
157
+
158
+ ; Dimensions
159
+ def width:EVGUnit
160
+ def height:EVGUnit
161
+ def minWidth:EVGUnit
162
+ def minHeight:EVGUnit
163
+ def maxWidth:EVGUnit
164
+ def maxHeight:EVGUnit
165
+
166
+ ; Position (for absolute positioning)
167
+ def left:EVGUnit
168
+ def top:EVGUnit
169
+ def right:EVGUnit
170
+ def bottom:EVGUnit
171
+ def x:EVGUnit
172
+ def y:EVGUnit
173
+
174
+ ; Box model
175
+ def box:EVGBox
176
+
177
+ ; Visual properties
178
+ def backgroundColor:EVGColor
179
+ def opacity:double 1.0
180
+ ; `backdrop-filter: blur(Npx)` — how far to blur what is BEHIND this
181
+ ; element, in pixels, before painting the element over it. 0 is no filter.
182
+ ;
183
+ ; Behind, not itself: this does nothing to the element's own pixels. A
184
+ ; modal's scrim wants the page under it softened while the scrim's own
185
+ ; text stays sharp, and `filter: blur()` would do the opposite.
186
+ ;
187
+ ; The number is CSS's radius, which is the Gaussian's SIGMA and not its
188
+ ; half-width — measured in `oracle/css-blur.json`, along with the more
189
+ ; surprising half: browsers do not compute a Gaussian at all but the SVG
190
+ ; spec's three-box approximation of one, and the difference is up to 14
191
+ ; luminance levels at a hard edge.
192
+ def backdropBlur:double 0.0
193
+
194
+ ; Linear-gradient background (RGU1 GRAD_FROM/GRAD_TO/GRAD_DIR). When gradientSet
195
+ ; the renderer fills the (rounded) box with a 2-stop linear gradient instead of
196
+ ; the solid backgroundColor. gradientDir: 0 = vertical (top->bottom), 1 =
197
+ ; horizontal (left->right).
198
+ def gradientSet:boolean false
199
+ def gradientFrom:EVGColor
200
+ def gradientTo:EVGColor
201
+ def gradientDir:int 0
202
+
203
+ ; Absolute placement (RGU1 ABS_X/ABS_Y): when absPosSet the element is laid out
204
+ ; at (absX, absY) in page pixels instead of participating in flex flow — used
205
+ ; for coord-reported overlay effects.
206
+ def absPosSet:boolean false
207
+ def absX:double 0.0
208
+ def absY:double 0.0
209
+
210
+ ; Animated glow strength 0..1 (RGU1 GLOW prop). The guest drives this over
211
+ ; time (a procedural effect animation); the renderer draws a soft halo behind
212
+ ; the element scaled by it. 0 = no glow.
213
+ def glowIntensity:double 0.0
214
+
215
+ ; Background image clipped to the (rounded) box (RGU1 BG_IMAGE prop). When
216
+ ; bgImageSet the renderer paints bgImagePath inside the rounded rect instead of
217
+ ; the solid/gradient fill. The path is host-resolvable (the guest is sandboxed;
218
+ ; the host loads the pixels), so an .as menu can show real image tiles.
219
+ def bgImageSet:boolean false
220
+ def bgImagePath:string ""
221
+
222
+ ; Layout properties
223
+
224
+ ; CSS `direction`, which is NOT the `direction` below it.
225
+ ;
226
+ ; The property under that name here has always meant the flow AXIS —
227
+ ; "row" or "column" — which predates this and is still what those two
228
+ ; values mean. CSS's own `direction` takes "ltr" and "rtl" and means
229
+ ; something else entirely: which end of a container its children start
230
+ ; from. Two properties cannot share a name, and renaming the older one
231
+ ; would break every element that sets it, so `setStyle` decides by VALUE:
232
+ ; "ltr" and "rtl" land here, everything else lands on the axis.
233
+ ;
234
+ ; "" means the element said nothing and inherits, which is what `direction`
235
+ ; does in CSS and what makes one setting on the root turn a whole UI
236
+ ; around.
237
+ def textDir:string ""
238
+ ; What that resolves to after inheritance. This is what the layout reads.
239
+ def resolvedRtl:boolean false
240
+
241
+ def direction:string "row"
242
+ def align:string "left"
243
+ def verticalAlign:string "top"
244
+ def isInline:boolean false
245
+ def lineBreak:boolean false
246
+ def overflow:string "visible"
247
+
248
+ ; `cursor`, and it INHERITS, like the CSS property it is: a title bar says
249
+ ; `cursor: move` once and the words on it do not have to say it again.
250
+ ; Empty means nobody has said anything, which is what makes inheriting
251
+ ; distinguishable from an author writing `default` on purpose.
252
+ def cursor:string ""
253
+ ; The scroll container's bar, as CSS styles it: `scrollbar-width` (auto,
254
+ ; thin, none), `scrollbar-color` (the thumb's colour, then the track's;
255
+ ; `auto` for either is ink on whatever the bar is over), and the EVG
256
+ ; extension `evg-scrollbar-label` (percent, none). Read by
257
+ ; `EVGDisplayList.addScrollbar`, which draws the bar when an app asks
258
+ ; the list to.
259
+ def scrollbarWidth:string ""
260
+ def scrollbarThumb:EVGColor
261
+ def scrollbarTrack:EVGColor
262
+ def scrollbarLabel:string ""
263
+
264
+ ; --- A SURFACE EFFECT ------------------------------------------------------
265
+ ;
266
+ ; `evg-surface-effect: ripple`, and the family of `evg-ripple-*` numbers
267
+ ; under it. The `evg-` prefix is not decoration: this is an EVG EXTENSION
268
+ ; and not a CSS property, so nothing in the gallery should ever measure it
269
+ ; against a browser and find a divergence — there is nothing to diverge
270
+ ; from. Every other property in this file is CSS and is measured; these are
271
+ ; ours, and they say so in their names.
272
+ ;
273
+ ; The effect belongs to a SURFACE, not to a box. It names a post-process
274
+ ; over everything that was drawn: the display list carries it beside the
275
+ ; commands rather than as one of them, and a backend that has never heard
276
+ ; of it draws the page and ignores the field.
277
+ def surfaceEffect:string ""
278
+ ; How fast the ring travels, in pixels a second.
279
+ def rippleSpeed:double 220.0
280
+ ; How wide the ring is — the Gaussian envelope's sigma, in pixels.
281
+ def rippleWidth:double 28.0
282
+ ; How far the surface is pushed at the crest, in pixels.
283
+ def rippleStrength:double 7.0
284
+ ; How fast the whole thing dies away, per second.
285
+ def rippleDecay:double 1.8
286
+ ; How much the crest lightens. Small: this is the difference between water
287
+ ; and a filter somebody put on a video.
288
+ def rippleHighlight:double 0.08
289
+
290
+ ; --- how many rings ONE touch sends -----------------------------------------
291
+ ;
292
+ ; A drop on water does not make one ring. It makes a TRAIN of them, born at
293
+ ; the same point a moment apart and travelling out together, each fainter
294
+ ; than the one in front: the shape is a single expanding target rather than
295
+ ; a single circle.
296
+ ;
297
+ ; This costs no state at all. The rings of one touch differ only in when
298
+ ; they started, and that is the touch's age minus a multiple of the stagger
299
+ ; — so the shader works them out from the one age it already has, and a
300
+ ; ring whose start is still in the future simply has not been born yet.
301
+ def rippleRings:double 3.0
302
+ ; Seconds between one wavefront and the next.
303
+ def rippleStagger:double 0.09
304
+ ; What each ring behind the front is worth. Below about a half the train
305
+ ; reads as one ring with a smudge behind it.
306
+ def rippleFalloff:double 0.62
307
+
308
+ ; --- and what makes it look WET ---------------------------------------------
309
+ ;
310
+ ; A brightness proportional to the wave's height is an AMBIENT term: every
311
+ ; part of a crest lights up by the same amount however it is tilted, which
312
+ ; is exactly what a matte surface does. Water is not matte. What makes it
313
+ ; read as water is a SPECULAR: a highlight that depends on the angle
314
+ ; between the light, the eye and the surface's own normal, so it lives on
315
+ ; the FLANK of a wave rather than on its top and slides along as the wave
316
+ ; moves.
317
+ ;
318
+ ; The normal comes from the height field the rings already sum to — its
319
+ ; screen-space gradient is the slope, and the slope is the normal. Nothing
320
+ ; new has to be computed or stored.
321
+ ;
322
+ ; How strong the glint is. 0 turns it off and leaves the ambient term.
323
+ def rippleShine:double 0.45
324
+ ; The Blinn-Phong exponent: how TIGHT the glint is. Low is a broad sheen
325
+ ; like wet stone, high is a sharp point like a bead of water. Past about
326
+ ; 200 the glint stops being an arc along the ring and breaks into
327
+ ; separate specks, because the surviving band gets narrower than a pixel.
328
+ def rippleGloss:double 120.0
329
+ ; How much the wave's height tilts the normal. This is the one number with
330
+ ; no physical meaning — the height field is in arbitrary units, so this is
331
+ ; what turns it into a slope.
332
+ def rippleBump:double 70.0
333
+ ; Where the light is. The default is over the viewer's left shoulder,
334
+ ; which is where every UI in this gallery already pretends its light is.
335
+ def rippleLightX:double -0.45
336
+ def rippleLightY:double -0.65
337
+ def rippleLightZ:double 0.62
338
+
339
+ ; --- and the state it needs, which is not styling ---------------------------
340
+ ;
341
+ ; Where the surface was touched and how long ago — for EVERY touch still in
342
+ ; flight, not just the last one. Set by the APPLICATION, like `scrollTop`:
343
+ ; a stylesheet has no business owning where somebody clicked.
344
+ ;
345
+ ; Three parallel arrays rather than an array of triples, because that is
346
+ ; what goes into a `vec3` uniform array without a copy at the other end.
347
+ ; Empty means nothing is in flight.
348
+ ;
349
+ ; MANY of them, and that is the whole difference between an effect and a
350
+ ; surface: two rings that cross ADD, so the crests reinforce where they
351
+ ; meet and cancel a quarter-wavelength away. Interference is not something
352
+ ; the shader implements — it is what summing waves does.
353
+ def rippleXs:[double]
354
+ def rippleYs:[double]
355
+ def rippleAges:[double]
356
+
357
+ ; --- WHICH BOX, AND WHAT STARTS IT ----------------------------------------
358
+ ;
359
+ ; `evg-surface-effect` above says WHAT to run. These two say WHERE and
360
+ ; WHEN, and they are the whole difference between an effect a page has and
361
+ ; an effect a page's code has to remember to drive.
362
+ ;
363
+ ; WHERE is this element's own border box: an effect declared on a card is
364
+ ; the card's, and the display list carries the box the layout gave it, so
365
+ ; nothing downstream has to be told a rectangle somebody typed.
366
+ ;
367
+ ; WHEN is `evg-effect-on`, a list of the pointer things that feed it —
368
+ ; `press`, `drag`, `hover` — or `always` for one that runs on its own
369
+ ; clock and needs no pointer at all. The default is EMPTY, which means the
370
+ ; application drives it, and that is what the original ripple did from the
371
+ ; page's own `pointerdown`: the property is how a page stops having to.
372
+ ;
373
+ ; Nothing in EVG reads this. It is written into the display list and read
374
+ ; by the host's effect driver, for the same reason `cursor` is: what a
375
+ ; pointer is belongs to whatever owns the pointer.
376
+ def effectTrigger:string ""
377
+
378
+ ; --- THE PARAMETERS, FOR AN EFFECT THIS FILE HAS NEVER HEARD OF -----------
379
+ ;
380
+ ; `evg-ripple-speed` above is a field because the ripple was the only
381
+ ; effect there was. The second effect cannot be — a starfield's density
382
+ ; and a glitch's jitter are not fields on every element in the document,
383
+ ; and a renderer plugin nobody has written yet cannot have its fields
384
+ ; declared here in advance.
385
+ ;
386
+ ; So: `evg-fx-<name>: <number>` lands in this pair of parallel arrays
387
+ ; under `<name>`, travels through the display list untouched, and means
388
+ ; whatever the plugin that reads it says it means. Two arrays rather than
389
+ ; a map because the order is the order they were written in, and a JSON
390
+ ; writer that reorders keys between two runs makes every golden file a
391
+ ; coin toss.
392
+ ; THE DOCUMENT'S STYLESHEET, on the root element and nowhere else.
393
+ ;
394
+ ; A `.evg.json` could only ever say things inline: every node carried its
395
+ ; own copy of the same six properties, and an agent asked to restyle a
396
+ ; screen had to touch forty nodes to change one colour. EVG has had a CSS
397
+ ; engine the whole time — `EVGStyleSheet`, with classes, pseudo-classes,
398
+ ; themes and media queries — and the editable format simply had nowhere to
399
+ ; put it.
400
+ ;
401
+ ; It is a STRING here, not a parsed sheet, because parsing it would make
402
+ ; every element depend on the stylesheet and the stylesheet already depends
403
+ ; on the element. `EVGLayout` reads it, parses it once and applies it; an
404
+ ; inline property still outranks it, which is what makes the two mix.
405
+ def documentCss:string ""
406
+
407
+ def fxNames:[string]
408
+ def fxValues:[double]
409
+
410
+ ; --- SCROLLING ------------------------------------------------------------
411
+ ;
412
+ ; How far the content inside this element is pushed up and left. Set by the
413
+ ; APPLICATION, not by a stylesheet — a scroll position is state, and the
414
+ ; sheet has no business owning it — and clamped by the layout to what there
415
+ ; actually is to scroll, the way a browser clamps `scrollTop`.
416
+ def scrollTop:double 0.0
417
+ def scrollLeft:double 0.0
418
+ ; How big the content turned out to be, written by the layout. These are
419
+ ; the numbers a scrollbar is sized from and the reason a scroll container
420
+ ; is not the same thing as a clipped box: a clipped box throws the overflow
421
+ ; away, a scroll container remembers how much of it there was.
422
+ ;
423
+ ; Measured the way the DOM measures them, PADDING BOX and not content box:
424
+ ; eight 60-pixel rows in a box with 10 of padding gives 500, not 480. The
425
+ ; two definitions produce the same `maxScrollTop` — the paddings cancel —
426
+ ; so this could have been either, and it is the browser's, because a number
427
+ ; that is only right after you subtract something is a number somebody
428
+ ; will forget to subtract.
429
+ def scrollWidth:double 0.0
430
+ def scrollHeight:double 0.0
431
+ ; The offset the LAYOUT has actually translated this subtree by, which is
432
+ ; not the same thing as the offset the application asked for. A wheel event
433
+ ; moves `scrollTop` and nothing else; these say where the boxes are now, so
434
+ ; `EVGLayout.scrollOnly` can move them by the difference instead of laying
435
+ ; the document out again to arrive at the same widths.
436
+ def appliedScrollTop:double 0.0
437
+ def appliedScrollLeft:double 0.0
438
+
439
+ ; --- WHERE THIS SUBTREE DRAWS ---------------------------------------------
440
+ ;
441
+ ; The union of this element's own box and every box under it, written by
442
+ ; the layout and moved with the subtree. It is what lets the display list
443
+ ; skip a subtree that is nowhere near the scroll viewport WITHOUT walking
444
+ ; into it first: an element's own box says nothing about where its
445
+ ; children are — a zero-height wrapper is at the top of the document and
446
+ ; its rows are ten thousand pixels down — so the box alone is not a safe
447
+ ; thing to cull against, and this is.
448
+ ;
449
+ ; `paintUnbounded` is the honest answer for a subtree whose drawn output
450
+ ; is NOT its layout boxes: a transform moves the pixels after the fact and
451
+ ; a rotation moves them somewhere no rectangle here predicts. Such a
452
+ ; subtree is never culled, and the flag rises to its ancestors — which
453
+ ; costs those ancestors their own culling and nothing else, because their
454
+ ; other children are still tested on their own bounds.
455
+ def paintLeft:double 0.0
456
+ def paintTop:double 0.0
457
+ def paintRight:double 0.0
458
+ def paintBottom:double 0.0
459
+ def paintUnbounded:boolean false
460
+ ; A SCROLL OWED TO THE SUBTREE. This element's own box and paint bounds
461
+ ; are where they should be; everything beneath it still has to move by
462
+ ; this much. `EVGLayout.scrollOnly` writes it on the rows a scroll moved
463
+ ; that are far from the viewport instead of walking their thousands of
464
+ ; descendants — on a long diary that walk was the largest self-time in a
465
+ ; profile of a fling, and it moved boxes nobody would look at. It is
466
+ ; paid by `settleShift` the moment anything descends into the subtree:
467
+ ; the display list, the hit test and the accessibility walk each settle
468
+ ; a node before they look at its children, so a consumer reads a stale
469
+ ; box only if it reaches one without going through those, and the
470
+ ; whole-tree readers call `settleAll` first. A full layout resets it
471
+ ; with the rest of the layout state.
472
+ def shiftDx:double 0.0
473
+ def shiftDy:double 0.0
474
+ ; A LAYOUT KEPT. An app that knows a subtree has not changed — a card
475
+ ; built from an entry and kept across rebuilds — says so with
476
+ ; `keepLayout`, and the layout then answers for that subtree with what
477
+ ; it computed last time, moved to where the flow puts it now, when
478
+ ; three things hold: it was laid out before (`hasLayout`), the sheet's
479
+ ; last pass wrote no layout property anywhere in it (`layoutClean`,
480
+ ; which the sheet computes bottom-up for every element), and the flow
481
+ ; hands it the same inputs — the width and height it is laid out in,
482
+ ; and the flex width its parent gave it. The flow has already put the
483
+ ; element's own box where it goes; what is under it follows by the
484
+ ; difference, read off where the first child sat relative to the box
485
+ ; when the subtree was last measured (`kidOffX`, `kidOffY`). A subtree
486
+ ; skipped this way is `layoutSkipped`, so the paint-bounds pass leaves
487
+ ; its bounds, which moved with it, alone. Anything else lays it out as
488
+ ; before.
489
+ def keepLayout:boolean false
490
+ def hasLayout:boolean false
491
+ def layoutClean:boolean false
492
+ def layoutSkipped:boolean false
493
+ def kidOffX:double 0.0
494
+ def kidOffY:double 0.0
495
+ ; Whether `paintTop` and the rest were measured since the subtree was
496
+ ; last laid out for real: a subtree skipped in a later pass of the same
497
+ ; layout — a parent's second pass — moved bounds that were never
498
+ ; measured, and the measuring pass must not take a skipped subtree's
499
+ ; word for it until they were.
500
+ def boundsFresh:boolean false
501
+ ; …and the same question for the overlay pass: does this subtree hold a
502
+ ; surface that has to be placed against an anchor, and has it been asked?
503
+ ; A subtree the layout kept moved WHOLE — the surface and the anchor it
504
+ ; sits against went together — so its placement still holds and the pass
505
+ ; can take the answer it gave last time instead of walking it. See
506
+ ; `EVGLayout.placeOverlaysIn`.
507
+ def hasOverlayBelow:boolean false
508
+ def overlayScanned:boolean false
509
+ def lastParentW:double 0.0
510
+ def lastParentH:double 0.0
511
+ def lastFlex:boolean false
512
+ def lastFlexW:double 0.0
513
+ ; …and the height the parent handed down (grown, stretched or shrunk).
514
+ def lastFlexH:double 0.0
515
+ def hasIntrinsicMin:boolean false
516
+ def hasIntrinsicMax:boolean false
517
+ def intrinsicMin:double 0.0
518
+ def intrinsicMax:double 0.0
519
+ ; THE COMMANDS KEPT. A kept subtree draws the same commands each time,
520
+ ; so the display list keeps what it emitted for it and emits it again.
521
+ ; `paintStamp` says whether they still hold: the sheet bumps it when a
522
+ ; pass wrote paint anywhere in the subtree (`paintClean` is that pass's
523
+ ; answer), and the layout bumps it when it lays the subtree out for
524
+ ; real. `EVGDisplayList.walk` has the rest.
525
+ def paintClean:boolean false
526
+ def paintStamp:int 0
527
+ ; Does anything in this subtree carry a `surfaceEffect`? Written by the
528
+ ; same pass, and read by `EVGDisplayList.readEffect` — which used to walk
529
+ ; the WHOLE document on every frame looking for the one element that
530
+ ; might, and in a page that has none walked all of it and found nothing.
531
+ ; It was 11% of a scroll frame doing exactly that.
532
+ def paintHasEffect:boolean false
533
+ ; The name the display list's effect pass gave this element's instance on
534
+ ; the last build — the element's own `id` when it has one. Written there
535
+ ; and read by the walk a moment later, so the rectangle that carries the
536
+ ; effect and the instance it refers to cannot disagree about its name.
537
+ ;
538
+ ; IN-FLIGHT STATE, and `adoptFrom` deliberately does not carry it across a
539
+ ; reconcile: `EVGDisplayList.build` names every instance again before the
540
+ ; walk that reads them, so a kept element's stale name and a fresh
541
+ ; element's empty one come to exactly the same thing one build later.
542
+ ; `scripts/check-evg-adopt.mjs` holds that decision on the record.
543
+ def effectRuntimeId:string ""
544
+
545
+ ; --- WHERE THE ELEMENT'S OWN TEXT SITS INSIDE IT --------------------------
546
+ ;
547
+ ; Normally nowhere: a block's line boxes start at the top of its content
548
+ ; box and that is the end of it. But an element that is a FLEX CONTAINER
549
+ ; and carries text of its own does not lay that text out as a block —
550
+ ; CSS wraps it in an ANONYMOUS FLEX ITEM, and from then on `align-items`
551
+ ; and `justify-content` place it like any other item.
552
+ ;
553
+ ; That is the pill idiom, and it is written that way everywhere:
554
+ ;
555
+ ; .pill { display: flex; align-items: center; height: 34px; padding: 0 12px }
556
+ ;
557
+ ; with the label as the element's own text. EVG placed the line at the top
558
+ ; of the content box and never consulted `align-items`, so the label sat
559
+ ; against the pill's top edge instead of in the middle of it.
560
+ ;
561
+ ; The layout writes the offset here and the display list adds it to every
562
+ ; line, so the box's height and the painter's baseline agree — as they
563
+ ; must, because `calculatedBaseline` includes it too.
564
+ def textShiftY:double 0.0
565
+
566
+ ; Typography (inherited)
567
+ def fontSize:EVGUnit
568
+ ; fontSize starts UNSET: an element is born without one so that "the author
569
+ ; asked for this size" stays distinguishable from "nobody said". Layout
570
+ ; fills it in from the inherited size for every element that does not set
571
+ ; one, so by the time a renderer reads it, it is always set — and remembers
572
+ ; that it did so, or a re-layout would mistake its own fill-in for an
573
+ ; authored size and stop inheriting.
574
+ def fontSizeInherited:boolean false
575
+ ; The size inherited from the parent, kept apart from the resolved one so
576
+ ; that an `em` font-size resolved twice does not compound.
577
+ def fontSizeBase:double 14.0
578
+ ; The font-size of the tree's root, which is what `rem` resolves against.
579
+ ; Inherited unchanged all the way down.
580
+ def rootFontSize:double 14.0
581
+ ; What `vw` and `vh` on this element resolve against: the layout's page
582
+ ; size, published onto the root by `EVGLayout.layout` and inherited from
583
+ ; there like the root font size. Zero until a layout says otherwise — see
584
+ ; the note at the top of EVGUnit.
585
+ def viewportW:double 0.0
586
+ def viewportH:double 0.0
587
+ ; A BOX THAT IS A VIEWPORT OF ITS OWN. `fixed` is positioned against the
588
+ ; viewport, and until now there was exactly one of those per layout — the
589
+ ; page. A stage that shows the same document at 1440, 768 and 390 in one
590
+ ; tree has three, and a modal in the phone frame belongs over the phone
591
+ ; frame and not over the whole stage. An element with this set keeps its
592
+ ; own `viewportW/H` and `viewportX/Y` instead of inheriting them, and
593
+ ; every `fixed` box under it resolves against that rectangle. Off, the
594
+ ; viewport is the page's, as it always was.
595
+ def viewportRoot:boolean false
596
+ ; Where that viewport's own (0,0) is, in the coordinates the layout works
597
+ ; in. Zero for the page, which is where it starts.
598
+ def viewportX:double 0.0
599
+ def viewportY:double 0.0
600
+ def fontFamily:string "Noto Sans"
601
+ def fontWeight:string "normal"
602
+ ; Extra advance after every character, in pixels — CSS `letter-spacing`,
603
+ ; and negative is the tracking a designer tightens a heading with.
604
+ ; ZERO is the font's own spacing and every measurement and every painter
605
+ ; takes the same path it always did, so nothing that does not set this
606
+ ; can be moved by it.
607
+ def letterSpacing:double 0.0
608
+ ; Extra leading above every paragraph after the first, in pixels. CSS
609
+ ; has no such property — it puts paragraphs in elements and gives them
610
+ ; margins — but a text element here holds the whole block, newlines
611
+ ; included, so the gap has to be a property of the block. ZERO is the
612
+ ; single-spaced block every caller had before.
613
+ def paragraphSpacing:double 0.0
614
+ ; SVG's `stroke-linecap` and `stroke-linejoin` on a stroked path.
615
+ ; "butt" and "miter" are what the painter drew before either existed,
616
+ ; so a caller that says nothing keeps the picture it had.
617
+ def strokeLineCap:string "butt"
618
+ def strokeLineJoin:string "miter"
619
+ ; ZERO means `normal`, which is the FACE's own line box and is asked of
620
+ ; the text engine. It used to default to 1.2 — a number people type, not
621
+ ; one a browser computes: measured, the sans fallback's normal is 1.15em
622
+ ; and monospace's 1.164, so every text box in the gallery was four per
623
+ ; cent taller than the same declarations give in a browser.
624
+ def lineHeight:double 0.0
625
+ ; `line-height` WRITTEN AS A LENGTH, which is a different thing from
626
+ ; written as a number and CSS keeps them apart on purpose: `1.5` is one
627
+ ; and a half times whatever font size the element ends up with, `24px` is
628
+ ; twenty-four pixels whatever the font size is, and `150%` is computed
629
+ ; once against this element's own size and then inherited as that
630
+ ; computed value.
631
+ ;
632
+ ; It used to be one double, so `line-height: 24px` went through
633
+ ; `to_double`, came out 24, and was used as a MULTIPLIER: a 14px run got a
634
+ ; 336-pixel line box. The text then sat at the top of a line box twelve
635
+ ; times too tall — which reads, on the screen, as a line that has fallen
636
+ ; to the bottom of everything around it.
637
+ ;
638
+ ; Unset means the number above (or `normal`, which is zero there). See
639
+ ; `lineBoxFor`, which is the only thing that should read either.
640
+ def lineHeightUnit:EVGUnit
641
+ def textAlign:string "left"
642
+ ; `white-space`, of which two values matter here: `normal`, which wraps at
643
+ ; the box's width, and `nowrap`, which does not wrap at all — the line is
644
+ ; as long as the text and the box clips it if it clips anything.
645
+ ;
646
+ ; A ONE-LINE FIELD IS THE REASON. A text input is one line whatever is in
647
+ ; it: an `<input>` never wraps, it scrolls. Without this, a placeholder
648
+ ; longer than its box broke onto a second line and fell out of the bottom
649
+ ; of a 40-pixel row — which is what the quick entry on Home did with the
650
+ ; longer of its two placeholders.
651
+ ;
652
+ ; INHERITED, as CSS has it, so `white-space: nowrap` on the field reaches
653
+ ; the run inside it without every child having to say so.
654
+ def whiteSpace:string "normal"
655
+ def color:EVGColor
656
+ ; What to paint glyphs that came from a FALLBACK face — emoji, in
657
+ ; practice — when they should not be the colour of the text around them.
658
+ ;
659
+ ; Unset by default, which means "the same as `color`", so nothing changes
660
+ ; for a document that does not ask. It exists because a monochrome emoji
661
+ ; face can be filled with any colour at all, and the one thing the engine
662
+ ; could not do was fill it with a DIFFERENT one from the sentence it sits
663
+ ; in — there are no inline spans here to hang a second colour on.
664
+ def emojiColor:EVGColor
665
+ def textContent:string ""
666
+
667
+ ; CSS Flexbox properties
668
+ def display:string "block"
669
+ def flex:double 0.0
670
+ ; flex-basis: the main-axis size a flex item starts from, before grow or
671
+ ; shrink. Unset means "use width". The `flex: N` shorthand sets it to 0,
672
+ ; which is what makes `flex: 1` share space evenly even when the item also
673
+ ; carries a width — without it an explicit width made `flex` a no-op.
674
+ ; flex-shrink: how readily this item gives up space when the line
675
+ ; overflows, relative to its siblings. CSS default is 1; 0 means "never
676
+ ; shrink me", which is how a logo or a fixed label keeps its size while the
677
+ ; rest of the row compresses.
678
+ def flexShrink:double 1.0
679
+ def flexBasis:EVGUnit
680
+ def flexDirection:string "column"
681
+ def justifyContent:string "flex-start"
682
+ def alignItems:string "flex-start"
683
+ ; The item's own answer to its container's `align-items`. Empty means "did
684
+ ; not say" — CSS spells that `auto`, and both are accepted — so an item
685
+ ; that never mentions it costs one string comparison and nothing else.
686
+ def alignSelf:string ""
687
+ ; align-content: how WRAPPED LINES are distributed along the cross axis,
688
+ ; as opposed to alignItems which positions children within one line. Only
689
+ ; meaningful when the content wraps and the container has room to spare.
690
+ def alignContent:string "flex-start"
691
+ ; flexWrap: "wrap" (row auto-wraps — historical EVG default) or "nowrap"
692
+ ; (CSS default; children never wrap, they overflow / shrink instead).
693
+ def flexWrap:string "wrap"
694
+ def gap:EVGUnit
695
+ ; Axis-specific gaps. When unset, `gap` applies to both axes.
696
+ def rowGap:EVGUnit
697
+ def columnGap:EVGUnit
698
+ ; CSS Grid subset (see EVGGrid.rgr). Empty means "not a grid track spec";
699
+ ; a grid container with no column template gets a single 1fr column.
700
+ def gridTemplateColumns:string ""
701
+ def gridTemplateRows:string ""
702
+ ; Per-item placement, e.g. "span 2" or "2 / 4". Empty means auto-flow.
703
+ ; grid-template-areas: rows of names, e.g.
704
+ ; "hero hero" "left right"
705
+ ; An item claims a region with grid-area.
706
+ ; Track sizes handed down by an enclosing grid when this element declares
707
+ ; `grid-template-columns: subgrid` / `grid-template-rows: subgrid`. Filled
708
+ ; in by EVGLayout just before the child is laid out, because the parent is
709
+ ; the only thing that knows its own tracks.
710
+ def subgridColumnSizes:[double]
711
+ def subgridRowSizes:[double]
712
+ ; The row heights this element's own grid worked out, left behind for an
713
+ ; enclosing grid to read. A row subgrid always spans several of its
714
+ ; parent's rows, and with subgrid it is the GRANDCHILDREN that have to size
715
+ ; those rows — so the parent measures the subgrid once and adopts the rows
716
+ ; it came up with, rather than trying to see through it.
717
+ def computedRowSizes:[double]
718
+ ; Set by the enclosing grid as soon as it has claimed this element, which
719
+ ; is well before it can size its tracks. It is what tells a subgrid child
720
+ ; laid out during the parent's content-measuring pass — where the sizes do
721
+ ; not exist yet — apart from one with no enclosing grid at all. Without it
722
+ ; the first kind reported "no enclosing grid", which was simply untrue.
723
+ def subgridPending:boolean false
724
+ def gridTemplateAreas:string ""
725
+ ; grid-auto-flow: "row" (default) or "row dense" — dense backfills earlier
726
+ ; holes instead of only moving forward.
727
+ def gridAutoFlow:string "row"
728
+ ; Full-bleed: this element should run past the trim edge into the bleed
729
+ ; area, so the cut falls inside its ink rather than at its edge. Only has
730
+ ; an effect when the output was asked for a bleed, and only on the edges
731
+ ; the element already touches.
732
+ def fullBleed:boolean false
733
+ def gridArea:string ""
734
+ def gridColumn:string ""
735
+ def gridRow:string ""
736
+ def position:string "relative"
737
+
738
+ ; CSS Margin/Padding (shorthand expansion handled at parse time)
739
+ def marginTop:EVGUnit
740
+ def marginRight:EVGUnit
741
+ def marginBottom:EVGUnit
742
+ def marginLeft:EVGUnit
743
+ def paddingTop:EVGUnit
744
+ def paddingRight:EVGUnit
745
+ def paddingBottom:EVGUnit
746
+ def paddingLeft:EVGUnit
747
+
748
+ ; CSS Border
749
+ def borderWidth:EVGUnit
750
+ def borderTopWidth:EVGUnit
751
+ def borderRightWidth:EVGUnit
752
+ def borderBottomWidth:EVGUnit
753
+ def borderLeftWidth:EVGUnit
754
+ def borderColor:EVGColor
755
+ ; NOTE: border-radius lives on `box`, not here. A duplicate field on the
756
+ ; element used to shadow it — setAttribute wrote box.borderRadius while the
757
+ ; raster renderer read the element's, so rounded corners silently came out
758
+ ; square in PNG and correct in PDF. Do not reintroduce it.
759
+
760
+ ; Image properties
761
+ def src:string ""
762
+ def alt:string ""
763
+ ; The part of the SOURCE the box shows: "x y w h" as fractions of the
764
+ ; bitmap, or the same four as percentages. It is the window, not the
765
+ ; destination: "0 0 0.5 1" shows the left half of the picture stretched
766
+ ; across the whole box. Numbers outside 0..1 are legal and mean the
767
+ ; window reaches past the bitmap — the picture then does not cover the
768
+ ; box and the rest of it stays empty, which is what a Figma crop zoomed
769
+ ; out past the image does.
770
+ def imageViewBox:string ""
771
+ ; Parsed image viewBox values, as fractions of the source
772
+ def imageViewBoxX:double 0.0
773
+ def imageViewBoxY:double 0.0
774
+ def imageViewBoxW:double 1.0
775
+ def imageViewBoxH:double 1.0
776
+ def imageViewBoxSet:boolean false
777
+ ; Image offset for positioning within container (can be negative)
778
+ ; Works with object-position CSS property
779
+ def imageOffsetX:EVGUnit
780
+ def imageOffsetY:EVGUnit
781
+ ; Image sizing: "cover", "contain", "fill", "none"
782
+ ; Default is "cover" - preserves aspect ratio, fills container, clips overflow
783
+ def objectFit:string "cover"
784
+ ; Original image dimensions (set by image loader)
785
+ def sourceWidth:double 0.0
786
+ def sourceHeight:double 0.0
787
+
788
+ ; SVG Path properties
789
+ def svgPath:string ""
790
+ ; How the outline is fitted into the box, in SVG's own words. `meet`
791
+ ; keeps the shape and letterboxes it; `none` stretches. Everything here
792
+ ; is authored at its own size and so fits either way — until a shape is
793
+ ; RESIZED, and then the two are the difference between a wider shape and
794
+ ; the same shape in a wider frame.
795
+ def preserveAspectRatio:string "xMidYMid meet"
796
+ ; --- the flattened outline, kept ------------------------------------------
797
+ ;
798
+ ; `d` is a string, and the display list wants points: parsing it and
799
+ ; flattening its curves is the walk's largest cost on anything vector —
800
+ ; a page of text drawn as glyph outlines is thousands of paths, and a
801
+ ; Figma board panned at 60 Hz re-parsed every one of them for every
802
+ ; frame, 1.2 seconds a frame on a board of 3,565 nodes.
803
+ ;
804
+ ; The result depends on the path and on the box it is drawn in, and
805
+ ; neither changes while the view moves — a transform moves the pixels
806
+ ; after the boxes are placed. So it is kept here, and re-flattened when
807
+ ; either changes. `EVGDisplayList.copyRings` copies out of this into the
808
+ ; command, so what is cached is never what a painter mutates.
809
+ def ringsCache:[PathRing]
810
+ def ringsHave:boolean false
811
+ def ringsPath:string ""
812
+ def ringsX:double 0.0
813
+ def ringsY:double 0.0
814
+ def ringsW:double 0.0
815
+ def ringsH:double 0.0
816
+ def ringsSteps:int 0
817
+ ; The viewBox scale the outline was flattened at, which is what a stroke
818
+ ; width is in user units of.
819
+ def ringsScale:double 1.0
820
+ def ringsViewBox:string ""
821
+ def ringsFit:string ""
822
+ ; An entire imported SVG document, as markup. Where `svgPath` is one `d`
823
+ ; string with the element's own fill and stroke, this is a drawing with its
824
+ ; own shapes, groups, transforms and paint, which SvgParser resolves into a
825
+ ; flat list of drawables (PLAN_VECTOR_IR.md Stage 4). The two are mutually
826
+ ; exclusive on one element and an import wins, because a document that set
827
+ ; both meant the document.
828
+ ;
829
+ ; It lives on the path element rather than on a new element type: an
830
+ ; imported drawing is laid out, sized and positioned exactly like a `<Path>`
831
+ ; is, so a separate type would mean a second copy of that in the layout
832
+ ; engine and in all three renderers for no behavioural difference.
833
+ def svgSource:string ""
834
+ ; `svgSource`, parsed — by the display list, the first time it draws this
835
+ ; element, and kept for every frame after. The markup is the same string
836
+ ; on every frame and parsing it again per frame was a sixth of the
837
+ ; display-list build on a scrolled page with an icon per row. The key is
838
+ ; what the parse depends on: the source and the fill the element gives it
839
+ ; (`SvgParser.setInitialFill`), so a changed icon or a recoloured one is
840
+ ; parsed again and a cached document can never be the wrong drawing. A
841
+ ; rebuilt tree has fresh elements and parses once more; that is a press,
842
+ ; not a frame.
843
+ def svgDoc@(optional):SvgDocument
844
+ def svgDocKey:string ""
845
+ def viewBox:string ""
846
+ def fillColor:EVGColor
847
+ def strokeColor:EVGColor
848
+ def strokeWidth:double 0.0
849
+ ; "nonzero" (the SVG default) or "evenodd". They differ where a shape
850
+ ; overlaps or nests inside itself, which is exactly what decides whether the
851
+ ; middle of a ring is a hole or is filled.
852
+ def fillRule:string "nonzero"
853
+ ; Dash pattern as SVG writes it: alternating on/off lengths in user
854
+ ; units, e.g. "4 2". Empty means a solid stroke. Gridlines and target
855
+ ; lines are the reason this exists.
856
+ def strokeDashArray:string ""
857
+ def strokeDashOffset:double 0.0
858
+
859
+ ; ---------------------------------------------------------------------
860
+ ; Anchors and connectors
861
+ ; ---------------------------------------------------------------------
862
+ ;
863
+ ; `anchor-name` is CSS Anchor Positioning's own property: a name other
864
+ ; elements can point at, written `--orders` by convention. It is kept as
865
+ ; written, so `--orders` and `orders` are two different names, the way the
866
+ ; CSS one is.
867
+ def anchorName:string ""
868
+
869
+ ; A `connector` is a path whose `d` the LAYOUT writes. The author says
870
+ ; which two boxes it joins and how it should look; where those boxes end
871
+ ; up is the layout's business, and it is re-solved on every layout pass,
872
+ ; which is the whole point — a hand-placed `left: 171px` arrow is wrong
873
+ ; the moment the grid it was measured against reflows.
874
+ ;
875
+ ; `from`/`to` name an anchor (`--orders`) or an id (`#orders`).
876
+ def connectorFrom:string ""
877
+ def connectorTo:string ""
878
+ ; Which edge of the box to leave from and arrive at: left, right, top,
879
+ ; bottom, center — or `auto`, which picks the pair facing each other. Auto
880
+ ; is what makes the same connector correct in both a two-column desktop
881
+ ; layout and a stacked phone one.
882
+ def connectorFromSide:string "auto"
883
+ def connectorToSide:string "auto"
884
+ ; straight | orthogonal | bezier
885
+ def connectorRouting:string "straight"
886
+ ; A gap left between the box edge and the line's end, along the side's
887
+ ; own normal.
888
+ def connectorFromOffset:double 0.0
889
+ def connectorToOffset:double 0.0
890
+ ; none | triangle | open. `triangle` is filled, `open` is two strokes;
891
+ ; both follow the direction of the segment they sit on.
892
+ def arrowStart:string "none"
893
+ def arrowEnd:string "none"
894
+ def arrowSize:double 10.0
895
+ ; The heads, as their own `d`. They are kept OUT of `svgPath` because a
896
+ ; fill fills every subpath of the path it is on, closing each one
897
+ ; implicitly — an orthogonal shaft and its head on one path would come
898
+ ; back as a filled triangle of shaft. So the shaft is stroked and this is
899
+ ; filled, which is also what the two of them are.
900
+ def arrowPath:string ""
901
+ ; Set by the layout when it could not resolve `from` or `to`; the draw is
902
+ ; skipped rather than pointed at the origin.
903
+ def connectorResolved:boolean false
904
+
905
+ ; Clip path (for any element)
906
+ def clipPath:string ""
907
+
908
+ ; CSS class — selects stylesheet rules (see EVGStyleSheet.rgr). Space
909
+ ; separated, like HTML: "photo span-2".
910
+ def className:string ""
911
+ ; Theme name for stylesheet resolution, read from the document root.
912
+ def theme:string ""
913
+ ; Property names the authoring layer set directly on this element, in
914
+ ; kebab-case. A stylesheet must not overwrite these — inline wins the
915
+ ; cascade — so front-ends call markInline() for every attribute they apply.
916
+ def inlineProps:[string]
917
+
918
+ ; WHICH PROPERTIES CAME FROM THE DOCUMENT'S STYLESHEET.
919
+ ;
920
+ ; The sheet is applied by writing values onto elements, which makes a
921
+ ; cascaded value indistinguishable from an authored one the moment layout
922
+ ; has run — and anything that serializes the tree afterwards writes the
923
+ ; whole cascade into every node, turning a stylesheet into a one-time
924
+ ; macro. It happened: a page with six `.glass` cards came back with the
925
+ ; rule copied onto all six, class and all.
926
+ ;
927
+ ; So the sheet says so. `EVGStyleSheet` marks what it set, `EVGLayout`
928
+ ; clears the marks before re-applying, and `EVGTreeJson` leaves a marked
929
+ ; property out — the document keeps saying `class-name`, and the rule
930
+ ; stays one place. An inline property is never marked, because the sheet
931
+ ; does not touch one.
932
+ def cssProps:[string]
933
+
934
+ ; PDF/Print settings (for Print element)
935
+ def imageQuality:int 0 ; JPEG quality 1-100 (0 = use default)
936
+ def maxImageSize:int 0 ; Max image dimension in pixels (0 = use default)
937
+
938
+ ; --- Transform --------------------------------------------------------------
939
+ ;
940
+ ; `transform: rotate(15deg) scale(1.05) translate(4px, 0)` is composed at
941
+ ; parse time into ONE similarity — an angle, a uniform scale and an offset —
942
+ ; because that is the largest family closed under composition that a display
943
+ ; list of axis-aligned boxes can still represent exactly. Rotation, uniform
944
+ ; scale and translation compose to another rotation, uniform scale and
945
+ ; translation, in any order and to any depth. `scale(2, 1)` and `skew()` do
946
+ ; not; they are reported rather than approximated.
947
+ ;
948
+ ; Composing here rather than in the painter is what makes the four numbers
949
+ ; INTERPOLABLE: `transition: transform 200ms` is then four ordinary number
950
+ ; flights, and a rotation that is also being scaled does the right thing
951
+ ; without a matrix decomposition on every frame.
952
+ ;
953
+ def rotate:double 0.0
954
+ def scale:double 1.0
955
+ ; A REFLECTION, which a rotation and a factor cannot say between them.
956
+ ; `scale(1, -1)` and `scale(-1, 1)` have the same magnitude on both axes
957
+ ; and differ only in sign, so the similarity survives — it is a turn, a
958
+ ; factor and a flip, not a non-uniform scale. Figma writes a mirrored
959
+ ; layer exactly this way (its matrix has a negative determinant), and
960
+ ; read as a plain rotation a mirrored arrow points the wrong way.
961
+ def flipY:boolean false
962
+ def translateX:double 0.0
963
+ def translateY:double 0.0
964
+ ; Kept verbatim so the transform survives a round trip and so a reader can
965
+ ; see what was written, not only what it reduced to.
966
+ def transformSpec:string ""
967
+
968
+ ; --- transform-origin ----------------------------------------------------
969
+ ;
970
+ ; What the rotation and the scale happen ABOUT, as a length or a percentage
971
+ ; of the border box on each axis. Unset means CSS's default of 50% 50%,
972
+ ; which is the element's own centre — measured, not assumed: a 100x40 box
973
+ ; turned a quarter turn lands at (30, -30, 40, 100), whose centre is where
974
+ ; it started.
975
+ ;
976
+ ; Resolved LATE, in the display list, where the laid-out box is known.
977
+ ; Resolving at parse time would need a size nobody has yet, and resolving
978
+ ; when the stylesheet is applied would use the previous frame's box.
979
+ def transformOriginX:EVGUnit (EVGUnit.unset())
980
+ def transformOriginY:EVGUnit (EVGUnit.unset())
981
+ def transformOriginSpec:string ""
982
+
983
+ ; Shadow
984
+ def shadowRadius:EVGUnit
985
+ def shadowColor:EVGColor
986
+ def shadowOffsetX:EVGUnit
987
+ def shadowOffsetY:EVGUnit
988
+
989
+ ; Gradient (CSS gradient string like "linear-gradient(90deg, #ff0000, #0000ff)")
990
+ def backgroundGradient:string ""
991
+ ; Parsed gradient (for renderers to use)
992
+ def gradient:EVGGradient (new EVGGradient())
993
+
994
+ ; Calculated values (output from layout)
995
+ def calculatedX:double 0.0
996
+ def calculatedY:double 0.0
997
+ def calculatedWidth:double 0.0
998
+ def calculatedHeight:double 0.0
999
+ def calculatedInnerWidth:double 0.0
1000
+ def calculatedInnerHeight:double 0.0
1001
+ def calculatedFlexWidth:double 0.0
1002
+ ; Whether the flex pass DECIDED that width, as opposed to it merely being
1003
+ ; zero. Its own flag because the pass used to record the decision by
1004
+ ; clearing `width.isSet` — which destroyed the author's declaration, so the
1005
+ ; second frame laid out from a tree the stylesheet no longer described.
1006
+ ; Layout output is layout state; it does not get to edit the style.
1007
+ def hasFlexWidth:boolean false
1008
+ ; Column-axis flex grow (and row alignItems:stretch) target height, resolved
1009
+ ; by EVGLayout and consumed by layoutElement when height is not explicitly set.
1010
+ def calculatedFlexHeight:double 0.0
1011
+ ; Whether the column's flex-shrink DECIDED that height, over a declared
1012
+ ; one — the same flag `hasFlexWidth` is along a row, for the same reason:
1013
+ ; the pass used to say so by clearing `height.isSet`.
1014
+ def hasFlexHeight:boolean false
1015
+ ; Distance from this element's top border edge down to the baseline of its
1016
+ ; FIRST line of text, and the matching descent. Filled in by EVGLayout for
1017
+ ; text nodes from their real font metrics, and propagated up from the first
1018
+ ; in-flow child for containers, so `align-items: baseline` has something to
1019
+ ; line up. 0 means "no text baseline" — CSS then uses the bottom margin edge.
1020
+ def calculatedBaseline:double 0.0
1021
+ def calculatedDescent:double 0.0
1022
+ def hasBaseline:boolean false
1023
+ ; True when this element's height was definite (explicit, page-sized, or
1024
+ ; already resolved by flex) at the moment its children were laid out, which
1025
+ ; is the only time `calculatedInnerHeight` is meaningful as a constraint.
1026
+ ; Auto-height elements size to their content, so main-axis grow/shrink must
1027
+ ; not run against them — there is nothing to grow into or shrink to.
1028
+ def hasDefiniteHeight:boolean false
1029
+ def calculatedPage:int 0
1030
+ def isAbsolute:boolean false
1031
+
1032
+ ; --- overlays -------------------------------------------------------------
1033
+ ; An overlay surface stays exactly where it is in the tree: still its
1034
+ ; parent's child, still the owner of its subtree, still where an event or an
1035
+ ; accessibility walk finds it. Only LAYOUT and PAINT treat it differently —
1036
+ ; it takes no space in its parent's flow, and it is drawn after the whole
1037
+ ; normal tree, above it.
1038
+ ;
1039
+ ; That is the difference between this and a portal. Moving the element into
1040
+ ; some overlay root would move its ownership with it, and every question
1041
+ ; that follows the tree — who handles this click, what does a reader read
1042
+ ; next, when is this freed — would then have the wrong answer.
1043
+ def isOverlay:boolean false
1044
+ ; What it is positioned against. Left empty by the author: the layout pass
1045
+ ; fills it from the surface's own parent, by finding the sibling that
1046
+ ; declared itself the anchor.
1047
+ def overlayAnchor@(weak optional):EVGElement
1048
+ def isOverlayAnchor:boolean false
1049
+ ; "bottom", "top", "right", "left", or "center" for a modal that has no
1050
+ ; anchor. Words rather than an enum because a tree literal assigns these as
1051
+ ; ordinary typed fields — `(overlaySide "bottom")` type-checks, and a code
1052
+ ; would have been a magic number at every call site.
1053
+ def overlaySide:string "bottom"
1054
+ ; "start", "center" or "end", along the side's cross axis.
1055
+ def overlayAlign:string "start"
1056
+ def overlayGap:double 4.0
1057
+ ; Where the overlay pass PUT it. Deliberately not written back into `left`
1058
+ ; and `top`: those are the author's inputs, and a layout that writes its
1059
+ ; output into its own input reads last frame's answer on the next pass.
1060
+ ; That is not hypothetical — `resolveUnits` had exactly this shape and
1061
+ ; froze every percentage after the first layout.
1062
+ def overlayX:double 0.0
1063
+ def overlayY:double 0.0
1064
+ ; Which side the surface ENDED UP on, which is not always the one it asked
1065
+ ; for: near an edge it flips. Radix publishes the same thing as `data-side`
1066
+ ; and stylesheets use it — an arrow points the other way when a menu opens
1067
+ ; upwards — so it is a placement result, not an internal.
1068
+ def overlayPlacedSide:string ""
1069
+ ; …and which alignment, for the same reason: a fallback may change both.
1070
+ def overlayPlacedAlign:string ""
1071
+ ; anchored | sheet | fullscreen — what the surface ended up being. A menu
1072
+ ; that turned into a bottom sheet on a phone is a different widget, and a
1073
+ ; stylesheet, a test and an accessibility tree all want to know.
1074
+ def overlayPlacedPresentation:string ""
1075
+ ; The placement had to make it smaller than it asked to be, so it clips and
1076
+ ; has somewhere to scroll — `scrollHeight` is the extent, as everywhere.
1077
+ def overlayClamped:boolean false
1078
+
1079
+ ; --- popover: anchoring by name, fallbacks, presentation ------------------
1080
+ ;
1081
+ ; CSS Anchor Positioning's own vocabulary, as far as this engine carries
1082
+ ; it. The alternative was a second EVG-shaped spelling of the same idea,
1083
+ ; which costs everyone who already knows the CSS one.
1084
+ ;
1085
+ ; `position-anchor` names the box to position against — an `anchor-name`
1086
+ ; (`--file-menu`) or an `#id`. It also MARKS the element as a surface:
1087
+ ; anything positioned against another box wants to be out of flow and in
1088
+ ; the top layer, and making that follow from one property is the whole
1089
+ ; ergonomic difference between this and writing four.
1090
+ def positionAnchor:string ""
1091
+ ; A comma-separated list of position-areas to try when the declared one
1092
+ ; does not fit: "top start, right start, left start". CSS spells it
1093
+ ; `position-try-fallbacks`; the shorter `position-try` is accepted too.
1094
+ def positionTryFallbacks:string ""
1095
+ ; "" tries them in order and takes the first that fits. "most-space" scores
1096
+ ; them all and takes the roomiest, which is CSS's `position-try-order`
1097
+ ; heading in the same direction.
1098
+ def positionTryOrder:string ""
1099
+ ; anchored | sheet | fullscreen. The one thing here that is not CSS: on a
1100
+ ; phone an anchored menu is the wrong widget, not a badly placed one, and
1101
+ ; no amount of fallback placement fixes that.
1102
+ def presentation:string ""
1103
+ ; Become a sheet at or below this page width. A `@media` block setting
1104
+ ; `presentation` is the general route; this is the one line that covers the
1105
+ ; case everybody has.
1106
+ def sheetBelow:double 0.0
1107
+ ; Clamp the surface to the room it actually has, rather than letting it run
1108
+ ; off the page. With `overflow` other than `visible` it then scrolls.
1109
+ def fitViewport:boolean false
1110
+
1111
+ ; CSS's `anchor()` in an inset property: `left: anchor(right)` puts this
1112
+ ; element's left edge on the anchor's right edge, and
1113
+ ; `calc(anchor(right) + 8px)` puts it eight past. Each edge keeps which
1114
+ ; edge of the ANCHOR it was given and how far off it sits; empty means the
1115
+ ; edge was an ordinary length and `left`/`top` hold it as usual.
1116
+ ;
1117
+ ; This is the other half of `position-anchor`. `position-area` places a
1118
+ ; whole surface on a side of its anchor, which is what a menu wants;
1119
+ ; `anchor()` places one EDGE, which is what a badge hanging off a corner
1120
+ ; wants, and there is no way to say that with an area.
1121
+ def anchorLeftSide:string ""
1122
+ def anchorLeftOffset:double 0.0
1123
+ def anchorTopSide:string ""
1124
+ def anchorTopOffset:double 0.0
1125
+ def anchorRightSide:string ""
1126
+ def anchorRightOffset:double 0.0
1127
+ def anchorBottomSide:string ""
1128
+ def anchorBottomOffset:double 0.0
1129
+
1130
+ ; --- interaction state -------------------------------------------------------
1131
+ ; What is happening to this element right now, so a STYLESHEET can say what
1132
+ ; that should look like. `.btn:hover` is a rule that asks for the first of
1133
+ ; these; without them the only way to change a look on hover is for the
1134
+ ; controller to write a second class, which makes every controller know
1135
+ ; about presentation.
1136
+ ;
1137
+ ; Set by whoever routes input — the host that hit-tests a pointer already
1138
+ ; knows which element is under it, and the one that owns focus already
1139
+ ; knows which is focused.
1140
+ def isHovered:boolean false
1141
+ def isFocused:boolean false
1142
+ ; The pointer is down on it: CSS's `:active`.
1143
+ def isPressed:boolean false
1144
+
1145
+ ; --- transitions --------------------------------------------------------------
1146
+ ; `transition: background-color 150ms` as the stylesheet declared it, and
1147
+ ; the flights currently in the air. Both live on the element because a
1148
+ ; transition is a fact about THIS box moving, not about the rule that
1149
+ ; started it — the rule may not even match any more by the time it lands.
1150
+ def transitionSpec:string ""
1151
+ def transitions:[EVGFlight]
1152
+
1153
+ ; --- accessibility ---------------------------------------------------------
1154
+ ; What this element MEANS, next to what it looks like. A canvas contributes
1155
+ ; one empty graphic to the browser's accessibility tree no matter what was
1156
+ ; drawn into it, so an EVG app has to publish the meaning separately —
1157
+ ; and the only place that will not drift out of step with the picture is
1158
+ ; the declaration that built the picture.
1159
+ ;
1160
+ ; The role is the ARIA spelling ("menuitem", "toolbar", "separator"),
1161
+ ; because that is the vocabulary the platform mirrors speak and inventing a
1162
+ ; second one buys nothing. Empty means "not in the accessibility tree",
1163
+ ; which is the right default: most elements in a UI are boxes.
1164
+ def role:string ""
1165
+ ; The accessible name, when it is not simply what the element says. Left
1166
+ ; empty, the name is taken from the text of the descendants that have no
1167
+ ; role of their own — the same rule the HTML name calculation uses, and the
1168
+ ; reason a row of Label + Shortcut announces as one thing.
1169
+ def a11yLabel:string ""
1170
+ ; What a text field CONTAINS, as opposed to what it is called.
1171
+ ;
1172
+ ; A reader that is told "Full name, text box" and not the name in it is
1173
+ ; being told half of what is on screen — and this was missing entirely:
1174
+ ; the tree carried the label and the role, the DOM mirror asked for
1175
+ ; `node.value`, and nothing on this side ever set it. The conformance
1176
+ ; trace has had the value all along through `UiRow.text`, which is the
1177
+ ; CONTROLLER's path; a demo that draws its own tree went without.
1178
+ ;
1179
+ ; A password field must leave this EMPTY. `value` is the text and
1180
+ ; `glyphText()` is the bullets, and neither belongs to a reader: a browser
1181
+ ; exposes no value for `type=password` either.
1182
+ def a11yValue:string ""
1183
+ ; `aria-required`, `aria-invalid` and `aria-readonly`, as the DOM spells
1184
+ ; them: "true", "false", or "" for a control that says nothing.
1185
+ ;
1186
+ ; STRINGS AND NOT BOOLEANS, and that is the whole point of them. Absent
1187
+ ; and "false" are different claims — a field that says
1188
+ ; `aria-required="false"` has been asked and answered, and one that says
1189
+ ; nothing has not. The controller path has spelled them this way since the
1190
+ ; `input_required` spec recorded the rule against a real <input>
1191
+ ; ("aria-required is published only when it was asked for"); the element
1192
+ ; path could not say either at all, so a demo-built form drew a red ring
1193
+ ; and a message for a person and told a reader nothing.
1194
+ def a11yRequired:string ""
1195
+ def a11yInvalid:string ""
1196
+ def a11yReadOnly:string ""
1197
+ ; What a reader says INSTEAD of the role — "sortable" on a draggable row.
1198
+ def a11yRoleDescription:string ""
1199
+ ; `aria-describedby`, ALREADY RESOLVED to the sentence it points at — a
1200
+ ; form field's hint, or its error message once there is one.
1201
+ ;
1202
+ ; The text and not an id, for the reason the conformance trace carries the
1203
+ ; text: an id is a fact about a document and the sentence is what a reader
1204
+ ; hears, and this tree has no document to hold ids in. A control with no
1205
+ ; description says nothing, which is what an absent attribute means.
1206
+ def a11yDescription:string ""
1207
+ ; `aria-describedby` as a REFERENCE: the id of the element in this tree
1208
+ ; that carries the sentence — a field's visible error message. A host that
1209
+ ; has a document (the DOM mirror) points at that element, so a reader hears
1210
+ ; the message it can also walk to; `a11yDescription` keeps the resolved
1211
+ ; text for hosts that have no ids.
1212
+ def a11yDescribedBy:string ""
1213
+ ; `aria-controls` and `aria-labelledby`, as REFERENCES to other elements'
1214
+ ; ids in this tree: a tab names the panel it shows, and the panel is named
1215
+ ; by its tab. Hosts with a document (the DOM mirror) resolve them.
1216
+ def a11yControls:string ""
1217
+ def a11yLabelledBy:string ""
1218
+ ; `aria-haspopup`: "menu", "listbox", "dialog". The one thing a reader
1219
+ ; cannot see about a trigger — that pressing it opens something — and a
1220
+ ; select's closed trigger is a box with a word in it without it.
1221
+ def a11yHasPopup:string ""
1222
+ ; `aria-rowcount` and `aria-rowindex`. A virtualised table has a dozen rows
1223
+ ; in the tree and ten thousand in the data, and these two are the only
1224
+ ; thing that says so — 0 for a table that publishes neither, which is what
1225
+ ; a non-virtualised one does.
1226
+ def a11yRowCount:int 0
1227
+ def a11yRowIndex:int 0
1228
+ ; `aria-hidden`: this element and everything under it is DECORATION and
1229
+ ; contributes nothing to the accessible tree — no node, and no words to an
1230
+ ; ancestor's name.
1231
+ ;
1232
+ ; The chevron on a submenu row is the case that asked for it. It is a ">"
1233
+ ; drawn beside the label, and without this the row announces as "Share
1234
+ ; greater-than" — the glyph read aloud as if it were part of the name, when
1235
+ ; `aria-haspopup` has already said the same thing properly.
1236
+ def a11yHidden:boolean false
1237
+ ; `aria-modal`: while this surface is up, the rest of the page is not
1238
+ ; there — a reader does not walk it and the host hides it. A menu that
1239
+ ; takes the wheel and the arrow keys says so here, so the tree the
1240
+ ; hosts read agrees with what the app does with the keys.
1241
+ def a11yModal:boolean false
1242
+ ; `aria-sort` on a column header: 0 none, 1 ascending, 2 descending.
1243
+ def a11ySorted:int 0
1244
+ ; `aria-orientation`. Its own field for the reason `aria-sort` is one: on a
1245
+ ; SPLITTER it decides what a key means — ArrowLeft moves a horizontal one
1246
+ ; and does nothing at all to a vertical one — and every other observation
1247
+ ; about the two is identical. A toolbar, a slider, a menubar and a
1248
+ ; separator all publish it too.
1249
+ def a11yOrientation:string ""
1250
+ ; `aria-current`. "page" on the crumb you are on.
1251
+ def a11yCurrent:string ""
1252
+ ; `aria-valuenow` / `valuemin` / `valuemax`. Absent until a page built out
1253
+ ; of ELEMENTS needed to publish a splitter, and the gap was invisible
1254
+ ; because a page built out of a CONTROLLER publishes them through `UiRow`
1255
+ ; instead — two paths to the same tree and only one of them complete. Axe
1256
+ ; found it: a focusable `separator` without a value is not a splitter, it
1257
+ ; is a decoration that steals a tab stop.
1258
+ ;
1259
+ ; `hasValue` and `hasRange` are separate from the numbers for the reason
1260
+ ; `EVGA11yNode` keeps them separate: a progress bar with a range and NO
1261
+ ; position is how every platform spells "busy, length unknown", and 0 is a
1262
+ ; different claim.
1263
+ def a11yHasValue:boolean false
1264
+ def a11yValueNow:int 0
1265
+ def a11yHasRange:boolean false
1266
+ def a11yValueMin:int 0
1267
+ def a11yValueMax:int 0
1268
+ ; Tri-states, EVGA11yTri: 0 not applicable, 1 no, 2 yes, 3 mixed. Not
1269
+ ; booleans — `aria-checked="false"` on something that is not checkable makes
1270
+ ; a reader announce a checkbox that does not exist.
1271
+ def a11yChecked:int 0
1272
+ ; `aria-pressed`, tri-state. A carried sortable row and a show/hide toggle
1273
+ ; are both buttons that are ON, and neither is CHECKED — a reader says
1274
+ ; "pressed" for one and "checked" for the other, and they are not
1275
+ ; interchangeable.
1276
+ def a11yPressed:int 0
1277
+ def a11yExpanded:int 0
1278
+ def a11ySelected:int 0
1279
+ def a11yDisabled:boolean false
1280
+ def a11yFocusable:boolean false
1281
+ ; Where this row sits in a set the tree does not fully contain. A submenu
1282
+ ; that is closed is not in the tree, so its parent still has to say how many
1283
+ ; rows there are.
1284
+ def a11yPosInSet:int 0
1285
+ def a11ySetSize:int 0
1286
+ ; `aria-level`, for a treeitem in a flat tree. 0 means unset.
1287
+ def a11yLevel:int 0
1288
+
1289
+ ; --- what the stylesheet last wrote here ---------------------------------
1290
+ ;
1291
+ ; The cascade's answer for an element is a function of four things, and
1292
+ ; these remember which four produced what the element is holding now. When
1293
+ ; they all still agree on the next pass, the element already contains
1294
+ ; exactly what the sheet would write, and writing it again is pure cost —
1295
+ ; on a 1600-row table a hover changes ONE row and the other 22,402 elements
1296
+ ; were being re-declared regardless.
1297
+ ;
1298
+ ; `styleGen` is the sheet's own counter, so re-parsing or resizing
1299
+ ; invalidates every element at once without touching any of them.
1300
+ ; `EVGTransition.writeBack` sets it to -1, because after a flight has
1301
+ ; written a showing value the element no longer holds what the sheet
1302
+ ; declared — and the sheet has to re-declare it or the flight stops dead.
1303
+ def styleClass:string ""
1304
+ def styleTheme:string ""
1305
+ def styleBits:int 0
1306
+ def styleGen:int 0
1307
+ ; Which plan produced it, so two passes can be compared for whether
1308
+ ; anything LAYOUT-relevant actually differs. -1 is "no plan yet".
1309
+ def styleSlot:int (0 - 1)
1310
+ ; How many children it had when it was styled.
1311
+ ;
1312
+ ; The stylesheet cannot see a child being added or removed — that is not a
1313
+ ; property and no rule mentions it — so without this an element whose class
1314
+ ; never changed would be skipped, the pass would report itself clean, layout
1315
+ ; would be skipped, and the remaining children would be drawn at the
1316
+ ; positions they held before the removal. Comparing one integer removes a
1317
+ ; whole class of host mistake, and a removal is the case that has no other
1318
+ ; symptom: an ADDED child is caught anyway, because a new element has never
1319
+ ; been styled.
1320
+ def styleKids:int (0 - 1)
1321
+
1322
+ ; Where this element sits in the last inspect walk, or -1 when the
1323
+ ; inspector has never run. `EVGInspect` stamps it, and two things read it:
1324
+ ; the display list, to say which element emitted a command, and the hit
1325
+ ; test, to turn a point into a node path without comparing object
1326
+ ; identity — which Ranger has no operator for. One int, untouched and
1327
+ ; meaningless while nothing inspects anything.
1328
+ def inspectSlot:int (0 - 1)
1329
+
1330
+ def isLayoutComplete:boolean false
1331
+ def unitsResolved:boolean false
1332
+
1333
+ ; For statement evaluation: indicates if a return was executed
1334
+ def hasReturn:boolean false
1335
+ def hasBreak:boolean false
1336
+ def hasContinue:boolean false
1337
+
1338
+ ; Inherited font size in pixels
1339
+ def inheritedFontSize:double 14.0
1340
+
1341
+ Constructor () {
1342
+ tagName = "div"
1343
+ elementType = 0
1344
+
1345
+ width = (EVGUnit.unset())
1346
+ height = (EVGUnit.unset())
1347
+ minWidth = (EVGUnit.unset())
1348
+ minHeight = (EVGUnit.unset())
1349
+ maxWidth = (EVGUnit.unset())
1350
+ maxHeight = (EVGUnit.unset())
1351
+
1352
+ left = (EVGUnit.unset())
1353
+ top = (EVGUnit.unset())
1354
+ right = (EVGUnit.unset())
1355
+ bottom = (EVGUnit.unset())
1356
+ x = (EVGUnit.unset())
1357
+ y = (EVGUnit.unset())
1358
+ gap = (EVGUnit.unset())
1359
+ flexBasis = (EVGUnit.unset())
1360
+ rowGap = (EVGUnit.unset())
1361
+ columnGap = (EVGUnit.unset())
1362
+
1363
+ def newBox (new EVGBox())
1364
+ box = newBox
1365
+
1366
+ backgroundColor = (EVGColor.noColor())
1367
+ color = (EVGColor.black())
1368
+ emojiColor = (EVGColor.noColor())
1369
+
1370
+ fontSize = (EVGUnit.unset())
1371
+ lineHeightUnit = (EVGUnit.unset())
1372
+
1373
+ shadowRadius = (EVGUnit.unset())
1374
+ shadowColor = (EVGColor.noColor())
1375
+ shadowOffsetX = (EVGUnit.unset())
1376
+ shadowOffsetY = (EVGUnit.unset())
1377
+
1378
+ imageOffsetX = (EVGUnit.unset())
1379
+ imageOffsetY = (EVGUnit.unset())
1380
+
1381
+ fillColor = (EVGColor.noColor())
1382
+ scrollbarThumb = (EVGColor.noColor())
1383
+ scrollbarTrack = (EVGColor.noColor())
1384
+ strokeColor = (EVGColor.noColor())
1385
+
1386
+ ; The per-side box fields the stylesheet writes when it has a value.
1387
+ ; They used to be left undefined until then, which gave an element a
1388
+ ; different hidden class depending on which of them had ever been
1389
+ ; set, and made every layout read of them polymorphic. Every field
1390
+ ; is now present from birth, at no cost: the defaults are shared.
1391
+ marginTop = (EVGUnit.unset())
1392
+ marginRight = (EVGUnit.unset())
1393
+ marginBottom = (EVGUnit.unset())
1394
+ marginLeft = (EVGUnit.unset())
1395
+ paddingTop = (EVGUnit.unset())
1396
+ paddingRight = (EVGUnit.unset())
1397
+ paddingBottom = (EVGUnit.unset())
1398
+ paddingLeft = (EVGUnit.unset())
1399
+ borderWidth = (EVGUnit.unset())
1400
+ borderTopWidth = (EVGUnit.unset())
1401
+ borderRightWidth = (EVGUnit.unset())
1402
+ borderBottomWidth = (EVGUnit.unset())
1403
+ borderLeftWidth = (EVGUnit.unset())
1404
+ borderColor = (EVGColor.noColor())
1405
+ gradientFrom = (EVGColor.noColor())
1406
+ gradientTo = (EVGColor.noColor())
1407
+ }
1408
+
1409
+ sfn createDiv:EVGElement () {
1410
+ def el (new EVGElement())
1411
+ el.tagName = "div"
1412
+ el.elementType = 0
1413
+ return el
1414
+ }
1415
+
1416
+ sfn createSpan:EVGElement () {
1417
+ def el (new EVGElement())
1418
+ el.tagName = "span"
1419
+ el.elementType = 1
1420
+ return el
1421
+ }
1422
+
1423
+ sfn createImg:EVGElement () {
1424
+ def el (new EVGElement())
1425
+ el.tagName = "img"
1426
+ el.elementType = 2
1427
+ return el
1428
+ }
1429
+
1430
+ sfn createPath:EVGElement () {
1431
+ def el (new EVGElement())
1432
+ el.tagName = "path"
1433
+ el.elementType = 3
1434
+ return el
1435
+ }
1436
+
1437
+ fn addChild:void (child:EVGElement) {
1438
+ ; NOTE: no `child.parent = this` back-reference — it was write-only (never
1439
+ ; read) and, in the C++ backend, assigning `this` to a shared_ptr field
1440
+ ; forces enable_shared_from_this on EVGElement, which broke the native
1441
+ ; build. Children are tracked by `children[]` (forward edges) only.
1442
+ push children child
1443
+ }
1444
+
1445
+ ; A subtree whose layout is being kept — see `keepLayout` — is left as
1446
+ ; it is: the layout decides, when the flow reaches it, whether it can
1447
+ ; be answered from what it holds, and resets it itself when not.
1448
+ fn resetLayoutState:void () {
1449
+ if (keepLayout && (hasLayout && layoutClean)) {
1450
+ ; Except the height the parent hands down, which the parent works
1451
+ ; out again this pass if it still holds; left in place, one that no
1452
+ ; longer does would be read back as this box's input — see
1453
+ ; `resetLayoutNow` — and would match `lastFlexH`.
1454
+ calculatedFlexHeight = 0.0
1455
+ hasFlexHeight = false
1456
+ return
1457
+ }
1458
+ this.resetLayoutNow()
1459
+ }
1460
+
1461
+ ; Nothing under here has been laid out, as far as `keepLayout` is
1462
+ ; concerned. `EVGLayout.layout` says so when the page size changed: a
1463
+ ; `vw` or `vh` anywhere in a kept subtree means something else now,
1464
+ ; though nothing the flow hands the subtree's root need have changed.
1465
+ fn forgetLayout:void () {
1466
+ hasLayout = false
1467
+ def i:int 0
1468
+ while (i < (array_length children)) {
1469
+ def child:EVGElement (itemAt children i)
1470
+ child.forgetLayout()
1471
+ i = (i + 1)
1472
+ }
1473
+ }
1474
+
1475
+ fn resetLayoutNow:void () {
1476
+ unitsResolved = false
1477
+ shiftDx = 0.0
1478
+ shiftDy = 0.0
1479
+ hasIntrinsicMin = false
1480
+ hasIntrinsicMax = false
1481
+ layoutSkipped = false
1482
+ boundsFresh = false
1483
+ overlayScanned = false
1484
+ calculatedX = 0.0
1485
+ calculatedY = 0.0
1486
+ calculatedWidth = 0.0
1487
+ calculatedHeight = 0.0
1488
+ ; Cleared with the rest of the layout state, so a pass that no longer
1489
+ ; shrinks a child stops claiming it did.
1490
+ calculatedFlexWidth = 0.0
1491
+ hasFlexWidth = false
1492
+ hasDefiniteHeight = false
1493
+ ; And the rest of what the last pass computed, which the layout reads
1494
+ ; back as INPUT when it is still there: a flex-grown or stretched
1495
+ ; height is adopted by `layoutElement` whenever `height` is not set,
1496
+ ; and an auto-height row stretches its items to its inner height
1497
+ ; before it has worked out a new one. Laid out again at another page
1498
+ ; size, a card kept the 248px its column grew it to after its own
1499
+ ; height went `auto`, and a row stretched its columns to the height
1500
+ ; their text had on one line. The parent writes these again this pass
1501
+ ; if they still hold.
1502
+ calculatedFlexHeight = 0.0
1503
+ hasFlexHeight = false
1504
+ calculatedInnerWidth = 0.0
1505
+ calculatedInnerHeight = 0.0
1506
+ calculatedBaseline = 0.0
1507
+ calculatedDescent = 0.0
1508
+ hasBaseline = false
1509
+ ; Recursively reset children
1510
+ def i:int 0
1511
+ while (i < (array_length children)) {
1512
+ def child:EVGElement (itemAt children i)
1513
+ child.resetLayoutState()
1514
+ i = i + 1
1515
+ }
1516
+ }
1517
+
1518
+ fn getChildCount:int () {
1519
+ return (array_length children)
1520
+ }
1521
+
1522
+ ; Move this box and its paint bounds, and nothing beneath it.
1523
+ fn moveSelf:void (dx:double dy:double) {
1524
+ calculatedX = (calculatedX + dx)
1525
+ calculatedY = (calculatedY + dy)
1526
+ paintLeft = (paintLeft + dx)
1527
+ paintRight = (paintRight + dx)
1528
+ paintTop = (paintTop + dy)
1529
+ paintBottom = (paintBottom + dy)
1530
+ }
1531
+
1532
+ ; Move this box and every box beneath it. A descendant that itself owes
1533
+ ; a shift keeps owing it: its own shift is relative to its own box, which
1534
+ ; has just moved with the rest.
1535
+ fn moveSubtree:void (dx:double dy:double) {
1536
+ this.moveSelf(dx dy)
1537
+ def i:int 0
1538
+ def n:int (array_length children)
1539
+ while (i < n) {
1540
+ def kid:EVGElement (itemAt children i)
1541
+ kid.moveSubtree(dx dy)
1542
+ i = (i + 1)
1543
+ }
1544
+ }
1545
+
1546
+ ; Pay the scroll this subtree is owed — see `shiftDx`. Cheap to ask on
1547
+ ; a node that owes nothing, which is every node but the far rows.
1548
+ fn settleShift:void () {
1549
+ if ((shiftDx == 0.0) && (shiftDy == 0.0)) {
1550
+ return
1551
+ }
1552
+ def dx:double shiftDx
1553
+ def dy:double shiftDy
1554
+ shiftDx = 0.0
1555
+ shiftDy = 0.0
1556
+ def i:int 0
1557
+ def n:int (array_length children)
1558
+ while (i < n) {
1559
+ def kid:EVGElement (itemAt children i)
1560
+ kid.moveSubtree(dx dy)
1561
+ i = (i + 1)
1562
+ }
1563
+ }
1564
+
1565
+ ; Every box in the tree where it should be, for a reader that is about
1566
+ ; to look at all of them.
1567
+ fn settleAll:void () {
1568
+ this.settleShift()
1569
+ def i:int 0
1570
+ def n:int (array_length children)
1571
+ while (i < n) {
1572
+ def kid:EVGElement (itemAt children i)
1573
+ kid.settleAll()
1574
+ i = (i + 1)
1575
+ }
1576
+ }
1577
+
1578
+ ; --- scrolling ------------------------------------------------------------
1579
+ ;
1580
+ ; A box that does not let its content out. `visible` is the only CSS value
1581
+ ; that does not clip, and every other one — `hidden` included — makes a
1582
+ ; SCROLL CONTAINER. That `hidden` scrolls is the part worth stating: it is
1583
+ ; a container with no scrollbar, not a box that throws the overflow away,
1584
+ ; and a program can scroll it even though a pointer cannot.
1585
+ fn clipsContent:boolean () {
1586
+ return (overflow != "visible")
1587
+ }
1588
+
1589
+ ; The visible box, the DOM's `clientHeight`/`clientWidth`: everything
1590
+ ; inside the border, padding included.
1591
+ fn clientHeight:double () {
1592
+ return (calculatedHeight - (box.borderWidthPx * 2.0))
1593
+ }
1594
+
1595
+ fn clientWidth:double () {
1596
+ return (calculatedWidth - (box.borderWidthPx * 2.0))
1597
+ }
1598
+
1599
+ ; How far there is to go. Zero when the content fits, which is also the
1600
+ ; answer for every element that is not a scroll container at all, because
1601
+ ; the layout only ever writes an extent for one that is.
1602
+ fn maxScrollTop:double () {
1603
+ def m:double (scrollHeight - (this.clientHeight()))
1604
+ if (m < 0.0) {
1605
+ return 0.0
1606
+ }
1607
+ return m
1608
+ }
1609
+
1610
+ fn maxScrollLeft:double () {
1611
+ def m:double (scrollWidth - (this.clientWidth()))
1612
+ if (m < 0.0) {
1613
+ return 0.0
1614
+ }
1615
+ return m
1616
+ }
1617
+
1618
+ fn getChild:EVGElement (index:int) {
1619
+ return (itemAt children index)
1620
+ }
1621
+
1622
+ fn hasParent:boolean () {
1623
+ ; Check if parent is set - use simple boolean check
1624
+ if parent {
1625
+ return true
1626
+ }
1627
+ return false
1628
+ }
1629
+
1630
+ fn isContainer:boolean () {
1631
+ return (elementType == 0)
1632
+ }
1633
+
1634
+ fn isText:boolean () {
1635
+ return (elementType == 1)
1636
+ }
1637
+
1638
+ fn isImage:boolean () {
1639
+ return (elementType == 2)
1640
+ }
1641
+
1642
+ fn isPath:boolean () {
1643
+ return (elementType == 3)
1644
+ }
1645
+
1646
+ ; A tag's props arrive as strings from a stylesheet and as whatever the
1647
+ ; author wrote from a tree literal, so "true", "1" and a bare word all mean
1648
+ ; the same thing here.
1649
+ sfn truthy:boolean (value:string) {
1650
+ def v:string (trim value)
1651
+ if (v == "true") {
1652
+ return true
1653
+ }
1654
+ if (v == "1") {
1655
+ return true
1656
+ }
1657
+ if (v == "yes") {
1658
+ return true
1659
+ }
1660
+ return false
1661
+ }
1662
+
1663
+ ; "true"/"false"/"mixed" as EVGA11yTri, which is 0 for anything else —
1664
+ ; including the empty string, i.e. "this element has no such state".
1665
+ sfn triState:int (value:string) {
1666
+ def v:string (trim value)
1667
+ if (v == "true") {
1668
+ return 2
1669
+ }
1670
+ if (v == "false") {
1671
+ return 1
1672
+ }
1673
+ if (v == "mixed") {
1674
+ return 3
1675
+ }
1676
+ return 0
1677
+ }
1678
+
1679
+ ; `display: none` — the element is not there: no box, no paint, no hit,
1680
+ ; not in the accessible tree. It is the way a stylesheet folds a bar or
1681
+ ; a rail away at a breakpoint, and what `@media` is for.
1682
+ fn isHidden:boolean () {
1683
+ return (display == "none")
1684
+ }
1685
+
1686
+ ; THE LINE BOX, in pixels, and the only thing that should read
1687
+ ; `lineHeight` or `lineHeightUnit`. CSS gives three ways to write it and
1688
+ ; they are three different computations:
1689
+ ;
1690
+ ; `normal` the FACE's own line box — `normalPx`, which the caller got
1691
+ ; from the text engine because only it knows about faces
1692
+ ; a number that many times this element's font size
1693
+ ; a length itself, with `%` and `em` against this element's OWN font
1694
+ ; size (not its parent's, which is what a percentage means
1695
+ ; everywhere else and does not mean here) and `rem` against
1696
+ ; the root's
1697
+ ;
1698
+ ; The layout reserves the height by this and the display list steps the
1699
+ ; lines by it, so there is one answer and both sides read it.
1700
+ fn lineBoxFor:double (fontSize:double normalPx:double) {
1701
+ if lineHeightUnit.isSet {
1702
+ def u:EVGUnit lineHeightUnit
1703
+ u.setContext(rootFontSize viewportW viewportH)
1704
+ ; The parent size a `line-height` percentage resolves against IS
1705
+ ; the font size. CSS 2.1 10.8.1, and it is the one percentage in
1706
+ ; the box model that does not look at the containing block.
1707
+ u.resolve(fontSize fontSize)
1708
+ if (u.pixels > 0.0) {
1709
+ return u.pixels
1710
+ }
1711
+ return 0.0
1712
+ }
1713
+ if (lineHeight > 0.0) {
1714
+ return (fontSize * lineHeight)
1715
+ }
1716
+ return normalPx
1717
+ }
1718
+
1719
+ ; The width a line is broken into, for this element's own text: the box's
1720
+ ; content width, or ZERO — which `EVGTextEngine` reads as "do not wrap" —
1721
+ ; when `white-space` says the text is one line. One answer, so the layout
1722
+ ; that reserves the height and the display list that draws the lines
1723
+ ; cannot disagree about how many there are.
1724
+ fn wrapWidth:double (contentWidth:double) {
1725
+ if (whiteSpace == "nowrap") {
1726
+ return 0.0
1727
+ }
1728
+ if (whiteSpace == "pre") {
1729
+ return 0.0
1730
+ }
1731
+ return contentWidth
1732
+ }
1733
+
1734
+ ; Is this element's drawn output somewhere its layout box does not say?
1735
+ ; A transform moves the pixels after the boxes are placed, so the answer
1736
+ ; decides whether the subtree can be culled at all — see `paintUnbounded`.
1737
+ fn hasTransform:boolean () {
1738
+ if (rotate != 0.0) {
1739
+ return true
1740
+ }
1741
+ if flipY {
1742
+ return true
1743
+ }
1744
+ if ((fabs (scale - 1.0)) > 0.000001) {
1745
+ return true
1746
+ }
1747
+ if (translateX != 0.0) {
1748
+ return true
1749
+ }
1750
+ if (translateY != 0.0) {
1751
+ return true
1752
+ }
1753
+ return false
1754
+ }
1755
+
1756
+ ; Positioned against the VIEWPORT and not moved by a scroll — the one
1757
+ ; thing that separates it from `absolute`, which is positioned against the
1758
+ ; nearest container and travels with it.
1759
+ fn isFixedPosition:boolean () {
1760
+ return (position == "fixed")
1761
+ }
1762
+
1763
+ ; The same walk as `moveSubtree`, except that a FIXED box and everything
1764
+ ; under it stays where it is. A scroll moves the content of a container;
1765
+ ; a fixed box is not content, it is furniture over the viewport, and the
1766
+ ; whole point of the mode is that the page slides under it.
1767
+ fn moveSubtreeUnfixed:void (dx:double dy:double) {
1768
+ this.moveSelf(dx dy)
1769
+ def i:int 0
1770
+ def n:int (array_length children)
1771
+ while (i < n) {
1772
+ def kid:EVGElement (itemAt children i)
1773
+ if (kid.isFixedPosition()) {
1774
+ } {
1775
+ kid.moveSubtreeUnfixed(dx dy)
1776
+ }
1777
+ i = (i + 1)
1778
+ }
1779
+ }
1780
+
1781
+ ; --- position-area --------------------------------------------------------
1782
+ ;
1783
+ ; `bottom`, `bottom start`, `top end`, `right center`, `center`, `cover`.
1784
+ ; The first word is the side; the second is where along that side's cross
1785
+ ; axis the surface sits. CSS's own grammar writes the second word as a
1786
+ ; physical direction as often as a logical one — `bottom left` — so both
1787
+ ; spellings are read, and which axis a physical word means depends on the
1788
+ ; side, exactly as it does there.
1789
+ sfn areaSide:string (area:string) {
1790
+ def first:string (EVGElement.areaWord(area 0))
1791
+ if (((first == "top") || (first == "bottom")) || (first == "left")) {
1792
+ return first
1793
+ }
1794
+ if (((first == "right") || (first == "center")) || (first == "cover")) {
1795
+ return first
1796
+ }
1797
+ if (first == "free") {
1798
+ return first
1799
+ }
1800
+ return ""
1801
+ }
1802
+
1803
+ sfn areaAlign:string (area:string) {
1804
+ def side:string (EVGElement.areaWord(area 0))
1805
+ def word:string (EVGElement.areaWord(area 1))
1806
+ if ((strlen word) == 0) {
1807
+ return "start"
1808
+ }
1809
+ if (((word == "start") || (word == "center")) || (word == "end")) {
1810
+ return word
1811
+ }
1812
+ ; A physical word. Along a horizontal side the cross axis is vertical,
1813
+ ; and the other way round.
1814
+ if ((side == "top") || (side == "bottom")) {
1815
+ if (word == "left") {
1816
+ return "start"
1817
+ }
1818
+ if (word == "right") {
1819
+ return "end"
1820
+ }
1821
+ }
1822
+ if ((side == "left") || (side == "right")) {
1823
+ if (word == "top") {
1824
+ return "start"
1825
+ }
1826
+ if (word == "bottom") {
1827
+ return "end"
1828
+ }
1829
+ }
1830
+ return "start"
1831
+ }
1832
+
1833
+ ; The n-th whitespace-separated word of an area, or "".
1834
+ sfn areaWord:string (area:string index:int) {
1835
+ def out:string ""
1836
+ def seen:int 0
1837
+ def cur:string ""
1838
+ def i:int 0
1839
+ def n:int (strlen area)
1840
+ while (i <= n) {
1841
+ def isBreak:boolean (i == n)
1842
+ if (isBreak == false) {
1843
+ def c:int (charAt area i)
1844
+ if (((c == 32) || (c == 9)) || (c == 10)) {
1845
+ isBreak = true
1846
+ }
1847
+ }
1848
+ if isBreak {
1849
+ if ((strlen cur) > 0) {
1850
+ if (seen == index) {
1851
+ out = cur
1852
+ return out
1853
+ }
1854
+ seen = (seen + 1)
1855
+ cur = ""
1856
+ }
1857
+ } {
1858
+ cur = (cur + (substring area i (i + 1)))
1859
+ }
1860
+ i = (i + 1)
1861
+ }
1862
+ return out
1863
+ }
1864
+
1865
+ ; --- anchor() ---------------------------------------------------------------
1866
+ ;
1867
+ ; `anchor(right)` and `calc(anchor(right) + 8px)`. Only those two shapes:
1868
+ ; one anchor reference and at most one length added to or subtracted from
1869
+ ; it, which is what every real use of this is. Anything more is `calc()`,
1870
+ ; and this engine does not have one.
1871
+
1872
+ sfn mentionsAnchor:boolean (value:string) {
1873
+ return ((indexOf value "anchor(") >= 0)
1874
+ }
1875
+
1876
+ ; Which edge of the anchor was named. An inset property takes an edge on
1877
+ ; its own axis; `anchor(center)` is the middle of that axis. An edge from
1878
+ ; the wrong axis is a mistake worth reporting rather than guessing at.
1879
+ sfn anchorFnSide:string (prop:string value:string) {
1880
+ def open:int (indexOf value "anchor(")
1881
+ def rest:string (substring value (open + 7) (strlen value))
1882
+ def close:int (indexOf rest ")")
1883
+ if (close < 0) {
1884
+ EVGReject.note("unsupported value" prop value)
1885
+ return ""
1886
+ }
1887
+ def side:string (trim (substring rest 0 close))
1888
+ def vertical:boolean ((prop == "top") || (prop == "bottom"))
1889
+ if (side == "center") {
1890
+ return "center"
1891
+ }
1892
+ if vertical {
1893
+ if ((side == "top") || (side == "bottom")) {
1894
+ return side
1895
+ }
1896
+ } {
1897
+ if ((side == "left") || (side == "right")) {
1898
+ return side
1899
+ }
1900
+ }
1901
+ EVGReject.note("unsupported value" prop value)
1902
+ return ""
1903
+ }
1904
+
1905
+ ; What `+ 8px` or `- 8px` after the reference adds. No length, no offset.
1906
+ sfn anchorFnOffset:double (value:string) {
1907
+ def open:int (indexOf value "anchor(")
1908
+ def rest:string (substring value (open + 7) (strlen value))
1909
+ def close:int (indexOf rest ")")
1910
+ if (close < 0) {
1911
+ return 0.0
1912
+ }
1913
+ def tail:string (trim (substring rest (close + 1) (strlen rest)))
1914
+ if ((strlen tail) == 0) {
1915
+ return 0.0
1916
+ }
1917
+ def sign:double 1.0
1918
+ def first:string (substring tail 0 1)
1919
+ if (first == "-") {
1920
+ sign = (0.0 - 1.0)
1921
+ } {
1922
+ if ((first == "+") == false) {
1923
+ return 0.0
1924
+ }
1925
+ }
1926
+ def num:string (trim (substring tail 1 (strlen tail)))
1927
+ ; Drop a trailing unit: px is the only one an offset can mean here,
1928
+ ; because there is nothing to take a percentage of.
1929
+ def digits:string ""
1930
+ def i:int 0
1931
+ while (i < (strlen num)) {
1932
+ def ch:int (charAt num i)
1933
+ def ok:boolean (((ch >= 48) && (ch <= 57)) || (ch == 46))
1934
+ if ok {
1935
+ digits = (digits + (substring num i (i + 1)))
1936
+ } {
1937
+ i = (strlen num)
1938
+ }
1939
+ i = (i + 1)
1940
+ }
1941
+ def v@(optional):double (to_double (trim digits))
1942
+ if v {
1943
+ return (sign * (unwrap v))
1944
+ }
1945
+ return 0.0
1946
+ }
1947
+
1948
+ ; Any inset written as `anchor(…)`. The layout then places this element
1949
+ ; edge by edge instead of by an area.
1950
+ fn hasAnchorInsets:boolean () {
1951
+ if ((strlen anchorLeftSide) > 0) {
1952
+ return true
1953
+ }
1954
+ if ((strlen anchorTopSide) > 0) {
1955
+ return true
1956
+ }
1957
+ if ((strlen anchorRightSide) > 0) {
1958
+ return true
1959
+ }
1960
+ if ((strlen anchorBottomSide) > 0) {
1961
+ return true
1962
+ }
1963
+ return false
1964
+ }
1965
+
1966
+ ; A SURFACE: out of its parent's flow, drawn in the top layer, placed
1967
+ ; against an anchor rather than by the flow. `overlay: true` says so, and
1968
+ ; so does the `popover` tag, which is the web's name for the same thing and
1969
+ ; saves the property.
1970
+ fn isSurface:boolean () {
1971
+ if isOverlay {
1972
+ return true
1973
+ }
1974
+ if (tagName == "popover") {
1975
+ return true
1976
+ }
1977
+ return false
1978
+ }
1979
+
1980
+ ; What an `arrowPath` is filled with: the stroke's own colour, because an
1981
+ ; arrow whose head is a different colour from its shaft is a mistake far
1982
+ ; more often than a choice. An explicit `fill` still wins.
1983
+ fn arrowFillColor:EVGColor () {
1984
+ def out:EVGColor (EVGColor.noColor())
1985
+ if fillColor.isSet {
1986
+ def f:EVGColor (unwrap fillColor)
1987
+ out = f
1988
+ return out
1989
+ }
1990
+ if strokeColor.isSet {
1991
+ def st:EVGColor (unwrap strokeColor)
1992
+ out = st
1993
+ }
1994
+ return out
1995
+ }
1996
+
1997
+ ; Drawn as a vector path by every renderer. A `connector` is one: the
1998
+ ; difference between it and a `path` is who wrote the `d`, which no
1999
+ ; renderer has any reason to care about.
2000
+ fn drawsPath:boolean () {
2001
+ if (tagName == "path") {
2002
+ return true
2003
+ }
2004
+ if (tagName == "Path") {
2005
+ return true
2006
+ }
2007
+ if (tagName == "connector") {
2008
+ return true
2009
+ }
2010
+ ; An `svg` element is the same family — `EVGPatch.familyOf` has said so
2011
+ ; since it was written, and the display list draws an inline document
2012
+ ; in `walkPath` — but this answered no for the tag, so the one branch
2013
+ ; that reaches that code was never taken. A node an agent could create,
2014
+ ; address, and fill with a traced picture drew nothing at all.
2015
+ if (tagName == "svg") {
2016
+ return true
2017
+ }
2018
+ if (tagName == "Svg") {
2019
+ return true
2020
+ }
2021
+ return false
2022
+ }
2023
+
2024
+ fn hasAbsolutePosition:boolean () {
2025
+ ; Fixed is out of flow by the same rule absolute is; where it differs
2026
+ ; is which box it is positioned against, which is the layout's business
2027
+ ; and not this one's.
2028
+ if (position == "fixed") {
2029
+ return true
2030
+ }
2031
+ ; An overlay surface is out of flow by the same rule an absolutely
2032
+ ; positioned element is, and saying so here rather than adding a second
2033
+ ; condition to every size and space calculation in EVGLayout is the
2034
+ ; whole reason the flow half of this feature is one line. The overlay
2035
+ ; pass then MOVES it; until then it sits where layoutAbsolute put it.
2036
+ if (this.isSurface()) {
2037
+ return true
2038
+ }
2039
+ ; A hidden element is out of flow the same way: nothing around it
2040
+ ; moves for it, and the gap between its neighbours is one gap.
2041
+ if (display == "none") {
2042
+ return true
2043
+ }
2044
+ ; Check if element has absolute positioning
2045
+ ; Layer elements are always absolute (overlay parent)
2046
+ if ((tagName == "layer") || (tagName == "Layer")) {
2047
+ return true
2048
+ }
2049
+ ; A connector is geometry over the layout, never in it: it joins two
2050
+ ; boxes and must not push either of them about.
2051
+ if (tagName == "connector") {
2052
+ return true
2053
+ }
2054
+ ; An inset written as `anchor(…)` is still an inset.
2055
+ if (this.hasAnchorInsets()) {
2056
+ return true
2057
+ }
2058
+ if left.isSet {
2059
+ return true
2060
+ }
2061
+ if top.isSet {
2062
+ return true
2063
+ }
2064
+ if right.isSet {
2065
+ return true
2066
+ }
2067
+ if bottom.isSet {
2068
+ return true
2069
+ }
2070
+ if x.isSet {
2071
+ return true
2072
+ }
2073
+ if y.isSet {
2074
+ return true
2075
+ }
2076
+ return false
2077
+ }
2078
+
2079
+ ; Resolve book format to page dimensions in points (72 DPI)
2080
+ ; Returns [width, height] based on format and orientation
2081
+ fn resolveBookFormat:void () {
2082
+ def w:double 595.0 ; Default A4 width
2083
+ def h:double 842.0 ; Default A4 height
2084
+
2085
+ ; Blurb book formats (width x height in points @ 72 DPI)
2086
+ if (format == "a4") {
2087
+ w = 595.0
2088
+ h = 842.0
2089
+ }
2090
+ if (format == "letter") {
2091
+ w = 612.0
2092
+ h = 792.0
2093
+ }
2094
+ if (format == "trade-5x8") {
2095
+ w = 360.0
2096
+ h = 576.0
2097
+ }
2098
+ if (format == "trade-6x9") {
2099
+ w = 432.0
2100
+ h = 648.0
2101
+ }
2102
+ if (format == "trade-8x10") {
2103
+ w = 576.0
2104
+ h = 720.0
2105
+ }
2106
+ if (format == "mini-square") {
2107
+ w = 360.0
2108
+ h = 360.0
2109
+ }
2110
+ if (format == "small-square") {
2111
+ w = 504.0
2112
+ h = 504.0
2113
+ }
2114
+ if (format == "standard-portrait") {
2115
+ w = 576.0
2116
+ h = 720.0
2117
+ }
2118
+ if (format == "standard-landscape") {
2119
+ w = 720.0
2120
+ h = 576.0
2121
+ }
2122
+ if (format == "large-landscape") {
2123
+ w = 936.0
2124
+ h = 792.0
2125
+ }
2126
+ if (format == "large-square") {
2127
+ w = 864.0
2128
+ h = 864.0
2129
+ }
2130
+ if (format == "magazine") {
2131
+ w = 612.0
2132
+ h = 792.0
2133
+ }
2134
+
2135
+ ; Apply orientation (swap dimensions for landscape if not already landscape format)
2136
+ if (orientation == "landscape") {
2137
+ if (w < h) {
2138
+ ; Swap width and height for landscape
2139
+ def temp:double w
2140
+ w = h
2141
+ h = temp
2142
+ }
2143
+ }
2144
+ if (orientation == "portrait") {
2145
+ if (w > h) {
2146
+ ; Swap width and height for portrait
2147
+ def temp:double w
2148
+ w = h
2149
+ h = temp
2150
+ }
2151
+ }
2152
+
2153
+ ; If custom dimensions provided, use them
2154
+ if (pageWidth > 0.0) {
2155
+ w = pageWidth
2156
+ }
2157
+ if (pageHeight > 0.0) {
2158
+ h = pageHeight
2159
+ }
2160
+
2161
+ ; Store resolved dimensions
2162
+ pageWidth = w
2163
+ pageHeight = h
2164
+ }
2165
+
2166
+ ; The face this element is actually set in, weight included.
2167
+ ;
2168
+ ; The renderers pick a bold face by appending "-Bold" to the family at
2169
+ ; PAINT time, while layout measured the plain family — so a bold heading
2170
+ ; was measured in the regular cut and drawn in the wider bold one, and
2171
+ ; wrapped a line later than the box it had been given. Both sides go
2172
+ ; through here now, which is §4.1 rule 1: one face, one measurement.
2173
+ ; The destination in force here: this element's own, or the nearest
2174
+ ; ancestor's. `<a>` wrapping a row is the shape this exists for.
2175
+ fn effectiveHref:string () {
2176
+ if ((strlen href) > 0) {
2177
+ return href
2178
+ }
2179
+ def p@(optional):EVGElement parent
2180
+ def guard:int 0
2181
+ while ((!null? p) && (guard < 4096)) {
2182
+ def up:EVGElement (unwrap p)
2183
+ if ((strlen up.href) > 0) {
2184
+ return up.href
2185
+ }
2186
+ p = up.parent
2187
+ guard = (guard + 1)
2188
+ }
2189
+ return ""
2190
+ }
2191
+
2192
+ fn effectiveFontFamily:string () {
2193
+ if (fontWeight == "bold") {
2194
+ return (fontFamily + "-Bold")
2195
+ }
2196
+ return fontFamily
2197
+ }
2198
+
2199
+ ; The border, from wherever it was actually written.
2200
+ ;
2201
+ ; There are TWO places a border can land. `setAttribute` — which is what a
2202
+ ; stylesheet and every kebab-case JSX attribute go through — writes
2203
+ ; `box.borderWidth` / `box.borderColor`. The camelCase style-object path in
2204
+ ; JSXToEVG writes the element-level `borderWidth` / `borderColor`. Each
2205
+ ; painter then picked one:
2206
+ ;
2207
+ ; PDF box.borderWidth + box.borderColor -> drew it
2208
+ ; HTML the browser, from the emitted CSS -> drew it
2209
+ ; PNG el.borderWidth + el.borderColor -> drew NOTHING
2210
+ ; list box.borderWidthPx + el.borderColor -> drew NOTHING
2211
+ ;
2212
+ ; So a CSS-declared border printed and previewed correctly and was missing
2213
+ ; from the raster and the GPU backend — the same shape of bug as the
2214
+ ; border-radius that worked in PDF and silently not in PNG. One accessor,
2215
+ ; and every painter reads it.
2216
+ fn effectiveBorderWidthPx:double () {
2217
+ if (box.borderWidthPx > 0.0) {
2218
+ return box.borderWidthPx
2219
+ }
2220
+ if borderWidth {
2221
+ if borderWidth.isSet {
2222
+ return borderWidth.pixels
2223
+ }
2224
+ }
2225
+ return 0.0
2226
+ }
2227
+
2228
+ fn effectiveBorderColor:EVGColor () {
2229
+ if box.borderColor {
2230
+ def bc:EVGColor (unwrap box.borderColor)
2231
+ if bc.isSet {
2232
+ return bc
2233
+ }
2234
+ }
2235
+ if borderColor {
2236
+ def ec:EVGColor (unwrap borderColor)
2237
+ if ec.isSet {
2238
+ return ec
2239
+ }
2240
+ }
2241
+ ; A width with no colour is a black border, which is what CSS says and
2242
+ ; what the PDF path already assumed.
2243
+ return (EVGColor.black())
2244
+ }
2245
+
2246
+ fn hasBorder:boolean () {
2247
+ if ((this.effectiveBorderWidthPx()) <= 0.0) {
2248
+ return false
2249
+ }
2250
+ return true
2251
+ }
2252
+
2253
+ ; The colour fallback glyphs are painted in: the explicit one when set,
2254
+ ; otherwise the text colour, so an undeclared document looks exactly as it
2255
+ ; did.
2256
+ fn effectiveEmojiColor:EVGColor () {
2257
+ if emojiColor.isSet {
2258
+ return (unwrap emojiColor)
2259
+ }
2260
+ return (unwrap color)
2261
+ }
2262
+
2263
+ fn inheritProperties:void (parentEl:EVGElement) {
2264
+ ; Inherit properties from parent
2265
+ if (fontFamily == "Noto Sans") {
2266
+ fontFamily = parentEl.fontFamily
2267
+ }
2268
+ if (color.isSet == false) {
2269
+ color = parentEl.color
2270
+ }
2271
+ ; Inherited like `color`: a deck sets it once and every caption in it
2272
+ ; picks it up.
2273
+ if (emojiColor.isSet == false) {
2274
+ emojiColor = parentEl.emojiColor
2275
+ }
2276
+ ; And so does `cursor`. This is what makes "what is the cursor here" a
2277
+ ; question about ONE element rather than a walk up a tree that has no
2278
+ ; parent links.
2279
+ if ((strlen cursor) == 0) {
2280
+ cursor = parentEl.cursor
2281
+ }
2282
+ ; `white-space` is inherited, which is what lets a one-line field say
2283
+ ; `nowrap` once and have the run inside it obey.
2284
+ if (whiteSpace == "normal") {
2285
+ whiteSpace = parentEl.whiteSpace
2286
+ }
2287
+ fontSizeBase = parentEl.inheritedFontSize
2288
+ rootFontSize = parentEl.rootFontSize
2289
+ ; The viewport travels the same way the root font size does rather
2290
+ ; than being looked up — except across a box that declares one of its
2291
+ ; own, which is where the walk stops.
2292
+ if (viewportRoot == false) {
2293
+ viewportW = parentEl.viewportW
2294
+ viewportH = parentEl.viewportH
2295
+ viewportX = parentEl.viewportX
2296
+ viewportY = parentEl.viewportY
2297
+ }
2298
+ this.applyOwnFontSize()
2299
+ this.applyOwnDirection(parentEl.resolvedRtl)
2300
+ }
2301
+
2302
+ ; Settle this element's direction against the one it inherits. Inherited
2303
+ ; like `color`, and for the same reason: a UI is turned around by saying so
2304
+ ; ONCE, on the root, rather than by marking every panel in it.
2305
+ fn applyOwnDirection:void (inherited:boolean) {
2306
+ resolvedRtl = inherited
2307
+ if (textDir == "rtl") {
2308
+ resolvedRtl = true
2309
+ }
2310
+ if (textDir == "ltr") {
2311
+ resolvedRtl = false
2312
+ }
2313
+ }
2314
+
2315
+ ; Settle this element's font-size against the one it inherits, and publish
2316
+ ; the result as what its own `em` lengths and its descendants resolve
2317
+ ; against.
2318
+ ;
2319
+ ; Two things were broken before this existed. A root has no parent, so
2320
+ ; inheritProperties never ran on it and a font-size on a Page or Print was
2321
+ ; dropped outright. And below the root, every element was constructed with
2322
+ ; a 14px font-size, so an inherited size was immediately overwritten by
2323
+ ; that default — font-size only ever applied to the element that carried it.
2324
+ ; EVGLayout.layout calls this on the root; inheritProperties on the rest.
2325
+ fn applyOwnFontSize:void () {
2326
+ def authored:boolean fontSize.isSet
2327
+ if fontSizeInherited {
2328
+ authored = false
2329
+ }
2330
+ if authored {
2331
+ ; `em` on font-size itself is relative to the *parent's* size,
2332
+ ; which is why the base is kept rather than read back out of
2333
+ ; inheritedFontSize.
2334
+ fontSize.setContext(rootFontSize viewportW viewportH)
2335
+ fontSize.resolve(fontSizeBase fontSizeBase)
2336
+ inheritedFontSize = fontSize.pixels
2337
+ } {
2338
+ inheritedFontSize = fontSizeBase
2339
+ fontSize = (EVGUnit.px(fontSizeBase))
2340
+ fontSizeInherited = true
2341
+ }
2342
+ }
2343
+
2344
+ fn resolveUnits:void (parentWidth:double parentHeight:double) {
2345
+ ; Skip if already resolved - prevents double-resolution of percentages
2346
+ if unitsResolved {
2347
+ return
2348
+ }
2349
+ unitsResolved = true
2350
+
2351
+ ; Resolve all unit values to pixels
2352
+ def fs:double inheritedFontSize
2353
+ def rfs:double rootFontSize
2354
+
2355
+ ; `rem` needs the root's font size and `vw`/`vh` need the viewport,
2356
+ ; neither of which a unit can reach on its own — hand both over before
2357
+ ; anything is resolved.
2358
+ def vpw:double viewportW
2359
+ def vph:double viewportH
2360
+ width.setContext(rfs vpw vph)
2361
+ height.setContext(rfs vpw vph)
2362
+ flexBasis.setContext(rfs vpw vph)
2363
+ minWidth.setContext(rfs vpw vph)
2364
+ minHeight.setContext(rfs vpw vph)
2365
+ maxWidth.setContext(rfs vpw vph)
2366
+ maxHeight.setContext(rfs vpw vph)
2367
+ left.setContext(rfs vpw vph)
2368
+ top.setContext(rfs vpw vph)
2369
+ right.setContext(rfs vpw vph)
2370
+ bottom.setContext(rfs vpw vph)
2371
+ x.setContext(rfs vpw vph)
2372
+ y.setContext(rfs vpw vph)
2373
+ shadowRadius.setContext(rfs vpw vph)
2374
+ shadowOffsetX.setContext(rfs vpw vph)
2375
+ shadowOffsetY.setContext(rfs vpw vph)
2376
+
2377
+ ; Dimensions - width uses parentWidth, height uses parentHeight
2378
+ width.resolveWithHeight(parentWidth parentHeight fs)
2379
+ height.resolveForHeight(parentWidth parentHeight fs)
2380
+ flexBasis.resolve(parentWidth fs)
2381
+ minWidth.resolve(parentWidth fs)
2382
+ minHeight.resolve(parentHeight fs)
2383
+ maxWidth.resolve(parentWidth fs)
2384
+ maxHeight.resolve(parentHeight fs)
2385
+
2386
+ ; Position
2387
+ left.resolve(parentWidth fs)
2388
+ top.resolve(parentHeight fs)
2389
+ right.resolve(parentWidth fs)
2390
+ bottom.resolve(parentHeight fs)
2391
+ x.resolve(parentWidth fs)
2392
+ y.resolve(parentHeight fs)
2393
+
2394
+ ; Box model
2395
+ box.resolveUnits(parentWidth parentHeight fs rfs vpw vph)
2396
+
2397
+ ; Shadow
2398
+ shadowRadius.resolve(parentWidth fs)
2399
+ shadowOffsetX.resolve(parentWidth fs)
2400
+ shadowOffsetY.resolve(parentHeight fs)
2401
+
2402
+ ; Determine if absolute positioning
2403
+ isAbsolute = (this.hasAbsolutePosition())
2404
+ }
2405
+
2406
+ ; Normalize a property spelling so `fontSize` and `font-size` are one key.
2407
+ sfn toKebab:string (name:string) {
2408
+ def out:string ""
2409
+ def len:int (strlen name)
2410
+ def i:int 0
2411
+ while (i < len) {
2412
+ def c:int (charAt name i)
2413
+ if ((c >= 65) && (c <= 90)) {
2414
+ if (i > 0) {
2415
+ out = out + "-"
2416
+ }
2417
+ out = out + (strfromcode (c + 32))
2418
+ } {
2419
+ out = out + (strfromcode c)
2420
+ }
2421
+ i = (i + 1)
2422
+ }
2423
+ return out
2424
+ }
2425
+
2426
+ ; --- transform ---------------------------------------------------------------
2427
+
2428
+ ; Compose the function list into one similarity: angle, uniform scale,
2429
+ ; offset. Left to right, the way CSS reads them, and the composition is the
2430
+ ; reason order is honoured for free:
2431
+ ;
2432
+ ; M = (a, s, t) means p -> R(a)·s·p + t
2433
+ ; M ∘ N means angle a+b, scale s·s2, offset t + R(a)·s·u
2434
+ ;
2435
+ ; which is why `rotate(90deg) translate(10px, 0)` moves DOWN the page and
2436
+ ; `translate(10px, 0) rotate(90deg)` moves right. Applying the pieces
2437
+ ; independently gets both of those wrong and looks correct whenever only one
2438
+ ; of them is present, which is most of the time.
2439
+ fn applyTransform:void (value:string) {
2440
+ transformSpec = (trim value)
2441
+ rotate = 0.0
2442
+ scale = 1.0
2443
+ flipY = false
2444
+ translateX = 0.0
2445
+ translateY = 0.0
2446
+ if ((transformSpec == "none") || ((strlen transformSpec) == 0)) {
2447
+ return
2448
+ }
2449
+ def parts:[string] (EVGElement.splitWords(transformSpec))
2450
+ def a:double 0.0
2451
+ def sc:double 1.0
2452
+ def flip:boolean false
2453
+ def tx:double 0.0
2454
+ def ty:double 0.0
2455
+ def i:int 0
2456
+ while (i < (array_length parts)) {
2457
+ def one:string (trim (itemAt parts i))
2458
+ def b:double 0.0
2459
+ def s2:double 1.0
2460
+ def f2:boolean false
2461
+ def ux:double 0.0
2462
+ def uy:double 0.0
2463
+ def args:string (EVGElement.callArgs(one "rotate"))
2464
+ if ((strlen args) > 0) {
2465
+ b = (EVGElement.parseAngleDeg(args))
2466
+ } {
2467
+ def sargs:string (EVGElement.callArgs(one "scale"))
2468
+ if ((strlen sargs) > 0) {
2469
+ def nums:[double] (EVGElement.numberList(sargs))
2470
+ if ((array_length nums) > 0) {
2471
+ s2 = (itemAt nums 0)
2472
+ }
2473
+ ; Two arguments of the same magnitude and opposite sign
2474
+ ; are `s · diag(1, -1)`: the factor is the first of them
2475
+ ; and the rest is a flip.
2476
+ if ((array_length nums) > 1) {
2477
+ if (((itemAt nums 0) * (itemAt nums 1)) < 0.0) {
2478
+ f2 = true
2479
+ }
2480
+ }
2481
+ } {
2482
+ def targs:string (EVGElement.callArgs(one "translate"))
2483
+ if ((strlen targs) > 0) {
2484
+ def tn:[double] (EVGElement.numberList(targs))
2485
+ if ((array_length tn) > 0) {
2486
+ ux = (itemAt tn 0)
2487
+ }
2488
+ if ((array_length tn) > 1) {
2489
+ uy = (itemAt tn 1)
2490
+ }
2491
+ } {
2492
+ def xargs:string (EVGElement.callArgs(one "translateX"))
2493
+ if ((strlen xargs) > 0) {
2494
+ def xn:[double] (EVGElement.numberList(xargs))
2495
+ if ((array_length xn) > 0) {
2496
+ ux = (itemAt xn 0)
2497
+ }
2498
+ } {
2499
+ def yargs:string (EVGElement.callArgs(one "translateY"))
2500
+ if ((strlen yargs) > 0) {
2501
+ def yn:[double] (EVGElement.numberList(yargs))
2502
+ if ((array_length yn) > 0) {
2503
+ uy = (itemAt yn 0)
2504
+ }
2505
+ }
2506
+ }
2507
+ }
2508
+ }
2509
+ }
2510
+ def rad:double ((a * 3.14159265358979) / 180.0)
2511
+ def cs:double (cos rad)
2512
+ def sn:double (sin rad)
2513
+ ; A flip already in force turns the pieces after it around: the
2514
+ ; offset is mirrored before it is rotated, and the angle of a
2515
+ ; rotation the other side of a mirror is its negative.
2516
+ ; F·R(b) = R(-b)·F
2517
+ ; which is what keeps `scale(1,-1) rotate(30deg)` different from
2518
+ ; `rotate(30deg) scale(1,-1)`, the way the order already matters
2519
+ ; for a translate.
2520
+ def uyf:double uy
2521
+ def bf:double b
2522
+ if flip {
2523
+ uyf = (0.0 - uy)
2524
+ bf = (0.0 - b)
2525
+ }
2526
+ tx = (tx + (sc * ((ux * cs) - (uyf * sn))))
2527
+ ty = (ty + (sc * ((ux * sn) + (uyf * cs))))
2528
+ a = (a + bf)
2529
+ sc = (sc * s2)
2530
+ if f2 {
2531
+ flip = (flip == false)
2532
+ }
2533
+ i = (i + 1)
2534
+ }
2535
+ rotate = a
2536
+ scale = sc
2537
+ flipY = flip
2538
+ translateX = tx
2539
+ translateY = ty
2540
+ }
2541
+
2542
+ ; --- transform-origin ---------------------------------------------------------
2543
+
2544
+ ; `transform-origin: left top`, `25% 10px`, `bottom`, `10px`.
2545
+ ;
2546
+ ; The grammar is not "first value is x". A lone LENGTH sets x and leaves y
2547
+ ; at 50%, but a lone `top` or `bottom` is a Y keyword and sets the other
2548
+ ; axis — so `transform-origin: 10px` is `10px 50%` while
2549
+ ; `transform-origin: top` is `50% 0%`. And with two keywords the ORDER IS
2550
+ ; FREE: `top left` and `left top` are the same point. Both of those are
2551
+ ; measured, not read off a grammar: a browser was asked for the resolved
2552
+ ; value of seventeen spellings and `oracle/css-timing.json` holds the
2553
+ ; answers.
2554
+ ;
2555
+ ; A third value is the z origin. It is accepted and ignored; this is a 2-D
2556
+ ; engine and rejecting the declaration outright would throw away the x and
2557
+ ; y with it.
2558
+ fn applyTransformOrigin:void (value:string) {
2559
+ transformOriginSpec = (trim value)
2560
+ def words:[string] (EVGElement.splitWords(transformOriginSpec))
2561
+ def n:int (array_length words)
2562
+ if (n == 0) {
2563
+ transformOriginX = (new EVGUnit())
2564
+ transformOriginY = (new EVGUnit())
2565
+ return
2566
+ }
2567
+ def first:string (trim (itemAt words 0))
2568
+ if (n == 1) {
2569
+ if (EVGElement.isYKeyword(first)) {
2570
+ transformOriginX = (EVGUnit.percent(50.0))
2571
+ transformOriginY = (EVGElement.originUnit(first))
2572
+ } {
2573
+ transformOriginX = (EVGElement.originUnit(first))
2574
+ transformOriginY = (EVGUnit.percent(50.0))
2575
+ }
2576
+ return
2577
+ }
2578
+ def second:string (trim (itemAt words 1))
2579
+ ; Swap when the pair was written the other way round. Only a keyword can
2580
+ ; say which axis it belongs to, so only a keyword can trigger this — two
2581
+ ; lengths are always x then y.
2582
+ def swap:boolean false
2583
+ if (EVGElement.isYKeyword(first)) { swap = true }
2584
+ if (EVGElement.isXKeyword(second)) { swap = true }
2585
+ if swap {
2586
+ transformOriginX = (EVGElement.originUnit(second))
2587
+ transformOriginY = (EVGElement.originUnit(first))
2588
+ } {
2589
+ transformOriginX = (EVGElement.originUnit(first))
2590
+ transformOriginY = (EVGElement.originUnit(second))
2591
+ }
2592
+ }
2593
+
2594
+ sfn isXKeyword:boolean (w:string) {
2595
+ return ((w == "left") || (w == "right"))
2596
+ }
2597
+
2598
+ sfn isYKeyword:boolean (w:string) {
2599
+ return ((w == "top") || (w == "bottom"))
2600
+ }
2601
+
2602
+ ; A keyword is a percentage of the box. `center` belongs to neither axis
2603
+ ; and means the same on both, which is why it is not in either list above.
2604
+ sfn originUnit:EVGUnit (w:string) {
2605
+ if ((w == "left") || (w == "top")) { return (EVGUnit.percent(0.0)) }
2606
+ if ((w == "right") || (w == "bottom")) { return (EVGUnit.percent(100.0)) }
2607
+ if (w == "center") { return (EVGUnit.percent(50.0)) }
2608
+ return (EVGUnit.parse(w))
2609
+ }
2610
+
2611
+ ; The origin in pixels, from the box's own top-left corner. `size` is that
2612
+ ; edge of the border box.
2613
+ ;
2614
+ ; Unset is 50%, not 0: an element that never mentions `transform-origin`
2615
+ ; turns about its centre, and reading an unset unit as zero would put every
2616
+ ; untouched rotation on the top-left corner instead.
2617
+ sfn resolveOrigin:double (u:EVGUnit size:double) {
2618
+ if (u.isSet == false) {
2619
+ return (size / 2.0)
2620
+ }
2621
+ ; 1 is PERCENT, 3 is HEIGHT_PERCENT. Both resolve against the edge
2622
+ ; being asked about here, because the caller passes the right one.
2623
+ if ((u.unitType == 1) || (u.unitType == 3)) {
2624
+ return ((u.value / 100.0) * size)
2625
+ }
2626
+ return u.value
2627
+ }
2628
+
2629
+ ; "" when the value is one this engine can represent exactly, otherwise the
2630
+ ; reason. A stylesheet reports it; approximating a skew as a rotation would
2631
+ ; be worse than saying no.
2632
+ sfn transformProblem:string (value:string) {
2633
+ def v:string (trim value)
2634
+ if ((v == "none") || ((strlen v) == 0)) {
2635
+ return ""
2636
+ }
2637
+ def parts:[string] (EVGElement.splitWords(v))
2638
+ def i:int 0
2639
+ while (i < (array_length parts)) {
2640
+ def one:string (trim (itemAt parts i))
2641
+ def known:boolean false
2642
+ if ((strlen (EVGElement.callArgs(one "rotate"))) > 0) { known = true }
2643
+ if ((strlen (EVGElement.callArgs(one "scale"))) > 0) { known = true }
2644
+ if ((strlen (EVGElement.callArgs(one "translate"))) > 0) { known = true }
2645
+ if ((strlen (EVGElement.callArgs(one "translateX"))) > 0) { known = true }
2646
+ if ((strlen (EVGElement.callArgs(one "translateY"))) > 0) { known = true }
2647
+ if (known == false) {
2648
+ return ("Unsupported transform (rotate, scale, translate, translateX, translateY): " + one)
2649
+ }
2650
+ ; A non-uniform scale is not a similarity, so the display list
2651
+ ; cannot carry it as an angle and a factor. Two arguments whose
2652
+ ; MAGNITUDES differ is exactly that case — a difference of sign
2653
+ ; alone is a reflection, which is still a similarity and is
2654
+ ; carried as a flip beside the angle and the factor.
2655
+ def sargs:string (EVGElement.callArgs(one "scale"))
2656
+ if ((strlen sargs) > 0) {
2657
+ def nums:[double] (EVGElement.numberList(sargs))
2658
+ if ((array_length nums) > 1) {
2659
+ if ((fabs ((fabs (itemAt nums 0)) - (fabs (itemAt nums 1)))) > 0.0001) {
2660
+ return ("Only a uniform scale is supported: " + one)
2661
+ }
2662
+ }
2663
+ }
2664
+ i = (i + 1)
2665
+ }
2666
+ return ""
2667
+ }
2668
+
2669
+ ; The inside of `name(...)`, or "" if `one` is not that call. The name has
2670
+ ; to match WHOLE: `translateX(4px)` is not a `translate`, and accepting it
2671
+ ; as one silently drops the X.
2672
+ sfn callArgs:string (one:string name:string) {
2673
+ def nl2:int (strlen name)
2674
+ def tl:int (strlen one)
2675
+ if (tl < (nl2 + 3)) {
2676
+ return ""
2677
+ }
2678
+ if ((substring one 0 nl2) != name) {
2679
+ return ""
2680
+ }
2681
+ if ((charAt one nl2) != 40) {
2682
+ return ""
2683
+ }
2684
+ if ((charAt one (tl - 1)) != 41) {
2685
+ return ""
2686
+ }
2687
+ def inner:string (substring one (nl2 + 1) (tl - 1))
2688
+ if ((strlen inner) == 0) {
2689
+ return ""
2690
+ }
2691
+ return inner
2692
+ }
2693
+
2694
+ ; Comma-separated numbers with a unit suffix that is ignored: everything
2695
+ ; here is already in device pixels and a percentage would need the box.
2696
+ sfn numberList:[double] (s:string) {
2697
+ def out:[double]
2698
+ def parts:[string] (EVGElement.splitOnChar(s 44))
2699
+ def i:int 0
2700
+ while (i < (array_length parts)) {
2701
+ push out (EVGElement.leadingNumber((itemAt parts i)))
2702
+ i = (i + 1)
2703
+ }
2704
+ return out
2705
+ }
2706
+
2707
+ sfn splitOnChar:[string] (s:string ch:int) {
2708
+ def out:[string]
2709
+ def start:int 0
2710
+ def i:int 0
2711
+ def n:int (strlen s)
2712
+ while (i < n) {
2713
+ if ((charAt s i) == ch) {
2714
+ push out (substring s start i)
2715
+ start = (i + 1)
2716
+ }
2717
+ i = (i + 1)
2718
+ }
2719
+ push out (substring s start n)
2720
+ return out
2721
+ }
2722
+
2723
+ ; The number at the front, unit discarded. `4px` and `4` are the same
2724
+ ; length here; a bare `0` is legal CSS and has to parse.
2725
+ sfn leadingNumber:double (s:string) {
2726
+ def t:string (trim s)
2727
+ def n:int (strlen t)
2728
+ def stop:int 0
2729
+ def scanning:boolean true
2730
+ while (scanning && (stop < n)) {
2731
+ def c:int (charAt t stop)
2732
+ def digit:boolean ((c >= 48) && (c <= 57))
2733
+ def signOrDot:boolean (((c == 45) || (c == 43)) || (c == 46))
2734
+ if (digit || signOrDot) {
2735
+ stop = (stop + 1)
2736
+ } {
2737
+ scanning = false
2738
+ }
2739
+ }
2740
+ if (stop == 0) {
2741
+ return 0.0
2742
+ }
2743
+ def v@(optional):double (to_double (substring t 0 stop))
2744
+ if v {
2745
+ return (unwrap v)
2746
+ }
2747
+ return 0.0
2748
+ }
2749
+
2750
+ ; deg, rad, turn and grad, because a stylesheet that says `0.25turn` means
2751
+ ; a quarter circle and reading it as a quarter of a degree is a bug nobody
2752
+ ; would think to look for. `turn` and `grad` are checked BEFORE `rad`: both
2753
+ ; of them end in those three letters.
2754
+ sfn parseAngleDeg:double (text:string) {
2755
+ def t:string (trim text)
2756
+ def v:double (EVGElement.leadingNumber(t))
2757
+ def n:int (strlen t)
2758
+ if (n > 4) {
2759
+ def four:string (substring t (n - 4) n)
2760
+ if (four == "turn") {
2761
+ return (v * 360.0)
2762
+ }
2763
+ if (four == "grad") {
2764
+ return ((v * 360.0) / 400.0)
2765
+ }
2766
+ }
2767
+ if (n > 3) {
2768
+ def three:string (substring t (n - 3) n)
2769
+ if (three == "rad") {
2770
+ return ((v * 180.0) / 3.14159265358979)
2771
+ }
2772
+ if (three == "deg") {
2773
+ return v
2774
+ }
2775
+ }
2776
+ return v
2777
+ }
2778
+
2779
+ ; Split a CSS value on whitespace, but never inside parentheses —
2780
+ ; `rgb(203, 213, 225)` is ONE token, and splitting it would hand the
2781
+ ; border shorthand three pieces of nonsense.
2782
+ ; CSS box shorthands take one to four values, and EVG used to read the
2783
+ ; whole declaration as ONE unit: `padding: 0 16px` put nothing on the sides
2784
+ ; at all, because the parser saw the leading `0` and applied it four times.
2785
+ ; Every two-, three- and four-value shorthand in the gallery had been wrong
2786
+ ; since the sheets were written, which nobody noticed because a padding
2787
+ ; that is too small looks like a layout someone chose.
2788
+ ;
2789
+ ; The mapping and the failure rules are measured in `oracle/css-box.json`:
2790
+ ; one value is all four sides, two are vertical then horizontal, three are
2791
+ ; top/horizontal/bottom, four are top/right/bottom/left. Anything else —
2792
+ ; five values, a component that is not a length, a negative PADDING — drops
2793
+ ; the whole declaration rather than the offending component, so the element
2794
+ ; keeps whatever it had. An empty result here means exactly that.
2795
+ sfn boxSides:[EVGUnit] (value:string isMargin:boolean) {
2796
+ def out:[EVGUnit]
2797
+ def words:[string] (EVGElement.splitWords(value))
2798
+ def n:int (array_length words)
2799
+ if (n < 1) {
2800
+ return out
2801
+ }
2802
+ if (n > 4) {
2803
+ return out
2804
+ }
2805
+ def parts:[EVGUnit]
2806
+ def i:int 0
2807
+ while (i < n) {
2808
+ def w:string (itemAt words i)
2809
+ def u:EVGUnit (EVGUnit.parse(w))
2810
+ if (u.isSet == false) {
2811
+ ; `auto` is the one word that legally parses to no length, and
2812
+ ; only on a margin. EVG has no auto-margin centring, so it lands
2813
+ ; where an unset margin lands: at zero.
2814
+ if (isMargin == false) {
2815
+ return out
2816
+ }
2817
+ if (w != "auto") {
2818
+ return out
2819
+ }
2820
+ } {
2821
+ if (u.value < 0.0) {
2822
+ if (isMargin == false) {
2823
+ return out
2824
+ }
2825
+ }
2826
+ }
2827
+ push parts u
2828
+ i = (i + 1)
2829
+ }
2830
+ def top:EVGUnit (itemAt parts 0)
2831
+ def right:EVGUnit top
2832
+ def bottom:EVGUnit top
2833
+ def left:EVGUnit top
2834
+ if (n > 1) {
2835
+ right = (itemAt parts 1)
2836
+ left = right
2837
+ }
2838
+ if (n > 2) {
2839
+ bottom = (itemAt parts 2)
2840
+ }
2841
+ if (n > 3) {
2842
+ left = (itemAt parts 3)
2843
+ }
2844
+ push out top
2845
+ push out right
2846
+ push out bottom
2847
+ push out left
2848
+ return out
2849
+ }
2850
+
2851
+ ; The radius out of `blur(12px)`, or 0 for `none` and for anything this
2852
+ ; does not understand. Called by `setStyle`, and separately by the style
2853
+ ; sheet's validator so an unsupported filter function is REPORTED — see
2854
+ ; `blurProblem`.
2855
+ ; "x y w h", four numbers or four percentages, into the element's four
2856
+ ; fractions. Anything else leaves them alone and says so, so a typo is a
2857
+ ; picture drawn whole rather than a picture drawn nowhere.
2858
+ sfn parseViewBox:boolean (value:string el:EVGElement) {
2859
+ def v:string (trim value)
2860
+ if ((strlen v) == 0) {
2861
+ return false
2862
+ }
2863
+ def words:[string] (EVGElement.splitWords(v))
2864
+ if ((array_length words) != 4) {
2865
+ return false
2866
+ }
2867
+ def got:[double]
2868
+ def i:int 0
2869
+ while (i < 4) {
2870
+ def w:string (itemAt words i)
2871
+ def u:EVGUnit (EVGUnit.parse(w))
2872
+ if (u.isSet == false) {
2873
+ return false
2874
+ }
2875
+ def n:double u.value
2876
+ if (u.isPercent()) {
2877
+ n = (n / 100.0)
2878
+ }
2879
+ push got n
2880
+ i = (i + 1)
2881
+ }
2882
+ def cw:double (itemAt got 2)
2883
+ def ch:double (itemAt got 3)
2884
+ ; A window with no width shows nothing; that is a typo, not a crop.
2885
+ if ((fabs cw) < 0.000001) {
2886
+ return false
2887
+ }
2888
+ if ((fabs ch) < 0.000001) {
2889
+ return false
2890
+ }
2891
+ el.imageViewBoxX = (itemAt got 0)
2892
+ el.imageViewBoxY = (itemAt got 1)
2893
+ el.imageViewBoxW = cw
2894
+ el.imageViewBoxH = ch
2895
+ return true
2896
+ }
2897
+
2898
+ sfn blurRadiusOf:double (value:string) {
2899
+ def v:string (trim value)
2900
+ if (v == "none") {
2901
+ return 0.0
2902
+ }
2903
+ if ((strlen v) == 0) {
2904
+ return 0.0
2905
+ }
2906
+ def words:[string] (EVGElement.splitWords(v))
2907
+ ; One function, and it has to be blur(). A list of filters is legal CSS
2908
+ ; and is not supported here; saying so is the point of returning 0 and
2909
+ ; letting `blurProblem` name it.
2910
+ if ((array_length words) != 1) {
2911
+ return 0.0
2912
+ }
2913
+ def one:string (itemAt words 0)
2914
+ def inner:string (EVGElement.callArgs(one "blur"))
2915
+ if ((strlen inner) == 0) {
2916
+ return 0.0
2917
+ }
2918
+ def u:EVGUnit (EVGUnit.parse(inner))
2919
+ if (u.isSet == false) {
2920
+ return 0.0
2921
+ }
2922
+ ; A negative radius is not a filter, it is a typo.
2923
+ if (u.value < 0.0) {
2924
+ return 0.0
2925
+ }
2926
+ ; Only absolute lengths: a percentage has nothing to resolve against
2927
+ ; here, since a blur radius is not a fraction of anything.
2928
+ if (u.unitType != 0) {
2929
+ return 0.0
2930
+ }
2931
+ return u.pixels
2932
+ }
2933
+
2934
+ ; Why a `backdrop-filter` did nothing, or "" when it is fine. The sheet
2935
+ ; reports this the way it reports an unknown timing function: a filter that
2936
+ ; is quietly ignored looks exactly like a backend that has not caught up.
2937
+ sfn blurProblem:string (value:string) {
2938
+ def v:string (trim value)
2939
+ if (v == "none") {
2940
+ return ""
2941
+ }
2942
+ if ((strlen v) == 0) {
2943
+ return ""
2944
+ }
2945
+ def words:[string] (EVGElement.splitWords(v))
2946
+ if ((array_length words) != 1) {
2947
+ return ("backdrop-filter: only a single blur() is supported, got '" + v + "'")
2948
+ }
2949
+ def one:string (itemAt words 0)
2950
+ def inner:string (EVGElement.callArgs(one "blur"))
2951
+ if ((strlen inner) == 0) {
2952
+ return ("backdrop-filter: only blur() is supported, got '" + v + "'")
2953
+ }
2954
+ def u:EVGUnit (EVGUnit.parse(inner))
2955
+ if (u.isSet == false) {
2956
+ return ("backdrop-filter: '" + inner + "' is not a length")
2957
+ }
2958
+ if (u.value < 0.0) {
2959
+ return ("backdrop-filter: a blur radius cannot be negative")
2960
+ }
2961
+ if (u.unitType != 0) {
2962
+ return ("backdrop-filter: blur() needs an absolute length, got '" + inner + "'")
2963
+ }
2964
+ return ""
2965
+ }
2966
+
2967
+ sfn splitWords:[string] (s:string) {
2968
+ def out:[string]
2969
+ def cur:string ""
2970
+ def depth:int 0
2971
+ def i:int 0
2972
+ def len:int (strlen s)
2973
+ while (i < len) {
2974
+ def c:int (charAt s i)
2975
+ if (c == 40) {
2976
+ depth = (depth + 1)
2977
+ }
2978
+ if (c == 41) {
2979
+ depth = (depth - 1)
2980
+ }
2981
+ def isSpace:boolean false
2982
+ if (depth == 0) {
2983
+ if (c == 32) {
2984
+ isSpace = true
2985
+ }
2986
+ if (c == 9) {
2987
+ isSpace = true
2988
+ }
2989
+ }
2990
+ if isSpace {
2991
+ if ((strlen cur) > 0) {
2992
+ push out cur
2993
+ cur = ""
2994
+ }
2995
+ } {
2996
+ cur = cur + (strfromcode c)
2997
+ }
2998
+ i = (i + 1)
2999
+ }
3000
+ if ((strlen cur) > 0) {
3001
+ push out cur
3002
+ }
3003
+ return out
3004
+ }
3005
+
3006
+ sfn isBorderStyleWord:boolean (tok:string) {
3007
+ if (tok == "solid") {
3008
+ return true
3009
+ }
3010
+ if (tok == "dashed") {
3011
+ return true
3012
+ }
3013
+ if (tok == "dotted") {
3014
+ return true
3015
+ }
3016
+ if (tok == "double") {
3017
+ return true
3018
+ }
3019
+ if (tok == "none") {
3020
+ return true
3021
+ }
3022
+ if (tok == "hidden") {
3023
+ return true
3024
+ }
3025
+ return false
3026
+ }
3027
+
3028
+ ; A width starts with a digit or a dot; anything else in the shorthand that
3029
+ ; is not a style keyword is the colour — `#abc`, `rgb(...)` or a name.
3030
+ ; A bare number, with a `px` or other unit allowed and ignored: these are
3031
+ ; EVG's own properties and they are all in page pixels or in per-second,
3032
+ ; so there is no unit to resolve — `28` and `28px` mean the same thing and
3033
+ ; both are things somebody will write.
3034
+ sfn numberOr:double (value:string fallback:double) {
3035
+ def digits:string ""
3036
+ def i:int 0
3037
+ while (i < (strlen value)) {
3038
+ ; `charAt` gives a CODEPOINT, not a one-character string, so the
3039
+ ; comparison is against numbers: 48..57 are the digits, 46 is the
3040
+ ; point and 45 the minus.
3041
+ def code:int (charAt value i)
3042
+ def keep:boolean ((code >= 48) && (code <= 57))
3043
+ if (code == 46) { keep = true }
3044
+ if (code == 45) { keep = true }
3045
+ if keep {
3046
+ digits = (digits + (substring value i (i + 1)))
3047
+ }
3048
+ i = (i + 1)
3049
+ }
3050
+ if ((strlen digits) == 0) {
3051
+ return fallback
3052
+ }
3053
+ def v@(optional):double (to_double digits)
3054
+ if v {
3055
+ return (unwrap v)
3056
+ }
3057
+ return fallback
3058
+ }
3059
+
3060
+ sfn looksLikeColor:boolean (tok:string) {
3061
+ if ((strlen tok) == 0) {
3062
+ return false
3063
+ }
3064
+ def c:int (charAt tok 0)
3065
+ if ((c >= 48) && (c <= 57)) {
3066
+ return false
3067
+ }
3068
+ if (c == 46) {
3069
+ return false
3070
+ }
3071
+ return true
3072
+ }
3073
+
3074
+ ; Record that the authoring layer set `name` directly on this element.
3075
+ ; The local is `prop` and not `key`: `key` is a field of this class now,
3076
+ ; and a local shadowing it here would be a rename waiting to go wrong.
3077
+ fn markCss:void (name:string) {
3078
+ def prop:string (EVGElement.toKebab(name))
3079
+ if ((this.fromCss(prop)) == false) {
3080
+ push cssProps prop
3081
+ }
3082
+ }
3083
+
3084
+ fn fromCss:boolean (name:string) {
3085
+ if ((array_length cssProps) == 0) {
3086
+ return false
3087
+ }
3088
+ def prop:string (EVGElement.toKebab(name))
3089
+ def i:int 0
3090
+ while (i < (array_length cssProps)) {
3091
+ if ((itemAt cssProps i) == prop) {
3092
+ return true
3093
+ }
3094
+ i = (i + 1)
3095
+ }
3096
+ return false
3097
+ }
3098
+
3099
+ fn clearCssMarks:void () {
3100
+ def empty:[string]
3101
+ cssProps = empty
3102
+ }
3103
+
3104
+ fn markInline:void (name:string) {
3105
+ def prop:string (EVGElement.toKebab(name))
3106
+ if ((this.hasInline(prop)) == false) {
3107
+ push inlineProps prop
3108
+ }
3109
+ }
3110
+
3111
+ ; Forget that the authoring layer set `name` directly.
3112
+ ;
3113
+ ; `markInline` had no inverse, which was fine while the only writer was a
3114
+ ; parser reading a document once. An edit that is undone needs one: putting
3115
+ ; the value back without clearing this leaves the property outranking every
3116
+ ; stylesheet rule, so the tree would look restored and cascade differently.
3117
+ fn unmarkInline:void (name:string) {
3118
+ def prop:string (EVGElement.toKebab(name))
3119
+ def kept:[string]
3120
+ def i:int 0
3121
+ while (i < (array_length inlineProps)) {
3122
+ def cur:string (itemAt inlineProps i)
3123
+ if (cur != prop) {
3124
+ push kept cur
3125
+ }
3126
+ i = (i + 1)
3127
+ }
3128
+ inlineProps = kept
3129
+ }
3130
+
3131
+ fn hasInline:boolean (name:string) {
3132
+ ; The overwhelmingly common answer, reached without converting the
3133
+ ; name. The stylesheet asks this once per declaration per element per
3134
+ ; frame — about 180,000 times on a 1600-row table — and almost every
3135
+ ; element has nothing inline at all, so paying for a kebab-case
3136
+ ; conversion in order to look in an empty list was most of what it cost.
3137
+ if ((array_length inlineProps) == 0) {
3138
+ return false
3139
+ }
3140
+ def prop:string (EVGElement.toKebab(name))
3141
+ def i:int 0
3142
+ while (i < (array_length inlineProps)) {
3143
+ if ((itemAt inlineProps i) == prop) {
3144
+ return true
3145
+ }
3146
+ i = (i + 1)
3147
+ }
3148
+ return false
3149
+ }
3150
+
3151
+ ; CSS `flex` shorthand: "1", "1 1 auto", "2 0 120px".
3152
+ ; one number -> grow, and basis 0 (CSS uses 0%, not auto)
3153
+ ; number number -> grow, shrink
3154
+ ; n n <length> -> grow, shrink, basis
3155
+ ; A single non-numeric value is treated as a basis, matching `flex: auto`
3156
+ ; and `flex: 120px`.
3157
+ fn setFlexShorthand:void (value:string) {
3158
+ def parts:[string] (EVGElement.splitSpaces(value))
3159
+ def n:int (array_length parts)
3160
+ if (n == 0) {
3161
+ return
3162
+ }
3163
+ def first:string (itemAt parts 0)
3164
+ ; to_double("150px") happily returns 150, so a bare unit value would be
3165
+ ; read as a grow factor. Only a token that is entirely numeric is one.
3166
+ def growVal@(optional):double (to_double first)
3167
+ if (EVGElement.isPlainNumber(first)) {
3168
+ flex = (unwrap growVal)
3169
+ ; A bare `flex: N` starts from zero, so items share the line in
3170
+ ; proportion to N regardless of any width they also carry.
3171
+ flexBasis = (EVGUnit.px(0.0))
3172
+ if (n >= 2) {
3173
+ def shrinkVal@(optional):double (to_double (itemAt parts 1))
3174
+ if shrinkVal {
3175
+ flexShrink = (unwrap shrinkVal)
3176
+ }
3177
+ }
3178
+ if (n >= 3) {
3179
+ flexBasis = (EVGUnit.parse((itemAt parts 2)))
3180
+ }
3181
+ } {
3182
+ ; `flex: auto` / `flex: 120px` — a basis with default grow/shrink.
3183
+ flexBasis = (EVGUnit.parse(first))
3184
+ flex = 1.0
3185
+ }
3186
+ }
3187
+
3188
+ sfn isPlainNumber:boolean (s:string) {
3189
+ def len:int (strlen s)
3190
+ if (len == 0) {
3191
+ return false
3192
+ }
3193
+ def digits:int 0
3194
+ def i:int 0
3195
+ while (i < len) {
3196
+ def c:int (charAt s i)
3197
+ def isDigit:boolean ((c >= 48) && (c <= 57))
3198
+ if isDigit {
3199
+ digits = (digits + 1)
3200
+ } {
3201
+ ; '.', '-' and '+' may appear; anything else means a unit.
3202
+ if (((c != 46) && (c != 45)) && (c != 43)) {
3203
+ return false
3204
+ }
3205
+ }
3206
+ i = (i + 1)
3207
+ }
3208
+ return (digits > 0)
3209
+ }
3210
+
3211
+ sfn splitSpaces:[string] (s:string) {
3212
+ def out:[string]
3213
+ def len:int (strlen s)
3214
+ def start:int 0
3215
+ def inTok:boolean false
3216
+ def i:int 0
3217
+ while (i < len) {
3218
+ def c:int (charAt s i)
3219
+ def isSpace:boolean (((c == 32) || (c == 9)) || ((c == 10) || (c == 13)))
3220
+ if isSpace {
3221
+ if inTok {
3222
+ push out (substring s start i)
3223
+ inTok = false
3224
+ }
3225
+ } {
3226
+ if (inTok == false) {
3227
+ start = i
3228
+ inTok = true
3229
+ }
3230
+ }
3231
+ i = (i + 1)
3232
+ }
3233
+ if inTok {
3234
+ push out (substring s start len)
3235
+ }
3236
+ return out
3237
+ }
3238
+
3239
+ ; Take on everything `other` describes, keeping this element's IDENTITY.
3240
+ ; (The parameter is `other` and not `src` because `src` is already a field
3241
+ ; of this class — the image source.)
3242
+ ;
3243
+ ; Every field of EVGElement is copied EXCEPT three:
3244
+ ;
3245
+ ; `parent` and `children` — structure, which `EVGReconcile` owns. Copying
3246
+ ; them here would splice the discarded tree back in.
3247
+ ;
3248
+ ; `transitions` — the in-flight animations, which are the whole reason
3249
+ ; this element is being kept rather than replaced. A flight remembers its
3250
+ ; own target and what it last wrote (`EVGFlight.hasWrote`), so preserving
3251
+ ; the list and overwriting everything else is exactly right: the sheet
3252
+ ; re-declares the destination, and `EVGTransition.reconcile` compares the
3253
+ ; two and either continues, redirects or lands.
3254
+ ;
3255
+ ; Object-typed fields are shared, not cloned. That is safe because `other`
3256
+ ; is a node of a tree being DISCARDED — it has exactly one live holder after
3257
+ ; this returns — and it is why this must never be used to copy one live
3258
+ ; element onto another.
3259
+ ;
3260
+ ; A field added to this class and not added here would be silently dropped
3261
+ ; on every rebuild, which is a bug that shows up as "it works until you
3262
+ ; interact with it". `scripts/check-evg-adopt.mjs` fails the build when the
3263
+ ; two lists disagree, so this cannot rot quietly.
3264
+ fn adoptFrom:void (other:EVGElement) {
3265
+ id = other.id
3266
+ key = other.key
3267
+ ; The link a subtree sits inside. A reconciled element that loses it
3268
+ ; stops being a link on the next rebuild — and in a PDF, where the
3269
+ ; href is an annotation rather than blue ink, there is nothing left on
3270
+ ; the page to say it ever was one.
3271
+ href = other.href
3272
+ tagName = other.tagName
3273
+ elementType = other.elementType
3274
+ format = other.format
3275
+ orientation = other.orientation
3276
+ pageWidth = other.pageWidth
3277
+ pageHeight = other.pageHeight
3278
+ width = other.width
3279
+ height = other.height
3280
+ minWidth = other.minWidth
3281
+ minHeight = other.minHeight
3282
+ maxWidth = other.maxWidth
3283
+ maxHeight = other.maxHeight
3284
+ left = other.left
3285
+ top = other.top
3286
+ right = other.right
3287
+ bottom = other.bottom
3288
+ x = other.x
3289
+ y = other.y
3290
+ box = other.box
3291
+ backgroundColor = other.backgroundColor
3292
+ opacity = other.opacity
3293
+ backdropBlur = other.backdropBlur
3294
+ gradientSet = other.gradientSet
3295
+ gradientFrom = other.gradientFrom
3296
+ gradientTo = other.gradientTo
3297
+ gradientDir = other.gradientDir
3298
+ absPosSet = other.absPosSet
3299
+ absX = other.absX
3300
+ absY = other.absY
3301
+ glowIntensity = other.glowIntensity
3302
+ bgImageSet = other.bgImageSet
3303
+ bgImagePath = other.bgImagePath
3304
+ textDir = other.textDir
3305
+ resolvedRtl = other.resolvedRtl
3306
+ direction = other.direction
3307
+ align = other.align
3308
+ verticalAlign = other.verticalAlign
3309
+ isInline = other.isInline
3310
+ lineBreak = other.lineBreak
3311
+ overflow = other.overflow
3312
+ cursor = other.cursor
3313
+ surfaceEffect = other.surfaceEffect
3314
+ rippleSpeed = other.rippleSpeed
3315
+ rippleWidth = other.rippleWidth
3316
+ rippleStrength = other.rippleStrength
3317
+ rippleDecay = other.rippleDecay
3318
+ rippleHighlight = other.rippleHighlight
3319
+ rippleRings = other.rippleRings
3320
+ rippleStagger = other.rippleStagger
3321
+ rippleFalloff = other.rippleFalloff
3322
+ rippleShine = other.rippleShine
3323
+ rippleGloss = other.rippleGloss
3324
+ rippleBump = other.rippleBump
3325
+ rippleLightX = other.rippleLightX
3326
+ rippleLightY = other.rippleLightY
3327
+ rippleLightZ = other.rippleLightZ
3328
+ rippleXs = other.rippleXs
3329
+ rippleYs = other.rippleYs
3330
+ rippleAges = other.rippleAges
3331
+ effectTrigger = other.effectTrigger
3332
+ fxNames = other.fxNames
3333
+ fxValues = other.fxValues
3334
+ documentCss = other.documentCss
3335
+ ; The marks go with the values. An element that took another's
3336
+ ; cascaded properties and not the record of where they came from is
3337
+ ; an element that writes the whole stylesheet into itself the next
3338
+ ; time the tree is serialized.
3339
+ cssProps = other.cssProps
3340
+ scrollTop = other.scrollTop
3341
+ scrollLeft = other.scrollLeft
3342
+ scrollWidth = other.scrollWidth
3343
+ scrollHeight = other.scrollHeight
3344
+ fontSize = other.fontSize
3345
+ fontSizeInherited = other.fontSizeInherited
3346
+ fontSizeBase = other.fontSizeBase
3347
+ rootFontSize = other.rootFontSize
3348
+ viewportW = other.viewportW
3349
+ viewportH = other.viewportH
3350
+ viewportRoot = other.viewportRoot
3351
+ viewportX = other.viewportX
3352
+ viewportY = other.viewportY
3353
+ fontFamily = other.fontFamily
3354
+ fontWeight = other.fontWeight
3355
+ letterSpacing = other.letterSpacing
3356
+ paragraphSpacing = other.paragraphSpacing
3357
+ strokeLineCap = other.strokeLineCap
3358
+ strokeLineJoin = other.strokeLineJoin
3359
+ lineHeight = other.lineHeight
3360
+ lineHeightUnit = other.lineHeightUnit
3361
+ textAlign = other.textAlign
3362
+ ; Written by the layout, not by the author — but adopted with the
3363
+ ; rest, because a reconciled element that lost it would draw its own
3364
+ ; text against the top of a box it was centred in until something
3365
+ ; forced a fresh layout.
3366
+ textShiftY = other.textShiftY
3367
+ whiteSpace = other.whiteSpace
3368
+ color = other.color
3369
+ emojiColor = other.emojiColor
3370
+ textContent = other.textContent
3371
+ display = other.display
3372
+ flex = other.flex
3373
+ flexShrink = other.flexShrink
3374
+ flexBasis = other.flexBasis
3375
+ flexDirection = other.flexDirection
3376
+ justifyContent = other.justifyContent
3377
+ alignItems = other.alignItems
3378
+ alignSelf = other.alignSelf
3379
+ alignContent = other.alignContent
3380
+ flexWrap = other.flexWrap
3381
+ gap = other.gap
3382
+ rowGap = other.rowGap
3383
+ columnGap = other.columnGap
3384
+ gridTemplateColumns = other.gridTemplateColumns
3385
+ gridTemplateRows = other.gridTemplateRows
3386
+ subgridColumnSizes = other.subgridColumnSizes
3387
+ subgridRowSizes = other.subgridRowSizes
3388
+ computedRowSizes = other.computedRowSizes
3389
+ subgridPending = other.subgridPending
3390
+ gridTemplateAreas = other.gridTemplateAreas
3391
+ gridAutoFlow = other.gridAutoFlow
3392
+ fullBleed = other.fullBleed
3393
+ gridArea = other.gridArea
3394
+ gridColumn = other.gridColumn
3395
+ gridRow = other.gridRow
3396
+ position = other.position
3397
+ marginTop = other.marginTop
3398
+ marginRight = other.marginRight
3399
+ marginBottom = other.marginBottom
3400
+ marginLeft = other.marginLeft
3401
+ paddingTop = other.paddingTop
3402
+ paddingRight = other.paddingRight
3403
+ paddingBottom = other.paddingBottom
3404
+ paddingLeft = other.paddingLeft
3405
+ borderWidth = other.borderWidth
3406
+ borderTopWidth = other.borderTopWidth
3407
+ borderRightWidth = other.borderRightWidth
3408
+ borderBottomWidth = other.borderBottomWidth
3409
+ borderLeftWidth = other.borderLeftWidth
3410
+ borderColor = other.borderColor
3411
+ src = other.src
3412
+ alt = other.alt
3413
+ preserveAspectRatio = other.preserveAspectRatio
3414
+ imageViewBox = other.imageViewBox
3415
+ imageViewBoxX = other.imageViewBoxX
3416
+ imageViewBoxY = other.imageViewBoxY
3417
+ imageViewBoxW = other.imageViewBoxW
3418
+ imageViewBoxH = other.imageViewBoxH
3419
+ imageViewBoxSet = other.imageViewBoxSet
3420
+ imageOffsetX = other.imageOffsetX
3421
+ imageOffsetY = other.imageOffsetY
3422
+ objectFit = other.objectFit
3423
+ sourceWidth = other.sourceWidth
3424
+ sourceHeight = other.sourceHeight
3425
+ svgPath = other.svgPath
3426
+ svgSource = other.svgSource
3427
+ svgDoc = other.svgDoc
3428
+ svgDocKey = other.svgDocKey
3429
+ ; What the layout wrote and what it keeps — see `keepLayout` — come
3430
+ ; from the fresh element, which is to say they are reset: a reconciled
3431
+ ; element is laid out, measured and drawn again. `paintStamp` is this
3432
+ ; element's own count of paint changes, and adopting is one.
3433
+ appliedScrollTop = other.appliedScrollTop
3434
+ appliedScrollLeft = other.appliedScrollLeft
3435
+ paintLeft = other.paintLeft
3436
+ paintTop = other.paintTop
3437
+ paintRight = other.paintRight
3438
+ paintBottom = other.paintBottom
3439
+ paintUnbounded = other.paintUnbounded
3440
+ paintHasEffect = other.paintHasEffect
3441
+ shiftDx = other.shiftDx
3442
+ shiftDy = other.shiftDy
3443
+ keepLayout = other.keepLayout
3444
+ hasOverlayBelow = other.hasOverlayBelow
3445
+ overlayScanned = other.overlayScanned
3446
+ hasLayout = other.hasLayout
3447
+ layoutClean = other.layoutClean
3448
+ layoutSkipped = other.layoutSkipped
3449
+ kidOffX = other.kidOffX
3450
+ kidOffY = other.kidOffY
3451
+ boundsFresh = other.boundsFresh
3452
+ lastParentW = other.lastParentW
3453
+ lastParentH = other.lastParentH
3454
+ lastFlex = other.lastFlex
3455
+ lastFlexW = other.lastFlexW
3456
+ lastFlexH = other.lastFlexH
3457
+ hasIntrinsicMin = other.hasIntrinsicMin
3458
+ hasIntrinsicMax = other.hasIntrinsicMax
3459
+ intrinsicMin = other.intrinsicMin
3460
+ intrinsicMax = other.intrinsicMax
3461
+ paintClean = other.paintClean
3462
+ paintStamp = (paintStamp + 1)
3463
+ scrollbarWidth = other.scrollbarWidth
3464
+ scrollbarThumb = other.scrollbarThumb
3465
+ scrollbarTrack = other.scrollbarTrack
3466
+ scrollbarLabel = other.scrollbarLabel
3467
+ viewBox = other.viewBox
3468
+ fillColor = other.fillColor
3469
+ strokeColor = other.strokeColor
3470
+ strokeWidth = other.strokeWidth
3471
+ fillRule = other.fillRule
3472
+ strokeDashArray = other.strokeDashArray
3473
+ strokeDashOffset = other.strokeDashOffset
3474
+ anchorName = other.anchorName
3475
+ connectorFrom = other.connectorFrom
3476
+ connectorTo = other.connectorTo
3477
+ connectorFromSide = other.connectorFromSide
3478
+ connectorToSide = other.connectorToSide
3479
+ connectorRouting = other.connectorRouting
3480
+ connectorFromOffset = other.connectorFromOffset
3481
+ connectorToOffset = other.connectorToOffset
3482
+ arrowStart = other.arrowStart
3483
+ arrowEnd = other.arrowEnd
3484
+ arrowSize = other.arrowSize
3485
+ arrowPath = other.arrowPath
3486
+ connectorResolved = other.connectorResolved
3487
+ clipPath = other.clipPath
3488
+ className = other.className
3489
+ theme = other.theme
3490
+ inlineProps = other.inlineProps
3491
+ imageQuality = other.imageQuality
3492
+ maxImageSize = other.maxImageSize
3493
+ rotate = other.rotate
3494
+ scale = other.scale
3495
+ flipY = other.flipY
3496
+ translateX = other.translateX
3497
+ translateY = other.translateY
3498
+ transformSpec = other.transformSpec
3499
+ transformOriginX = other.transformOriginX
3500
+ transformOriginY = other.transformOriginY
3501
+ transformOriginSpec = other.transformOriginSpec
3502
+ shadowRadius = other.shadowRadius
3503
+ shadowColor = other.shadowColor
3504
+ shadowOffsetX = other.shadowOffsetX
3505
+ shadowOffsetY = other.shadowOffsetY
3506
+ backgroundGradient = other.backgroundGradient
3507
+ gradient = other.gradient
3508
+ calculatedX = other.calculatedX
3509
+ calculatedY = other.calculatedY
3510
+ calculatedWidth = other.calculatedWidth
3511
+ calculatedHeight = other.calculatedHeight
3512
+ calculatedInnerWidth = other.calculatedInnerWidth
3513
+ calculatedInnerHeight = other.calculatedInnerHeight
3514
+ calculatedFlexWidth = other.calculatedFlexWidth
3515
+ hasFlexWidth = other.hasFlexWidth
3516
+ calculatedFlexHeight = other.calculatedFlexHeight
3517
+ hasFlexHeight = other.hasFlexHeight
3518
+ calculatedBaseline = other.calculatedBaseline
3519
+ calculatedDescent = other.calculatedDescent
3520
+ hasBaseline = other.hasBaseline
3521
+ hasDefiniteHeight = other.hasDefiniteHeight
3522
+ calculatedPage = other.calculatedPage
3523
+ isAbsolute = other.isAbsolute
3524
+ isOverlay = other.isOverlay
3525
+ positionAnchor = other.positionAnchor
3526
+ positionTryFallbacks = other.positionTryFallbacks
3527
+ positionTryOrder = other.positionTryOrder
3528
+ presentation = other.presentation
3529
+ sheetBelow = other.sheetBelow
3530
+ fitViewport = other.fitViewport
3531
+ anchorLeftSide = other.anchorLeftSide
3532
+ anchorLeftOffset = other.anchorLeftOffset
3533
+ anchorTopSide = other.anchorTopSide
3534
+ anchorTopOffset = other.anchorTopOffset
3535
+ anchorRightSide = other.anchorRightSide
3536
+ anchorRightOffset = other.anchorRightOffset
3537
+ anchorBottomSide = other.anchorBottomSide
3538
+ anchorBottomOffset = other.anchorBottomOffset
3539
+ overlayPlacedAlign = other.overlayPlacedAlign
3540
+ overlayPlacedPresentation = other.overlayPlacedPresentation
3541
+ overlayClamped = other.overlayClamped
3542
+ overlayAnchor = other.overlayAnchor
3543
+ isOverlayAnchor = other.isOverlayAnchor
3544
+ overlaySide = other.overlaySide
3545
+ overlayAlign = other.overlayAlign
3546
+ overlayGap = other.overlayGap
3547
+ overlayX = other.overlayX
3548
+ overlayY = other.overlayY
3549
+ overlayPlacedSide = other.overlayPlacedSide
3550
+ isHovered = other.isHovered
3551
+ isFocused = other.isFocused
3552
+ isPressed = other.isPressed
3553
+ transitionSpec = other.transitionSpec
3554
+ role = other.role
3555
+ a11yLabel = other.a11yLabel
3556
+ a11yValue = other.a11yValue
3557
+ a11yRequired = other.a11yRequired
3558
+ a11yInvalid = other.a11yInvalid
3559
+ a11yReadOnly = other.a11yReadOnly
3560
+ a11yRoleDescription = other.a11yRoleDescription
3561
+ a11yDescription = other.a11yDescription
3562
+ a11yDescribedBy = other.a11yDescribedBy
3563
+ a11yControls = other.a11yControls
3564
+ a11yLabelledBy = other.a11yLabelledBy
3565
+ a11yHasPopup = other.a11yHasPopup
3566
+ a11yRowCount = other.a11yRowCount
3567
+ a11yRowIndex = other.a11yRowIndex
3568
+ a11yHidden = other.a11yHidden
3569
+ a11yModal = other.a11yModal
3570
+ a11ySorted = other.a11ySorted
3571
+ a11yOrientation = other.a11yOrientation
3572
+ a11yCurrent = other.a11yCurrent
3573
+ a11yHasValue = other.a11yHasValue
3574
+ a11yValueNow = other.a11yValueNow
3575
+ a11yHasRange = other.a11yHasRange
3576
+ a11yValueMin = other.a11yValueMin
3577
+ a11yValueMax = other.a11yValueMax
3578
+ a11yChecked = other.a11yChecked
3579
+ a11yPressed = other.a11yPressed
3580
+ a11yExpanded = other.a11yExpanded
3581
+ a11ySelected = other.a11ySelected
3582
+ a11yDisabled = other.a11yDisabled
3583
+ a11yFocusable = other.a11yFocusable
3584
+ a11yPosInSet = other.a11yPosInSet
3585
+ a11ySetSize = other.a11ySetSize
3586
+ a11yLevel = other.a11yLevel
3587
+ ; Copied, and the values copied are the FRESH element's — which has
3588
+ ; never been styled. So a reconciled element always reads as
3589
+ ; "the sheet has not written here", and re-styles. That is the honest
3590
+ ; answer: `adoptFrom` has just overwritten every value the sheet had
3591
+ ; put there.
3592
+ styleClass = other.styleClass
3593
+ styleTheme = other.styleTheme
3594
+ styleBits = other.styleBits
3595
+ styleGen = other.styleGen
3596
+ styleSlot = other.styleSlot
3597
+ styleKids = other.styleKids
3598
+ ; Where the inspector last saw this element. Declared with the field
3599
+ ; and not copied here, so `evg:adopt:check` — which reads the field
3600
+ ; list and this function and compares them — named it the moment the
3601
+ ; suite could run again. A reconciled element that loses its slot is
3602
+ ; an element the display list and the hit test can no longer name.
3603
+ inspectSlot = other.inspectSlot
3604
+ isLayoutComplete = other.isLayoutComplete
3605
+ unitsResolved = other.unitsResolved
3606
+ hasReturn = other.hasReturn
3607
+ hasBreak = other.hasBreak
3608
+ hasContinue = other.hasContinue
3609
+ inheritedFontSize = other.inheritedFontSize
3610
+ }
3611
+
3612
+ ; `scrollbar-color: <thumb> <track>` — two colours, split on the space
3613
+ ; between them and not on the spaces inside an `rgba( )`. One colour
3614
+ ; is the thumb alone; `auto` for either is no colour of its own.
3615
+ fn setScrollbarColor:void (value:string) {
3616
+ def parts:[string]
3617
+ def cur@(mutable):string ""
3618
+ def depth@(mutable):int 0
3619
+ def i:int 0
3620
+ def n:int (strlen value)
3621
+ while (i < n) {
3622
+ def ch:string (substring value i (i + 1))
3623
+ if (ch == "(") {
3624
+ depth = (depth + 1)
3625
+ }
3626
+ if (ch == ")") {
3627
+ depth = (depth - 1)
3628
+ }
3629
+ if (((ch == " ") || (ch == "\t")) && (depth == 0)) {
3630
+ if ((strlen cur) > 0) {
3631
+ push parts cur
3632
+ cur = ""
3633
+ }
3634
+ } {
3635
+ cur = (cur + ch)
3636
+ }
3637
+ i = (i + 1)
3638
+ }
3639
+ if ((strlen cur) > 0) {
3640
+ push parts cur
3641
+ }
3642
+ scrollbarThumb = (EVGColor.noColor())
3643
+ scrollbarTrack = (EVGColor.noColor())
3644
+ if ((array_length parts) > 0) {
3645
+ def t:string (itemAt parts 0)
3646
+ if (t != "auto") {
3647
+ scrollbarThumb = (EVGColor.parse(t))
3648
+ }
3649
+ }
3650
+ if ((array_length parts) > 1) {
3651
+ def tr:string (itemAt parts 1)
3652
+ if (tr != "auto") {
3653
+ scrollbarTrack = (EVGColor.parse(tr))
3654
+ }
3655
+ }
3656
+ }
3657
+
3658
+ ; Parse a length, and SAY SO when it could not be parsed.
3659
+ ;
3660
+ ; `EVGUnit` deliberately leaves an unrecognised value unset, which is what
3661
+ ; a browser does with a declaration it cannot read — but a browser then
3662
+ ; shows you the struck-through line in its inspector, and this engine used
3663
+ ; to show you nothing. An unset width is not neutral here either: in a row
3664
+ ; it fills the parent, so `width: calc(100% - 40px)` did not fail to apply,
3665
+ ; it applied as `width: 100%`.
3666
+ ;
3667
+ ; The cost is one `isSet` test on a path that already parsed a string.
3668
+ ;
3669
+ ; `fit-content` is the one value that is understood AND left unset — the
3670
+ ; layout reads its unit type (6), not `isSet` — so it is not a rejection.
3671
+ ; Rave's strip counted it as one for every button on the page.
3672
+ fn unitOf:EVGUnit (name:string value:string) {
3673
+ def u:EVGUnit (EVGUnit.parse(value))
3674
+ if ((u.isSet == false) && (u.unitType != 6)) {
3675
+ if ((EVGReject.isAbsent(value)) == false) {
3676
+ EVGReject.note("unsupported length" name value)
3677
+ }
3678
+ }
3679
+ return u
3680
+ }
3681
+
3682
+ ; --- the generic effect parameters ---------------------------------------
3683
+ ;
3684
+ ; Last write wins, in place, so a rule that restates a parameter replaces
3685
+ ; it rather than appending a second one the reader would have to know to
3686
+ ; ignore.
3687
+ fn setFx:void (key:string value:double) {
3688
+ def i:int 0
3689
+ while (i < (array_length fxNames)) {
3690
+ if ((itemAt fxNames i) == key) {
3691
+ set fxValues i value
3692
+ return
3693
+ }
3694
+ i = (i + 1)
3695
+ }
3696
+ push fxNames key
3697
+ push fxValues value
3698
+ }
3699
+
3700
+ ; What was written under `key`, or the fallback. The plugin's defaults are
3701
+ ; the plugin's business — this only answers whether the page said anything.
3702
+ fn fxValue:double (key:string fallback:double) {
3703
+ def i:int 0
3704
+ while (i < (array_length fxNames)) {
3705
+ if ((itemAt fxNames i) == key) {
3706
+ return (itemAt fxValues i)
3707
+ }
3708
+ i = (i + 1)
3709
+ }
3710
+ return fallback
3711
+ }
3712
+
3713
+ fn setAttribute:void (name:string value:string) {
3714
+ ; Set attribute by name (for parser)
3715
+
3716
+ if ((name == "className") || (name == "class-name")) {
3717
+ className = value
3718
+ return
3719
+ }
3720
+ if (name == "theme") {
3721
+ theme = value
3722
+ return
3723
+ }
3724
+ if (name == "id") {
3725
+ id = value
3726
+ return
3727
+ }
3728
+ ; `src` and `alt` are set directly by the JSX layer AND forwarded here,
3729
+ ; so they have to mean something here too or every image on the page
3730
+ ; reports an unknown property. They are also the right home for them:
3731
+ ; an element built without a JSX layer should be able to say `src`.
3732
+ if (name == "src") {
3733
+ src = value
3734
+ return
3735
+ }
3736
+ if (name == "alt") {
3737
+ alt = value
3738
+ return
3739
+ }
3740
+ if (name == "href") {
3741
+ href = value
3742
+ return
3743
+ }
3744
+ if (name == "key") {
3745
+ key = value
3746
+ return
3747
+ }
3748
+
3749
+ ; Book format properties
3750
+ if (name == "format") {
3751
+ format = (to_lowercase value)
3752
+ return
3753
+ }
3754
+ if (name == "orientation") {
3755
+ orientation = (to_lowercase value)
3756
+ return
3757
+ }
3758
+ if (name == "pageWidth") {
3759
+ def pw@(optional):double (to_double value)
3760
+ if pw {
3761
+ pageWidth = (unwrap pw)
3762
+ }
3763
+ return
3764
+ }
3765
+ if (name == "pageHeight") {
3766
+ def ph@(optional):double (to_double value)
3767
+ if ph {
3768
+ pageHeight = (unwrap ph)
3769
+ }
3770
+ return
3771
+ }
3772
+
3773
+ ; Dimensions
3774
+ if (name == "width") {
3775
+ width = (this.unitOf(name value))
3776
+ return
3777
+ }
3778
+ if (name == "height") {
3779
+ height = (this.unitOf(name value))
3780
+ return
3781
+ }
3782
+ if ((name == "min-width") || (name == "minWidth")) {
3783
+ minWidth = (this.unitOf(name value))
3784
+ return
3785
+ }
3786
+ if ((name == "min-height") || (name == "minHeight")) {
3787
+ minHeight = (this.unitOf(name value))
3788
+ return
3789
+ }
3790
+ if ((name == "max-width") || (name == "maxWidth")) {
3791
+ maxWidth = (this.unitOf(name value))
3792
+ return
3793
+ }
3794
+ if ((name == "max-height") || (name == "maxHeight")) {
3795
+ maxHeight = (this.unitOf(name value))
3796
+ return
3797
+ }
3798
+
3799
+ ; Overlays
3800
+ if ((name == "overlay") || (name == "isOverlay")) {
3801
+ isOverlay = (EVGElement.truthy(value))
3802
+ return
3803
+ }
3804
+ if ((name == "overlay-anchor-role") || (name == "overlayAnchorRole")) {
3805
+ isOverlayAnchor = (EVGElement.truthy(value))
3806
+ return
3807
+ }
3808
+ if ((name == "overlay-side") || (name == "overlaySide")) {
3809
+ overlaySide = (trim value)
3810
+ return
3811
+ }
3812
+ if ((name == "overlay-align") || (name == "overlayAlign")) {
3813
+ overlayAlign = (trim value)
3814
+ return
3815
+ }
3816
+ ; `position-area: bottom start` is `overlay-side` and `overlay-align`
3817
+ ; in one declaration, which is how CSS writes it and how a fallback
3818
+ ; list has to write it — a fallback changes both at once.
3819
+ if ((name == "position-area") || (name == "positionArea")) {
3820
+ def pa:string (trim value)
3821
+ def paSide:string (EVGElement.areaSide(pa))
3822
+ if ((strlen paSide) == 0) {
3823
+ EVGReject.note("unsupported value" "position-area" pa)
3824
+ return
3825
+ }
3826
+ overlaySide = paSide
3827
+ overlayAlign = (EVGElement.areaAlign(pa))
3828
+ return
3829
+ }
3830
+ if ((name == "position-anchor") || (name == "positionAnchor")) {
3831
+ positionAnchor = (trim value)
3832
+ ; Positioned against another box: out of flow, top layer. The
3833
+ ; property says the whole thing.
3834
+ if ((strlen positionAnchor) > 0) {
3835
+ isOverlay = true
3836
+ }
3837
+ return
3838
+ }
3839
+ if (((name == "position-try-fallbacks") || (name == "positionTryFallbacks")) || (name == "position-try")) {
3840
+ positionTryFallbacks = (trim value)
3841
+ return
3842
+ }
3843
+ if ((name == "position-try-order") || (name == "positionTryOrder")) {
3844
+ positionTryOrder = (trim value)
3845
+ return
3846
+ }
3847
+ if (name == "presentation") {
3848
+ def pr:string (trim value)
3849
+ if ((((pr == "anchored") || (pr == "sheet")) || (pr == "fullscreen")) == false) {
3850
+ EVGReject.note("unsupported value" "presentation" pr)
3851
+ return
3852
+ }
3853
+ presentation = pr
3854
+ return
3855
+ }
3856
+ if ((name == "sheet-below") || (name == "sheetBelow")) {
3857
+ def sb:EVGUnit (this.unitOf(name value))
3858
+ if sb.isSet {
3859
+ sheetBelow = sb.pixels
3860
+ }
3861
+ return
3862
+ }
3863
+ if ((name == "fit-viewport") || (name == "fitViewport")) {
3864
+ fitViewport = (EVGElement.truthy(value))
3865
+ return
3866
+ }
3867
+ if ((name == "overlay-gap") || (name == "overlayGap")) {
3868
+ def g@(optional):double (to_double value)
3869
+ if g {
3870
+ overlayGap = (unwrap g)
3871
+ }
3872
+ return
3873
+ }
3874
+
3875
+ if (name == "transition") {
3876
+ transitionSpec = (trim value)
3877
+ return
3878
+ }
3879
+
3880
+ ; Accessibility. The ARIA spellings, so a tree literal and a stylesheet
3881
+ ; and a hand-written element all say it the same way.
3882
+ if ((name == "role") || (name == "a11yRole")) {
3883
+ role = (trim value)
3884
+ return
3885
+ }
3886
+ ; A BARE `return`, like every other branch here: `setAttribute` is
3887
+ ; declared `:void`. These four said `return true`, which JavaScript
3888
+ ; ignores and C++ refuses — `return-statement with a value, in function
3889
+ ; returning 'void'` — so the whole surface compiled and every JS suite
3890
+ ; passed while the C++ build of the deck editor was broken.
3891
+ if ((name == "a11yRequired") || (name == "aria-required")) {
3892
+ a11yRequired = value
3893
+ return
3894
+ }
3895
+ if ((name == "a11yInvalid") || (name == "aria-invalid")) {
3896
+ a11yInvalid = value
3897
+ return
3898
+ }
3899
+ if ((name == "a11yReadOnly") || (name == "aria-readonly")) {
3900
+ a11yReadOnly = value
3901
+ return
3902
+ }
3903
+ if ((name == "a11yValue") || (name == "aria-valuetext")) {
3904
+ a11yValue = value
3905
+ return
3906
+ }
3907
+ if ((name == "aria-label") || (name == "a11yLabel")) {
3908
+ a11yLabel = value
3909
+ return
3910
+ }
3911
+ if ((name == "aria-current") || (name == "a11yCurrent")) {
3912
+ a11yCurrent = value
3913
+ return
3914
+ }
3915
+ if ((name == "aria-orientation") || (name == "a11yOrientation")) {
3916
+ a11yOrientation = (to_lowercase value)
3917
+ return
3918
+ }
3919
+ if ((name == "aria-sort") || (name == "a11ySorted")) {
3920
+ if (value == "none") { a11ySorted = 1 }
3921
+ if (value == "ascending") { a11ySorted = 2 }
3922
+ if (value == "descending") { a11ySorted = 3 }
3923
+ return
3924
+ }
3925
+ if ((name == "aria-hidden") || (name == "a11yHidden")) {
3926
+ a11yHidden = (EVGElement.truthy(value))
3927
+ return
3928
+ }
3929
+ ; `to_int` on a string is OPTIONAL here — a value that is not a number
3930
+ ; has no integer to be — so it is unwrapped through a default rather
3931
+ ; than assigned straight across. Zero is the right default: it is what
3932
+ ; "this node publishes no row count" already means everywhere else.
3933
+ if ((name == "aria-rowcount") || (name == "a11yRowCount")) {
3934
+ def rc@(optional):int (to_int value)
3935
+ a11yRowCount = 0
3936
+ if rc {
3937
+ a11yRowCount = (unwrap rc)
3938
+ }
3939
+ return
3940
+ }
3941
+ if ((name == "aria-rowindex") || (name == "a11yRowIndex")) {
3942
+ def ri@(optional):int (to_int value)
3943
+ a11yRowIndex = 0
3944
+ if ri {
3945
+ a11yRowIndex = (unwrap ri)
3946
+ }
3947
+ return
3948
+ }
3949
+ if ((name == "aria-haspopup") || (name == "a11yHasPopup")) {
3950
+ a11yHasPopup = value
3951
+ return
3952
+ }
3953
+ if ((name == "aria-describedby") || (name == "a11yDescription")) {
3954
+ a11yDescription = value
3955
+ return
3956
+ }
3957
+ if (name == "a11yDescribedBy") {
3958
+ a11yDescribedBy = value
3959
+ return
3960
+ }
3961
+ if ((name == "aria-controls") || (name == "a11yControls")) {
3962
+ a11yControls = value
3963
+ return
3964
+ }
3965
+ if ((name == "aria-labelledby") || (name == "a11yLabelledBy")) {
3966
+ a11yLabelledBy = value
3967
+ return
3968
+ }
3969
+ if ((name == "aria-roledescription") || (name == "a11yRoleDescription")) {
3970
+ a11yRoleDescription = value
3971
+ return
3972
+ }
3973
+ if ((name == "aria-pressed") || (name == "a11yPressed")) {
3974
+ a11yPressed = (EVGElement.triState(value))
3975
+ return
3976
+ }
3977
+ if ((name == "aria-checked") || (name == "a11yChecked")) {
3978
+ a11yChecked = (EVGElement.triState(value))
3979
+ return
3980
+ }
3981
+ if ((name == "aria-expanded") || (name == "a11yExpanded")) {
3982
+ a11yExpanded = (EVGElement.triState(value))
3983
+ return
3984
+ }
3985
+ if ((name == "aria-selected") || (name == "a11ySelected")) {
3986
+ a11ySelected = (EVGElement.triState(value))
3987
+ return
3988
+ }
3989
+ if ((name == "aria-disabled") || (name == "a11yDisabled")) {
3990
+ a11yDisabled = (EVGElement.truthy(value))
3991
+ return
3992
+ }
3993
+ if ((name == "aria-focusable") || (name == "a11yFocusable")) {
3994
+ a11yFocusable = (EVGElement.truthy(value))
3995
+ return
3996
+ }
3997
+
3998
+ ; Position
3999
+ if (name == "left") {
4000
+ if (EVGElement.mentionsAnchor(value)) {
4001
+ anchorLeftSide = (EVGElement.anchorFnSide(name value))
4002
+ anchorLeftOffset = (EVGElement.anchorFnOffset(value))
4003
+ return
4004
+ }
4005
+ left = (this.unitOf(name value))
4006
+ return
4007
+ }
4008
+ if (name == "top") {
4009
+ if (EVGElement.mentionsAnchor(value)) {
4010
+ anchorTopSide = (EVGElement.anchorFnSide(name value))
4011
+ anchorTopOffset = (EVGElement.anchorFnOffset(value))
4012
+ return
4013
+ }
4014
+ top = (this.unitOf(name value))
4015
+ return
4016
+ }
4017
+ if (name == "right") {
4018
+ if (EVGElement.mentionsAnchor(value)) {
4019
+ anchorRightSide = (EVGElement.anchorFnSide(name value))
4020
+ anchorRightOffset = (EVGElement.anchorFnOffset(value))
4021
+ return
4022
+ }
4023
+ right = (this.unitOf(name value))
4024
+ return
4025
+ }
4026
+ if (name == "bottom") {
4027
+ if (EVGElement.mentionsAnchor(value)) {
4028
+ anchorBottomSide = (EVGElement.anchorFnSide(name value))
4029
+ anchorBottomOffset = (EVGElement.anchorFnOffset(value))
4030
+ return
4031
+ }
4032
+ bottom = (this.unitOf(name value))
4033
+ return
4034
+ }
4035
+ if (name == "x") {
4036
+ x = (this.unitOf(name value))
4037
+ return
4038
+ }
4039
+ if (name == "y") {
4040
+ y = (this.unitOf(name value))
4041
+ return
4042
+ }
4043
+
4044
+ ; Margin
4045
+ if (name == "margin") {
4046
+ def ms:[EVGUnit] (EVGElement.boxSides(value true))
4047
+ if ((array_length ms) == 4) {
4048
+ box.setMarginValues((itemAt ms 0) (itemAt ms 1) (itemAt ms 2) (itemAt ms 3))
4049
+ }
4050
+ return
4051
+ }
4052
+ if ((name == "margin-left") || (name == "marginLeft")) {
4053
+ box.marginLeft = (this.unitOf(name value))
4054
+ return
4055
+ }
4056
+ if ((name == "margin-right") || (name == "marginRight")) {
4057
+ box.marginRight = (this.unitOf(name value))
4058
+ return
4059
+ }
4060
+ if ((name == "margin-top") || (name == "marginTop")) {
4061
+ box.marginTop = (this.unitOf(name value))
4062
+ return
4063
+ }
4064
+ if ((name == "margin-bottom") || (name == "marginBottom")) {
4065
+ box.marginBottom = (this.unitOf(name value))
4066
+ return
4067
+ }
4068
+
4069
+ ; Padding
4070
+ if (name == "padding") {
4071
+ def ps:[EVGUnit] (EVGElement.boxSides(value false))
4072
+ if ((array_length ps) == 4) {
4073
+ box.setPaddingValues((itemAt ps 0) (itemAt ps 1) (itemAt ps 2) (itemAt ps 3))
4074
+ }
4075
+ return
4076
+ }
4077
+ if ((name == "padding-left") || (name == "paddingLeft")) {
4078
+ box.paddingLeft = (this.unitOf(name value))
4079
+ return
4080
+ }
4081
+ if ((name == "padding-right") || (name == "paddingRight")) {
4082
+ box.paddingRight = (this.unitOf(name value))
4083
+ return
4084
+ }
4085
+ if ((name == "padding-top") || (name == "paddingTop")) {
4086
+ box.paddingTop = (this.unitOf(name value))
4087
+ return
4088
+ }
4089
+ if ((name == "padding-bottom") || (name == "paddingBottom")) {
4090
+ box.paddingBottom = (this.unitOf(name value))
4091
+ return
4092
+ }
4093
+
4094
+ ; Border
4095
+ ;
4096
+ ; The shorthand, because it is what anyone writing CSS reaches for and
4097
+ ; it used to be dropped in silence: no error from the stylesheet, no
4098
+ ; border on the screen. Order-independent, like CSS — a length is the
4099
+ ; width, a colour is the colour, and a style keyword is accepted and
4100
+ ; ignored, since EVG strokes one way.
4101
+ if (name == "border") {
4102
+ def parts:[string] (EVGElement.splitWords(value))
4103
+ def i:int 0
4104
+ while (i < (array_length parts)) {
4105
+ def tok:string (itemAt parts i)
4106
+ if (EVGElement.isBorderStyleWord(tok)) {
4107
+ ; `none` means no border at all, whatever else was written.
4108
+ if (tok == "none") {
4109
+ box.borderWidth = (EVGUnit.px(0.0))
4110
+ }
4111
+ } {
4112
+ if (EVGElement.looksLikeColor(tok)) {
4113
+ box.borderColor = (EVGColor.parse(tok))
4114
+ } {
4115
+ box.borderWidth = (EVGUnit.parse(tok))
4116
+ }
4117
+ }
4118
+ i = (i + 1)
4119
+ }
4120
+ return
4121
+ }
4122
+ if ((name == "border-width") || (name == "borderWidth")) {
4123
+ box.borderWidth = (this.unitOf(name value))
4124
+ return
4125
+ }
4126
+ if ((name == "border-color") || (name == "borderColor")) {
4127
+ box.borderColor = (EVGColor.parse(value))
4128
+ return
4129
+ }
4130
+ ; ONE TO FOUR VALUES, clockwise from the top left. The fill-in rules
4131
+ ; are the shorthand's own and are not the ones `padding` uses: two
4132
+ ; values are (TL+BR, TR+BL) — a DIAGONAL pair — where two paddings are
4133
+ ; vertical and horizontal. Three are (TL, TR+BL, BR). Measured against
4134
+ ; a browser in `oracle/radius.json` rather than recalled.
4135
+ if ((name == "border-radius") || (name == "borderRadius")) {
4136
+ def parts:[string] (EVGElement.splitWords(value))
4137
+ def n:int (array_length parts)
4138
+ if (n < 2) {
4139
+ box.borderRadius = (this.unitOf(name value))
4140
+ box.borderRadiusTL = (EVGUnit.unset())
4141
+ box.borderRadiusTR = (EVGUnit.unset())
4142
+ box.borderRadiusBR = (EVGUnit.unset())
4143
+ box.borderRadiusBL = (EVGUnit.unset())
4144
+ return
4145
+ }
4146
+ def tl:string (itemAt parts 0)
4147
+ def tr:string (itemAt parts 1)
4148
+ def br:string tl
4149
+ def bl:string tr
4150
+ if (n > 2) {
4151
+ br = (itemAt parts 2)
4152
+ }
4153
+ if (n > 3) {
4154
+ bl = (itemAt parts 3)
4155
+ }
4156
+ box.borderRadiusTL = (EVGUnit.parse(tl))
4157
+ box.borderRadiusTR = (EVGUnit.parse(tr))
4158
+ box.borderRadiusBR = (EVGUnit.parse(br))
4159
+ box.borderRadiusBL = (EVGUnit.parse(bl))
4160
+ ; The uniform value stays the FIRST corner, because every backend
4161
+ ; but the WebGL painter still reads one number and the top left is
4162
+ ; the one the shorthand names first.
4163
+ box.borderRadius = (EVGUnit.parse(tl))
4164
+ return
4165
+ }
4166
+
4167
+ ; ---- rich EVG effect props, drawn by WasmUiRenderer from these fields.
4168
+ ; camelCase spellings so the TSX parser (which splits '-') can carry them.
4169
+ ; Animated glow halo strength (0..1).
4170
+ if (name == "glow") {
4171
+ def gv@(optional):double (to_double value)
4172
+ glowIntensity = (unwrap gv)
4173
+ return
4174
+ }
4175
+ ; Background art clipped to the rounded box (the .as menu's tile image).
4176
+ if ((name == "background-image") || (name == "backgroundImage")) {
4177
+ bgImageSet = true
4178
+ bgImagePath = value
4179
+ return
4180
+ }
4181
+ ; 2-stop linear gradient fill (RGU1 GRAD_FROM/GRAD_TO/GRAD_DIR).
4182
+ if ((name == "gradient-from") || (name == "gradientFrom")) {
4183
+ gradientFrom = (EVGColor.parse(value))
4184
+ gradientSet = true
4185
+ return
4186
+ }
4187
+ if ((name == "gradient-to") || (name == "gradientTo")) {
4188
+ gradientTo = (EVGColor.parse(value))
4189
+ gradientSet = true
4190
+ return
4191
+ }
4192
+ if ((name == "gradient-dir") || (name == "gradientDir")) {
4193
+ def dv@(optional):int (to_int value)
4194
+ gradientDir = (unwrap dv)
4195
+ return
4196
+ }
4197
+
4198
+ ; Colors
4199
+ if ((name == "background-color") || (name == "backgroundColor")) {
4200
+ backgroundColor = (EVGColor.parse(value))
4201
+ return
4202
+ }
4203
+ if ((name == "background-gradient") || (name == "backgroundGradient")) {
4204
+ backgroundGradient = value
4205
+ gradient = (EVGGradient.parse(value))
4206
+ return
4207
+ }
4208
+ ; Also support "background" which can be a gradient
4209
+ if (name == "background") {
4210
+ ; Check if it's a gradient
4211
+ if ((contains value "linear-gradient") || (contains value "radial-gradient")) {
4212
+ backgroundGradient = value
4213
+ gradient = (EVGGradient.parse(value))
4214
+ } {
4215
+ ; Treat as color
4216
+ backgroundColor = (EVGColor.parse(value))
4217
+ }
4218
+ return
4219
+ }
4220
+ if (name == "color") {
4221
+ color = (EVGColor.parse(value))
4222
+ return
4223
+ }
4224
+ if (name == "emoji-color") {
4225
+ emojiColor = (EVGColor.parse(value))
4226
+ return
4227
+ }
4228
+ if (name == "opacity") {
4229
+ def val@(optional):double (to_double value)
4230
+ opacity = (unwrap val)
4231
+ return
4232
+ }
4233
+ ; `backdrop-filter: blur(12px)`. Only `blur()` is understood; anything
4234
+ ; else in the property is reported rather than half-applied, because a
4235
+ ; `saturate()` that silently did nothing would look like a backend that
4236
+ ; had not implemented it yet.
4237
+ if ((name == "backdrop-filter") || (name == "backdropFilter")) {
4238
+ backdropBlur = (EVGElement.blurRadiusOf(value))
4239
+ return
4240
+ }
4241
+
4242
+ ; Image properties
4243
+ if ((name == "object-fit") || (name == "objectFit")) {
4244
+ objectFit = value
4245
+ return
4246
+ }
4247
+ ; The crop window into the source. A picture is not always shown
4248
+ ; whole: Figma keeps a crop rectangle on the paint, and a backend
4249
+ ; that drops it draws the whole frame where a detail belongs — two
4250
+ ; boxes showing two halves of one screenshot come out as the same
4251
+ ; picture twice.
4252
+ if ((name == "preserve-aspect-ratio") || (name == "preserveAspectRatio")) {
4253
+ preserveAspectRatio = value
4254
+ return
4255
+ }
4256
+ if ((name == "image-view-box") || (name == "imageViewBox")) {
4257
+ imageViewBox = value
4258
+ imageViewBoxSet = (EVGElement.parseViewBox(value this))
4259
+ return
4260
+ }
4261
+ if ((name == "image-offset-x") || (name == "imageOffsetX")) {
4262
+ imageOffsetX = (this.unitOf(name value))
4263
+ return
4264
+ }
4265
+ if ((name == "image-offset-y") || (name == "imageOffsetY")) {
4266
+ imageOffsetY = (this.unitOf(name value))
4267
+ return
4268
+ }
4269
+
4270
+ ; Layout
4271
+ ; `direction` is two properties sharing a name; the value says which.
4272
+ ; See the note on `textDir`.
4273
+ if (name == "direction") {
4274
+ if ((value == "rtl") || (value == "ltr")) {
4275
+ textDir = value
4276
+ return
4277
+ }
4278
+ direction = value
4279
+ return
4280
+ }
4281
+ if (name == "align") {
4282
+ align = value
4283
+ return
4284
+ }
4285
+ if ((name == "vertical-align") || (name == "verticalAlign")) {
4286
+ verticalAlign = value
4287
+ return
4288
+ }
4289
+ if (name == "inline") {
4290
+ isInline = (value == "true")
4291
+ return
4292
+ }
4293
+ if ((name == "line-break") || (name == "lineBreak")) {
4294
+ lineBreak = (value == "true")
4295
+ return
4296
+ }
4297
+ if ((name == "letter-spacing") || (name == "letterSpacing")) {
4298
+ ; `normal` is the font's own, which is zero extra.
4299
+ if (value == "normal") {
4300
+ letterSpacing = 0.0
4301
+ return
4302
+ }
4303
+ def lsu:EVGUnit (this.unitOf(name value))
4304
+ if lsu.isSet {
4305
+ letterSpacing = lsu.pixels
4306
+ }
4307
+ return
4308
+ }
4309
+ if ((name == "paragraph-spacing") || (name == "paragraphSpacing")) {
4310
+ def psu:EVGUnit (this.unitOf(name value))
4311
+ if psu.isSet {
4312
+ paragraphSpacing = psu.pixels
4313
+ }
4314
+ return
4315
+ }
4316
+ if ((name == "stroke-linecap") || (name == "strokeLinecap")) {
4317
+ strokeLineCap = value
4318
+ return
4319
+ }
4320
+ if ((name == "stroke-linejoin") || (name == "strokeLinejoin")) {
4321
+ strokeLineJoin = value
4322
+ return
4323
+ }
4324
+ if (name == "overflow") {
4325
+ overflow = value
4326
+ return
4327
+ }
4328
+ ; ONE OVERFLOW, BOTH AXES. This engine clips a box or it does not;
4329
+ ; there is no per-axis clip, and the honest thing is to say so rather
4330
+ ; than drop the declaration. `overflow-y: auto` on a menu is how the
4331
+ ; web writes "and scroll if it is too tall", and dropping it left the
4332
+ ; menu running off the page with no sign anything had been ignored.
4333
+ ; A surface that clips both axes is not a problem for a submenu: a
4334
+ ; nested surface is drawn in the top layer, outside every clip.
4335
+ if (((name == "overflow-y") || (name == "overflowY")) || (name == "overflow-x")) {
4336
+ overflow = (trim value)
4337
+ return
4338
+ }
4339
+ if (name == "overflowX") {
4340
+ overflow = (trim value)
4341
+ return
4342
+ }
4343
+ if (name == "cursor") {
4344
+ cursor = value
4345
+ return
4346
+ }
4347
+ if ((name == "scrollbar-width") || (name == "scrollbarWidth")) {
4348
+ scrollbarWidth = value
4349
+ return
4350
+ }
4351
+ if ((name == "scrollbar-color") || (name == "scrollbarColor")) {
4352
+ this.setScrollbarColor(value)
4353
+ return
4354
+ }
4355
+ if (name == "evg-scrollbar-label") {
4356
+ scrollbarLabel = value
4357
+ return
4358
+ }
4359
+ ; The EVG effect family. Prefixed, because these are EXTENSIONS and not
4360
+ ; CSS: a reader who meets `evg-ripple-speed` should not go looking for
4361
+ ; it in a specification.
4362
+ if (name == "evg-surface-effect") {
4363
+ surfaceEffect = value
4364
+ return
4365
+ }
4366
+ if (name == "evg-effect-on") {
4367
+ effectTrigger = value
4368
+ return
4369
+ }
4370
+ ; `evg-fx-<name>` — anything at all, under whatever name the plugin
4371
+ ; that reads it chose. Matched by PREFIX and not against a list,
4372
+ ; because the list is not in this file and never will be.
4373
+ if ((strlen name) > 7) {
4374
+ def head:string (substring name 0 7)
4375
+ if (head == "evg-fx-") {
4376
+ def key:string (substring name 7 (strlen name))
4377
+ this.setFx(key (EVGElement.numberOr(value 0.0)))
4378
+ return
4379
+ }
4380
+ }
4381
+ if (name == "evg-ripple-speed") {
4382
+ rippleSpeed = (EVGElement.numberOr(value rippleSpeed))
4383
+ return
4384
+ }
4385
+ if (name == "evg-ripple-width") {
4386
+ rippleWidth = (EVGElement.numberOr(value rippleWidth))
4387
+ return
4388
+ }
4389
+ if (name == "evg-ripple-strength") {
4390
+ rippleStrength = (EVGElement.numberOr(value rippleStrength))
4391
+ return
4392
+ }
4393
+ if (name == "evg-ripple-decay") {
4394
+ rippleDecay = (EVGElement.numberOr(value rippleDecay))
4395
+ return
4396
+ }
4397
+ if (name == "evg-ripple-highlight") {
4398
+ rippleHighlight = (EVGElement.numberOr(value rippleHighlight))
4399
+ return
4400
+ }
4401
+ if (name == "evg-ripple-rings") {
4402
+ rippleRings = (EVGElement.numberOr(value rippleRings))
4403
+ return
4404
+ }
4405
+ if (name == "evg-ripple-stagger") {
4406
+ rippleStagger = (EVGElement.numberOr(value rippleStagger))
4407
+ return
4408
+ }
4409
+ if (name == "evg-ripple-ring-falloff") {
4410
+ rippleFalloff = (EVGElement.numberOr(value rippleFalloff))
4411
+ return
4412
+ }
4413
+ if (name == "evg-ripple-shine") {
4414
+ rippleShine = (EVGElement.numberOr(value rippleShine))
4415
+ return
4416
+ }
4417
+ if (name == "evg-ripple-gloss") {
4418
+ rippleGloss = (EVGElement.numberOr(value rippleGloss))
4419
+ return
4420
+ }
4421
+ if (name == "evg-ripple-bump") {
4422
+ rippleBump = (EVGElement.numberOr(value rippleBump))
4423
+ return
4424
+ }
4425
+ ; Three numbers, and they do not have to be normalised — the shader
4426
+ ; does that, because a direction somebody typed is a direction and not
4427
+ ; a unit vector.
4428
+ if (name == "evg-ripple-light") {
4429
+ def lp:[string] (EVGElement.splitWords(value))
4430
+ if ((array_length lp) > 0) {
4431
+ rippleLightX = (EVGElement.numberOr((itemAt lp 0) rippleLightX))
4432
+ }
4433
+ if ((array_length lp) > 1) {
4434
+ rippleLightY = (EVGElement.numberOr((itemAt lp 1) rippleLightY))
4435
+ }
4436
+ if ((array_length lp) > 2) {
4437
+ rippleLightZ = (EVGElement.numberOr((itemAt lp 2) rippleLightZ))
4438
+ }
4439
+ return
4440
+ }
4441
+ ; The scroll POSITION is settable, because a demo restoring where it
4442
+ ; was is setting state and not styling. The extents are not: they are
4443
+ ; the layout's answer, and an author writing one would only be telling
4444
+ ; the layout what it is about to work out for itself.
4445
+ if ((name == "scroll-top") || (name == "scrollTop")) {
4446
+ def st@(optional):double (to_double value)
4447
+ scrollTop = (unwrap st)
4448
+ return
4449
+ }
4450
+ if ((name == "scroll-left") || (name == "scrollLeft")) {
4451
+ def sl@(optional):double (to_double value)
4452
+ scrollLeft = (unwrap sl)
4453
+ return
4454
+ }
4455
+
4456
+ ; Flexbox
4457
+ ; `display` had no case here, so a stylesheet could set every grid
4458
+ ; property and still lay out as a block — the container is only treated
4459
+ ; as a grid when display == "grid".
4460
+ if (name == "display") {
4461
+ display = value
4462
+ return
4463
+ }
4464
+ if ((name == "flex-direction") || (name == "flexDirection")) {
4465
+ flexDirection = value
4466
+ ; The reversed directions PARSE and then do nothing: the layout
4467
+ ; branches on `== "column"` and lays every other value out as a
4468
+ ; forward row. A value that is accepted and then ignored is the
4469
+ ; same silence as one that is rejected, and worse, because the
4470
+ ; property looks supported.
4471
+ if ((value == "row-reverse") || (value == "column-reverse")) {
4472
+ EVGReject.note("unsupported value" name value)
4473
+ }
4474
+ return
4475
+ }
4476
+ if ((name == "flex-wrap") || (name == "flexWrap")) {
4477
+ flexWrap = value
4478
+ return
4479
+ }
4480
+ ; `flex-grow`, WHICH WAS NOT HERE. `flex-shrink`, `flex-basis` and the
4481
+ ; `flex` shorthand all were, so a stylesheet using the shorthand worked
4482
+ ; and one using the longhand was silently ignored — the property name
4483
+ ; falls through to the end of this function and nothing says so.
4484
+ ;
4485
+ ; What that looked like: `.cx-rail { flex-grow: 1 }` in the stepper.
4486
+ ; With the grow factor dropped the rail has no width of its own, so it
4487
+ ; took the parent's whole 852px, each of the three did, the row
4488
+ ; overflowed by a factor of three and the steps stacked down the page
4489
+ ; on top of one another. Chromium gives each rail 320 — the 640 of free
4490
+ ; space split two ways — which is what the oracle beside this now
4491
+ ; asserts.
4492
+ ; `position`, WHICH WAS ALSO NOT HERE. The field exists, the layout
4493
+ ; branches on it — `if (kid.position == "absolute")` in half a dozen
4494
+ ; places — and until now the ONLY way to set it was from Ranger code.
4495
+ ; Every `position: absolute` written in a stylesheet was read, matched,
4496
+ ; and dropped on the floor.
4497
+ ;
4498
+ ; What that looked like: `.cx-thumb { position: absolute }` on the
4499
+ ; sliders. The thumb stayed in flow, so it took horizontal space beside
4500
+ ; the filled range instead of riding over it, and the two overlapped by
4501
+ ; exactly the thumb's overhang. `.fd-menu` in the filter bar is the same
4502
+ ; fault with a louder symptom — a popup that pushes the rows below it
4503
+ ; instead of covering them.
4504
+ ;
4505
+ ; `fixed` IS ITS OWN MODE NOW. It used to map to `absolute` because
4506
+ ; nothing here modelled a viewport separate from the root; `viewportW`
4507
+ ; and `viewportH` are inherited down the tree, so the containing block
4508
+ ; a fixed box wants is one field away, and the one thing that made the
4509
+ ; difference visible — a scroll — is a single translation this can be
4510
+ ; left out of. See `EVGLayout.layoutChildren` and `applyScroll`.
4511
+ ; `sticky` has no implementation at all and stays in flow, which is what
4512
+ ; it does until it sticks.
4513
+ if (name == "position") {
4514
+ def pv:string (trim value)
4515
+ if (pv == "fixed") {
4516
+ position = "fixed"
4517
+ return
4518
+ }
4519
+ if (pv == "absolute") {
4520
+ position = "absolute"
4521
+ return
4522
+ }
4523
+ if ((pv == "static") || (pv == "relative") || (pv == "sticky")) {
4524
+ position = "relative"
4525
+ return
4526
+ }
4527
+ return
4528
+ }
4529
+ if ((name == "flex-grow") || (name == "flexGrow")) {
4530
+ def gv@(optional):double (to_double value)
4531
+ if gv {
4532
+ flex = (unwrap gv)
4533
+ }
4534
+ return
4535
+ }
4536
+ if ((name == "flex-shrink") || (name == "flexShrink")) {
4537
+ def sv@(optional):double (to_double value)
4538
+ if sv {
4539
+ flexShrink = (unwrap sv)
4540
+ }
4541
+ return
4542
+ }
4543
+ if ((name == "flex-basis") || (name == "flexBasis")) {
4544
+ flexBasis = (this.unitOf(name value))
4545
+ return
4546
+ }
4547
+ if (name == "flex") {
4548
+ this.setFlexShorthand(value)
4549
+ return
4550
+ }
4551
+ if (name == "gap") {
4552
+ gap = (this.unitOf(name value))
4553
+ return
4554
+ }
4555
+ if ((name == "row-gap") || (name == "rowGap")) {
4556
+ rowGap = (this.unitOf(name value))
4557
+ return
4558
+ }
4559
+ if ((name == "column-gap") || (name == "columnGap")) {
4560
+ columnGap = (this.unitOf(name value))
4561
+ return
4562
+ }
4563
+ if ((name == "grid-template-columns") || (name == "gridTemplateColumns")) {
4564
+ gridTemplateColumns = value
4565
+ return
4566
+ }
4567
+ if ((name == "grid-template-rows") || (name == "gridTemplateRows")) {
4568
+ gridTemplateRows = value
4569
+ return
4570
+ }
4571
+ if ((name == "grid-template-areas") || (name == "gridTemplateAreas")) {
4572
+ gridTemplateAreas = value
4573
+ return
4574
+ }
4575
+ if ((name == "grid-auto-flow") || (name == "gridAutoFlow")) {
4576
+ gridAutoFlow = value
4577
+ return
4578
+ }
4579
+ if ((name == "full-bleed") || (name == "fullBleed")) {
4580
+ fullBleed = ((value == "true") || (value == "1"))
4581
+ return
4582
+ }
4583
+ if ((name == "grid-area") || (name == "gridArea")) {
4584
+ gridArea = value
4585
+ return
4586
+ }
4587
+ if ((name == "grid-column") || (name == "gridColumn")) {
4588
+ gridColumn = value
4589
+ return
4590
+ }
4591
+ if ((name == "grid-row") || (name == "gridRow")) {
4592
+ gridRow = value
4593
+ return
4594
+ }
4595
+ if ((name == "justify-content") || (name == "justifyContent")) {
4596
+ justifyContent = value
4597
+ return
4598
+ }
4599
+ if ((name == "align-content") || (name == "alignContent")) {
4600
+ alignContent = value
4601
+ return
4602
+ }
4603
+ if ((name == "align-self") || (name == "alignSelf")) {
4604
+ if (value == "auto") {
4605
+ alignSelf = ""
4606
+ } {
4607
+ alignSelf = value
4608
+ }
4609
+ return
4610
+ }
4611
+ if ((name == "align-items") || (name == "alignItems")) {
4612
+ alignItems = value
4613
+ return
4614
+ }
4615
+
4616
+ ; Typography
4617
+ if ((name == "font-size") || (name == "fontSize")) {
4618
+ fontSize = (this.unitOf(name value))
4619
+ fontSizeInherited = false
4620
+ return
4621
+ }
4622
+ if ((name == "font-family") || (name == "fontFamily")) {
4623
+ fontFamily = value
4624
+ return
4625
+ }
4626
+ if ((name == "font-weight") || (name == "fontWeight")) {
4627
+ fontWeight = value
4628
+ return
4629
+ }
4630
+ if ((name == "text-align") || (name == "textAlign")) {
4631
+ textAlign = value
4632
+ return
4633
+ }
4634
+ if ((name == "white-space") || (name == "whiteSpace")) {
4635
+ whiteSpace = (trim value)
4636
+ return
4637
+ }
4638
+ if ((name == "line-height") || (name == "lineHeight")) {
4639
+ ; `normal` is the face's own line box, which is what zero means
4640
+ ; here — see `lineHeight`.
4641
+ def t:string (trim value)
4642
+ if (t == "normal") {
4643
+ lineHeight = 0.0
4644
+ lineHeightUnit = (EVGUnit.unset())
4645
+ return
4646
+ }
4647
+ ; A BARE NUMBER is a multiplier and a length is not. `to_double`
4648
+ ; stops at the first character it cannot use, so "24px" would come
4649
+ ; back 24 and be indistinguishable from "24" — which is exactly
4650
+ ; how a 14px run got a 336-pixel line box.
4651
+ if (EVGUnit.isNumeric(t)) {
4652
+ def val@(optional):double (to_double t)
4653
+ if val {
4654
+ lineHeight = (unwrap val)
4655
+ lineHeightUnit = (EVGUnit.unset())
4656
+ }
4657
+ return
4658
+ }
4659
+ def u:EVGUnit (EVGUnit.parse(t))
4660
+ if u.isSet {
4661
+ lineHeightUnit = u
4662
+ lineHeight = 0.0
4663
+ }
4664
+ return
4665
+ }
4666
+
4667
+ ; Transform
4668
+ if (name == "transform") {
4669
+ this.applyTransform(value)
4670
+ return
4671
+ }
4672
+ if ((name == "transform-origin") || (name == "transformOrigin")) {
4673
+ this.applyTransformOrigin(value)
4674
+ return
4675
+ }
4676
+ if ((name == "translate-x") || (name == "translateX")) {
4677
+ def tvx@(optional):double (to_double value)
4678
+ if tvx { translateX = (unwrap tvx) }
4679
+ return
4680
+ }
4681
+ if ((name == "translate-y") || (name == "translateY")) {
4682
+ def tvy@(optional):double (to_double value)
4683
+ if tvy { translateY = (unwrap tvy) }
4684
+ return
4685
+ }
4686
+ if (name == "rotate") {
4687
+ def val@(optional):double (to_double value)
4688
+ rotate = (unwrap val)
4689
+ return
4690
+ }
4691
+ if (name == "scale") {
4692
+ def val@(optional):double (to_double value)
4693
+ scale = (unwrap val)
4694
+ return
4695
+ }
4696
+
4697
+ ; Shadow
4698
+ if ((name == "shadow-radius") || (name == "shadowRadius")) {
4699
+ shadowRadius = (this.unitOf(name value))
4700
+ return
4701
+ }
4702
+ if ((name == "shadow-color") || (name == "shadowColor")) {
4703
+ shadowColor = (EVGColor.parse(value))
4704
+ return
4705
+ }
4706
+ if ((name == "shadow-offset-x") || (name == "shadowOffsetX")) {
4707
+ shadowOffsetX = (this.unitOf(name value))
4708
+ return
4709
+ }
4710
+ if ((name == "shadow-offset-y") || (name == "shadowOffsetY")) {
4711
+ shadowOffsetY = (this.unitOf(name value))
4712
+ return
4713
+ }
4714
+
4715
+ ; Clip path (SVG path for clipping)
4716
+ if ((name == "clip-path") || (name == "clipPath")) {
4717
+ clipPath = value
4718
+ return
4719
+ }
4720
+ if (((name == "d") || (name == "svgPath")) || (name == "path")) {
4721
+ svgPath = value
4722
+ return
4723
+ }
4724
+
4725
+ ; PDF/Print settings
4726
+ if (name == "imageQuality") {
4727
+ def val@(optional):int (to_int value)
4728
+ if val {
4729
+ imageQuality = (unwrap val)
4730
+ }
4731
+ return
4732
+ }
4733
+ if (name == "maxImageSize") {
4734
+ def val@(optional):int (to_int value)
4735
+ if val {
4736
+ maxImageSize = (unwrap val)
4737
+ }
4738
+ return
4739
+ }
4740
+
4741
+ ; Anchors and connectors. `anchor-name` is CSS's; the rest belong to
4742
+ ; the `connector` tag, which is EVG's own — CSS has no property that
4743
+ ; draws a line between two elements.
4744
+ if ((name == "anchor-name") || (name == "anchorName")) {
4745
+ anchorName = (trim value)
4746
+ return
4747
+ }
4748
+ if (name == "from") {
4749
+ connectorFrom = (trim value)
4750
+ return
4751
+ }
4752
+ if (name == "to") {
4753
+ connectorTo = (trim value)
4754
+ return
4755
+ }
4756
+ if ((name == "from-side") || (name == "fromSide")) {
4757
+ connectorFromSide = (trim value)
4758
+ return
4759
+ }
4760
+ if ((name == "to-side") || (name == "toSide")) {
4761
+ connectorToSide = (trim value)
4762
+ return
4763
+ }
4764
+ if (name == "routing") {
4765
+ connectorRouting = (trim value)
4766
+ return
4767
+ }
4768
+ if ((name == "from-offset") || (name == "fromOffset")) {
4769
+ def fou:EVGUnit (this.unitOf(name value))
4770
+ if fou.isSet {
4771
+ connectorFromOffset = fou.pixels
4772
+ }
4773
+ return
4774
+ }
4775
+ if ((name == "to-offset") || (name == "toOffset")) {
4776
+ def tou:EVGUnit (this.unitOf(name value))
4777
+ if tou.isSet {
4778
+ connectorToOffset = tou.pixels
4779
+ }
4780
+ return
4781
+ }
4782
+ if ((name == "arrow-start") || (name == "arrowStart")) {
4783
+ arrowStart = (trim value)
4784
+ return
4785
+ }
4786
+ if ((name == "arrow-end") || (name == "arrowEnd")) {
4787
+ arrowEnd = (trim value)
4788
+ return
4789
+ }
4790
+ if ((name == "arrow-size") || (name == "arrowSize")) {
4791
+ def asu:EVGUnit (this.unitOf(name value))
4792
+ if asu.isSet {
4793
+ arrowSize = asu.pixels
4794
+ }
4795
+ return
4796
+ }
4797
+
4798
+ ; SVG Path attributes
4799
+ if ((name == "d") || (name == "svgPath")) {
4800
+ svgPath = value
4801
+ return
4802
+ }
4803
+ if ((name == "svg") || (name == "svgSource")) {
4804
+ svgSource = value
4805
+ return
4806
+ }
4807
+ if ((name == "viewBox") || (name == "view-box")) {
4808
+ viewBox = value
4809
+ return
4810
+ }
4811
+ if (name == "fill") {
4812
+ fillColor = (EVGColor.parse(value))
4813
+ return
4814
+ }
4815
+ if (name == "stroke") {
4816
+ strokeColor = (EVGColor.parse(value))
4817
+ return
4818
+ }
4819
+ if ((name == "stroke-width") || (name == "strokeWidth")) {
4820
+ def val@(optional):double (to_double value)
4821
+ if val {
4822
+ strokeWidth = (unwrap val)
4823
+ }
4824
+ return
4825
+ }
4826
+ if ((name == "stroke-dasharray") || (name == "strokeDasharray")) {
4827
+ strokeDashArray = value
4828
+ return
4829
+ }
4830
+ if ((name == "stroke-dashoffset") || (name == "strokeDashoffset")) {
4831
+ def dv@(optional):double (to_double value)
4832
+ if dv {
4833
+ strokeDashOffset = (unwrap dv)
4834
+ }
4835
+ return
4836
+ }
4837
+ if ((name == "fill-rule") || (name == "fillRule")) {
4838
+ if (value == "evenodd") {
4839
+ fillRule = "evenodd"
4840
+ } {
4841
+ fillRule = "nonzero"
4842
+ }
4843
+ return
4844
+ }
4845
+ ; Nothing above claimed it. A property this engine does not implement
4846
+ ; is the other half of the silence problem: `align-self` and
4847
+ ; `aspect-ratio` used to fall off the end of this chain and change
4848
+ ; nothing, with no way to tell that from a property that had been
4849
+ ; applied and made no difference.
4850
+ if ((EVGElement.isHostProp(name)) == false) {
4851
+ EVGReject.note("unknown property" name value)
4852
+ }
4853
+ }
4854
+
4855
+ ; Names that reach `setAttribute` from an authoring layer and are NOT
4856
+ ; styling: React-shaped handlers, the tree factory's own bookkeeping, and
4857
+ ; the `data-` namespace an application owns. Rejecting these would bury the
4858
+ ; real complaints in noise from every button on the page.
4859
+ sfn isHostProp:boolean (name:string) {
4860
+ def n:int (strlen name)
4861
+ if (n == 0) {
4862
+ return true
4863
+ }
4864
+ if (n > 2) {
4865
+ def c0:int (charAt name 0)
4866
+ def c1:int (charAt name 1)
4867
+ def c2:int (charAt name 2)
4868
+ ; onClick, onPress, onChange … — "on" then a capital, so `opacity`
4869
+ ; and `order` are not swept up with them.
4870
+ if (((c0 == 111) && (c1 == 110)) && ((c2 >= 65) && (c2 <= 90))) {
4871
+ return true
4872
+ }
4873
+ }
4874
+ if (n > 5) {
4875
+ if ((EVGElement.startsWithStr(name "data-")) ) {
4876
+ return true
4877
+ }
4878
+ }
4879
+ if (name == "children") { return true }
4880
+ if (name == "style") { return true }
4881
+ if (name == "ref") { return true }
4882
+ if (name == "tag") { return true }
4883
+ if (name == "tagName") { return true }
4884
+ if (name == "text") { return true }
4885
+ if (name == "textContent") { return true }
4886
+ if (name == "elementType") { return true }
4887
+ if (name == "test-id") { return true }
4888
+ if (name == "testId") { return true }
4889
+ return false
4890
+ }
4891
+
4892
+ sfn startsWithStr:boolean (s:string prefix:string) {
4893
+ def n:int (strlen prefix)
4894
+ if ((strlen s) < n) {
4895
+ return false
4896
+ }
4897
+ def i:int 0
4898
+ while (i < n) {
4899
+ if ((charAt s i) != (charAt prefix i)) {
4900
+ return false
4901
+ }
4902
+ i = (i + 1)
4903
+ }
4904
+ return true
4905
+ }
4906
+
4907
+ fn getCalculatedBounds:string () {
4908
+ return ("(" + (to_string calculatedX) + ", " + (to_string calculatedY) + ") " + (to_string calculatedWidth) + "x" + (to_string calculatedHeight))
4909
+ }
4910
+
4911
+ fn toString:string () {
4912
+ return ("<" + tagName + " id=\"" + id + "\" " + (this.getCalculatedBounds()) + ">")
4913
+ }
4914
+ }