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,4220 @@
1
+ ; SPDX-License-Identifier: MIT
2
+
3
+ ; EVGLayout.rgr - Layout algorithm for EVG
4
+
5
+ Import "EVGElement.rgr"
6
+ Import "EVGStyleSheet.rgr"
7
+ Import "EVGTextMeasurer.rgr"
8
+ Import "EVGImageMeasurer.rgr"
9
+ Import "EVGGrid.rgr"
10
+ Import "EVGReject.rgr"
11
+ Import "EVGTextEngine.rgr"
12
+ Import "EVGConnector.rgr"
13
+
14
+ ; Where one candidate placement would put a surface, and what it costs.
15
+ class OverlaySpot {
16
+ def x:double 0.0
17
+ def y:double 0.0
18
+ ; Pixels of the surface that would be off the page.
19
+ def over:double 0.0
20
+ ; Free space left between the surface and the page edge on that side.
21
+ def room:double 0.0
22
+ }
23
+
24
+ class EVGLayout {
25
+ def measurer:EVGTextMeasurer
26
+ ; The one text measurement + wrapping implementation. Layout must break
27
+ ; lines exactly where paint will, so both go through this.
28
+ def textEngine:EVGTextEngine (new EVGTextEngine())
29
+ def imageMeasurer:EVGImageMeasurer
30
+ def pageWidth:double 612.0
31
+ def pageHeight:double 792.0
32
+ def currentPage:int 0
33
+ def debug:boolean false
34
+ ; Overlay surfaces the pass could not place, by id and reason. An overlay
35
+ ; with no anchor would otherwise land at the top-left corner and read as a
36
+ ; styling mistake; this is what makes it a reportable one.
37
+ def overlayErrors:[string]
38
+ ; Every `anchor-name` and `#id` in the tree, collected once per layout and
39
+ ; read by both things that point at a box by name: the connectors and the
40
+ ; surfaces. See `EVGConnector.collectAnchors`.
41
+ def anchorNames:[string]
42
+ def anchorNodes:[EVGElement]
43
+ ; Grid track lists and placements that the engine could not honour.
44
+ ;
45
+ ; These used to go to this.log(), which is a no-op unless `debug` is on, so
46
+ ; `fit-content()`, row `subgrid` and a mistyped template were silently
47
+ ; dropped in every normal run — the page just came out with the wrong
48
+ ; number of columns. They are collected here instead and reported by the
49
+ ; tools the same way font warnings are.
50
+ def warnings:[string]
51
+ ; The document's stylesheet, parsed. Kept beside the text it came from so
52
+ ; a document laid out every frame parses its CSS once.
53
+ def docSheet@(optional):EVGStyleSheet
54
+ def docCssText:string ""
55
+
56
+ Constructor () {
57
+ ; The process-wide default (`EVGDefaultMeasurer`): a host's own
58
+ ; measurer when one is installed, the table otherwise.
59
+ def m:EVGTextMeasurer (EVGDefaultMeasurer.measurer())
60
+ measurer = m
61
+ textEngine.setMeasurer(m)
62
+ def im (new SimpleImageMeasurer())
63
+ imageMeasurer = im
64
+ }
65
+
66
+ fn setMeasurer:void (m:EVGTextMeasurer) {
67
+ measurer = m
68
+ textEngine.setMeasurer(m)
69
+ }
70
+
71
+ fn getTextEngine:EVGTextEngine () {
72
+ return textEngine
73
+ }
74
+
75
+ ; Refuse to measure a custom-font document with guessed widths (§4.1 rule 1).
76
+ fn setStrictFonts:void (s:boolean) {
77
+ textEngine.setStrict(s)
78
+ }
79
+
80
+ fn setImageMeasurer:void (m:EVGImageMeasurer) {
81
+ imageMeasurer = m
82
+ }
83
+
84
+ fn setPageSize:void (w:double h:double) {
85
+ pageWidth = w
86
+ pageHeight = h
87
+ }
88
+
89
+ fn setDebug:void (d:boolean) {
90
+ debug = d
91
+ }
92
+
93
+ fn log:void (msg:string) {
94
+ if debug {
95
+ print msg
96
+ }
97
+ }
98
+
99
+ ; Record something the author asked for that the engine could not do.
100
+ ; Deduplicated: one bad template on a repeated component would otherwise
101
+ ; report once per instance and bury everything else.
102
+ fn warn:void (msg:string) {
103
+ def i:int 0
104
+ while (i < (array_length warnings)) {
105
+ if ((itemAt warnings i) == msg) {
106
+ return
107
+ }
108
+ i = (i + 1)
109
+ }
110
+ push warnings msg
111
+ this.log((" " + msg))
112
+ }
113
+
114
+ ; THE DOCUMENT'S OWN STYLESHEET.
115
+ ;
116
+ ; A `.evg.json` can carry one (`"css"` beside `"root"`), and this is where
117
+ ; it takes effect — one place rather than at every call site that loads a
118
+ ; document, so a tool that only knows how to lay a tree out gets the
119
+ ; cascade for free.
120
+ ;
121
+ ; Parsed once per text: the same document laid out again on every frame
122
+ ; must not re-parse a stylesheet that has not changed. Applying it again
123
+ ; is harmless and necessary — a rule is a value on an element, and layout
124
+ ; resets those.
125
+ ;
126
+ ; An inline property still wins, which is what lets the two mix: the sheet
127
+ ; says what a `.card` is, the node says what THIS card does differently,
128
+ ; and an agent's `set-prop` keeps meaning what it meant.
129
+ fn applyDocumentCss:void (root:EVGElement) {
130
+ if ((strlen root.documentCss) == 0) {
131
+ return
132
+ }
133
+ if (root.documentCss != docCssText) {
134
+ def s (new EVGStyleSheet())
135
+ s.parse(root.documentCss)
136
+ docSheet = s
137
+ docCssText = root.documentCss
138
+ }
139
+ if (null? docSheet) {
140
+ return
141
+ }
142
+ def sheet:EVGStyleSheet (unwrap docSheet)
143
+ ; Last pass's marks first. A rule that was edited out would otherwise
144
+ ; leave a property marked as the sheet's forever, and the document
145
+ ; would stop writing down a value it is the only record of.
146
+ EVGLayout.clearCssMarks(root)
147
+ ; The root's `theme` turns on the sheet's `.theme-<name> .class`
148
+ ; rules. Passing "" here made every theme-scoped rule in a document
149
+ ; dead: `{"theme":"dark"}` on the root parsed, round-tripped, and
150
+ ; styled nothing.
151
+ sheet.applyTree(root root.theme)
152
+ }
153
+
154
+ sfn clearCssMarks:void (el:EVGElement) {
155
+ el.clearCssMarks()
156
+ def i:int 0
157
+ while (i < (el.getChildCount())) {
158
+ EVGLayout.clearCssMarks((el.getChild(i)))
159
+ i = (i + 1)
160
+ }
161
+ }
162
+
163
+ fn warningCount:int () {
164
+ return (array_length warnings)
165
+ }
166
+
167
+ fn warningAt:string (i:int) {
168
+ return (itemAt warnings i)
169
+ }
170
+
171
+ fn layout:void (root:EVGElement) {
172
+ ; Layout the entire element tree
173
+ this.log("EVGLayout: Starting layout")
174
+ currentPage = 0
175
+ this.applyDocumentCss(root)
176
+
177
+ ; Start from a clean slate. `resolveUnits` refuses to run twice on the
178
+ ; same element — deliberately, so a percentage is not resolved against
179
+ ; an already-resolved parent — and it had no counterpart: nothing in the
180
+ ; engine called resetLayoutState(), so an element laid out ONCE kept its
181
+ ; resolved pixels forever, and a percentage written after that first
182
+ ; pass resolved to zero.
183
+ ;
184
+ ; Measured, not theorised: a slider whose thumb is placed with
185
+ ; `left: <n>%` sat correctly on first paint and then jumped to the far
186
+ ; left the moment it was dragged, because the fresh unit the controller
187
+ ; wrote was never resolved. Any retained tree that is laid out more than
188
+ ; once had the same bug; a document that is laid out once did not, which
189
+ ; is why it survived this long.
190
+ ;
191
+ ; A NEW PAGE SIZE is a new layout for every kept subtree too. The
192
+ ; root's viewport is what the last pass was given; when it differs, a
193
+ ; `vw` or `vh` anywhere below may mean something else, and nothing a
194
+ ; kept box compares — what the flow hands it — need have changed. The
195
+ ; stylesheet catches this for the trees it styles (a resize is a new
196
+ ; generation, so every element is layout-dirty); a tree with no sheet,
197
+ ; or one laid out without re-applying it, has only this.
198
+ if (root.hasLayout && ((root.viewportW != pageWidth) || (root.viewportH != pageHeight))) {
199
+ root.forgetLayout()
200
+ }
201
+ root.resetLayoutState()
202
+
203
+ ; Resolve root dimensions
204
+ if (root.width.isSet == false) {
205
+ root.width = (EVGUnit.px(pageWidth))
206
+ }
207
+ if (root.height.isSet == false) {
208
+ root.height = (EVGUnit.px(pageHeight))
209
+ }
210
+
211
+ ; The root inherits from nothing, so its own font-size has to be folded
212
+ ; in here or it is silently dropped. `rem` on the root itself resolves
213
+ ; against the initial size, which is why rootFontSize is only published
214
+ ; afterwards.
215
+ root.applyOwnFontSize()
216
+ root.rootFontSize = root.inheritedFontSize
217
+ ; THE VIEWPORT, published once and inherited from here. `vw` and `vh`
218
+ ; mean the page size this layout was given — which on paper is the page
219
+ ; AREA, because that is what the print renderers set it to: the section
220
+ ; less two margins. A host that wants `100vh` to mean its window says
221
+ ; so with `setPageSize`, which every host already calls.
222
+ root.viewportW = pageWidth
223
+ root.viewportH = pageHeight
224
+ root.viewportX = 0.0
225
+ root.viewportY = 0.0
226
+ ; The root has no parent to inherit a direction from, so it settles
227
+ ; its own. This is the switch: `direction: rtl` here and every
228
+ ; container below turns around with it.
229
+ root.applyOwnDirection(false)
230
+
231
+ ; Explicitly set root position to (0, 0)
232
+ ; This is important when re-laying out a page that was previously
233
+ ; positioned elsewhere in a multi-page document
234
+ root.calculatedX = 0.0
235
+ root.calculatedY = 0.0
236
+
237
+ ; Layout root at (0, 0)
238
+ this.layoutElement(root 0.0 0.0 pageWidth pageHeight)
239
+
240
+ ; The names first: a surface may be positioned against any box in the
241
+ ; tree, not only a sibling, and so may a connector.
242
+ clear anchorNames
243
+ clear anchorNodes
244
+ EVGConnector.collectAnchors(root anchorNames anchorNodes)
245
+
246
+ ; Then the overlays, once everything they anchor to has a rectangle.
247
+ clear overlayErrors
248
+ def anySurface:boolean (this.placeOverlaysIn(root))
249
+
250
+ ; The connectors, once every box they join has its final rectangle —
251
+ ; the overlay pass above is the last thing that moves one, and a line
252
+ ; solved before it would point at where a card used to be.
253
+ def connWarnings:[string] (EVGConnector.resolveWith(root anchorNames anchorNodes))
254
+ def cwi:int 0
255
+ while (cwi < (array_length connWarnings)) {
256
+ this.warn((itemAt connWarnings cwi))
257
+ cwi = (cwi + 1)
258
+ }
259
+
260
+ ; And where each subtree DRAWS, which is not where its box is: see
261
+ ; `EVGElement.paintTop`. Last, because it is a union over final
262
+ ; positions and the overlay pass above is the last thing that moves
263
+ ; one.
264
+ this.measurePaintBounds(root)
265
+
266
+ ; Everything `setAttribute` could not read, folded into the same list a
267
+ ; build already fails on. It is drained here rather than reported at
268
+ ; the point of parse because a parse has no idea whether anyone will
269
+ ; ever lay the tree out, and a warning nobody collects is the silence
270
+ ; this was written to end.
271
+ ;
272
+ ; The collector de-duplicates and caps itself, so this loop is bounded
273
+ ; by the number of DISTINCT bad declarations in the document — four,
274
+ ; typically, and never more than sixty-four.
275
+ this.drainRejects()
276
+
277
+ this.log("EVGLayout: Layout complete")
278
+ }
279
+
280
+ ; Move `EVGReject`'s entries into the layout's own warnings, once.
281
+ fn drainRejects:void () {
282
+ def n:int (EVGReject.noteCount())
283
+ if (n == 0) {
284
+ return
285
+ }
286
+ def i:int 0
287
+ while (i < n) {
288
+ this.warn((EVGReject.noteAt(i)))
289
+ i = (i + 1)
290
+ }
291
+ def total:int (EVGReject.noteTotal())
292
+ if (total > n) {
293
+ this.warn((((("… " + (to_string total)) + " rejected declarations in all, ") + (to_string n)) + " distinct"))
294
+ }
295
+ }
296
+
297
+ ; The union of an element's own box and every box beneath it, bottom-up.
298
+ ;
299
+ ; This is what `EVGDisplayList` culls against, and the reason it can cull
300
+ ; a subtree WITHOUT DESCENDING INTO IT — which is the whole saving. An
301
+ ; element's own rectangle would not do: a zero-height wrapper sits at the
302
+ ; top of the document while its rows are ten thousand pixels below it, and
303
+ ; culling on the wrapper would take the rows with it.
304
+ fn measurePaintBounds:void (el:EVGElement) {
305
+ ; A subtree the layout kept moved with its bounds; they still hold —
306
+ ; when they were measured since it was last laid out for real.
307
+ if (el.layoutSkipped && el.boundsFresh) {
308
+ return
309
+ }
310
+ el.boundsFresh = true
311
+ ; Where the first child sits relative to the box, for a kept subtree
312
+ ; to be moved by — see `EVGElement.kidOffX`.
313
+ if ((el.getChildCount()) > 0) {
314
+ def k0:EVGElement (el.getChild(0))
315
+ el.kidOffX = (k0.calculatedX - el.calculatedX)
316
+ el.kidOffY = (k0.calculatedY - el.calculatedY)
317
+ }
318
+ ; A degenerate box draws nothing of its own — a wrapper with no size
319
+ ; is a place in the tree, not a place on the page — so it does not
320
+ ; drag the union back to wherever it happens to sit.
321
+ def has:boolean ((el.calculatedWidth > 0.0) && (el.calculatedHeight > 0.0))
322
+ def minX:double el.calculatedX
323
+ def minY:double el.calculatedY
324
+ def maxX:double (el.calculatedX + el.calculatedWidth)
325
+ def maxY:double (el.calculatedY + el.calculatedHeight)
326
+ def started:boolean has
327
+ def unbounded:boolean (el.hasTransform())
328
+ def effect:boolean ((strlen el.surfaceEffect) > 0)
329
+
330
+ def i:int 0
331
+ while (i < (el.getChildCount())) {
332
+ def kid:EVGElement (el.getChild(i))
333
+ this.measurePaintBounds(kid)
334
+ ; `display: none` paints nothing and nothing under it, so its
335
+ ; rectangle — usually a stale one, or the origin — is not part of
336
+ ; where this subtree draws.
337
+ if ((kid.isHidden()) == false) {
338
+ if kid.paintUnbounded {
339
+ unbounded = true
340
+ }
341
+ if kid.paintHasEffect {
342
+ effect = true
343
+ }
344
+ if (started == false) {
345
+ minX = kid.paintLeft
346
+ minY = kid.paintTop
347
+ maxX = kid.paintRight
348
+ maxY = kid.paintBottom
349
+ started = true
350
+ } {
351
+ if (kid.paintLeft < minX) { minX = kid.paintLeft }
352
+ if (kid.paintTop < minY) { minY = kid.paintTop }
353
+ if (kid.paintRight > maxX) { maxX = kid.paintRight }
354
+ if (kid.paintBottom > maxY) { maxY = kid.paintBottom }
355
+ }
356
+ }
357
+ i = (i + 1)
358
+ }
359
+
360
+ el.paintLeft = minX
361
+ el.paintTop = minY
362
+ el.paintRight = maxX
363
+ el.paintBottom = maxY
364
+ el.paintUnbounded = unbounded
365
+ el.paintHasEffect = effect
366
+ }
367
+
368
+ ; Whether the element about to be laid out sits along a ROW.
369
+ ;
370
+ ; A field rather than a parameter, and rather than reading `child.parent`,
371
+ ; because EVGElement deliberately has no parent back-reference: it was
372
+ ; write-only and, in the C++ backend, assigning `this` to a shared_ptr
373
+ ; field forces `enable_shared_from_this` and breaks the native build. Its
374
+ ; own comment says so, which is the only reason this is a field.
375
+ ;
376
+ ; The flex pass sets it before each child and puts it back afterwards. The
377
+ ; default is true, which is the block behaviour the root and any non-flex
378
+ ; parent want.
379
+ def layingOutAlongRow:boolean true
380
+
381
+ ; And, for a COLUMN parent, whether the cross axis shrink-wraps.
382
+ ;
383
+ ; `width: auto` on a flex item is stretch only while `align-items` is
384
+ ; stretch; under `center` or `flex-end` it is fit-content, which is the
385
+ ; difference between a breadcrumb trail centred under the label it belongs
386
+ ; to and one pinned to the left edge of a box it has filled.
387
+ ;
388
+ ; This engine's `alignItems` default is "flex-start" where CSS's is
389
+ ; "stretch", and everything built so far leans on the stretch. So only an
390
+ ; EXPLICIT centre or end shrinks: a container that never mentioned
391
+ ; `align-items` keeps exactly the behaviour it has today.
392
+ def columnCrossShrinks:boolean false
393
+
394
+ fn layoutElement:void (element:EVGElement parentX:double parentY:double parentWidth:double parentHeight:double) {
395
+ ; A subtree the app keeps — see `EVGElement.keepLayout` — with the
396
+ ; same inputs as last time is the same layout, moved: the flow
397
+ ; reads its size and baseline off it as if it had been laid out.
398
+ if element.keepLayout {
399
+ if (element.hasLayout && element.layoutClean) {
400
+ if ((((element.lastParentW == parentWidth) && (element.lastParentH == parentHeight))
401
+ && (element.lastFlex == element.hasFlexWidth)) && ((element.lastFlexW == element.calculatedFlexWidth) && (element.lastFlexH == element.calculatedFlexHeight))) {
402
+ ; The box is at (parentX, parentY): the flow wrote it there
403
+ ; before this call, or means it at the origin to move it
404
+ ; after. What is under it follows by the difference between
405
+ ; where the first child is and where it sat relative to the
406
+ ; box when the subtree was measured; the bounds, which were
407
+ ; measured with the children, follow by the same.
408
+ element.calculatedX = parentX
409
+ element.calculatedY = parentY
410
+ def n:int (element.getChildCount())
411
+ if (n > 0) {
412
+ def k0:EVGElement (element.getChild(0))
413
+ def dx:double ((parentX + element.kidOffX) - k0.calculatedX)
414
+ def dy:double ((parentY + element.kidOffY) - k0.calculatedY)
415
+ if ((dx != 0.0) || (dy != 0.0)) {
416
+ def i:int 0
417
+ while (i < n) {
418
+ def kid:EVGElement (element.getChild(i))
419
+ kid.moveSubtree(dx dy)
420
+ i = (i + 1)
421
+ }
422
+ element.paintLeft = (element.paintLeft + dx)
423
+ element.paintRight = (element.paintRight + dx)
424
+ element.paintTop = (element.paintTop + dy)
425
+ element.paintBottom = (element.paintBottom + dy)
426
+ }
427
+ } {
428
+ element.boundsFresh = false
429
+ }
430
+ element.layoutSkipped = true
431
+ return
432
+ }
433
+ }
434
+ ; Laid out for real: what the root's reset left in place goes,
435
+ ; but not what the parent has just handed down — the flex width,
436
+ ; the flex height, and the box's place, which the flow wrote before
437
+ ; this call.
438
+ def flex:boolean element.hasFlexWidth
439
+ def flexW:double element.calculatedFlexWidth
440
+ def flexHSet:boolean element.hasFlexHeight
441
+ def flexH:double element.calculatedFlexHeight
442
+ def bx:double element.calculatedX
443
+ def by:double element.calculatedY
444
+ element.resetLayoutNow()
445
+ element.hasFlexWidth = flex
446
+ element.calculatedFlexWidth = flexW
447
+ element.hasFlexHeight = flexHSet
448
+ element.calculatedFlexHeight = flexH
449
+ element.calculatedX = bx
450
+ element.calculatedY = by
451
+ }
452
+ element.hasLayout = true
453
+ element.layoutSkipped = false
454
+ element.paintStamp = (element.paintStamp + 1)
455
+ element.lastParentW = parentWidth
456
+ element.lastParentH = parentHeight
457
+ element.lastFlex = element.hasFlexWidth
458
+ element.lastFlexW = element.calculatedFlexWidth
459
+ element.lastFlexH = element.calculatedFlexHeight
460
+ element.resolveUnits(parentWidth parentHeight)
461
+ ; `display: none` has no box and lays out nothing under it.
462
+ if (element.isHidden()) {
463
+ element.calculatedX = parentX
464
+ element.calculatedY = parentY
465
+ element.calculatedWidth = 0.0
466
+ element.calculatedHeight = 0.0
467
+ return
468
+ }
469
+
470
+ ; Calculate element dimensions. Same order as `layoutChildren`: a width
471
+ ; the flex pass decided wins over the declared one, because it IS the
472
+ ; declared one after shrinking.
473
+ def width:double parentWidth
474
+ if element.width.isSet {
475
+ width = element.width.pixels
476
+ }
477
+ if element.hasFlexWidth {
478
+ width = element.calculatedFlexWidth
479
+ }
480
+
481
+ ; Text leaf nodes without explicit width shrink-wrap to measured content
482
+ if ((element.width.isSet == false) && (element.hasFlexWidth == false)) {
483
+ def textContent:string element.textContent
484
+ if ((strlen textContent) > 0) {
485
+ if ((element.getChildCount()) == 0) {
486
+ def fontSize:double element.inheritedFontSize
487
+ if element.fontSize.isSet {
488
+ fontSize = element.fontSize.pixels
489
+ }
490
+ if (fontSize <= 0.0) {
491
+ fontSize = 14.0
492
+ }
493
+ def contentW:double (textEngine.maxLineWidthSpaced(textContent (element.effectiveFontFamily()) fontSize element.letterSpacing))
494
+ def measuredW:double (contentW + element.box.paddingLeftPx + element.box.paddingRightPx + (element.box.borderWidthPx * 2.0))
495
+ if (measuredW < parentWidth) {
496
+ width = measuredW
497
+ }
498
+ }
499
+ } {
500
+ ; A width-less flex CONTAINER shrink-wraps too — but only in a
501
+ ; ROW. This is `width: auto` in CSS and it resolves two
502
+ ; different ways: fit-content for an item laid out along a row,
503
+ ; and STRETCH for one in a column, where the cross axis fills
504
+ ; because `align-items` defaults to stretch.
505
+ ;
506
+ ; The first version of this shrank in both, and the damage was
507
+ ; exactly what the rule predicts: in a vertical panel group,
508
+ ; every panel collapsed to the width of the label inside it and
509
+ ; the divider collapsed to the width of its grip. Correct on
510
+ ; the axis it was written for and wrong on the other one.
511
+ if (element.display == "flex") {
512
+ if (this.crossAxisFits(element)) {
513
+ def intrinsic:double (this.intrinsicWidth(element false))
514
+ if (intrinsic > 0.0) {
515
+ if (intrinsic < parentWidth) {
516
+ width = intrinsic
517
+ }
518
+ }
519
+ }
520
+ }
521
+ }
522
+ }
523
+
524
+ def height:double 0.0
525
+ def autoHeight:boolean true
526
+
527
+ ; Special handling for Page elements - they always use page size
528
+ if ((element.tagName == "Page") || (element.tagName == "page")) {
529
+ if (element.width.isSet == false) {
530
+ width = pageWidth
531
+ }
532
+ if (element.height.isSet == false) {
533
+ height = pageHeight
534
+ autoHeight = false
535
+ }
536
+ }
537
+ if element.height.isSet {
538
+ height = element.height.pixels
539
+ autoHeight = false
540
+ }
541
+ ; Column-axis flex grow / row alignItems:stretch: EVGLayout resolved a
542
+ ; target height for this child (height not explicitly set) — adopt it.
543
+ if (element.height.isSet == false) {
544
+ if (element.calculatedFlexHeight > 0.0) {
545
+ height = element.calculatedFlexHeight
546
+ autoHeight = false
547
+ }
548
+ }
549
+ ; Column-axis flex shrink squeezed a declared height: the squeezed one
550
+ ; wins, as `hasFlexWidth` does for the width.
551
+ if element.hasFlexHeight {
552
+ height = element.calculatedFlexHeight
553
+ autoHeight = false
554
+ }
555
+
556
+ ; For image elements, calculate dimensions based on aspect ratio
557
+ ; IMPORTANT: If the parent has fixed dimensions and the image should fill the parent,
558
+ ; use the parent dimensions as the container. The objectFit property handles
559
+ ; how the actual image content is scaled within this container.
560
+ if ((element.tagName == "image") || (element.tagName == "Image") || (element.tagName == "img")) {
561
+ def imgSrc:string element.src
562
+ if ((strlen imgSrc) > 0) {
563
+ def dims:EVGImageDimensions (imageMeasurer.getImageDimensions(imgSrc))
564
+ if dims.isValid {
565
+ ; Store source dimensions for objectFit calculations in renderer
566
+ element.sourceWidth = (to_double dims.width)
567
+ element.sourceHeight = (to_double dims.height)
568
+
569
+ if (element.width.isSet && (element.height.isSet == false)) {
570
+ ; Width is set but height is not
571
+ ; If parent has a fixed height, use it as the container height
572
+ ; This matches HTML behavior where image container fills parent
573
+ if (parentHeight > 0.0) {
574
+ height = parentHeight
575
+ if debug {
576
+ this.log((" Image container using parent height: " + (to_string width) + "x" + (to_string height)))
577
+ }
578
+ } {
579
+ ; No parent height constraint - calculate from aspect ratio
580
+ height = (width / dims.aspectRatio)
581
+ if debug {
582
+ this.log((" Image aspect ratio: " + (to_string dims.aspectRatio) + " -> height=" + (to_string height)))
583
+ }
584
+ }
585
+ autoHeight = false
586
+ }
587
+ if ((element.width.isSet == false) && element.height.isSet) {
588
+ ; Height is set but width is not
589
+ ; If parent has a fixed width, use it as the container width
590
+ if (parentWidth > 0.0) {
591
+ width = parentWidth
592
+ if debug {
593
+ this.log((" Image container using parent width: " + (to_string width) + "x" + (to_string height)))
594
+ }
595
+ } {
596
+ ; No parent width constraint - calculate from aspect ratio
597
+ width = (height * dims.aspectRatio)
598
+ if debug {
599
+ this.log((" Image aspect ratio: " + (to_string dims.aspectRatio) + " -> width=" + (to_string width)))
600
+ }
601
+ }
602
+ }
603
+ if ((element.width.isSet == false) && (element.height.isSet == false)) {
604
+ ; Neither set - if parent has dimensions, fill parent (like 100% 100%)
605
+ if ((parentWidth > 0.0) && (parentHeight > 0.0)) {
606
+ width = parentWidth
607
+ height = parentHeight
608
+ if debug {
609
+ this.log((" Image filling parent: " + (to_string width) + "x" + (to_string height)))
610
+ }
611
+ } {
612
+ ; No parent constraints - use natural size (capped at parent)
613
+ width = (to_double dims.width)
614
+ height = (to_double dims.height)
615
+ if (width > parentWidth) {
616
+ if (parentWidth > 0.0) {
617
+ def scale:double (parentWidth / width)
618
+ width = parentWidth
619
+ height = (height * scale)
620
+ }
621
+ }
622
+ if debug {
623
+ this.log((" Image natural size: " + (to_string width) + "x" + (to_string height)))
624
+ }
625
+ }
626
+ autoHeight = false
627
+ }
628
+ }
629
+ }
630
+ }
631
+
632
+ ; Apply min/max constraints — MAX FIRST, so a min that contradicts it
633
+ ; wins. See `clampMain`; this is the same rule for a box that is not a
634
+ ; flex item, and it used to be the other way round here too.
635
+ if element.maxWidth.isSet {
636
+ if (width > element.maxWidth.pixels) {
637
+ width = element.maxWidth.pixels
638
+ }
639
+ }
640
+ if element.minWidth.isSet {
641
+ if (width < element.minWidth.pixels) {
642
+ width = element.minWidth.pixels
643
+ }
644
+ }
645
+
646
+ ; A BOX HAS NO NEGATIVE SIDE. `width: -40%` is not a width — CSS throws
647
+ ; the declaration away — and a box that keeps the number draws MIRRORED
648
+ ; through its own left edge, which is how a progress bar reading "week
649
+ ; -41 of 104" came out as a full blue bar instead of an empty one. The
650
+ ; app's arithmetic is the app's; what this owes it is zero rather than
651
+ ; a picture of a number nobody can see is wrong.
652
+ if (width < 0.0) {
653
+ width = 0.0
654
+ }
655
+ if (height < 0.0) {
656
+ height = 0.0
657
+ }
658
+
659
+ ; Store calculated dimensions (before content layout for children to reference)
660
+ element.calculatedWidth = width
661
+ element.calculatedInnerWidth = (element.box.getInnerWidth(width))
662
+
663
+ ; If height is explicitly set (not auto), pre-calculate inner height for children
664
+ ; This allows children with height="100%" to resolve correctly
665
+ element.hasDefiniteHeight = (autoHeight == false)
666
+ if (autoHeight == false) {
667
+ element.calculatedHeight = height
668
+ element.calculatedInnerHeight = (element.box.getInnerHeight(height))
669
+ }
670
+
671
+ ; Handle absolute positioning
672
+ if element.isAbsolute {
673
+ this.layoutAbsolute(element parentWidth parentHeight)
674
+ }
675
+
676
+ ; Layout children and get content height
677
+ def childCount:int (element.getChildCount())
678
+ def contentHeight:double 0.0
679
+
680
+ if (childCount > 0) {
681
+ contentHeight = (this.layoutChildren(element))
682
+ ; Right to left, after the fact.
683
+ ;
684
+ ; Doing it this way rather than inside the placement loops is a
685
+ ; deliberate choice and it is the reason this is ~30 lines instead
686
+ ; of a rewrite of both engines. Every way a container can place a
687
+ ; child along the horizontal axis — flex row, flex row-reverse,
688
+ ; wrapping, `justify-content` in all its values, `gap`, grid
689
+ ; tracks, named grid lines, `dense` packing — ends with children
690
+ ; sitting at x positions inside a content box. Reflecting those
691
+ ; positions about that box turns ALL of it around at once, and
692
+ ; correctly: reversed order, packed from the right, the first grid
693
+ ; column rightmost. Anything added to either engine later is
694
+ ; mirrored the day it lands, without knowing this exists.
695
+ this.mirrorChildren(element)
696
+
697
+ ; fit-content, resolved now that there is content to fit.
698
+ ;
699
+ ; The children were placed against the parent's width, because that
700
+ ; is what a width-less box gets in EVG, so this measures what they
701
+ ; actually occupy and lays them out AGAIN inside the smaller box.
702
+ ; The second pass is not optional: justify-content, text-align and
703
+ ; every percentage below aligned against the wider box, and keeping
704
+ ; those positions while narrowing the box is how a "fixed" layout
705
+ ; ends up subtly wrong instead of obviously wrong.
706
+ if (element.width.unitType == 6) {
707
+ def fitW:double (this.contentExtent(element))
708
+ if ((fitW > 0.0) && (fitW < element.calculatedWidth)) {
709
+ element.calculatedWidth = fitW
710
+ element.calculatedInnerWidth = (element.box.getInnerWidth(fitW))
711
+ def kr:int 0
712
+ while (kr < childCount) {
713
+ def kc:EVGElement (element.getChild(kr))
714
+ kc.resetLayoutState()
715
+ kr = (kr + 1)
716
+ }
717
+ contentHeight = (this.layoutChildren(element))
718
+ this.mirrorChildren(element)
719
+ }
720
+ }
721
+ } {
722
+ ; No children - check if element has text content
723
+ def textContent:string element.textContent
724
+ if ((strlen textContent) > 0) {
725
+ ; Element has text content (View with text, span, text, Label, etc.)
726
+ def fontSize:double element.inheritedFontSize
727
+ if element.fontSize.isSet {
728
+ fontSize = element.fontSize.pixels
729
+ }
730
+ if (fontSize <= 0.0) {
731
+ fontSize = 14.0
732
+ }
733
+
734
+ ; Get line height
735
+ ; `line-height: normal` — zero here means nobody asked for a
736
+ ; number — is the FACE's own line box, so it is asked of the
737
+ ; thing that knows about faces. It used to be a flat 1.2, which
738
+ ; is a number people type and not one a browser computes: the
739
+ ; sans fallback's normal is 1.15em and monospace's is 1.164.
740
+ ; One answer for the line box, shared with the display list:
741
+ ; `line-height` is a number, a length or `normal`, and only
742
+ ; `EVGElement.lineBoxFor` knows which. `normal` is the FACE's
743
+ ; box and so comes from the engine that knows about faces.
744
+ def lineSpacing:double (element.lineBoxFor(fontSize
745
+ (textEngine.lineHeightFor((element.effectiveFontFamily()) fontSize))))
746
+
747
+ ; Line count comes from the shared wrap, not a second estimate,
748
+ ; so the height reserved here matches the lines paint will draw.
749
+ ; THE SAME WIDTH THE PAINTER BREAKS AT. `getInnerWidth` takes
750
+ ; off the border as well as the padding, and it is what
751
+ ; `EVGDisplayList` hands the breaker; this took off only the
752
+ ; padding. A bordered text element was therefore counted at
753
+ ; one width here and broken at another there — the height was
754
+ ; reserved for the wrap the layout imagined and the painter
755
+ ; drew a different one, which is a box the last line falls out
756
+ ; of.
757
+ def availableWidth:double (element.wrapWidth((element.box.getInnerWidth(width))))
758
+ def lineCount:int (textEngine.lineCountSpaced(textContent (element.effectiveFontFamily()) fontSize availableWidth element.letterSpacing))
759
+
760
+ contentHeight = (lineSpacing * (to_double lineCount))
761
+ ; And the gaps between paragraphs, which are not line boxes:
762
+ ; reserving them here is what keeps the painter's last line
763
+ ; inside the box it was sized for.
764
+ contentHeight = (contentHeight + (textEngine.paragraphLeading(textContent (element.effectiveFontFamily()) fontSize availableWidth element.letterSpacing element.paragraphSpacing)))
765
+
766
+ ; Baseline of the FIRST line, measured from this element's top
767
+ ; border edge, for align-items: baseline. The glyphs sit inside
768
+ ; the line box, so half the leading is above the ascent.
769
+ ;
770
+ ; THE HALF-LEADING MAY BE NEGATIVE, and it used to be clamped at
771
+ ; zero. `line-height: 1` gives a line box smaller than the face
772
+ ; — 1.0em against 1.117 — and CSS lets the glyphs hang out of it
773
+ ; equally top and bottom rather than pushing them down to fit.
774
+ ; Measured: at line-height 1 the browser puts the baseline at
775
+ ; 0.846em, which is exactly -0.0585 + 0.905. The clamp made that
776
+ ; 0.905 and moved every tightly-led line down by the difference.
777
+ def metrics:EVGTextMetrics (textEngine.measureRunSpaced(textContent (element.effectiveFontFamily()) fontSize element.letterSpacing))
778
+ def leading:double ((lineSpacing - (metrics.ascent + metrics.descent)) / 2.0)
779
+ ; `textShiftY` is written below, once the height is final —
780
+ ; it is a share of the slack and there is no slack until then
781
+ ; — so the baseline is completed there as well.
782
+ element.calculatedBaseline = (element.box.paddingTopPx + element.box.borderWidthPx + leading + metrics.ascent)
783
+ element.calculatedDescent = metrics.descent
784
+ element.hasBaseline = true
785
+ }
786
+ }
787
+
788
+ ; Finalize height
789
+ if autoHeight {
790
+ ; Add padding and border
791
+ height = contentHeight + element.box.paddingTopPx + element.box.paddingBottomPx + (element.box.borderWidthPx * 2.0)
792
+ }
793
+
794
+ ; A border box cannot be smaller than its own padding and border. CSS
795
+ ; grows the box in that case rather than producing a negative content
796
+ ; box; EVG used to keep the declared size and collapse the children.
797
+ def vChrome:double (element.box.getVerticalChrome())
798
+ if (height < vChrome) {
799
+ height = vChrome
800
+ }
801
+ def hChrome:double (element.box.getHorizontalChrome())
802
+ if (width < hChrome) {
803
+ width = hChrome
804
+ }
805
+
806
+ ; Apply min/max height constraints — max first, min second, for the
807
+ ; reason given on the width above.
808
+ if element.maxHeight.isSet {
809
+ if (height > element.maxHeight.pixels) {
810
+ height = element.maxHeight.pixels
811
+ }
812
+ }
813
+ if element.minHeight.isSet {
814
+ if (height < element.minHeight.pixels) {
815
+ height = element.minHeight.pixels
816
+ }
817
+ }
818
+
819
+ element.calculatedHeight = height
820
+ element.calculatedInnerHeight = (element.box.getInnerHeight(height))
821
+ ; THE ANONYMOUS FLEX ITEM. An element that is a flex container and
822
+ ; carries text of its own does not lay that text out as a block: CSS
823
+ ; wraps it in an item, and `align-items` (across a row) or
824
+ ; `justify-content` (along a column) then places it. This is the pill
825
+ ; — `display:flex; align-items:center` with the label as the element's
826
+ ; own text — whose label sat against the top edge because the line box
827
+ ; started at the content top and nothing ever asked where it should
828
+ ; go. Written once here; `EVGElement.textShiftY` says the rest.
829
+ element.textShiftY = 0.0
830
+ if ((strlen element.textContent) > 0) {
831
+ if ((element.getChildCount()) == 0) {
832
+ if (element.display == "flex") {
833
+ def slack:double (element.calculatedInnerHeight - contentHeight)
834
+ if (slack > 0.0) {
835
+ ; A row's cross axis is the vertical one; a column's
836
+ ; main axis is.
837
+ def how@(mutable):string element.alignItems
838
+ if ((element.flexDirection == "column") || (element.flexDirection == "column-reverse")) {
839
+ how = element.justifyContent
840
+ }
841
+ if ((how == "center") || (how == "safe center")) {
842
+ element.textShiftY = (slack / 2.0)
843
+ }
844
+ if ((how == "flex-end") || (how == "end")) {
845
+ element.textShiftY = slack
846
+ }
847
+ }
848
+ }
849
+ }
850
+ if (element.hasBaseline && (element.textShiftY != 0.0)) {
851
+ element.calculatedBaseline = (element.calculatedBaseline + element.textShiftY)
852
+ }
853
+ }
854
+ element.calculatedPage = currentPage
855
+ element.isLayoutComplete = true
856
+
857
+ ; A container inherits the baseline of its first in-flow child (CSS
858
+ ; "first baseline"), so a Label wrapped in a View still aligns.
859
+ if ((element.hasBaseline == false) && (childCount > 0)) {
860
+ this.inheritBaselineFromFirstChild(element)
861
+ }
862
+
863
+ ; --- SCROLLING --------------------------------------------------------
864
+ ;
865
+ ; The last thing that happens to an element, and it has to be: the
866
+ ; extents are measured from where the children ENDED UP, so this runs
867
+ ; after every one of them is placed. `layoutChildren` has already been
868
+ ; through the whole subtree by the time control gets back here.
869
+ if (element.clipsContent()) {
870
+ this.applyScroll(element)
871
+ }
872
+
873
+ if debug {
874
+ this.log((" Laid out " + element.tagName + " id=" + element.id + " at (" + (to_string element.calculatedX) + "," + (to_string element.calculatedY) + ") size=" + (to_string width) + "x" + (to_string height)))
875
+ }
876
+ }
877
+
878
+ ; What a scroll container owes: how big its content is, and where the
879
+ ; content currently sits.
880
+ ;
881
+ ; The offset is applied as a TRANSLATION of the placed subtree rather than
882
+ ; as an origin the flow is run from. That is not a shortcut. The flow has
883
+ ; to run at the content's natural size for the extent to mean anything —
884
+ ; measure first, then move — and running it from a shifted origin would
885
+ ; make the extent a function of the scroll position, which is how a
886
+ ; scrollbar ends up changing size as you drag it.
887
+ ;
888
+ ; EVERYTHING inside moves, absolutely positioned children and overlay
889
+ ; surfaces alike. That is what a browser does: an absolute box is
890
+ ; positioned against this one, so it belongs to its content, and a dropdown
891
+ ; opened from a row inside a scrolled panel travels with the row. Only
892
+ ; `position: fixed` escapes — it is furniture over the viewport rather than
893
+ ; content, and `moveSubtreeUnfixed` is the walk that steps over it.
894
+ ;
895
+ ; It reuses the mover the RTL mirror already had. One function that walks a
896
+ ; placed subtree and adds an offset to it is enough; a second one would be
897
+ ; two places for the same rule to be got wrong.
898
+ ; How far the content reaches, measured from where the children ended up.
899
+ ; Separate from `applyScroll` because the overlay pass has to ask the
900
+ ; question again after it has resized a surface, WITHOUT translating
901
+ ; anything a second time.
902
+ fn measureScrollExtent:void (el:EVGElement) {
903
+ def innerX:double (el.calculatedX + el.box.borderWidthPx + el.box.paddingLeftPx)
904
+ def innerY:double (el.calculatedY + el.box.borderWidthPx + el.box.paddingTopPx)
905
+ def w:double 0.0
906
+ def h:double 0.0
907
+ def i:int 0
908
+ while (i < (el.getChildCount())) {
909
+ def kid:EVGElement (el.getChild(i))
910
+ def right:double ((kid.calculatedX + kid.calculatedWidth) - innerX)
911
+ def bottom:double ((kid.calculatedY + kid.calculatedHeight) - innerY)
912
+ if (right > w) {
913
+ w = right
914
+ }
915
+ if (bottom > h) {
916
+ h = bottom
917
+ }
918
+ i = (i + 1)
919
+ }
920
+ ; PADDING BOX, like the DOM: the content extent plus the paddings on
921
+ ; both sides. Measured, not assumed — eight 60-pixel rows in a box with
922
+ ; 10 of padding gives the browser 500.
923
+ el.scrollWidth = ((w + el.box.paddingLeftPx) + el.box.paddingRightPx)
924
+ el.scrollHeight = ((h + el.box.paddingTopPx) + el.box.paddingBottomPx)
925
+ }
926
+
927
+ fn applyScroll:void (el:EVGElement) {
928
+ this.measureScrollExtent(el)
929
+
930
+ ; Clamped, the way a browser clamps `scrollTop`: an application that
931
+ ; asks to scroll past the end has asked for the end. Doing it here and
932
+ ; not in the setter is what makes it correct — how far there is to go
933
+ ; is not known until the content has been measured, and it changes
934
+ ; every time the content does.
935
+ def maxY:double (el.maxScrollTop())
936
+ if (el.scrollTop > maxY) {
937
+ el.scrollTop = maxY
938
+ }
939
+ if (el.scrollTop < 0.0) {
940
+ el.scrollTop = 0.0
941
+ }
942
+ def maxX:double (el.maxScrollLeft())
943
+ if (el.scrollLeft > maxX) {
944
+ el.scrollLeft = maxX
945
+ }
946
+ if (el.scrollLeft < 0.0) {
947
+ el.scrollLeft = 0.0
948
+ }
949
+
950
+ ; Where the boxes now are, which is what `scrollOnly` moves FROM.
951
+ ; Written whether or not anything is translated: an unscrolled
952
+ ; container has applied zero, and that is a fact about it, not the
953
+ ; absence of one.
954
+ el.appliedScrollTop = el.scrollTop
955
+ el.appliedScrollLeft = el.scrollLeft
956
+
957
+ if ((el.scrollTop == 0.0) && (el.scrollLeft == 0.0)) {
958
+ return
959
+ }
960
+ def j:int 0
961
+ while (j < (el.getChildCount())) {
962
+ def moved:EVGElement (el.getChild(j))
963
+ if (moved.isFixedPosition()) {
964
+ } {
965
+ moved.moveSubtreeUnfixed((0.0 - el.scrollLeft) (0.0 - el.scrollTop))
966
+ }
967
+ j = (j + 1)
968
+ }
969
+ }
970
+
971
+ ; --- SCROLLING WITHOUT LAYING OUT -----------------------------------------
972
+ ;
973
+ ; A SCROLL DOES NOT CHANGE A LAYOUT. `applyScroll` above lays the content
974
+ ; out as if nothing were scrolled and then TRANSLATES the subtree by
975
+ ; -scrollTop, so two scroll positions of the same document differ by
976
+ ; exactly that translate: every width, every line break, every box is the
977
+ ; same at the top of the page as at the bottom.
978
+ ;
979
+ ; Running the whole layout again to arrive at that translate is what made
980
+ ; scrolling `gallery/realtrainer` with a real training diary in it slow
981
+ ; enough to see. Every wheel event measured every string in the document
982
+ ; and broke every paragraph into lines again — seconds of `measureText` and
983
+ ; `breakLines` per frame in the profile — to compute, character for
984
+ ; character, what it had computed on the frame before. On a phone the page
985
+ ; simply stopped moving.
986
+ ;
987
+ ; This moves each scrolled subtree by the DIFFERENCE between where it is
988
+ ; and where the new offset puts it, and touches nothing else. Call it when
989
+ ; NOTHING BUT `scrollTop`/`scrollLeft` HAS CHANGED; anything that changes
990
+ ; content, styles, fonts or the viewport needs the real `layout` again,
991
+ ; because the extents this clamps against were measured by it.
992
+ fn scrollOnly:void (root:EVGElement) {
993
+ this.scrollOnlyWalk(root)
994
+ }
995
+
996
+ ; The same, for ONE container the caller knows it scrolled — no walk to
997
+ ; find it. A container inside it that scrolls on its own is not touched:
998
+ ; its offset did not change, and its boxes move with the rest.
999
+ fn scrollOnlyFrom:void (el:EVGElement) {
1000
+ if ((el.clipsContent()) == false) {
1001
+ return
1002
+ }
1003
+ this.clampScroll(el)
1004
+ def dy:double (el.appliedScrollTop - el.scrollTop)
1005
+ def dx:double (el.appliedScrollLeft - el.scrollLeft)
1006
+ if ((dx == 0.0) && (dy == 0.0)) {
1007
+ return
1008
+ }
1009
+ def j:int 0
1010
+ while (j < (el.getChildCount())) {
1011
+ this.shiftNear((el.getChild(j)) dx dy el)
1012
+ j = (j + 1)
1013
+ }
1014
+ el.appliedScrollTop = el.scrollTop
1015
+ el.appliedScrollLeft = el.scrollLeft
1016
+ }
1017
+
1018
+ ; How far from a scroll container's box, in its own heights, a row is
1019
+ ; still moved for real by `scrollOnly`. Rows beyond it are owed the move
1020
+ ; instead — `EVGElement.shiftDx` — and paid when they come within it or
1021
+ ; when something descends into them. Wider than the display list's
1022
+ ; overscan in either direction, so a row the list is about to draw has
1023
+ ; already been moved rather than settled on the way in.
1024
+ def nearHeights:double 3.0
1025
+
1026
+ ; Move a scrolled subtree the lazy way: the node's OWN box moves now —
1027
+ ; that is what the culling and the hit test look at — and the move goes
1028
+ ; on down only while the subtree is near the clip. A far node is left
1029
+ ; owing the move to its children (`EVGElement.shiftDx`), and a node that
1030
+ ; was owed one and has come near pays it together with this one. On a
1031
+ ; document of a thousand rows a frame touches the rows near the viewport
1032
+ ; and one node per row elsewhere, not every box in the document.
1033
+ fn shiftNear:void (kid:EVGElement dx:double dy:double clip:EVGElement) {
1034
+ ; A FIXED BOX IS NOT SCROLLED, here either. `applyScroll` steps over it
1035
+ ; on a full layout and this is the incremental path; a rule kept in one
1036
+ ; of the two is a rule that holds until the frame after the first one.
1037
+ if (kid.isFixedPosition()) {
1038
+ return
1039
+ }
1040
+ kid.moveSelf(dx dy)
1041
+ if (this.nearClip(kid clip)) {
1042
+ def tx:double (kid.shiftDx + dx)
1043
+ def ty:double (kid.shiftDy + dy)
1044
+ kid.shiftDx = 0.0
1045
+ kid.shiftDy = 0.0
1046
+ def i:int 0
1047
+ def n:int (kid.getChildCount())
1048
+ while (i < n) {
1049
+ this.shiftNear((kid.getChild(i)) tx ty clip)
1050
+ i = (i + 1)
1051
+ }
1052
+ return
1053
+ }
1054
+ kid.shiftDx = (kid.shiftDx + dx)
1055
+ kid.shiftDy = (kid.shiftDy + dy)
1056
+ }
1057
+
1058
+ ; Is this subtree within reach of the clip box, grown by `nearHeights`?
1059
+ ; A subtree whose drawn output is not its boxes is always near: nothing
1060
+ ; here can say where it will be looked at.
1061
+ fn nearClip:boolean (kid:EVGElement el:EVGElement) {
1062
+ if kid.paintUnbounded {
1063
+ return true
1064
+ }
1065
+ def margin:double (el.calculatedHeight * nearHeights)
1066
+ if (kid.paintBottom < (el.calculatedY - margin)) {
1067
+ return false
1068
+ }
1069
+ if (kid.paintTop > ((el.calculatedY + el.calculatedHeight) + margin)) {
1070
+ return false
1071
+ }
1072
+ def marginX:double (el.calculatedWidth * nearHeights)
1073
+ if (kid.paintRight < (el.calculatedX - marginX)) {
1074
+ return false
1075
+ }
1076
+ if (kid.paintLeft > ((el.calculatedX + el.calculatedWidth) + marginX)) {
1077
+ return false
1078
+ }
1079
+ return true
1080
+ }
1081
+
1082
+ ; The clamp alone, for a container whose offset was just written: what
1083
+ ; `scrollOnly` would do to it first, without the walk that moves the
1084
+ ; boxes. A host that gets six pointer reports between two frames clamps
1085
+ ; six times — cheap, and it is what makes "did the page move" and the
1086
+ ; offset it reads back right at once — and moves the subtree once, when
1087
+ ; the frame asks for the layout.
1088
+ fn clampScroll:void (el:EVGElement) {
1089
+ if ((el.clipsContent()) == false) {
1090
+ return
1091
+ }
1092
+ ; The same clamp `applyScroll` does, against the extents it
1093
+ ; measured. They are still right: the content did not move.
1094
+ def maxY:double (el.maxScrollTop())
1095
+ if (el.scrollTop > maxY) {
1096
+ el.scrollTop = maxY
1097
+ }
1098
+ if (el.scrollTop < 0.0) {
1099
+ el.scrollTop = 0.0
1100
+ }
1101
+ def maxX:double (el.maxScrollLeft())
1102
+ if (el.scrollLeft > maxX) {
1103
+ el.scrollLeft = maxX
1104
+ }
1105
+ if (el.scrollLeft < 0.0) {
1106
+ el.scrollLeft = 0.0
1107
+ }
1108
+ }
1109
+
1110
+ fn scrollOnlyWalk:void (el:EVGElement) {
1111
+ if (el.clipsContent()) {
1112
+ this.scrollOnlyFrom(el)
1113
+ }
1114
+ def i:int 0
1115
+ while (i < (el.getChildCount())) {
1116
+ def kid:EVGElement (el.getChild(i))
1117
+ ; A subtree still owed a move is far from any viewport: nothing
1118
+ ; in it is scrolling, so there is nothing to clamp in it.
1119
+ if ((kid.shiftDx == 0.0) && (kid.shiftDy == 0.0)) {
1120
+ this.scrollOnlyWalk(kid)
1121
+ }
1122
+ i = (i + 1)
1123
+ }
1124
+ }
1125
+
1126
+ ; What a flex item starts from on the main axis before it grows or shrinks.
1127
+ ; A stated `flex-basis` REPLACES `width` here, which is what CSS says and
1128
+ ; what lets `flex: 0 0 120px` mean the same as `width: 120px` — see the
1129
+ ; note in the basis branch of the row pass.
1130
+ fn hasBaseMain:boolean (c:EVGElement) {
1131
+ return (c.flexBasis.isSet || c.width.isSet)
1132
+ }
1133
+
1134
+ fn baseMain:double (c:EVGElement) {
1135
+ if c.flexBasis.isSet {
1136
+ return c.flexBasis.pixels
1137
+ }
1138
+ return c.width.pixels
1139
+ }
1140
+
1141
+ ; The cross-axis alignment that applies to ONE item: its own `align-self`
1142
+ ; when it stated one, otherwise the container's `align-items`.
1143
+ ;
1144
+ ; `realtrainer.css` has been writing `align-self` since before the property
1145
+ ; existed here, which is the usual way a gap gets found: somebody assumed
1146
+ ; the CSS they know, and the declaration went nowhere.
1147
+ fn crossAlignOf:string (child:EVGElement parentAlign:string) {
1148
+ if ((strlen child.alignSelf) > 0) {
1149
+ return child.alignSelf
1150
+ }
1151
+ return parentAlign
1152
+ }
1153
+
1154
+ ; A main-axis size, clamped the way CSS clamps it.
1155
+ ;
1156
+ ; THE ORDER IS THE WHOLE POINT. `max-width` is applied first and
1157
+ ; `min-width` second, so when the two conflict the MINIMUM wins:
1158
+ ; `width: 50px; min-width: 200px; max-width: 100px` is 200 in every
1159
+ ; browser. This engine applied them the other way round and got 100 — a
1160
+ ; box that obeyed a maximum it was told to ignore, which shows up as a
1161
+ ; panel that will not open to the size its own rule demands.
1162
+ ;
1163
+ ; `min-width: auto` on a flex item is NOT zero: it is the item's
1164
+ ; min-content size, and that is what the `else` branch computes. The
1165
+ ; exception is a clipping box, which is a statement that the content need
1166
+ ; not fit — see the long note in the grow loop, which this replaced.
1167
+ fn clampMain:double (c:EVGElement w:double) {
1168
+ def out:double w
1169
+ if c.maxWidth.isSet {
1170
+ if (out > c.maxWidth.pixels) {
1171
+ out = c.maxWidth.pixels
1172
+ }
1173
+ }
1174
+ if c.minWidth.isSet {
1175
+ if (out < c.minWidth.pixels) {
1176
+ out = c.minWidth.pixels
1177
+ }
1178
+ } {
1179
+ if ((c.clipsContent()) == false) {
1180
+ def autoMin:double (this.minIntrinsicWidthOf(c))
1181
+ if (out < autoMin) {
1182
+ out = autoMin
1183
+ }
1184
+ }
1185
+ }
1186
+ return out
1187
+ }
1188
+
1189
+ ; An out-of-flow child — `position: fixed`, a `Layer`, or an absolutely
1190
+ ; positioned box — sized and placed against this parent.
1191
+ ;
1192
+ ; Lifted out of `layoutChildren` because THE GRID NEVER RAN IT. `layoutGrid`
1193
+ ; collects the in-flow items and skips `isAbsolute` ones, correctly, and then
1194
+ ; nothing laid them out at all: an absolute child of a `display: grid`
1195
+ ; container kept zero size at (0,0), so a box vanished and a `path` drew its
1196
+ ; own coordinates in the page's top-left corner with `left`/`top` dropped.
1197
+ ; The same child under a flex or block parent was placed correctly, which is
1198
+ ; what made it look like a `path` bug rather than a grid one.
1199
+ fn layoutOutOfFlowChild:void (parent:EVGElement child:EVGElement innerWidth:double innerHeight:double startX:double startY:double alongRow:boolean) {
1200
+ ; A FIXED BOX IS POSITIONED AGAINST THE VIEWPORT, not against
1201
+ ; this parent: its `top`, `right`, `bottom` and `left` and every
1202
+ ; percentage inside it resolve against the page, and its origin
1203
+ ; is the page's own (0,0) — so unlike an absolute child it is
1204
+ ; NOT translated to the parent's content corner afterwards.
1205
+ ; `applyScroll` then leaves it alone, which is the whole
1206
+ ; difference between the two modes.
1207
+ if (child.isFixedPosition()) {
1208
+ child.unitsResolved = false
1209
+ child.resolveUnits(child.viewportW child.viewportH)
1210
+ def wasRowF:boolean layingOutAlongRow
1211
+ def wasShrinkF:boolean columnCrossShrinks
1212
+ layingOutAlongRow = alongRow
1213
+ columnCrossShrinks = (this.crossShrinksUnder(parent))
1214
+ this.layoutElement(child 0.0 0.0 child.viewportW child.viewportH)
1215
+ layingOutAlongRow = wasRowF
1216
+ columnCrossShrinks = wasShrinkF
1217
+ ; The viewport's own origin. It is (0,0) for a page, so
1218
+ ; this is a no-op for every host that has one viewport;
1219
+ ; a stage with a frame per width moves the box onto its
1220
+ ; frame, the same one translation the absolute branch
1221
+ ; below makes for its own reason.
1222
+ if ((child.viewportX != 0.0) || (child.viewportY != 0.0)) {
1223
+ this.translateSubtree(child child.viewportX child.viewportY)
1224
+ }
1225
+ return
1226
+ }
1227
+ ; Layer elements use parent's full dimensions, not inner dimensions
1228
+ if ((child.tagName == "layer") || (child.tagName == "Layer")) {
1229
+ ; Re-resolve Layer dimensions with parent's full width/height
1230
+ child.unitsResolved = false
1231
+ child.resolveUnits(parent.calculatedWidth parent.calculatedHeight)
1232
+
1233
+ ; Set Layer's calculated dimensions to parent's full size
1234
+ child.calculatedWidth = parent.calculatedWidth
1235
+ child.calculatedHeight = parent.calculatedHeight
1236
+ child.calculatedInnerWidth = (child.box.getInnerWidth(child.calculatedWidth))
1237
+ child.calculatedInnerHeight = (child.box.getInnerHeight(child.calculatedHeight))
1238
+
1239
+ ; Ensure height.isSet is true for alignment calculations.
1240
+ ; Replace the unit rather than flipping the one that is
1241
+ ; there: an unset height is the shared unset unit.
1242
+ if child.height.isSet {
1243
+ child.height.pixels = child.calculatedHeight
1244
+ } {
1245
+ child.height = (EVGUnit.px(child.calculatedHeight))
1246
+ }
1247
+
1248
+ this.layoutAbsolute(child parent.calculatedWidth parent.calculatedHeight)
1249
+ ; Layer positions relative to parent's edge (0,0), not content area
1250
+ child.calculatedX = child.calculatedX + parent.calculatedX
1251
+ child.calculatedY = child.calculatedY + parent.calculatedY
1252
+ } {
1253
+ ; An absolutely positioned element is still an element: it
1254
+ ; has a size. Nothing computed one — this branch called
1255
+ ; layoutAbsolute, which sets x and y and nothing else — so
1256
+ ; calculatedWidth stayed 0 for every one of them. A label
1257
+ ; then had no box for text-align to align in, a rotation
1258
+ ; "about its centre" turned about its left edge instead,
1259
+ ; and a background painted nothing.
1260
+ ;
1261
+ ; So lay it out the way any other element is laid out, and
1262
+ ; move the finished subtree into place afterwards: the
1263
+ ; positions inside it are relative to the element, so one
1264
+ ; translation carries all of them.
1265
+ def wasRow0:boolean layingOutAlongRow
1266
+ def wasShrink0:boolean columnCrossShrinks
1267
+ layingOutAlongRow = alongRow
1268
+ columnCrossShrinks = (this.crossShrinksUnder(parent))
1269
+ this.layoutElement(child 0.0 0.0 innerWidth innerHeight)
1270
+ layingOutAlongRow = wasRow0
1271
+ columnCrossShrinks = wasShrink0
1272
+ this.translateSubtree(child startX startY)
1273
+ }
1274
+ }
1275
+
1276
+ fn layoutChildren:double (parent:EVGElement) {
1277
+ ; Layout all children using flow algorithm
1278
+ ; Returns total content height
1279
+
1280
+ def childCount:int (parent.getChildCount())
1281
+ if (childCount == 0) {
1282
+ return 0.0
1283
+ }
1284
+
1285
+ ; A grid container runs a different placement algorithm entirely.
1286
+ if (parent.display == "grid") {
1287
+ return (this.layoutGrid(parent))
1288
+ }
1289
+
1290
+ def innerWidth:double parent.calculatedInnerWidth
1291
+ def innerHeight:double parent.calculatedInnerHeight
1292
+
1293
+ ; Starting position (after border and padding, NOT margin)
1294
+ ; Margin is OUTSIDE the element, padding is INSIDE
1295
+ def startX:double (parent.calculatedX + parent.box.borderWidthPx + parent.box.paddingLeftPx)
1296
+ def startY:double (parent.calculatedY + parent.box.borderWidthPx + parent.box.paddingTopPx)
1297
+
1298
+ def currentX:double startX
1299
+ def currentY:double startY
1300
+ def rowHeight:double 0.0
1301
+ def rowElements:[EVGElement]
1302
+ def totalHeight:double 0.0
1303
+ ; Every wrapped line, flattened, so align-content can move whole lines
1304
+ ; once the total content height is known.
1305
+ def lineMembers:[EVGElement]
1306
+ def lineCounts:[int]
1307
+ def lineHeights:[double]
1308
+ ; count of in-flow (non-absolute) children already placed, so `gap` is
1309
+ ; added BETWEEN consecutive children only (not before the first).
1310
+ def placedInFlow:int 0
1311
+
1312
+ ; Check layout direction using flexDirection (CSS standard)
1313
+ def isColumn:boolean (parent.flexDirection == "column")
1314
+
1315
+ ; The two gaps, on the two axes.
1316
+ ;
1317
+ ; `gap` is the shorthand and `row-gap` / `column-gap` are the longhands
1318
+ ; that override it — and which longhand belongs to which axis depends
1319
+ ; on the direction, not on the name: in a ROW the space BETWEEN items
1320
+ ; is `column-gap` (they are in different columns) and the space between
1321
+ ; wrapped LINES is `row-gap`. A column swaps them.
1322
+ ;
1323
+ ; Both longhands parsed before this and neither did anything: a flex
1324
+ ; container with `row-gap: 30px; column-gap: 5px` laid out as though it
1325
+ ; had no gaps at all, because only `parent.gap` was ever read. And the
1326
+ ; cross gap did not exist even for the shorthand, so `gap: 10px` on a
1327
+ ; wrapping row put ten pixels between the items and nothing between the
1328
+ ; lines.
1329
+ def rowGapUnit:EVGUnit parent.gap
1330
+ if parent.rowGap.isSet {
1331
+ rowGapUnit = parent.rowGap
1332
+ }
1333
+ def colGapUnit:EVGUnit parent.gap
1334
+ if parent.columnGap.isSet {
1335
+ colGapUnit = parent.columnGap
1336
+ }
1337
+ def mainGapUnit:EVGUnit colGapUnit
1338
+ def crossGapUnit:EVGUnit rowGapUnit
1339
+ if isColumn {
1340
+ mainGapUnit = rowGapUnit
1341
+ crossGapUnit = colGapUnit
1342
+ }
1343
+
1344
+ ; Percentages resolve against the inner extent of the axis they space;
1345
+ ; px is idempotent, so re-resolving every layout costs nothing.
1346
+ def gapPx:double 0.0
1347
+ if mainGapUnit.isSet {
1348
+ mainGapUnit.rootFontSize = parent.rootFontSize
1349
+ if isColumn {
1350
+ mainGapUnit.resolve(innerHeight parent.inheritedFontSize)
1351
+ } {
1352
+ mainGapUnit.resolve(innerWidth parent.inheritedFontSize)
1353
+ }
1354
+ gapPx = mainGapUnit.pixels
1355
+ }
1356
+ ; The cross gap is only ever read at a wrap, so a container that
1357
+ ; cannot wrap does not pay for resolving it. Most of them cannot: the
1358
+ ; benchmark fixtures say `flex-wrap: nowrap` on every row, and
1359
+ ; resolving a unit nobody reads showed up as a few per cent.
1360
+ def crossGapPx:double 0.0
1361
+ def canWrap:boolean ((isColumn == false) && (parent.flexWrap != "nowrap"))
1362
+ if (canWrap && crossGapUnit.isSet) {
1363
+ crossGapUnit.rootFontSize = parent.rootFontSize
1364
+ if isColumn {
1365
+ crossGapUnit.resolve(innerWidth parent.inheritedFontSize)
1366
+ } {
1367
+ crossGapUnit.resolve(innerHeight parent.inheritedFontSize)
1368
+ }
1369
+ crossGapPx = crossGapUnit.pixels
1370
+ }
1371
+
1372
+ ; For row layout with flex items, we need to calculate available space first
1373
+ if (isColumn == false) {
1374
+ ; Calculate total fixed width and total flex
1375
+ def fixedWidth:double 0.0
1376
+ def totalFlex:double 0.0
1377
+ def j:int 0
1378
+ while (j < childCount) {
1379
+ def c:EVGElement (parent.getChild(j))
1380
+ c.inheritProperties(parent)
1381
+ c.resolveUnits(innerWidth innerHeight)
1382
+ ; Out of flow takes no share of the line: an absolute child,
1383
+ ; or one the sheet has set `display: none`. This sum counted
1384
+ ; every child's width, so a rail folded away at a breakpoint
1385
+ ; still left a rail-wide hole beside the column that grew.
1386
+ def hasBasis:boolean c.flexBasis.isSet
1387
+ if c.isAbsolute {
1388
+ } {
1389
+ if ((c.flex > 0.0) && hasBasis) {
1390
+ totalFlex = totalFlex + c.flex
1391
+ fixedWidth = ((fixedWidth + c.flexBasis.pixels) + c.box.marginLeftPx) + c.box.marginRightPx
1392
+ } {
1393
+ ; A BASIS ON AN ITEM THAT DOES NOT GROW IS STILL ITS SIZE.
1394
+ ; This branch did not exist: `flex-basis: 120px` with no
1395
+ ; `flex-grow` fell past every test below into the
1396
+ ; content-estimate case, the item was laid out with no
1397
+ ; width at all and so filled the row, and the growing
1398
+ ; sibling beside it was left with nothing —
1399
+ ;
1400
+ ; width:120px + flex-grow:1 -> 120 | 480 right
1401
+ ; flex-basis:120px + flex-grow:1 -> 600 | 0 wrong
1402
+ ;
1403
+ ; which is a fixed rail beside a flexible panel, the most
1404
+ ; ordinary two-box layout there is. CSS says a definite
1405
+ ; basis REPLACES the width as the item's starting main
1406
+ ; size, so it is checked before `width` and not after.
1407
+ if hasBasis {
1408
+ def baseW:double (this.clampMain(c c.flexBasis.pixels))
1409
+ c.calculatedFlexWidth = baseW
1410
+ c.hasFlexWidth = true
1411
+ fixedWidth = ((fixedWidth + baseW) + c.box.marginLeftPx) + c.box.marginRightPx
1412
+ } {
1413
+ if c.width.isSet {
1414
+ fixedWidth = fixedWidth + c.width.pixels + c.box.marginLeftPx + c.box.marginRightPx
1415
+ } {
1416
+ if (c.flex > 0.0) {
1417
+ totalFlex = totalFlex + c.flex
1418
+ fixedWidth = fixedWidth + c.box.marginLeftPx + c.box.marginRightPx
1419
+ } {
1420
+ def avail:double (innerWidth - c.box.marginLeftPx - c.box.marginRightPx)
1421
+ ; This IS the row flex pass, so along a row by
1422
+ ; construction.
1423
+ def estW:double (this.estimateChildWidth(c avail true))
1424
+ fixedWidth = fixedWidth + estW + c.box.marginLeftPx + c.box.marginRightPx
1425
+ }
1426
+ }
1427
+ }
1428
+ }
1429
+ }
1430
+ j = j + 1
1431
+ }
1432
+
1433
+ ; Calculate available space for flex items (row gaps consume it too)
1434
+ def totalGap:double 0.0
1435
+ if (childCount > 1) {
1436
+ totalGap = ((to_double (childCount - 1)) * gapPx)
1437
+ }
1438
+ def availableForFlex:double ((innerWidth - fixedWidth) - totalGap)
1439
+ if (availableForFlex < 0.0) {
1440
+ availableForFlex = 0.0
1441
+ }
1442
+
1443
+ ; Assign flex widths to children: basis plus a share of the leftover.
1444
+ ;
1445
+ ; min/max are resolved as part of this rather than clamped after the
1446
+ ; fact. An item that hits its limit is frozen at the limit and taken
1447
+ ; out of the pool, and the space it did not use is offered to the
1448
+ ; others — otherwise clamping just left a hole in the row. This is
1449
+ ; the CSS "resolve flexible lengths" loop, bounded by the child count
1450
+ ; because each pass freezes at least one item.
1451
+ if (totalFlex > 0.0) {
1452
+ def frozen:[boolean]
1453
+ def jf:int 0
1454
+ while (jf < childCount) {
1455
+ push frozen false
1456
+ jf = (jf + 1)
1457
+ }
1458
+
1459
+ def poolSpace:double availableForFlex
1460
+ def poolFlex:double totalFlex
1461
+ def pass:int 0
1462
+ def settled:boolean false
1463
+ while ((pass < childCount) && (settled == false)) {
1464
+ settled = true
1465
+ j = 0
1466
+ while (j < childCount) {
1467
+ def c:EVGElement (parent.getChild(j))
1468
+ def isFlexItem:boolean false
1469
+ if (c.flex > 0.0) {
1470
+ if (c.flexBasis.isSet || (c.width.isSet == false)) {
1471
+ isFlexItem = true
1472
+ }
1473
+ }
1474
+ if (isFlexItem && ((itemAt frozen j) == false)) {
1475
+ def basisW:double 0.0
1476
+ if c.flexBasis.isSet {
1477
+ basisW = c.flexBasis.pixels
1478
+ }
1479
+ def sizeW:double basisW
1480
+ if (poolFlex > 0.0) {
1481
+ sizeW = (basisW + ((poolSpace * c.flex) / poolFlex))
1482
+ }
1483
+ ; Clamp, and if the clamp bit, freeze here and give
1484
+ ; the difference back to the remaining items.
1485
+ ;
1486
+ ; `clampMain` is shared with the basis pass above
1487
+ ; and with the shrink pass below, which is how the
1488
+ ; three of them stopped disagreeing about which of
1489
+ ; min and max wins. The long note about the
1490
+ ; automatic minimum lives there now.
1491
+ def clampedW:double (this.clampMain(c sizeW))
1492
+ if (clampedW != sizeW) {
1493
+ set frozen j true
1494
+ poolSpace = ((poolSpace - clampedW) + basisW)
1495
+ poolFlex = (poolFlex - c.flex)
1496
+ if (poolSpace < 0.0) {
1497
+ poolSpace = 0.0
1498
+ }
1499
+ settled = false
1500
+ }
1501
+ c.calculatedFlexWidth = clampedW
1502
+ c.hasFlexWidth = true
1503
+ }
1504
+ j = j + 1
1505
+ }
1506
+ pass = (pass + 1)
1507
+ }
1508
+ }
1509
+ }
1510
+
1511
+ ; Column-axis flex: grow (distribute leftover height to flex>0 children)
1512
+ ; and, when there is no grow flex, shrink-to-fit so fixed children that
1513
+ ; overflow the container height are scaled down instead of spilling out.
1514
+ ;
1515
+ ; Both need a definite container height to work against. An auto-height
1516
+ ; column sizes to its content, so `innerHeight` here is stale (it is only
1517
+ ; pre-computed in layoutElement when the height is definite) and using it
1518
+ ; would shrink content to a height that does not exist yet — which is how
1519
+ ; document flow (auto-height Views stacked down a page) differs from the
1520
+ ; fixed-size panels this was originally written for.
1521
+ if (isColumn && parent.hasDefiniteHeight) {
1522
+ def fixedHeight:double 0.0
1523
+ def totalFlexC:double 0.0
1524
+ def flowCountC:int 0
1525
+ def jc:int 0
1526
+ while (jc < childCount) {
1527
+ def c:EVGElement (parent.getChild(jc))
1528
+ c.inheritProperties(parent)
1529
+ c.resolveUnits(innerWidth innerHeight)
1530
+ if (c.isAbsolute == false) {
1531
+ flowCountC = (flowCountC + 1)
1532
+ def mAxis:double (c.box.marginTopPx + c.box.marginBottomPx)
1533
+ if c.height.isSet {
1534
+ fixedHeight = (fixedHeight + c.height.pixels + mAxis)
1535
+ } {
1536
+ if (c.flex > 0.0) {
1537
+ totalFlexC = (totalFlexC + c.flex)
1538
+ fixedHeight = (fixedHeight + mAxis)
1539
+ } {
1540
+ ; auto-height non-flex child: 0 main-axis basis estimate
1541
+ fixedHeight = (fixedHeight + mAxis)
1542
+ }
1543
+ }
1544
+ }
1545
+ jc = (jc + 1)
1546
+ }
1547
+ def gapTotalC:double 0.0
1548
+ if (flowCountC > 1) {
1549
+ gapTotalC = ((to_double (flowCountC - 1)) * gapPx)
1550
+ }
1551
+ if (totalFlexC > 0.0) {
1552
+ def availC:double ((innerHeight - fixedHeight) - gapTotalC)
1553
+ if (availC < 0.0) { availC = 0.0 }
1554
+ def jg:int 0
1555
+ while (jg < childCount) {
1556
+ def c:EVGElement (parent.getChild(jg))
1557
+ if (c.isAbsolute == false) {
1558
+ if ((c.height.isSet == false) && (c.flex > 0.0)) {
1559
+ c.calculatedFlexHeight = ((availC * c.flex) / totalFlexC)
1560
+ }
1561
+ }
1562
+ jg = (jg + 1)
1563
+ }
1564
+ } {
1565
+ ; No grow flex — shrink to fit. Each item gives up space in
1566
+ ; proportion to (flex-shrink x its size), so flex-shrink: 0
1567
+ ; holds its size while the rest absorb the overflow. With every
1568
+ ; factor left at the CSS default of 1 this is the same uniform
1569
+ ; scale as before.
1570
+ ;
1571
+ ; NOT in a scroll container. Shrinking is what a box does when
1572
+ ; the overflow has nowhere to go; a scroll container is the
1573
+ ; statement that it does have somewhere to go. This is the pass
1574
+ ; the virtualiser had to route around — a 230,000-pixel spacer
1575
+ ; in a 414-pixel column was scaled to fit and fourteen rows drew
1576
+ ; on one 46-pixel line.
1577
+ def contentH:double (fixedHeight + gapTotalC)
1578
+ def overflowH:double (contentH - innerHeight)
1579
+ if (parent.clipsContent()) {
1580
+ overflowH = 0.0
1581
+ }
1582
+ if ((overflowH > 0.0) && (fixedHeight > 0.0)) {
1583
+ def weightedH:double 0.0
1584
+ def jq:int 0
1585
+ while (jq < childCount) {
1586
+ def c:EVGElement (parent.getChild(jq))
1587
+ if ((c.isAbsolute == false) && c.height.isSet) {
1588
+ weightedH = (weightedH + (c.flexShrink * c.height.pixels))
1589
+ }
1590
+ jq = (jq + 1)
1591
+ }
1592
+ if (weightedH > 0.0) {
1593
+ def js:int 0
1594
+ while (js < childCount) {
1595
+ def c:EVGElement (parent.getChild(js))
1596
+ if ((c.isAbsolute == false) && c.height.isSet) {
1597
+ def cut:double ((overflowH * (c.flexShrink * c.height.pixels)) / weightedH)
1598
+ def finalH:double (c.height.pixels - cut)
1599
+ if (finalH < 0.0) {
1600
+ finalH = 0.0
1601
+ }
1602
+ ; Said with a flag, as `hasFlexWidth` says it
1603
+ ; along a row: clearing `height.isSet` here
1604
+ ; destroyed the declaration, so the next pass
1605
+ ; saw an auto-height child and never shrank it
1606
+ ; again — which only looked right while the
1607
+ ; stale flex height was never cleared.
1608
+ c.calculatedFlexHeight = finalH
1609
+ c.hasFlexHeight = true
1610
+ }
1611
+ js = (js + 1)
1612
+ }
1613
+ }
1614
+ }
1615
+ }
1616
+ }
1617
+
1618
+ ; Row-axis flex-shrink (nowrap only — with wrap the row wraps instead):
1619
+ ; when there is no grow flex and fixed children overflow the width, scale
1620
+ ; them to fit (they used to spill out). Applied by re-routing each fixed
1621
+ ; child through the flex-width path (calculatedFlexWidth + clear width),
1622
+ ; so layoutElement adopts the shrunk width instead of the explicit one.
1623
+ if (isColumn == false) {
1624
+ if (parent.flexWrap == "nowrap") {
1625
+ def fixedW2:double 0.0
1626
+ def totalFlex2:double 0.0
1627
+ def flowCount2:int 0
1628
+ def jr:int 0
1629
+ while (jr < childCount) {
1630
+ def c:EVGElement (parent.getChild(jr))
1631
+ if (c.isAbsolute == false) {
1632
+ flowCount2 = (flowCount2 + 1)
1633
+ if (this.hasBaseMain(c)) {
1634
+ fixedW2 = (fixedW2 + (this.baseMain(c)) + c.box.marginLeftPx + c.box.marginRightPx)
1635
+ } {
1636
+ if (c.flex > 0.0) { totalFlex2 = (totalFlex2 + c.flex) }
1637
+ }
1638
+ }
1639
+ jr = (jr + 1)
1640
+ }
1641
+ def gapTotal2:double 0.0
1642
+ if (flowCount2 > 1) { gapTotal2 = ((to_double (flowCount2 - 1)) * gapPx) }
1643
+ def contentW2:double (fixedW2 + gapTotal2)
1644
+ def overflowW:double (contentW2 - innerWidth)
1645
+ ; Same reason as the column above: a scroll container's content
1646
+ ; is not squeezed to fit, it is scrolled to.
1647
+ if (parent.clipsContent()) {
1648
+ overflowW = 0.0
1649
+ }
1650
+ if ((totalFlex2 == 0.0) && (overflowW > 0.0) && (fixedW2 > 0.0)) {
1651
+ ; SHRINKING IS THE SAME LOOP AS GROWING, AND IT WAS NOT.
1652
+ ;
1653
+ ; This used to be one weighted pass with no minimum in it
1654
+ ; at all: each item gave up (flex-shrink x its size) of the
1655
+ ; overflow and that was the answer. An item that could not
1656
+ ; shrink that far — because it has a `min-width`, or
1657
+ ; because its own content will not go narrower — was
1658
+ ; clamped afterwards by `layoutElement`, and the space it
1659
+ ; refused to give up was never asked of anyone else. Two
1660
+ ; 300px items in a 300px row, the first with
1661
+ ; `min-width: 200px`, came out 200 | 150: three hundred and
1662
+ ; fifty pixels in a three-hundred pixel row.
1663
+ ;
1664
+ ; So it is CSS's freeze-and-repeat, the same shape as the
1665
+ ; grow loop above: distribute, clamp, freeze whoever
1666
+ ; clamped, hand what they did not take to the rest, repeat.
1667
+ ; Bounded by the child count because every pass freezes at
1668
+ ; least one item.
1669
+ ;
1670
+ ; THE MINIMUMS ARE MEASURED ONCE, not once per pass. The
1671
+ ; automatic minimum is an intrinsic measurement of the
1672
+ ; item's whole subtree, and asking for it inside the loop
1673
+ ; would make an overflowing row quadratic in its own depth.
1674
+ def minsW:[double]
1675
+ def basesW:[double]
1676
+ def shrinkable:[boolean]
1677
+ def jm:int 0
1678
+ while (jm < childCount) {
1679
+ def c:EVGElement (parent.getChild(jm))
1680
+ def canShrink:boolean ((c.isAbsolute == false) && (this.hasBaseMain(c)))
1681
+ push shrinkable canShrink
1682
+ if canShrink {
1683
+ push basesW (this.baseMain(c))
1684
+ ; ONLY AN EXPLICIT `min-width` FLOORS THIS PASS.
1685
+ ;
1686
+ ; The automatic minimum is tempting here and it is
1687
+ ; wrong: `minIntrinsicWidthOf` answers with the
1688
+ ; item's DEFINITE WIDTH when it has one — a
1689
+ ; definite width is both its min- and its
1690
+ ; max-content size — and every item in this pass
1691
+ ; has one by construction, so using it would floor
1692
+ ; each item at the size it is being asked to give
1693
+ ; up and nothing would shrink at all. CSS agrees:
1694
+ ; the content-based minimum is the SMALLER of the
1695
+ ; content suggestion and the specified size, so an
1696
+ ; empty 300px box may shrink to nothing.
1697
+ ;
1698
+ ; What is left out is the item whose CONTENT will
1699
+ ; not go narrower than its box — a run of text at
1700
+ ; `width: 300px` should stop at its min-content
1701
+ ; width, and here it does not. That needs an
1702
+ ; intrinsic measurement that ignores the element's
1703
+ ; own width, which this engine cannot ask for yet.
1704
+ ; It is also not a regression: this pass had no
1705
+ ; minimum of any kind before.
1706
+ if c.minWidth.isSet {
1707
+ push minsW c.minWidth.pixels
1708
+ } {
1709
+ push minsW 0.0
1710
+ }
1711
+ } {
1712
+ push basesW 0.0
1713
+ push minsW 0.0
1714
+ }
1715
+ jm = (jm + 1)
1716
+ }
1717
+
1718
+ def frozenS:[boolean]
1719
+ def jf2:int 0
1720
+ while (jf2 < childCount) {
1721
+ push frozenS false
1722
+ jf2 = (jf2 + 1)
1723
+ }
1724
+
1725
+ def toCut:double overflowW
1726
+ def passS:int 0
1727
+ def settledS:boolean false
1728
+ while ((passS < childCount) && (settledS == false)) {
1729
+ settledS = true
1730
+ def weightedW:double 0.0
1731
+ def jz:int 0
1732
+ while (jz < childCount) {
1733
+ def c:EVGElement (parent.getChild(jz))
1734
+ if ((itemAt shrinkable jz) && ((itemAt frozenS jz) == false)) {
1735
+ weightedW = (weightedW + (c.flexShrink * (itemAt basesW jz)))
1736
+ }
1737
+ jz = (jz + 1)
1738
+ }
1739
+ if (weightedW > 0.0) {
1740
+ def jw:int 0
1741
+ while (jw < childCount) {
1742
+ def c:EVGElement (parent.getChild(jw))
1743
+ if ((itemAt shrinkable jw) && ((itemAt frozenS jw) == false)) {
1744
+ def baseW:double (itemAt basesW jw)
1745
+ def cutW:double ((toCut * (c.flexShrink * baseW)) / weightedW)
1746
+ def wantW:double (baseW - cutW)
1747
+ if (wantW < 0.0) {
1748
+ wantW = 0.0
1749
+ }
1750
+ def finalW:double wantW
1751
+ if c.maxWidth.isSet {
1752
+ if (finalW > c.maxWidth.pixels) {
1753
+ finalW = c.maxWidth.pixels
1754
+ }
1755
+ }
1756
+ def floorW:double (itemAt minsW jw)
1757
+ if (finalW < floorW) {
1758
+ finalW = floorW
1759
+ }
1760
+ if (finalW != wantW) {
1761
+ set frozenS jw true
1762
+ toCut = (toCut - (baseW - finalW))
1763
+ if (toCut < 0.0) {
1764
+ toCut = 0.0
1765
+ }
1766
+ settledS = false
1767
+ }
1768
+ c.calculatedFlexWidth = finalW
1769
+ c.hasFlexWidth = true
1770
+ }
1771
+ jw = (jw + 1)
1772
+ }
1773
+ }
1774
+ passS = (passS + 1)
1775
+ }
1776
+ }
1777
+ }
1778
+ }
1779
+
1780
+ def i:int 0
1781
+ while (i < childCount) {
1782
+ def child:EVGElement (parent.getChild(i))
1783
+
1784
+ ; Inherit properties from parent
1785
+ child.inheritProperties(parent)
1786
+
1787
+ ; Resolve child units
1788
+ child.resolveUnits(innerWidth innerHeight)
1789
+
1790
+ ; Out of flow: fixed, a Layer, or absolutely positioned. One pass,
1791
+ ; shared with the grid — see `layoutOutOfFlowChild`.
1792
+ if child.isAbsolute {
1793
+ this.layoutOutOfFlowChild(parent child innerWidth innerHeight startX startY (isColumn == false))
1794
+ i = i + 1
1795
+ continue
1796
+ }
1797
+
1798
+ ; Calculate child dimensions
1799
+ def availableForChild:double (innerWidth - child.box.marginLeftPx - child.box.marginRightPx)
1800
+ def childWidth:double (this.estimateChildWidth(child availableForChild ((isColumn == false) || (this.crossShrinksUnder(parent)))))
1801
+
1802
+ ; The flex pass gets the FIRST say, because when it has run it has
1803
+ ; already taken the declared width into account and squeezed it.
1804
+ ; It used to say so by clearing `width.isSet`, which destroyed the
1805
+ ; declaration for every frame after; now it says so with a flag and
1806
+ ; the declaration survives.
1807
+ if child.hasFlexWidth {
1808
+ childWidth = child.calculatedFlexWidth
1809
+ } {
1810
+ if child.width.isSet {
1811
+ ; If width is explicitly set and it's 100% (resolved to innerWidth),
1812
+ ; we should still subtract margins for proper fit
1813
+ if (child.width.pixels >= innerWidth) {
1814
+ ; Width was 100% - use available space after margins
1815
+ childWidth = availableForChild
1816
+ } {
1817
+ ; Width was set to a specific value - use it
1818
+ childWidth = child.width.pixels
1819
+ }
1820
+ }
1821
+ }
1822
+ ; row alignItems:stretch — a child with no explicit height fills the
1823
+ ; parent's cross (height) extent.
1824
+ if (isColumn == false) {
1825
+ if ((this.crossAlignOf(child parent.alignItems)) == "stretch") {
1826
+ if (child.height.isSet == false) {
1827
+ child.calculatedFlexHeight = innerHeight
1828
+ }
1829
+ }
1830
+ }
1831
+
1832
+ ; Total width including margins (estimated before layout)
1833
+ def childTotalWidth:double (childWidth + child.box.marginLeftPx + child.box.marginRightPx)
1834
+
1835
+ ; main-axis `gap`: advance the cursor before every in-flow child
1836
+ ; except the first on its row (row) / the first overall (column).
1837
+ if (gapPx > 0.0) {
1838
+ if (isColumn == false) {
1839
+ if ((array_length rowElements) > 0) { currentX = currentX + gapPx }
1840
+ } {
1841
+ if (placedInFlow > 0) {
1842
+ currentY = currentY + gapPx
1843
+ totalHeight = totalHeight + gapPx
1844
+ }
1845
+ }
1846
+ }
1847
+
1848
+ ; Check if we need to wrap to next row (for row direction). Gated on
1849
+ ; flexWrap: "nowrap" keeps everything on one line (children overflow /
1850
+ ; shrink); the historical/default "wrap" auto-wraps.
1851
+ ;
1852
+ ; --- WHY THE COMPARISON HAS A TOLERANCE ---------------------------
1853
+ ;
1854
+ ; A `flex: 1` child's width was computed BY THIS LAYOUT, out of the
1855
+ ; same `innerWidth` it is now being measured against: the flex pass
1856
+ ; took the line, subtracted the fixed children and the gaps, and
1857
+ ; handed back the remainder. Adding those parts back up is not
1858
+ ; guaranteed to return the number they were subtracted from — in
1859
+ ; binary floating point `(a - b - c) + b + c` can land one ulp
1860
+ ; above `a` — and a bare `>` then says the row is too wide for
1861
+ ; itself and wraps.
1862
+ ;
1863
+ ; That is not theoretical and it is not rare. A sidebar of 240px
1864
+ ; beside a `flex: 1` panel, with `4vw` page padding, wrapped at 127
1865
+ ; of the 680 window widths between 821 and 1500 — scattered, so
1866
+ ; dragging a window edge flickered the panel below the sidebar and
1867
+ ; back up again a pixel later. See lib/evg/web/responsive.
1868
+ ;
1869
+ ; A hundredth of a pixel is far below anything a painter can draw
1870
+ ; and far above the error being cancelled (~1e-13 at these
1871
+ ; magnitudes), so an overflow smaller than that is arithmetic
1872
+ ; rather than a layout that does not fit. An item that genuinely
1873
+ ; does not fit misses the line by whole pixels and still wraps.
1874
+ if (isColumn == false) {
1875
+ def availableWidth:double (((startX + innerWidth) - currentX) + 0.01)
1876
+ if (((childTotalWidth > availableWidth) && ((array_length rowElements) > 0)) && (parent.flexWrap != "nowrap")) {
1877
+ ; Wrap to next row
1878
+ this.alignRow(rowElements parent rowHeight startX innerWidth)
1879
+ this.recordLine(lineMembers lineCounts lineHeights rowElements rowHeight)
1880
+ currentY = ((currentY + rowHeight) + crossGapPx)
1881
+ totalHeight = ((totalHeight + rowHeight) + crossGapPx)
1882
+ currentX = startX
1883
+ rowHeight = 0.0
1884
+ clear rowElements
1885
+ }
1886
+ }
1887
+
1888
+ ; Position child
1889
+ child.calculatedX = currentX + child.box.marginLeftPx
1890
+ child.calculatedY = currentY + child.box.marginTopPx
1891
+
1892
+ ; Layout child element (recursive)
1893
+ ; Units were already resolved in layoutChildren, pass childWidth so layoutElement knows the actual size
1894
+ ; Tell the child which axis it is being laid along, so a width-less
1895
+ ; flex container knows whether `width: auto` means fit-content (a
1896
+ ; row) or stretch (a column). Restored afterwards because this pass
1897
+ ; recurses.
1898
+ def wasRow:boolean layingOutAlongRow
1899
+ def wasShrink:boolean columnCrossShrinks
1900
+ layingOutAlongRow = (isColumn == false)
1901
+ columnCrossShrinks = (this.crossShrinksUnder(parent))
1902
+ this.layoutElement(child child.calculatedX child.calculatedY childWidth innerHeight)
1903
+ layingOutAlongRow = wasRow
1904
+ columnCrossShrinks = wasShrink
1905
+
1906
+ ; Get actual child height after layout
1907
+ def childHeight:double child.calculatedHeight
1908
+ def childTotalHeight:double (childHeight + child.box.marginTopPx + child.box.marginBottomPx)
1909
+ ; After shrink-wrap, use real width for row cursor advance
1910
+ def placedWidth:double (child.calculatedWidth + child.box.marginLeftPx + child.box.marginRightPx)
1911
+
1912
+ ; Update position tracking
1913
+ if isColumn {
1914
+ ; Column direction: stack vertically
1915
+ currentY = currentY + childTotalHeight
1916
+ totalHeight = totalHeight + childTotalHeight
1917
+ } {
1918
+ ; Row direction: advance horizontally
1919
+ currentX = currentX + placedWidth
1920
+ push rowElements child
1921
+ if (childTotalHeight > rowHeight) {
1922
+ rowHeight = childTotalHeight
1923
+ }
1924
+ }
1925
+ placedInFlow = (placedInFlow + 1)
1926
+
1927
+ ; Handle explicit line break
1928
+ if child.lineBreak {
1929
+ if (isColumn == false) {
1930
+ this.alignRow(rowElements parent rowHeight startX innerWidth)
1931
+ this.recordLine(lineMembers lineCounts lineHeights rowElements rowHeight)
1932
+ currentY = ((currentY + rowHeight) + crossGapPx)
1933
+ totalHeight = ((totalHeight + rowHeight) + crossGapPx)
1934
+ currentX = startX
1935
+ rowHeight = 0.0
1936
+ clear rowElements
1937
+ }
1938
+ }
1939
+
1940
+ i = i + 1
1941
+ }
1942
+
1943
+ ; Handle final row (for row layout)
1944
+ if ((isColumn == false) && ((array_length rowElements) > 0)) {
1945
+ this.alignRow(rowElements parent rowHeight startX innerWidth)
1946
+ this.recordLine(lineMembers lineCounts lineHeights rowElements rowHeight)
1947
+ totalHeight = totalHeight + rowHeight
1948
+ }
1949
+
1950
+ ; Wrapped-line post-passes, in CSS order: wrap-reverse flips the cross
1951
+ ; axis first, then align-content distributes the lines along it.
1952
+ if (isColumn == false) {
1953
+ this.applyWrapReverse(parent lineMembers lineCounts lineHeights totalHeight crossGapPx)
1954
+ this.applyAlignContent(parent lineMembers lineCounts lineHeights totalHeight innerHeight)
1955
+ }
1956
+
1957
+ ; Handle column alignment (for column layout)
1958
+ if isColumn {
1959
+ this.alignColumn(parent totalHeight startX startY innerWidth innerHeight)
1960
+ }
1961
+
1962
+ return totalHeight
1963
+ }
1964
+
1965
+ ; Remember a finished line so align-content can move it later.
1966
+ fn recordLine:void (members:[EVGElement] counts:[int] heights:[double] rowElements:[EVGElement] rowHeight:double) {
1967
+ def n:int (array_length rowElements)
1968
+ if (n == 0) {
1969
+ return
1970
+ }
1971
+ def i:int 0
1972
+ while (i < n) {
1973
+ push members (itemAt rowElements i)
1974
+ i = (i + 1)
1975
+ }
1976
+ push counts n
1977
+ push heights rowHeight
1978
+ }
1979
+
1980
+ ; Distribute spare cross-axis space between wrapped lines. alignItems places
1981
+ ; a child WITHIN its line; this places the lines themselves, and only does
1982
+ ; anything when the content actually wrapped and the container has a
1983
+ ; definite height with room left over.
1984
+ ;
1985
+ ; `stretch` (the CSS default) would have to grow each line and re-lay its
1986
+ ; children out, so it is treated as flex-start here and noted in the plan.
1987
+ ; flex-wrap: wrap-reverse — the lines stack from the far edge of the cross
1988
+ ; axis instead of the near one, so the first line ends up at the bottom.
1989
+ ; The wrap POINTS are unchanged; only where each finished line sits moves.
1990
+ fn applyWrapReverse:void (parent:EVGElement members:[EVGElement] counts:[int] heights:[double] contentHeight:double gapPx:double) {
1991
+ if (parent.flexWrap != "wrap-reverse") {
1992
+ return
1993
+ }
1994
+ def lineCount:int (array_length counts)
1995
+ if (lineCount < 2) {
1996
+ return
1997
+ }
1998
+ def idx:int 0
1999
+ def offset:double 0.0
2000
+ def li:int 0
2001
+ while (li < lineCount) {
2002
+ def h:double (itemAt heights li)
2003
+ ; Mirror this line's position about the middle of the content box.
2004
+ def newOffset:double ((contentHeight - offset) - h)
2005
+ def shift:double (newOffset - offset)
2006
+ def n:int (itemAt counts li)
2007
+ def k:int 0
2008
+ while (k < n) {
2009
+ def el:EVGElement (itemAt members (idx + k))
2010
+ if (shift != 0.0) {
2011
+ el.calculatedY = (el.calculatedY + shift)
2012
+ this.propagateOffsetToChildren(el 0.0 shift)
2013
+ }
2014
+ k = (k + 1)
2015
+ }
2016
+ idx = (idx + n)
2017
+ ; A line's own position includes the gaps before it, or the mirror
2018
+ ; puts every line back one gap short of where it belongs.
2019
+ offset = ((offset + h) + gapPx)
2020
+ li = (li + 1)
2021
+ }
2022
+ }
2023
+
2024
+ ; align-content: stretch — grow every line by an equal share of the spare
2025
+ ; cross-axis space. A line growing means its items grow with it, so each
2026
+ ; item that did not ask for a specific height is resized and its own
2027
+ ; children are laid out again inside the taller box.
2028
+ fn applyAlignContentStretch:void (parent:EVGElement members:[EVGElement] counts:[int] heights:[double] contentHeight:double innerHeight:double) {
2029
+ def lineCount:int (array_length counts)
2030
+ def free:double (innerHeight - contentHeight)
2031
+ if (free <= 0.0) {
2032
+ return
2033
+ }
2034
+ def extra:double (free / (to_double lineCount))
2035
+ def idx:int 0
2036
+ def li:int 0
2037
+ while (li < lineCount) {
2038
+ def shift:double (extra * (to_double li))
2039
+ def grown:double ((itemAt heights li) + extra)
2040
+ def n:int (itemAt counts li)
2041
+ def k:int 0
2042
+ while (k < n) {
2043
+ def el:EVGElement (itemAt members (idx + k))
2044
+ if (shift != 0.0) {
2045
+ el.calculatedY = (el.calculatedY + shift)
2046
+ this.propagateOffsetToChildren(el 0.0 shift)
2047
+ }
2048
+ ; Only items that were sized by the line grow with it; an
2049
+ ; explicit height is still an explicit height.
2050
+ if ((el.height.isSet == false) && (el.isAbsolute == false)) {
2051
+ def target:double ((grown - el.box.marginTopPx) - el.box.marginBottomPx)
2052
+ if (target > el.calculatedHeight) {
2053
+ el.calculatedHeight = target
2054
+ el.calculatedInnerHeight = (el.box.getInnerHeight(target))
2055
+ el.hasDefiniteHeight = true
2056
+ if ((el.getChildCount()) > 0) {
2057
+ this.layoutChildren(el)
2058
+ }
2059
+ }
2060
+ }
2061
+ k = (k + 1)
2062
+ }
2063
+ idx = (idx + n)
2064
+ li = (li + 1)
2065
+ }
2066
+ }
2067
+
2068
+ fn applyAlignContent:void (parent:EVGElement members:[EVGElement] counts:[int] heights:[double] contentHeight:double innerHeight:double) {
2069
+ def lineCount:int (array_length counts)
2070
+ if (lineCount < 2) {
2071
+ return
2072
+ }
2073
+ if ((parent.hasDefiniteHeight) == false) {
2074
+ return
2075
+ }
2076
+ def mode:string parent.alignContent
2077
+ if (mode == "flex-start") {
2078
+ return
2079
+ }
2080
+ if (mode == "start") {
2081
+ return
2082
+ }
2083
+ if (mode == "stretch") {
2084
+ this.applyAlignContentStretch(parent members counts heights contentHeight innerHeight)
2085
+ return
2086
+ }
2087
+ def free:double (innerHeight - contentHeight)
2088
+ if (free <= 0.0) {
2089
+ return
2090
+ }
2091
+
2092
+ def first:double 0.0
2093
+ def between:double 0.0
2094
+ if ((mode == "flex-end") || (mode == "end")) {
2095
+ first = free
2096
+ }
2097
+ if (mode == "center") {
2098
+ first = (free / 2.0)
2099
+ }
2100
+ if (mode == "space-between") {
2101
+ between = (free / (to_double (lineCount - 1)))
2102
+ }
2103
+ if (mode == "space-around") {
2104
+ between = (free / (to_double lineCount))
2105
+ first = (between / 2.0)
2106
+ }
2107
+ if (mode == "space-evenly") {
2108
+ between = (free / (to_double (lineCount + 1)))
2109
+ first = between
2110
+ }
2111
+
2112
+ def idx:int 0
2113
+ def li:int 0
2114
+ while (li < lineCount) {
2115
+ def shift:double (first + (between * (to_double li)))
2116
+ def n:int (itemAt counts li)
2117
+ def k:int 0
2118
+ while (k < n) {
2119
+ def el:EVGElement (itemAt members (idx + k))
2120
+ if (shift != 0.0) {
2121
+ el.calculatedY = (el.calculatedY + shift)
2122
+ this.propagateOffsetToChildren(el 0.0 shift)
2123
+ }
2124
+ k = (k + 1)
2125
+ }
2126
+ idx = (idx + n)
2127
+ li = (li + 1)
2128
+ }
2129
+ }
2130
+
2131
+ fn alignColumn:void (parent:EVGElement contentHeight:double startX:double startY:double innerWidth:double innerHeight:double) {
2132
+ ; Apply justifyContent (vertical) and alignItems (horizontal) alignment for column layout
2133
+ def childCount:int (parent.getChildCount())
2134
+ if (childCount == 0) {
2135
+ return
2136
+ }
2137
+
2138
+ ; Get effective alignment values
2139
+ ; For column direction: justifyContent = vertical (main axis), alignItems = horizontal (cross axis)
2140
+ def verticalAlign:string parent.justifyContent
2141
+ def horizontalAlign:string parent.alignItems
2142
+ ; Legacy align= center|left|right (EVG spec horizontal alignment).
2143
+ ; Default "left" must not clobber a set alignItems (left == flex-start).
2144
+ if ((strlen parent.align) > 0) {
2145
+ if (parent.align != "left") {
2146
+ horizontalAlign = parent.align
2147
+ }
2148
+ }
2149
+
2150
+ ; Calculate vertical offset based on justifyContent
2151
+ def availableHeight:double innerHeight
2152
+ if parent.height.isSet {
2153
+ availableHeight = parent.calculatedInnerHeight
2154
+ }
2155
+
2156
+ def offsetY:double 0.0
2157
+ if (verticalAlign == "center") {
2158
+ offsetY = ((availableHeight - contentHeight) / 2.0)
2159
+ }
2160
+ if ((verticalAlign == "flex-end") || (verticalAlign == "end")) {
2161
+ offsetY = (availableHeight - contentHeight)
2162
+ }
2163
+
2164
+ ; Count non-absolute children for space distribution.
2165
+ def flowCount:int 0
2166
+ def fc:int 0
2167
+ while (fc < childCount) {
2168
+ def fchild:EVGElement (parent.getChild(fc))
2169
+ if (fchild.isAbsolute == false) {
2170
+ flowCount = (flowCount + 1)
2171
+ }
2172
+ fc = (fc + 1)
2173
+ }
2174
+
2175
+ ; Space distribution (justifyContent) along the vertical main axis.
2176
+ def freeSpaceY:double (availableHeight - contentHeight)
2177
+ if (freeSpaceY < 0.0) {
2178
+ freeSpaceY = 0.0
2179
+ }
2180
+ def distributeGapY:double 0.0
2181
+ def distributeFirstY:double 0.0
2182
+ if (verticalAlign == "space-between") {
2183
+ if (flowCount > 1) {
2184
+ distributeGapY = (freeSpaceY / (to_double (flowCount - 1)))
2185
+ }
2186
+ }
2187
+ if (verticalAlign == "space-around") {
2188
+ if (flowCount > 0) {
2189
+ distributeGapY = (freeSpaceY / (to_double flowCount))
2190
+ distributeFirstY = (distributeGapY / 2.0)
2191
+ }
2192
+ }
2193
+ if (verticalAlign == "space-evenly") {
2194
+ distributeGapY = (freeSpaceY / (to_double (flowCount + 1)))
2195
+ distributeFirstY = distributeGapY
2196
+ }
2197
+ def usesDistributeY:boolean ((distributeGapY > 0.0) || (distributeFirstY > 0.0))
2198
+
2199
+ ; Apply offsets to all non-absolute children
2200
+ def i:int 0
2201
+ def flowIndex:int 0
2202
+ while (i < childCount) {
2203
+ def child:EVGElement (parent.getChild(i))
2204
+
2205
+ ; Skip absolute positioned elements (they're handled separately)
2206
+ if (child.isAbsolute == false) {
2207
+ ; Apply vertical offset
2208
+ def elemOffsetY:double offsetY
2209
+ if usesDistributeY {
2210
+ elemOffsetY = (distributeFirstY + (distributeGapY * (to_double flowIndex)))
2211
+ }
2212
+ if (elemOffsetY != 0.0) {
2213
+ child.calculatedY = child.calculatedY + elemOffsetY
2214
+ ; Propagate to descendants
2215
+ this.propagateOffsetToChildren(child 0.0 elemOffsetY)
2216
+ }
2217
+ flowIndex = (flowIndex + 1)
2218
+
2219
+ ; Apply horizontal alignment (alignItems, or the child's own
2220
+ ; align-self where it has one — in a column the cross axis is
2221
+ ; the horizontal one).
2222
+ def childAlign:string (this.crossAlignOf(child horizontalAlign))
2223
+ def childTotalWidth:double (child.calculatedWidth + child.box.marginLeftPx + child.box.marginRightPx)
2224
+ def offsetX:double 0.0
2225
+
2226
+ if (childAlign == "center") {
2227
+ offsetX = ((innerWidth - childTotalWidth) / 2.0)
2228
+ }
2229
+ if ((childAlign == "flex-end") || (childAlign == "end")) {
2230
+ offsetX = (innerWidth - childTotalWidth)
2231
+ }
2232
+
2233
+ if (offsetX != 0.0) {
2234
+ child.calculatedX = child.calculatedX + offsetX
2235
+ ; Propagate to descendants
2236
+ this.propagateOffsetToChildren(child offsetX 0.0)
2237
+ }
2238
+ }
2239
+
2240
+ i = i + 1
2241
+ }
2242
+ }
2243
+
2244
+ ; Adopt the first in-flow child's baseline, expressed relative to this
2245
+ ; element's own top edge.
2246
+ fn inheritBaselineFromFirstChild:void (element:EVGElement) {
2247
+ def i:int 0
2248
+ def n:int (element.getChildCount())
2249
+ while (i < n) {
2250
+ def c:EVGElement (element.getChild(i))
2251
+ if (c.isAbsolute == false) {
2252
+ if c.hasBaseline {
2253
+ element.calculatedBaseline = ((c.calculatedY - element.calculatedY) + c.calculatedBaseline)
2254
+ element.calculatedDescent = c.calculatedDescent
2255
+ element.hasBaseline = true
2256
+ return
2257
+ }
2258
+ }
2259
+ i = (i + 1)
2260
+ }
2261
+ }
2262
+
2263
+ ; Distance from an element's top MARGIN edge to its baseline. CSS gives a
2264
+ ; box with no text baseline the bottom of its margin box, which is what
2265
+ ; makes an image in a row of captions sit on the text baseline.
2266
+ fn baselineOffsetOf:double (el:EVGElement) {
2267
+ if el.hasBaseline {
2268
+ return (el.box.marginTopPx + el.calculatedBaseline)
2269
+ }
2270
+ return (el.box.marginTopPx + el.calculatedHeight + el.box.marginBottomPx)
2271
+ }
2272
+
2273
+ fn alignRow:void (rowElements:[EVGElement] parent:EVGElement rowHeight:double startX:double innerWidth:double) {
2274
+ ; Apply horizontal and vertical alignment to a row of elements
2275
+ def elementCount:int (array_length rowElements)
2276
+ if (elementCount == 0) {
2277
+ return
2278
+ }
2279
+
2280
+ ; Calculate row width
2281
+ def rowWidth:double 0.0
2282
+ def i:int 0
2283
+ while (i < elementCount) {
2284
+ def el:EVGElement (itemAt rowElements i)
2285
+ rowWidth = rowWidth + el.calculatedWidth + el.box.marginLeftPx + el.box.marginRightPx
2286
+ i = i + 1
2287
+ }
2288
+ ; THE GAPS ARE PART OF THE ROW. This summed the boxes and forgot the
2289
+ ; space the flow had already put between them, so `justify-content:
2290
+ ; flex-end` pushed the row right by exactly the total gap and the last
2291
+ ; item hung over the end of its container — a footer's two buttons with
2292
+ ; an 8px gap ended 8px past the card. `space-between` and friends read
2293
+ ; the same number for their free space, so they were out by the same
2294
+ ; amount in the other direction.
2295
+ def rowGapPx:double 0.0
2296
+ if parent.gap.isSet {
2297
+ rowGapPx = parent.gap.pixels
2298
+ }
2299
+ if parent.columnGap.isSet {
2300
+ rowGapPx = parent.columnGap.pixels
2301
+ }
2302
+ if (elementCount > 1) {
2303
+ rowWidth = (rowWidth + ((to_double (elementCount - 1)) * rowGapPx))
2304
+ }
2305
+
2306
+ ; Determine alignment based on flexDirection
2307
+ ; For row direction: justifyContent = horizontal, alignItems = vertical
2308
+ ; For column direction: justifyContent = vertical, alignItems = horizontal
2309
+ def isColumn:boolean (parent.flexDirection == "column")
2310
+
2311
+ ; Get effective alignment values
2312
+ def mainAxisAlign:string parent.justifyContent
2313
+ def crossAxisAlign:string parent.alignItems
2314
+
2315
+ ; Horizontal alignment (justifyContent for row, alignItems for column)
2316
+ def horizontalAlign:string mainAxisAlign
2317
+ if isColumn {
2318
+ horizontalAlign = crossAxisAlign
2319
+ }
2320
+ ; Legacy 'align' override — but default "left" must not clobber a set
2321
+ ; justifyContent/alignItems (left == flex-start is a no-op anyway).
2322
+ if ((strlen parent.align) > 0) {
2323
+ if (parent.align != "left") {
2324
+ horizontalAlign = parent.align
2325
+ }
2326
+ }
2327
+
2328
+ def offsetX:double 0.0
2329
+ if (horizontalAlign == "center") {
2330
+ offsetX = ((innerWidth - rowWidth) / 2.0)
2331
+ }
2332
+ if ((horizontalAlign == "flex-end") || (horizontalAlign == "right")) {
2333
+ offsetX = (innerWidth - rowWidth)
2334
+ }
2335
+
2336
+ ; Space distribution (justifyContent). Applied as an incremental per-item
2337
+ ; gap on top of the sequential flow position.
2338
+ def freeSpace:double (innerWidth - rowWidth)
2339
+ if (freeSpace < 0.0) {
2340
+ freeSpace = 0.0
2341
+ }
2342
+ def distributeGap:double 0.0
2343
+ def distributeFirst:double 0.0
2344
+ if (horizontalAlign == "space-between") {
2345
+ if (elementCount > 1) {
2346
+ distributeGap = (freeSpace / (to_double (elementCount - 1)))
2347
+ }
2348
+ }
2349
+ if (horizontalAlign == "space-around") {
2350
+ distributeGap = (freeSpace / (to_double elementCount))
2351
+ distributeFirst = (distributeGap / 2.0)
2352
+ }
2353
+ if (horizontalAlign == "space-evenly") {
2354
+ distributeGap = (freeSpace / (to_double (elementCount + 1)))
2355
+ distributeFirst = distributeGap
2356
+ }
2357
+ def usesDistribute:boolean ((distributeGap > 0.0) || (distributeFirst > 0.0))
2358
+
2359
+ ; Vertical alignment (alignItems for row, justifyContent for column)
2360
+ def verticalAlignVal:string crossAxisAlign
2361
+ if isColumn {
2362
+ verticalAlignVal = mainAxisAlign
2363
+ }
2364
+ ; Also check legacy 'verticalAlign' property
2365
+ if ((strlen parent.verticalAlign) > 0) {
2366
+ if (parent.verticalAlign != "top") {
2367
+ verticalAlignVal = parent.verticalAlign
2368
+ }
2369
+ }
2370
+
2371
+ ; The box a child is centred in. `alignItems` places a child inside its
2372
+ ; LINE; when the parent cannot wrap there is only one line and the line
2373
+ ; IS the content box, so the child may use all of it.
2374
+ ;
2375
+ ; The old test was `parent.height.isSet`, which asks where the height
2376
+ ; came from rather than whether there is one. A divider whose height
2377
+ ; came from the group's `align-items: stretch` has a perfectly definite
2378
+ ; 298 pixels and no declaration, so it failed the test and its grip
2379
+ ; stayed pinned to the top of a divider it was supposed to sit in the
2380
+ ; middle of. `hasDefiniteHeight` is the question that was meant:
2381
+ ; declared or resolved, the height is known before the children are
2382
+ ; placed either way.
2383
+ ;
2384
+ ; A parent that CAN wrap keeps the old test. There the lines are placed
2385
+ ; by `applyAlignContent` and a line is not the box — letting each line
2386
+ ; claim the whole box would stack them all on top of one another.
2387
+ def effectiveRowHeight:double rowHeight
2388
+ def crossBox:double 0.0
2389
+ if (parent.flexWrap == "nowrap") {
2390
+ if parent.hasDefiniteHeight {
2391
+ crossBox = parent.calculatedInnerHeight
2392
+ }
2393
+ } {
2394
+ if parent.height.isSet {
2395
+ crossBox = parent.calculatedInnerHeight
2396
+ }
2397
+ }
2398
+ if (crossBox > rowHeight) {
2399
+ effectiveRowHeight = crossBox
2400
+ }
2401
+
2402
+ ; ALIGN-ITEMS: STRETCH IN A ROW WHOSE HEIGHT IS NOT KNOWN YET. The flow
2403
+ ; hands a stretch item the row's inner height before the row has one,
2404
+ ; so in an auto-height row that is nothing, and the item came out its
2405
+ ; content height — a 1px vertical rule came out 0px tall. It used to
2406
+ ; look right from the second layout on, because the row's inner height
2407
+ ; from the LAST pass was still there to be read; now that a pass starts
2408
+ ; from nothing (`EVGElement.resetLayoutNow`), the line is measured here
2409
+ ; and each stretch item that is shorter than it is laid out again at
2410
+ ; the line's height, which is what CSS does.
2411
+ if (isColumn == false) {
2412
+ def si:int 0
2413
+ while (si < elementCount) {
2414
+ def sel:EVGElement (itemAt rowElements si)
2415
+ if (((this.crossAlignOf(sel verticalAlignVal)) == "stretch") && (sel.height.isSet == false)) {
2416
+ def target:double ((effectiveRowHeight - sel.box.marginTopPx) - sel.box.marginBottomPx)
2417
+ if ((target > (sel.calculatedHeight + 0.01)) && ((sel.isHidden()) == false)) {
2418
+ def sx:double sel.calculatedX
2419
+ def sy:double sel.calculatedY
2420
+ sel.calculatedFlexHeight = target
2421
+ ; A kept item resets itself when its handed height
2422
+ ; differs from last time; anything else has only its
2423
+ ; children to clear — its own units were resolved
2424
+ ; against the row, and must not be again.
2425
+ if (sel.keepLayout == false) {
2426
+ def sk:int 0
2427
+ while (sk < (sel.getChildCount())) {
2428
+ def skid:EVGElement (sel.getChild(sk))
2429
+ skid.resetLayoutState()
2430
+ sk = (sk + 1)
2431
+ }
2432
+ }
2433
+ def wasRowS:boolean layingOutAlongRow
2434
+ def wasShrinkS:boolean columnCrossShrinks
2435
+ layingOutAlongRow = true
2436
+ columnCrossShrinks = (this.crossShrinksUnder(parent))
2437
+ this.layoutElement(sel sx sy sel.lastParentW sel.lastParentH)
2438
+ layingOutAlongRow = wasRowS
2439
+ columnCrossShrinks = wasShrinkS
2440
+ }
2441
+ }
2442
+ si = (si + 1)
2443
+ }
2444
+ }
2445
+
2446
+ ; align-items: baseline — line the row up on the text baselines rather
2447
+ ; than on the box edges, so captions of different sizes read as one line.
2448
+ ; Needs real ascent metrics, which is why this waited for the font work.
2449
+ def useBaseline:boolean (verticalAlignVal == "baseline")
2450
+ def maxBaseline:double 0.0
2451
+ if useBaseline {
2452
+ def b:int 0
2453
+ while (b < elementCount) {
2454
+ def bel:EVGElement (itemAt rowElements b)
2455
+ def bo:double (this.baselineOffsetOf(bel))
2456
+ if (bo > maxBaseline) {
2457
+ maxBaseline = bo
2458
+ }
2459
+ b = (b + 1)
2460
+ }
2461
+ }
2462
+
2463
+ ; Apply alignment offsets
2464
+ i = 0
2465
+ while (i < elementCount) {
2466
+ def el:EVGElement (itemAt rowElements i)
2467
+
2468
+ ; Horizontal offset - also propagate to children!
2469
+ def elemOffsetX:double offsetX
2470
+ if usesDistribute {
2471
+ elemOffsetX = (distributeFirst + (distributeGap * (to_double i)))
2472
+ }
2473
+ if (elemOffsetX != 0.0) {
2474
+ el.calculatedX = el.calculatedX + elemOffsetX
2475
+ ; Propagate offset to all descendants
2476
+ this.propagateOffsetToChildren(el elemOffsetX 0.0)
2477
+ }
2478
+
2479
+ ; Vertical alignment within row. `align-self` on the item wins
2480
+ ; over the container's `align-items`; the row's own baseline pass
2481
+ ; is only for the items that did not opt out of it.
2482
+ def childTotalHeight:double (el.calculatedHeight + el.box.marginTopPx + el.box.marginBottomPx)
2483
+ def offsetY:double 0.0
2484
+ def elAlign:string verticalAlignVal
2485
+ if (isColumn == false) {
2486
+ elAlign = (this.crossAlignOf(el verticalAlignVal))
2487
+ }
2488
+
2489
+ if (elAlign == "center") {
2490
+ offsetY = ((effectiveRowHeight - childTotalHeight) / 2.0)
2491
+ }
2492
+ if ((elAlign == "flex-end") || (elAlign == "bottom")) {
2493
+ offsetY = (effectiveRowHeight - childTotalHeight)
2494
+ }
2495
+ if (elAlign == "baseline") {
2496
+ if useBaseline {
2497
+ offsetY = (maxBaseline - (this.baselineOffsetOf(el)))
2498
+ }
2499
+ }
2500
+
2501
+ if (offsetY != 0.0) {
2502
+ el.calculatedY = el.calculatedY + offsetY
2503
+ ; Propagate offset to all descendants
2504
+ this.propagateOffsetToChildren(el 0.0 offsetY)
2505
+ }
2506
+
2507
+ i = i + 1
2508
+ }
2509
+ }
2510
+
2511
+ ; Called right after `parent`'s own box moved by the same offset, so the
2512
+ ; parent's paint bounds move here with everything under it: a kept
2513
+ ; subtree's bounds are not measured again, and have to stay in step with
2514
+ ; its boxes through every move the flow makes after laying it out.
2515
+ fn propagateOffsetToChildren:void (parent:EVGElement offsetX:double offsetY:double) {
2516
+ parent.paintLeft = (parent.paintLeft + offsetX)
2517
+ parent.paintRight = (parent.paintRight + offsetX)
2518
+ parent.paintTop = (parent.paintTop + offsetY)
2519
+ parent.paintBottom = (parent.paintBottom + offsetY)
2520
+ def childCount:int (parent.getChildCount())
2521
+ def i:int 0
2522
+ while (i < childCount) {
2523
+ def child:EVGElement (parent.getChild(i))
2524
+ if (offsetX != 0.0) {
2525
+ child.calculatedX = child.calculatedX + offsetX
2526
+ }
2527
+ if (offsetY != 0.0) {
2528
+ child.calculatedY = child.calculatedY + offsetY
2529
+ }
2530
+ ; Recurse to grandchildren
2531
+ this.propagateOffsetToChildren(child offsetX offsetY)
2532
+ i = i + 1
2533
+ }
2534
+ }
2535
+
2536
+ ; Move a finished subtree, keeping everything inside it in step.
2537
+ ; Reflect this container's children about its content box.
2538
+ ;
2539
+ ; What is reflected is each child's MARGIN box, and the border box is then
2540
+ ; put back at its own margin-left inside it. That is CSS's rule and it is
2541
+ ; worth being exact about, because the other reading is tempting and
2542
+ ; wrong: `margin-left` stays physically on the left in a right-to-left
2543
+ ; container. What reverses is the ORDER and the end the children pack
2544
+ ; against, not which side of a child its own left margin is on. An author
2545
+ ; who wants the margin to follow the direction says `margin-inline-start`.
2546
+ ; With symmetric margins — nearly every toolbar and panel — the two
2547
+ ; readings agree, which is exactly why picking the wrong one would have
2548
+ ; survived a long time before something looked a pixel out.
2549
+ ;
2550
+ ; Absolutely positioned children are left alone: they were placed against
2551
+ ; the edges they name, and `left: 0` means the left edge in any direction.
2552
+ fn mirrorChildren:void (parent:EVGElement) {
2553
+ if ((parent.resolvedRtl) == false) {
2554
+ return
2555
+ }
2556
+ def left:double ((parent.calculatedX + parent.box.borderWidthPx) + parent.box.paddingLeftPx)
2557
+ def right:double (left + parent.calculatedInnerWidth)
2558
+ def span:double (left + right)
2559
+ def n:int (parent.getChildCount())
2560
+ def i:int 0
2561
+ while (i < n) {
2562
+ def ch:EVGElement (parent.getChild(i))
2563
+ if (ch.isAbsolute == false) {
2564
+ def mL:double ch.box.marginLeftPx
2565
+ def mR:double ch.box.marginRightPx
2566
+ def marginRightEdge:double ((ch.calculatedX + ch.calculatedWidth) + mR)
2567
+ def nx:double ((span - marginRightEdge) + mL)
2568
+ this.translateSubtree(ch (nx - ch.calculatedX) 0.0)
2569
+ }
2570
+ i = (i + 1)
2571
+ }
2572
+ }
2573
+
2574
+ ; The paint bounds move with the boxes they are the union of — see
2575
+ ; `EVGElement.moveSubtree`. Missing that would leave `scrollOnly` culling
2576
+ ; against where the content used to be, which is a row that scrolls into
2577
+ ; view and stays blank.
2578
+ fn translateSubtree:void (element:EVGElement dx:double dy:double) {
2579
+ element.moveSubtree(dx dy)
2580
+ }
2581
+
2582
+ fn layoutAbsolute:void (element:EVGElement parentWidth:double parentHeight:double) {
2583
+ ; Position absolutely positioned element
2584
+
2585
+ ; BOTH EDGES AND NO SIZE IS A STRETCH. CSS 2.1 10.3.7: with `left` and
2586
+ ; `right` both set and `width: auto`, the width is what is left of the
2587
+ ; containing block between them. Without this the box kept its
2588
+ ; content's width and `right` was simply dropped — a bar written
2589
+ ; `left: 16; right: 16` came out as wide as its five icons.
2590
+ if ((element.left.isSet && element.right.isSet) && (element.width.isSet == false)) {
2591
+ def spanW:double ((((parentWidth - element.left.pixels) - element.right.pixels) - element.box.marginLeftPx) - element.box.marginRightPx)
2592
+ if (spanW > 0.0) {
2593
+ element.calculatedWidth = spanW
2594
+ element.calculatedInnerWidth = (element.box.getInnerWidth(spanW))
2595
+ }
2596
+ }
2597
+ if ((element.top.isSet && element.bottom.isSet) && (element.height.isSet == false)) {
2598
+ def spanH:double ((((parentHeight - element.top.pixels) - element.bottom.pixels) - element.box.marginTopPx) - element.box.marginBottomPx)
2599
+ if (spanH > 0.0) {
2600
+ element.calculatedHeight = spanH
2601
+ element.calculatedInnerHeight = (element.box.getInnerHeight(spanH))
2602
+ element.hasDefiniteHeight = true
2603
+ }
2604
+ }
2605
+
2606
+ ; X position
2607
+ if element.left.isSet {
2608
+ element.calculatedX = element.left.pixels + element.box.marginLeftPx
2609
+ } {
2610
+ if element.x.isSet {
2611
+ element.calculatedX = element.x.pixels + element.box.marginLeftPx
2612
+ } {
2613
+ if element.right.isSet {
2614
+ def width:double element.calculatedWidth
2615
+ if (width == 0.0) {
2616
+ if element.width.isSet {
2617
+ width = element.width.pixels
2618
+ }
2619
+ }
2620
+ element.calculatedX = parentWidth - element.right.pixels - width - element.box.marginRightPx
2621
+ }
2622
+ }
2623
+ }
2624
+
2625
+ ; Y position
2626
+ if element.top.isSet {
2627
+ element.calculatedY = element.top.pixels + element.box.marginTopPx
2628
+ } {
2629
+ if element.y.isSet {
2630
+ element.calculatedY = element.y.pixels + element.box.marginTopPx
2631
+ } {
2632
+ if element.bottom.isSet {
2633
+ def height:double element.calculatedHeight
2634
+ if (height == 0.0) {
2635
+ if element.height.isSet {
2636
+ height = element.height.pixels
2637
+ }
2638
+ }
2639
+ element.calculatedY = parentHeight - element.bottom.pixels - height - element.box.marginBottomPx
2640
+ }
2641
+ }
2642
+ }
2643
+ }
2644
+
2645
+ ; How far the in-flow children reach. Out-of-flow children — absolutes and
2646
+ ; overlay surfaces — are excluded on purpose: a menu is as wide as its
2647
+ ; trigger, not as wide as the panel that hangs off it.
2648
+ fn contentExtent:double (element:EVGElement) {
2649
+ def right:double 0.0
2650
+ def i:int 0
2651
+ def n:int (element.getChildCount())
2652
+ while (i < n) {
2653
+ def c:EVGElement (element.getChild(i))
2654
+ if (c.isAbsolute == false) {
2655
+ def r:double ((c.calculatedX + c.calculatedWidth) + c.box.marginRightPx)
2656
+ if (r > right) {
2657
+ right = r
2658
+ }
2659
+ }
2660
+ i = (i + 1)
2661
+ }
2662
+ if (right <= 0.0) {
2663
+ return 0.0
2664
+ }
2665
+ return (((right - element.calculatedX) + element.box.paddingRightPx) + element.box.borderWidthPx)
2666
+ }
2667
+
2668
+ ; --- overlays --------------------------------------------------------------
2669
+ ; Four phases, of which the first is everything above: normal layout, then
2670
+ ; anchor rectangles (already computed, because the anchors are ordinary flow
2671
+ ; elements), then the overlay placement below, then the display list.
2672
+ ;
2673
+ ; The walk is top-down and places a surface BEFORE descending into it, which
2674
+ ; is what makes a submenu work without knowing it is one: by the time the
2675
+ ; Share sub-surface asks where its trigger is, the File surface that
2676
+ ; contains that trigger has already moved, so the rectangle it reads is the
2677
+ ; real one. Anchor-dependency order falls out of the tree order; there is no
2678
+ ; second mechanism for nesting.
2679
+
2680
+ ; Every surface in the tree, placed against the anchor it names. This was
2681
+ ; a plain walk of every element on every layout — three milliseconds a
2682
+ ; frame on a long diary, to visit six thousand boxes and find the four
2683
+ ; that are surfaces.
2684
+ ;
2685
+ ; The saving is the one `measurePaintBounds` already makes: a subtree the
2686
+ ; layout KEPT moved whole, so a surface inside it moved with the anchor it
2687
+ ; is placed against and its placement still holds. What the subtree holds
2688
+ ; was counted the last time it was really laid out, and the answer is
2689
+ ; cached on it — `overlayScanned` is false until a real scan, so a subtree
2690
+ ; skipped in a later pass of the same layout is not taken at its word
2691
+ ; before it has been asked once.
2692
+ ;
2693
+ ; Answers whether this subtree holds a surface, which is what lets a
2694
+ ; parent cache it too.
2695
+ fn placeOverlaysIn:boolean (el:EVGElement) {
2696
+ if (el.layoutSkipped && el.overlayScanned) {
2697
+ return el.hasOverlayBelow
2698
+ }
2699
+ def any@(mutable):boolean false
2700
+ def i:int 0
2701
+ while (i < (array_length el.children)) {
2702
+ def c:EVGElement (itemAt el.children i)
2703
+ if (c.isSurface()) {
2704
+ this.placeOverlay(c el)
2705
+ any = true
2706
+ }
2707
+ if (this.placeOverlaysIn(c)) {
2708
+ any = true
2709
+ }
2710
+ i = (i + 1)
2711
+ }
2712
+ el.hasOverlayBelow = any
2713
+ el.overlayScanned = true
2714
+ return any
2715
+ }
2716
+
2717
+ ; Three ways to say what a surface is positioned against, in the order
2718
+ ; they win:
2719
+ ;
2720
+ ; * the element itself, set from code (`overlayAnchor`);
2721
+ ; * `position-anchor`, an `anchor-name` or an `#id` — CSS's own way, and
2722
+ ; the only one that reaches OUT of the surface's own parent, so a menu
2723
+ ; no longer has to be declared beside its trigger;
2724
+ ; * the sibling convention: whichever sibling declared
2725
+ ; `overlay-anchor-role`, which is what lets a factory say
2726
+ ; `(Menu (Trigger …) (Content …))` with nothing joining the two by hand.
2727
+ fn findOverlayAnchor@(optional):EVGElement (surface:EVGElement parent:EVGElement) {
2728
+ if surface.overlayAnchor {
2729
+ return surface.overlayAnchor
2730
+ }
2731
+ if ((strlen surface.positionAnchor) > 0) {
2732
+ def named@(optional):EVGElement (EVGConnector.lookup(surface.positionAnchor anchorNames anchorNodes))
2733
+ if (null? named) {
2734
+ } {
2735
+ return named
2736
+ }
2737
+ }
2738
+ def i:int 0
2739
+ while (i < (array_length parent.children)) {
2740
+ def c:EVGElement (itemAt parent.children i)
2741
+ if c.isOverlayAnchor {
2742
+ def hit@(optional):EVGElement c
2743
+ return hit
2744
+ }
2745
+ i = (i + 1)
2746
+ }
2747
+ def miss@(optional):EVGElement
2748
+ return miss
2749
+ }
2750
+
2751
+ ; --- collisions ------------------------------------------------------------
2752
+ ; The part of a surface that falls off the page cannot be clicked and cannot
2753
+ ; be read, so a menu that ignores the edge silently loses rows. Two moves,
2754
+ ; in this order, and they are the two every real menu makes:
2755
+ ;
2756
+ ; FLIP the side it opens on, when the asked side does not fit and the
2757
+ ; opposite one does. A menu at the bottom of the page opens
2758
+ ; upwards; a submenu at the right edge opens to the left.
2759
+ ; SHIFT along the other axis, sliding it back onto the page. This one is
2760
+ ; a slide and not a flip because the side was never the problem.
2761
+ ;
2762
+ ; Where NEITHER side fits — a surface taller than the page — the roomier
2763
+ ; side wins and the surface is clamped to the page. It then covers its
2764
+ ; anchor, which is what a browser does too: showing the rows is worth more
2765
+ ; than the gap.
2766
+
2767
+ sfn oppositeSide:string (side:string) {
2768
+ if (side == "top") { return "bottom" }
2769
+ if (side == "bottom") { return "top" }
2770
+ if (side == "left") { return "right" }
2771
+ if (side == "right") { return "left" }
2772
+ return side
2773
+ }
2774
+
2775
+ ; How much room there is between the anchor and the page edge on this side,
2776
+ ; gap included. Negative means the surface does not fit.
2777
+ fn overlayRoom:double (side:string ax:double ay:double aw:double ah:double w:double h:double gap:double) {
2778
+ if (side == "top") { return ((ay - gap) - h) }
2779
+ if (side == "bottom") { return ((pageHeight - ((ay + ah) + gap)) - h) }
2780
+ if (side == "left") { return ((ax - gap) - w) }
2781
+ if (side == "right") { return ((pageWidth - ((ax + aw) + gap)) - w) }
2782
+ return 0.0
2783
+ }
2784
+
2785
+ sfn clampTo:double (v:double lo:double hi:double) {
2786
+ if (hi < lo) {
2787
+ ; The surface is bigger than the page on this axis. Show the start
2788
+ ; of it: the first rows of a menu are the ones it puts its
2789
+ ; most-used item in.
2790
+ return lo
2791
+ }
2792
+ if (v < lo) {
2793
+ return lo
2794
+ }
2795
+ if (v > hi) {
2796
+ return hi
2797
+ }
2798
+ return v
2799
+ }
2800
+
2801
+ ; --- presentation ----------------------------------------------------------
2802
+ ;
2803
+ ; anchored | sheet | fullscreen. The one part of this that is not CSS, and
2804
+ ; the reason it is not: on a phone an anchored menu is the WRONG WIDGET
2805
+ ; rather than a badly placed one. No amount of fallback placement turns a
2806
+ ; dropdown into something usable at 390 wide; becoming a sheet along the
2807
+ ; bottom edge does. `sheet-below` is the one line that covers the common
2808
+ ; case; a `@media` block setting `presentation` is the general route, and
2809
+ ; both end up here.
2810
+ fn presentationOf:string (surface:EVGElement) {
2811
+ def p:string surface.presentation
2812
+ if ((strlen p) == 0) {
2813
+ p = "anchored"
2814
+ }
2815
+ if (surface.sheetBelow > 0.0) {
2816
+ if (pageWidth <= surface.sheetBelow) {
2817
+ p = "sheet"
2818
+ }
2819
+ }
2820
+ return p
2821
+ }
2822
+
2823
+ ; A new box for a surface, and its children laid out in it again. A sheet
2824
+ ; is as wide as the page and a menu is as wide as its content, so the two
2825
+ ; are not the same layout of the same children — text rewraps, a row that
2826
+ ; was two lines becomes one. Returns the content height that came out.
2827
+ fn resizeSurface:double (surface:EVGElement w:double h:double) {
2828
+ surface.calculatedWidth = w
2829
+ surface.calculatedHeight = h
2830
+ surface.calculatedInnerWidth = (surface.box.getInnerWidth(w))
2831
+ surface.calculatedInnerHeight = (surface.box.getInnerHeight(h))
2832
+ def kid:int 0
2833
+ while (kid < (surface.getChildCount())) {
2834
+ def kc:EVGElement (surface.getChild(kid))
2835
+ kc.resetLayoutState()
2836
+ kid = (kid + 1)
2837
+ }
2838
+ if ((surface.getChildCount()) == 0) {
2839
+ return 0.0
2840
+ }
2841
+ def content:double (this.layoutChildren(surface))
2842
+ this.mirrorChildren(surface)
2843
+ return content
2844
+ }
2845
+
2846
+ fn setSurfaceHeight:void (surface:EVGElement h:double) {
2847
+ surface.calculatedHeight = h
2848
+ surface.calculatedInnerHeight = (surface.box.getInnerHeight(h))
2849
+ }
2850
+
2851
+ ; A sheet: the page's full width, along its bottom edge, as tall as its
2852
+ ; content needs and never taller than the page.
2853
+ fn sheetHeight:double (surface:EVGElement) {
2854
+ def chrome:double (surface.calculatedHeight - surface.calculatedInnerHeight)
2855
+ def had:double surface.calculatedHeight
2856
+ def content:double (this.resizeSurface(surface pageWidth had))
2857
+ def h:double had
2858
+ if ((surface.getChildCount()) > 0) {
2859
+ h = (content + chrome)
2860
+ }
2861
+ if surface.height.isSet {
2862
+ h = surface.height.pixels
2863
+ }
2864
+ if (h > pageHeight) {
2865
+ h = pageHeight
2866
+ surface.overlayClamped = true
2867
+ }
2868
+ this.setSurfaceHeight(surface h)
2869
+ return h
2870
+ }
2871
+
2872
+ ; --- position-try ------------------------------------------------------------
2873
+ ;
2874
+ ; The areas a surface may be placed in, in the order they are tried: the
2875
+ ; one it declared, then `position-try-fallbacks`, then — always — the
2876
+ ; opposite side. That last candidate is the FLIP this pass has done since
2877
+ ; it was written, kept as the final entry so a surface that declares no
2878
+ ; fallbacks behaves exactly as it did before there were any.
2879
+ fn tryCandidates:void (surface:EVGElement sides:[string] aligns:[string]) {
2880
+ push sides surface.overlaySide
2881
+ push aligns surface.overlayAlign
2882
+ def spec:string surface.positionTryFallbacks
2883
+ if ((strlen spec) > 0) {
2884
+ def parts:[string] (EVGLayout.splitList(spec))
2885
+ def i:int 0
2886
+ while (i < (array_length parts)) {
2887
+ def area:string (itemAt parts i)
2888
+ def sd:string (EVGElement.areaSide(area))
2889
+ if ((strlen sd) == 0) {
2890
+ this.warn(("position-try-fallbacks: \"" + area + "\" is not a position-area"))
2891
+ } {
2892
+ push sides sd
2893
+ push aligns (EVGElement.areaAlign(area))
2894
+ }
2895
+ i = (i + 1)
2896
+ }
2897
+ }
2898
+ push sides (EVGLayout.oppositeSide(surface.overlaySide))
2899
+ push aligns surface.overlayAlign
2900
+ }
2901
+
2902
+ sfn splitList:[string] (spec:string) {
2903
+ def out:[string]
2904
+ def cur:string ""
2905
+ def i:int 0
2906
+ def n:int (strlen spec)
2907
+ while (i <= n) {
2908
+ def brk:boolean (i == n)
2909
+ if (brk == false) {
2910
+ if ((charAt spec i) == 44) {
2911
+ brk = true
2912
+ }
2913
+ }
2914
+ if brk {
2915
+ def t:string (trim cur)
2916
+ if ((strlen t) > 0) {
2917
+ push out t
2918
+ }
2919
+ cur = ""
2920
+ } {
2921
+ cur = (cur + (substring spec i (i + 1)))
2922
+ }
2923
+ i = (i + 1)
2924
+ }
2925
+ return out
2926
+ }
2927
+
2928
+ ; Where one candidate would put the surface, and what it would cost: how
2929
+ ; many pixels of it would be off the page, and how much free room is left
2930
+ ; on that side. The first number decides whether a candidate is usable at
2931
+ ; all; the second is what `position-try-order: most-space` maximises.
2932
+ fn overlaySpot:OverlaySpot (side:string align:string ax:double ay:double aw:double ah:double w:double h:double gap:double) {
2933
+ def spot (new OverlaySpot())
2934
+ def x:double 0.0
2935
+ def y:double 0.0
2936
+ if (side == "top") {
2937
+ x = (this.overlayCross(ax aw w align))
2938
+ y = ((ay - h) - gap)
2939
+ }
2940
+ if (side == "bottom") {
2941
+ x = (this.overlayCross(ax aw w align))
2942
+ y = ((ay + ah) + gap)
2943
+ }
2944
+ if (side == "right") {
2945
+ x = ((ax + aw) + gap)
2946
+ y = (this.overlayCross(ay ah h align))
2947
+ }
2948
+ if (side == "left") {
2949
+ x = ((ax - w) - gap)
2950
+ y = (this.overlayCross(ay ah h align))
2951
+ }
2952
+ if (side == "center") {
2953
+ x = ((pageWidth - w) / 2.0)
2954
+ y = ((pageHeight - h) / 2.0)
2955
+ }
2956
+ spot.x = x
2957
+ spot.y = y
2958
+ spot.room = (this.overlayRoom(side ax ay aw ah w h gap))
2959
+ def over:double 0.0
2960
+ if (x < 0.0) {
2961
+ over = (over - x)
2962
+ }
2963
+ def right:double ((x + w) - pageWidth)
2964
+ if (right > 0.0) {
2965
+ over = (over + right)
2966
+ }
2967
+ if (y < 0.0) {
2968
+ over = (over - y)
2969
+ }
2970
+ def below:double ((y + h) - pageHeight)
2971
+ if (below > 0.0) {
2972
+ over = (over + below)
2973
+ }
2974
+ spot.over = over
2975
+ return spot
2976
+ }
2977
+
2978
+ ; An edge of the anchor, in page coordinates — what `anchor(right)` means.
2979
+ sfn anchorEdgeX:double (a:EVGElement side:string) {
2980
+ if (side == "right") {
2981
+ return (a.calculatedX + a.calculatedWidth)
2982
+ }
2983
+ if (side == "center") {
2984
+ return (a.calculatedX + (a.calculatedWidth / 2.0))
2985
+ }
2986
+ return a.calculatedX
2987
+ }
2988
+
2989
+ sfn anchorEdgeY:double (a:EVGElement side:string) {
2990
+ if (side == "bottom") {
2991
+ return (a.calculatedY + a.calculatedHeight)
2992
+ }
2993
+ if (side == "center") {
2994
+ return (a.calculatedY + (a.calculatedHeight / 2.0))
2995
+ }
2996
+ return a.calculatedY
2997
+ }
2998
+
2999
+ ; `left: anchor(right)`, `top: calc(anchor(center) - 16px)`: the author has
3000
+ ; said where each EDGE goes, so none of the area machinery applies — there
3001
+ ; is no side to flip and no alignment to shift. Both insets on an axis
3002
+ ; stretch the box between them, the same rule `layoutAbsolute` follows for
3003
+ ; `left` and `right` together.
3004
+ fn placeByAnchorInsets:void (surface:EVGElement parent:EVGElement) {
3005
+ def maybe@(optional):EVGElement (this.findOverlayAnchor(surface parent))
3006
+ if (null? maybe) {
3007
+ push overlayErrors (surface.id + ": anchor() has no anchor — name one with `position-anchor`")
3008
+ return
3009
+ }
3010
+ def a:EVGElement (unwrap maybe)
3011
+ def w:double surface.calculatedWidth
3012
+ def h:double surface.calculatedHeight
3013
+ def x:double surface.calculatedX
3014
+ def y:double surface.calculatedY
3015
+
3016
+ def hasL:boolean ((strlen surface.anchorLeftSide) > 0)
3017
+ def hasR:boolean ((strlen surface.anchorRightSide) > 0)
3018
+ def hasT:boolean ((strlen surface.anchorTopSide) > 0)
3019
+ def hasB:boolean ((strlen surface.anchorBottomSide) > 0)
3020
+ def lx:double ((EVGLayout.anchorEdgeX(a surface.anchorLeftSide)) + surface.anchorLeftOffset)
3021
+ def rx:double ((EVGLayout.anchorEdgeX(a surface.anchorRightSide)) + surface.anchorRightOffset)
3022
+ def ty:double ((EVGLayout.anchorEdgeY(a surface.anchorTopSide)) + surface.anchorTopOffset)
3023
+ def by:double ((EVGLayout.anchorEdgeY(a surface.anchorBottomSide)) + surface.anchorBottomOffset)
3024
+
3025
+ if (hasL && hasR) {
3026
+ x = lx
3027
+ def spanW:double (rx - lx)
3028
+ if (spanW > 0.0) {
3029
+ this.resizeSurface(surface spanW h)
3030
+ w = spanW
3031
+ }
3032
+ } {
3033
+ if hasL {
3034
+ x = lx
3035
+ }
3036
+ if hasR {
3037
+ x = (rx - w)
3038
+ }
3039
+ }
3040
+ if (hasT && hasB) {
3041
+ y = ty
3042
+ def spanH:double (by - ty)
3043
+ if (spanH > 0.0) {
3044
+ this.setSurfaceHeight(surface spanH)
3045
+ h = spanH
3046
+ }
3047
+ } {
3048
+ if hasT {
3049
+ y = ty
3050
+ }
3051
+ if hasB {
3052
+ y = (by - h)
3053
+ }
3054
+ }
3055
+
3056
+ surface.overlayPlacedSide = "anchor"
3057
+ surface.overlayPlacedAlign = ""
3058
+ surface.overlayPlacedPresentation = "anchored"
3059
+ this.finishOverlay(surface x y)
3060
+ }
3061
+
3062
+ fn placeOverlay:void (surface:EVGElement parent:EVGElement) {
3063
+ def w:double surface.calculatedWidth
3064
+ def h:double surface.calculatedHeight
3065
+ def x:double 0.0
3066
+ def y:double 0.0
3067
+ surface.overlayClamped = false
3068
+ surface.overlayPlacedAlign = surface.overlayAlign
3069
+
3070
+ ; EDGES BEFORE SIDES. `anchor()` in an inset is a different statement
3071
+ ; from `position-area`: it places one edge of this box on one edge of
3072
+ ; the anchor, which is what a badge hanging off a corner needs and
3073
+ ; what no area can say.
3074
+ if (surface.hasAnchorInsets()) {
3075
+ this.placeByAnchorInsets(surface parent)
3076
+ return
3077
+ }
3078
+
3079
+ ; PRESENTATION FIRST, because it decides whether the anchor is
3080
+ ; consulted at all. A sheet is not positioned against anything.
3081
+ def pres:string (this.presentationOf(surface))
3082
+ if (pres == "fullscreen") {
3083
+ this.resizeSurface(surface pageWidth pageHeight)
3084
+ surface.overlayPlacedSide = "cover"
3085
+ surface.overlayPlacedPresentation = "fullscreen"
3086
+ this.finishOverlay(surface 0.0 0.0)
3087
+ return
3088
+ }
3089
+ if (pres == "sheet") {
3090
+ def sheetH:double (this.sheetHeight(surface))
3091
+ surface.overlayPlacedSide = "bottom"
3092
+ surface.overlayPlacedAlign = "start"
3093
+ surface.overlayPlacedPresentation = "sheet"
3094
+ this.finishOverlay(surface 0.0 (pageHeight - sheetH))
3095
+ return
3096
+ }
3097
+ surface.overlayPlacedPresentation = "anchored"
3098
+
3099
+ if (surface.overlaySide == "cover") {
3100
+ ; The page itself: a modal's backdrop. It is sized here rather than
3101
+ ; with `width: 100%` because a percentage resolves against the
3102
+ ; PARENT, and the parent of a dialog's backdrop is whatever part of
3103
+ ; the page happened to declare the dialog — a card, a toolbar, a
3104
+ ; row. A backdrop that covers its card and not the page is the
3105
+ ; thing it exists to prevent.
3106
+ x = 0.0
3107
+ y = 0.0
3108
+ this.resizeSurface(surface pageWidth pageHeight)
3109
+ w = pageWidth
3110
+ h = pageHeight
3111
+ surface.overlayPlacedSide = "cover"
3112
+ } {
3113
+ if (surface.overlaySide == "center") {
3114
+ ; Centred on the page: a modal, which has no anchor by definition.
3115
+ if surface.fitViewport {
3116
+ if (h > pageHeight) {
3117
+ h = pageHeight
3118
+ this.setSurfaceHeight(surface h)
3119
+ surface.overlayClamped = true
3120
+ }
3121
+ }
3122
+ x = ((pageWidth - w) / 2.0)
3123
+ y = ((pageHeight - h) / 2.0)
3124
+ surface.overlayPlacedSide = "center"
3125
+ } {
3126
+ if (surface.overlaySide == "free") {
3127
+ ; Wherever it was put. A window someone has dragged has no anchor
3128
+ ; and no side: its owner holds the coordinates and this pass takes
3129
+ ; them as given. It is still an overlay — out of its parent's flow,
3130
+ ; painted above the page, hit-tested in front — and that is the
3131
+ ; only reason it comes through here at all.
3132
+ ;
3133
+ ; Not clamped to the page either. Where a window is allowed to go
3134
+ ; is its controller's rule, not the layout's: `WindowCtl` keeps
3135
+ ; enough of it on screen to grab, and a different owner might want
3136
+ ; a palette that hangs off the edge.
3137
+ x = surface.overlayX
3138
+ y = surface.overlayY
3139
+ surface.overlayPlacedSide = "free"
3140
+ } {
3141
+ def maybe@(optional):EVGElement (this.findOverlayAnchor(surface parent))
3142
+ if (null? maybe) {
3143
+ ; Named, not silently at (0,0). An unanchored surface in the
3144
+ ; corner looks like a stylesheet mistake and is not one.
3145
+ push overlayErrors (surface.id + ": overlay has no anchor — give a sibling `overlay-anchor-role`, name one with `position-anchor`, or use `overlay-side: center`")
3146
+ return
3147
+ }
3148
+ def a:EVGElement (unwrap maybe)
3149
+ def ax:double a.calculatedX
3150
+ def ay:double a.calculatedY
3151
+ def aw:double a.calculatedWidth
3152
+ def ah:double a.calculatedHeight
3153
+ def gap:double surface.overlayGap
3154
+
3155
+ ; Try each candidate area and take the first that is wholly on the
3156
+ ; page — or, with `position-try-order: most-space`, the one with
3157
+ ; the most room left over. When NONE fits, the one that hangs off
3158
+ ; by the least wins: a surface half off the page is worse than one
3159
+ ; that covers its anchor.
3160
+ def sides:[string]
3161
+ def aligns:[string]
3162
+ this.tryCandidates(surface sides aligns)
3163
+ def mostSpace:boolean (surface.positionTryOrder == "most-space")
3164
+ def bestFit:int (0 - 1)
3165
+ def bestFitRoom:double 0.0
3166
+ def bestOver:int 0
3167
+ def bestOverVal:double 0.0
3168
+ def ci:int 0
3169
+ while (ci < (array_length sides)) {
3170
+ def cs:string (itemAt sides ci)
3171
+ def ca:string (itemAt aligns ci)
3172
+ def probe:OverlaySpot (this.overlaySpot(cs ca ax ay aw ah w h gap))
3173
+ if (ci == 0) {
3174
+ bestOverVal = probe.over
3175
+ } {
3176
+ if (probe.over < bestOverVal) {
3177
+ bestOver = ci
3178
+ bestOverVal = probe.over
3179
+ }
3180
+ }
3181
+ if (probe.over <= 0.0) {
3182
+ if (bestFit < 0) {
3183
+ bestFit = ci
3184
+ bestFitRoom = probe.room
3185
+ } {
3186
+ if mostSpace {
3187
+ if (probe.room > bestFitRoom) {
3188
+ bestFit = ci
3189
+ bestFitRoom = probe.room
3190
+ }
3191
+ }
3192
+ }
3193
+ }
3194
+ ci = (ci + 1)
3195
+ }
3196
+ def pick:int bestOver
3197
+ if (bestFit >= 0) {
3198
+ pick = bestFit
3199
+ }
3200
+ def side:string (itemAt sides pick)
3201
+ def align:string (itemAt aligns pick)
3202
+ def spot:OverlaySpot (this.overlaySpot(side align ax ay aw ah w h gap))
3203
+
3204
+ ; `fit-viewport`: take the room there is rather than running off
3205
+ ; the page. The surface then clips, and `scrollHeight` says how far
3206
+ ; there is to scroll — which is the rest of the menu.
3207
+ if surface.fitViewport {
3208
+ def availH:double pageHeight
3209
+ if ((side == "top") || (side == "bottom")) {
3210
+ availH = (spot.room + h)
3211
+ }
3212
+ if (availH < h) {
3213
+ h = availH
3214
+ if (h < 0.0) {
3215
+ h = 0.0
3216
+ }
3217
+ this.setSurfaceHeight(surface h)
3218
+ surface.overlayClamped = true
3219
+ spot = (this.overlaySpot(side align ax ay aw ah w h gap))
3220
+ }
3221
+ }
3222
+
3223
+ surface.overlayPlacedSide = side
3224
+ surface.overlayPlacedAlign = align
3225
+ x = spot.x
3226
+ y = spot.y
3227
+
3228
+ ; Shift onto the page. Both axes are clamped, not only the cross
3229
+ ; one: after a flip that did not help there is nothing else left to
3230
+ ; do, and a surface half off the page is worse than one that covers
3231
+ ; its anchor.
3232
+ x = (EVGLayout.clampTo(x 0.0 (pageWidth - w)))
3233
+ y = (EVGLayout.clampTo(y 0.0 (pageHeight - h)))
3234
+ }
3235
+ }
3236
+ }
3237
+
3238
+ this.finishOverlay(surface x y)
3239
+ }
3240
+
3241
+ ; Move the surface AND everything under it — the same helper a page break
3242
+ ; already uses. The subtree was laid out relative to wherever layoutAbsolute
3243
+ ; left the surface, so the whole thing travels together.
3244
+ ;
3245
+ ; Then, if it clips, how far there is to scroll. The extents are RE-MEASURED
3246
+ ; and not re-applied: `applyScroll` already ran during the normal pass and
3247
+ ; translated the children by `scrollTop`, and doing that twice would move
3248
+ ; them twice. What is stale after a clamp is only the extent.
3249
+ fn finishOverlay:void (surface:EVGElement x:double y:double) {
3250
+ surface.overlayX = x
3251
+ surface.overlayY = y
3252
+ this.translateSubtree(surface (x - surface.calculatedX) (y - surface.calculatedY))
3253
+ if (surface.clipsContent()) {
3254
+ this.measureScrollExtent(surface)
3255
+ }
3256
+ }
3257
+
3258
+ ; Start, centre or end along the axis the side does not occupy.
3259
+ fn overlayCross:double (anchorStart:double anchorSize:double size:double align:string) {
3260
+ if (align == "center") {
3261
+ return (anchorStart + ((anchorSize - size) / 2.0))
3262
+ }
3263
+ if (align == "end") {
3264
+ return ((anchorStart + anchorSize) - size)
3265
+ }
3266
+ return anchorStart
3267
+ }
3268
+
3269
+ fn getOverlayErrors:[string] () {
3270
+ return overlayErrors
3271
+ }
3272
+
3273
+ fn printLayout:void (element:EVGElement indent:int) {
3274
+ ; Print layout tree for debugging
3275
+ def indentStr:string ""
3276
+ def i:int 0
3277
+ while (i < indent) {
3278
+ indentStr = indentStr + " "
3279
+ i = i + 1
3280
+ }
3281
+
3282
+ print (indentStr + element.tagName + " id=\"" + element.id + "\" (" + (to_string element.calculatedX) + ", " + (to_string element.calculatedY) + ") " + (to_string element.calculatedWidth) + "x" + (to_string element.calculatedHeight))
3283
+
3284
+ def childCount:int (element.getChildCount())
3285
+ i = 0
3286
+ while (i < childCount) {
3287
+ def child:EVGElement (element.getChild(i))
3288
+ this.printLayout(child (indent + 1))
3289
+ i = i + 1
3290
+ }
3291
+ }
3292
+
3293
+ ; =========================================================================
3294
+ ; CSS Grid subset (see EVGGrid.rgr for the supported syntax)
3295
+ ; =========================================================================
3296
+ ;
3297
+ ; Auto-flow row placement over a fixed column count. Items may span columns
3298
+ ; and rows; an occupancy map keeps spans from overlapping. Returns the total
3299
+ ; content height, same contract as layoutChildren.
3300
+ ; What an item contributes to an intrinsic size — CSS's `max-content` and
3301
+ ; `min-content`, and now for CONTAINERS as well as leaves.
3302
+ ;
3303
+ ; A definite width is both the min- and max-content size, so it is the
3304
+ ; whole answer. A text leaf contributes its longest unbroken line (or its
3305
+ ; longest word, for the min). A CONTAINER contributes the sum of its
3306
+ ; children along the row axis and the widest of them down the column axis,
3307
+ ; plus gaps and its own chrome — which is the recursive definition, and it
3308
+ ; terminates on the leaves above.
3309
+ ;
3310
+ ; This used to stop at the leaves, and the comment here said why: sizing a
3311
+ ; container to its subtree needs a real intrinsic pass. This IS that pass.
3312
+ ; It was written when the alternative was reporting a made-up number; the
3313
+ ; alternative now is every component hand-setting a width its own children
3314
+ ; already know, which is worse and does not stay correct.
3315
+ fn intrinsicWidthOf:double (el:EVGElement) {
3316
+ return (this.intrinsicWidth(el false))
3317
+ }
3318
+
3319
+ ; The narrowest the item can be squeezed to — for text, its longest word.
3320
+ fn minIntrinsicWidthOf:double (el:EVGElement) {
3321
+ return (this.intrinsicWidth(el true))
3322
+ }
3323
+
3324
+ ; The intrinsic width of a kept subtree is kept with it: measuring it
3325
+ ; is a walk of the subtree, and a subtree that is the same has the
3326
+ ; same width.
3327
+ fn intrinsicWidth:double (el:EVGElement wantMin:boolean) {
3328
+ if (el.keepLayout && (el.hasLayout && el.layoutClean)) {
3329
+ if wantMin {
3330
+ if el.hasIntrinsicMin {
3331
+ return el.intrinsicMin
3332
+ }
3333
+ } {
3334
+ if el.hasIntrinsicMax {
3335
+ return el.intrinsicMax
3336
+ }
3337
+ }
3338
+ def measured:double (this.intrinsicWidthNow(el wantMin))
3339
+ if wantMin {
3340
+ el.intrinsicMin = measured
3341
+ el.hasIntrinsicMin = true
3342
+ } {
3343
+ el.intrinsicMax = measured
3344
+ el.hasIntrinsicMax = true
3345
+ }
3346
+ return measured
3347
+ }
3348
+ return (this.intrinsicWidthNow(el wantMin))
3349
+ }
3350
+
3351
+ fn intrinsicWidthNow:double (el:EVGElement wantMin:boolean) {
3352
+ def outer:double 0.0
3353
+ ; A PERCENTAGE IS NOT A DEFINITE WIDTH HERE. Intrinsic sizing is the
3354
+ ; question "how wide would this be with unlimited / with no space",
3355
+ ; asked while the containing block's width is still being worked out —
3356
+ ; so there is nothing for a percentage to be a percentage OF, and CSS
3357
+ ; says such a width behaves as `auto` for the contribution. Taking it
3358
+ ; as definite reads it as whatever the last resolve happened to leave
3359
+ ; behind, which in the middle of a flex pass is zero: a cell holding
3360
+ ; `.chartBox { width: 100% }` around a 220px chart reported a
3361
+ ; min-content width of NOTHING, and the automatic minimum that is
3362
+ ; supposed to stop the cell being squeezed below its contents let it
3363
+ ; be squeezed to half a row instead.
3364
+ def definite:boolean el.width.isSet
3365
+ if (el.width.isPercent()) {
3366
+ definite = false
3367
+ }
3368
+ if definite {
3369
+ ; A definite width is both the min- and the max-content size.
3370
+ outer = el.width.pixels
3371
+ } {
3372
+ def textContent:string el.textContent
3373
+ if ((strlen textContent) > 0) {
3374
+ if ((el.getChildCount()) == 0) {
3375
+ def fs:double el.inheritedFontSize
3376
+ if el.fontSize.isSet {
3377
+ fs = el.fontSize.pixels
3378
+ }
3379
+ if (fs <= 0.0) {
3380
+ fs = 14.0
3381
+ }
3382
+ def contentW:double 0.0
3383
+ if wantMin {
3384
+ contentW = (textEngine.minLineWidthSpaced(textContent (el.effectiveFontFamily()) fs el.letterSpacing))
3385
+ } {
3386
+ contentW = (textEngine.maxLineWidthSpaced(textContent (el.effectiveFontFamily()) fs el.letterSpacing))
3387
+ }
3388
+ outer = (contentW + (el.box.getHorizontalChrome()))
3389
+ }
3390
+ } {
3391
+ outer = (this.intrinsicOfChildren(el wantMin))
3392
+ }
3393
+ }
3394
+ if (outer <= 0.0) {
3395
+ return 0.0
3396
+ }
3397
+ return ((outer + el.box.marginLeftPx) + el.box.marginRightPx)
3398
+ }
3399
+
3400
+ ; Is this element laid out along a row? An element with no parent, or one
3401
+ ; whose parent is not a flex container, is treated as a row item — which is
3402
+ ; the block default: `width: auto` on a block fills, and the caller has
3403
+ ; already set `width = parentWidth` before asking.
3404
+ fn parentIsRow:boolean (el:EVGElement) {
3405
+ return layingOutAlongRow
3406
+ }
3407
+
3408
+ ; Does a width-less child of this container shrink to its content?
3409
+ ; Only when the container is a column that has explicitly asked for
3410
+ ; something other than the stretch.
3411
+ fn crossShrinksUnder:boolean (parent:EVGElement) {
3412
+ if (parent.flexDirection != "column") {
3413
+ return false
3414
+ }
3415
+ def a:string parent.alignItems
3416
+ if (a == "center") {
3417
+ return true
3418
+ }
3419
+ if (a == "flex-end") {
3420
+ return true
3421
+ }
3422
+ if (a == "end") {
3423
+ return true
3424
+ }
3425
+ return false
3426
+ }
3427
+
3428
+ ; Whether `width: auto` on this element means fit-content rather than
3429
+ ; stretch: along a row it always does, and down a column it does when the
3430
+ ; parent aligns its items anywhere but the stretch.
3431
+ fn crossAxisFits:boolean (el:EVGElement) {
3432
+ if (this.parentIsRow(el)) {
3433
+ return true
3434
+ }
3435
+ return columnCrossShrinks
3436
+ }
3437
+
3438
+ ; The recursive half. A row adds its children up; a column takes the widest.
3439
+ ; Absolutely positioned children are out of flow and contribute nothing, the
3440
+ ; same way they contribute nothing to the flow itself.
3441
+ ;
3442
+ ; `wrap` is treated as a row here on purpose: max-content is defined as the
3443
+ ; size the box would take with unlimited space, and with unlimited space a
3444
+ ; wrapping row does not wrap.
3445
+ fn intrinsicOfChildren:double (el:EVGElement wantMin:boolean) {
3446
+ def n:int (el.getChildCount())
3447
+ if (n == 0) {
3448
+ return 0.0
3449
+ }
3450
+ def isColumn:boolean ((el.flexDirection == "column") || (el.flexDirection == "column-reverse"))
3451
+ def gapPx:double 0.0
3452
+ if el.gap.isSet {
3453
+ gapPx = el.gap.pixels
3454
+ }
3455
+ if el.columnGap.isSet {
3456
+ gapPx = el.columnGap.pixels
3457
+ }
3458
+
3459
+ def total:double 0.0
3460
+ def widest:double 0.0
3461
+ def counted:int 0
3462
+ def i:int 0
3463
+ while (i < n) {
3464
+ def kid:EVGElement (el.getChild(i))
3465
+ ; Out of flow — absolute, or `display: none` — takes no share of
3466
+ ; the intrinsic size. This asked the position string rather than
3467
+ ; `isAbsolute`, so a rail the sheet had folded away still claimed
3468
+ ; its icons' width from the row and the page beside it came up
3469
+ ; 32px short.
3470
+ if (kid.isAbsolute || (kid.isHidden())) {
3471
+ } {
3472
+ ; RESOLVE THE KID FIRST. Its padding and border live in
3473
+ ; `kid.box` as pixels, and `resolveUnits` is what puts them
3474
+ ; there — the flow calls it on each child just before laying it
3475
+ ; out, so a child IS resolved by the time it is measured, and a
3476
+ ; GRANDCHILD is not. This pass walks grandchildren.
3477
+ ;
3478
+ ; The symptom was a container narrower than the things inside
3479
+ ; it: a footer's two buttons came to 216 and their row was
3480
+ ; sized 164, which is 216 less exactly two buttons' worth of
3481
+ ; chrome, and the pair hung out over the right edge of the
3482
+ ; card. "Containers leak over" is what a missing padding term
3483
+ ; looks like from the outside.
3484
+ ;
3485
+ ; The basis is this element's own inner box, which is what the
3486
+ ; flow will use a moment later. It matters only for a
3487
+ ; PERCENTAGE padding, whose max-content size is undefined in
3488
+ ; CSS anyway and which nothing in this gallery uses; `px` and
3489
+ ; `em` resolve the same whatever they are given.
3490
+ ;
3491
+ ; AND A MEASUREMENT MUST NOT FREEZE THE RESOLUTION. `el` is
3492
+ ; being MEASURED, so its inner width is not decided yet — a
3493
+ ; `flex: 1` item is asked for its min-content size in the
3494
+ ; middle of the pass that works out how wide it gets, and
3495
+ ; `calculatedInnerWidth` is still zero there. Resolving a kid
3496
+ ; against that zero is harmless for the measure (a percentage
3497
+ ; contributes nothing to min-content, which is what CSS says
3498
+ ; too) and fatal afterwards: `resolveUnits` latches
3499
+ ; `unitsResolved`, so `width: 100%` stayed at the 0 it was
3500
+ ; measured with and the flow never asked again.
3501
+ ;
3502
+ ; That is the charts sliding off the left of the showcase
3503
+ ; pages. `.chartBox { width: 100%; align-items: center }` in a
3504
+ ; `flex: 1` cell came out zero wide, and centring a 220px chart
3505
+ ; in a 0px box puts its left edge at -110.
3506
+ ;
3507
+ ; So a kid that was NOT resolved before this pass is handed
3508
+ ; back unresolved, and the flow resolves it for real against
3509
+ ; the width it actually got. A kid that was already resolved
3510
+ ; keeps what it had — the flow resolved it, not this.
3511
+ def wasResolved:boolean kid.unitsResolved
3512
+ kid.resolveUnits(el.calculatedInnerWidth el.calculatedInnerHeight)
3513
+ def w:double (this.intrinsicWidth(kid wantMin))
3514
+ if (wasResolved == false) {
3515
+ kid.unitsResolved = false
3516
+ }
3517
+ if (w > widest) {
3518
+ widest = w
3519
+ }
3520
+ if (counted > 0) {
3521
+ total = (total + gapPx)
3522
+ }
3523
+ total = (total + w)
3524
+ counted = (counted + 1)
3525
+ }
3526
+ i = (i + 1)
3527
+ }
3528
+ def inner:double total
3529
+ if isColumn {
3530
+ inner = widest
3531
+ }
3532
+ if (inner <= 0.0) {
3533
+ return 0.0
3534
+ }
3535
+ return (inner + (el.box.getHorizontalChrome()))
3536
+ }
3537
+
3538
+ ; The grid's out-of-flow children, once the tracks are sized.
3539
+ ;
3540
+ ; `layoutGrid` gathers the in-flow items and leaves an `isAbsolute` child
3541
+ ; out of the placement, which is right — an absolute box takes no track.
3542
+ ; What was missing is this: nothing laid it out afterwards either, so it
3543
+ ; kept zero size at the page origin. `contentHeight` is the rows' own
3544
+ ; height, used when the container has no definite one, so a percentage
3545
+ ; offset resolves against the box the grid actually came out as.
3546
+ fn layoutGridOutOfFlow:void (parent:EVGElement innerWidth:double contentHeight:double startX:double startY:double) {
3547
+ def n:int (parent.getChildCount())
3548
+ def i:int 0
3549
+ while (i < n) {
3550
+ def c:EVGElement (parent.getChild(i))
3551
+ if c.isAbsolute {
3552
+ c.unitsResolved = false
3553
+ c.resolveUnits(innerWidth contentHeight)
3554
+ this.layoutOutOfFlowChild(parent c innerWidth contentHeight startX startY true)
3555
+ }
3556
+ i = (i + 1)
3557
+ }
3558
+ }
3559
+
3560
+ fn layoutGrid:double (parent:EVGElement) {
3561
+ def innerWidth:double parent.calculatedInnerWidth
3562
+ def innerHeight:double parent.calculatedInnerHeight
3563
+
3564
+ def startX:double (parent.calculatedX + parent.box.borderWidthPx + parent.box.paddingLeftPx)
3565
+ def startY:double (parent.calculatedY + parent.box.borderWidthPx + parent.box.paddingTopPx)
3566
+
3567
+ ; `row-gap` / `column-gap` fall back to `gap` when unset.
3568
+ def colGapPx:double 0.0
3569
+ def rowGapPx:double 0.0
3570
+ if parent.gap.isSet {
3571
+ parent.gap.rootFontSize = parent.rootFontSize
3572
+ parent.gap.resolve(innerWidth parent.inheritedFontSize)
3573
+ colGapPx = parent.gap.pixels
3574
+ rowGapPx = parent.gap.pixels
3575
+ }
3576
+ if parent.columnGap.isSet {
3577
+ parent.columnGap.rootFontSize = parent.rootFontSize
3578
+ parent.columnGap.resolve(innerWidth parent.inheritedFontSize)
3579
+ colGapPx = parent.columnGap.pixels
3580
+ }
3581
+ if parent.rowGap.isSet {
3582
+ parent.rowGap.rootFontSize = parent.rootFontSize
3583
+ parent.rowGap.resolve(innerHeight parent.inheritedFontSize)
3584
+ rowGapPx = parent.rowGap.pixels
3585
+ }
3586
+
3587
+ ; Columns. With no template a grid is a single full-width column, which
3588
+ ; keeps `display: grid` from silently collapsing content to zero width.
3589
+ def colSpec:string parent.gridTemplateColumns
3590
+ if ((strlen colSpec) == 0) {
3591
+ colSpec = "1fr"
3592
+ }
3593
+
3594
+ ; grid-template-columns: subgrid — adopt the enclosing grid's tracks for
3595
+ ; the span this element occupies, so a nested card's columns line up with
3596
+ ; every other card's instead of each one splitting its own width. The
3597
+ ; parent filled these in before laying this element out; with no
3598
+ ; enclosing grid there is nothing to inherit and it falls back to 1fr.
3599
+ if (colSpec == "subgrid") {
3600
+ if ((array_length parent.subgridColumnSizes) > 0) {
3601
+ def sgSpec:string ""
3602
+ def sg:int 0
3603
+ while (sg < (array_length parent.subgridColumnSizes)) {
3604
+ if (sg > 0) {
3605
+ sgSpec = sgSpec + " "
3606
+ }
3607
+ sgSpec = sgSpec + (to_string (itemAt parent.subgridColumnSizes sg)) + "px"
3608
+ sg = (sg + 1)
3609
+ }
3610
+ colSpec = sgSpec
3611
+ } {
3612
+ ; During an enclosing grid's content-measuring pass the sizes
3613
+ ; do not exist yet, and falling back is correct rather than
3614
+ ; wrong — subgridPending is what tells that apart from having
3615
+ ; no enclosing grid at all.
3616
+ if (parent.subgridPending == false) {
3617
+ this.warn("grid-template-columns: subgrid has no enclosing grid to inherit tracks from; falling back to 1fr")
3618
+ }
3619
+ colSpec = "1fr"
3620
+ }
3621
+ }
3622
+ ; grid-template-areas names a rectangle per area and, when no explicit
3623
+ ; column template was given, decides the column count.
3624
+ def areas:EVGGridAreas (EVGGridAreas.parse(parent.gridTemplateAreas))
3625
+ def hasAreas:boolean false
3626
+ if ((strlen parent.gridTemplateAreas) > 0) {
3627
+ if (areas.hadError) {
3628
+ this.warn(("grid-template-areas: " + areas.errorText))
3629
+ } {
3630
+ hasAreas = true
3631
+ if ((strlen parent.gridTemplateColumns) == 0) {
3632
+ ; Derive the columns from the picture.
3633
+ def derived:string ""
3634
+ def dc:int 0
3635
+ while (dc < areas.columns) {
3636
+ if (dc > 0) {
3637
+ derived = derived + " "
3638
+ }
3639
+ derived = derived + "1fr"
3640
+ dc = (dc + 1)
3641
+ }
3642
+ colSpec = derived
3643
+ }
3644
+ }
3645
+ }
3646
+
3647
+ def cols:EVGGridTemplate (EVGGridTemplate.parse(colSpec))
3648
+ if (cols.hadError) {
3649
+ this.warn(("grid-template-columns: " + cols.errorText))
3650
+ }
3651
+ def colCount:int (cols.count())
3652
+ if (colCount < 1) {
3653
+ return 0.0
3654
+ }
3655
+ def colGapTotal:double ((to_double (colCount - 1)) * colGapPx)
3656
+ ; Column sizing is deferred until after placement: an `auto` or
3657
+ ; `fit-content()` track is sized from the items that land in it, so
3658
+ ; the tracks cannot be resolved until it is known which those are.
3659
+ ; Nothing between here and there reads a track extent.
3660
+
3661
+ ; The row template is parsed here as well as where the rows are sized,
3662
+ ; because a `grid-row` naming a line has to be resolved before
3663
+ ; placement runs. With no row template it holds no names, and every
3664
+ ; named row line is reported as unknown — which is the truth.
3665
+ def rowTemplateSpec:string parent.gridTemplateRows
3666
+ if (rowTemplateSpec == "subgrid") {
3667
+ ; Inherited tracks carry no names of their own.
3668
+ rowTemplateSpec = ""
3669
+ }
3670
+ def rowTemplate:EVGGridTemplate (EVGGridTemplate.parse(rowTemplateSpec))
3671
+
3672
+ ; Collect the in-flow children and their placements.
3673
+ def items:[EVGElement]
3674
+ def colStarts:[int]
3675
+ def colSpans:[int]
3676
+ def rowStarts:[int]
3677
+ def rowSpans:[int]
3678
+ def i:int 0
3679
+ def childCount:int (parent.getChildCount())
3680
+ while (i < childCount) {
3681
+ def c:EVGElement (parent.getChild(i))
3682
+ c.inheritProperties(parent)
3683
+ c.resolveUnits(innerWidth innerHeight)
3684
+ ; Claim it now: the tracks cannot be handed over until they are
3685
+ ; sized, but the child needs to know an enclosing grid exists long
3686
+ ; before that — it may be laid out during the measuring pass below.
3687
+ if ((c.gridTemplateColumns == "subgrid") || (c.gridTemplateRows == "subgrid")) {
3688
+ c.subgridPending = true
3689
+ }
3690
+ if (c.isAbsolute == false) {
3691
+ ; Named lines live on the container's template, so the
3692
+ ; placement is parsed first and pointed at real lines here,
3693
+ ; where both are in hand.
3694
+ def cp:EVGGridPlacement (EVGGridPlacement.parse(c.gridColumn))
3695
+ cp.resolveNames(cols)
3696
+ if (cp.hadError) {
3697
+ this.warn(("grid-column: " + cp.errorText))
3698
+ }
3699
+ def rp:EVGGridPlacement (EVGGridPlacement.parse(c.gridRow))
3700
+ rp.resolveNames(rowTemplate)
3701
+ if (rp.hadError) {
3702
+ this.warn(("grid-row: " + rp.errorText))
3703
+ }
3704
+ ; grid-area names win over grid-column/grid-row: the picture is
3705
+ ; the more specific statement of intent.
3706
+ if hasAreas {
3707
+ if ((strlen c.gridArea) > 0) {
3708
+ def at:int (areas.indexOfName(c.gridArea))
3709
+ if (at >= 0) {
3710
+ cp.start = ((itemAt areas.colStart at) + 1)
3711
+ cp.span = (itemAt areas.colSpan at)
3712
+ rp.start = ((itemAt areas.rowStart at) + 1)
3713
+ rp.span = (itemAt areas.rowSpan at)
3714
+ } {
3715
+ this.warn(("grid-area: no area named \"" + c.gridArea + "\" in grid-template-areas"))
3716
+ }
3717
+ }
3718
+ }
3719
+ def cspan:int cp.span
3720
+ if (cspan > colCount) {
3721
+ cspan = colCount
3722
+ }
3723
+ push items c
3724
+ push colStarts cp.start
3725
+ push colSpans cspan
3726
+ push rowStarts rp.start
3727
+ push rowSpans rp.span
3728
+ }
3729
+ i = (i + 1)
3730
+ }
3731
+ def itemCount:int (array_length items)
3732
+ if (itemCount == 0) {
3733
+ ; No tracks to size, but an overlay or a connector may still be
3734
+ ; sitting here out of flow.
3735
+ this.layoutGridOutOfFlow(parent innerWidth innerHeight startX startY)
3736
+ return 0.0
3737
+ }
3738
+
3739
+ ; --- placement -------------------------------------------------------
3740
+ def isDense:boolean ((indexOf parent.gridAutoFlow "dense") >= 0)
3741
+ ; occupancy[(row * colCount) + col]; grown a row at a time.
3742
+ def occupied:[boolean]
3743
+ def rowsUsed:int 0
3744
+ def placedRow:[int]
3745
+ def placedCol:[int]
3746
+
3747
+ def cursorRow:int 0
3748
+ def cursorCol:int 0
3749
+ def k:int 0
3750
+ while (k < itemCount) {
3751
+ def wantCol:int (itemAt colStarts k)
3752
+ def wantRow:int (itemAt rowStarts k)
3753
+ def span:int (itemAt colSpans k)
3754
+ def rspan:int (itemAt rowSpans k)
3755
+
3756
+ def col:int 0
3757
+ def row:int 0
3758
+ if (wantCol > 0) {
3759
+ col = (wantCol - 1)
3760
+ if (col > (colCount - span)) {
3761
+ col = (colCount - span)
3762
+ }
3763
+ if (col < 0) {
3764
+ col = 0
3765
+ }
3766
+ }
3767
+ if (wantRow > 0) {
3768
+ row = (wantRow - 1)
3769
+ }
3770
+
3771
+ if ((wantCol > 0) && (wantRow > 0)) {
3772
+ ; Fully explicit placement — take it as given.
3773
+ } {
3774
+ ; Auto-flow: scan forward from the cursor for a free run of
3775
+ ; `span` columns, wrapping to the next row when it does not fit.
3776
+ if (wantRow > 0) {
3777
+ cursorRow = row
3778
+ cursorCol = 0
3779
+ }
3780
+ if (wantCol > 0) {
3781
+ ; Column pinned, row auto: find the first row where it is free.
3782
+ row = cursorRow
3783
+ while (this.gridOccupied(occupied colCount row col span rspan)) {
3784
+ row = (row + 1)
3785
+ }
3786
+ } {
3787
+ ; grid-auto-flow: dense restarts the scan from the top for
3788
+ ; every item, so a later small item backfills a hole a wider
3789
+ ; one left behind. The default only ever moves forward,
3790
+ ; which keeps source order but can leave gaps.
3791
+ if isDense {
3792
+ row = 0
3793
+ col = 0
3794
+ } {
3795
+ row = cursorRow
3796
+ col = cursorCol
3797
+ }
3798
+ def searching:boolean true
3799
+ while searching {
3800
+ if ((col + span) > colCount) {
3801
+ row = (row + 1)
3802
+ col = 0
3803
+ } {
3804
+ if (this.gridOccupied(occupied colCount row col span rspan)) {
3805
+ col = (col + 1)
3806
+ } {
3807
+ searching = false
3808
+ }
3809
+ }
3810
+ }
3811
+ if (isDense == false) {
3812
+ cursorRow = row
3813
+ cursorCol = (col + span)
3814
+ }
3815
+ }
3816
+ }
3817
+
3818
+ ; Grow the occupancy map to cover this item, then mark it.
3819
+ def needRows:int (row + rspan)
3820
+ while (rowsUsed < needRows) {
3821
+ def g:int 0
3822
+ while (g < colCount) {
3823
+ push occupied false
3824
+ g = (g + 1)
3825
+ }
3826
+ rowsUsed = (rowsUsed + 1)
3827
+ }
3828
+ def rr:int 0
3829
+ while (rr < rspan) {
3830
+ def cc:int 0
3831
+ while (cc < span) {
3832
+ def idx:int ((((row + rr) * colCount) + col) + cc)
3833
+ set occupied idx true
3834
+ cc = (cc + 1)
3835
+ }
3836
+ rr = (rr + 1)
3837
+ }
3838
+ push placedRow row
3839
+ push placedCol col
3840
+ k = (k + 1)
3841
+ }
3842
+
3843
+ ; --- column sizing ---------------------------------------------------
3844
+ ; Intrinsic tracks take the widest item placed in them. Only items that
3845
+ ; occupy a single column contribute: an item spanning several has no
3846
+ ; one track to charge, and CSS distributes it in a later step this
3847
+ ; engine does not have — so it is left out rather than guessed at.
3848
+ def colContent:[double]
3849
+ def colMinContent:[double]
3850
+ def ci:int 0
3851
+ while (ci < colCount) {
3852
+ push colContent 0.0
3853
+ push colMinContent 0.0
3854
+ ci = (ci + 1)
3855
+ }
3856
+ if (cols.hasIntrinsicTrack()) {
3857
+ def ic:int 0
3858
+ while (ic < (array_length items)) {
3859
+ if ((itemAt colSpans ic) == 1) {
3860
+ def col2:int (itemAt placedCol ic)
3861
+ if (col2 < colCount) {
3862
+ def it:EVGElement (itemAt items ic)
3863
+ def w2:double (this.intrinsicWidthOf(it))
3864
+ if (w2 > (itemAt colContent col2)) {
3865
+ set colContent col2 w2
3866
+ }
3867
+ def m2:double (this.minIntrinsicWidthOf(it))
3868
+ if (m2 > (itemAt colMinContent col2)) {
3869
+ set colMinContent col2 m2
3870
+ }
3871
+ }
3872
+ }
3873
+ ic = (ic + 1)
3874
+ }
3875
+ }
3876
+ cols.resolveWithContent((innerWidth - colGapTotal) colContent colMinContent)
3877
+
3878
+ ; --- row sizing ------------------------------------------------------
3879
+ def rowSpec:string parent.gridTemplateRows
3880
+ def rowSizes:[double]
3881
+ def rowGapTotal:double 0.0
3882
+ if (rowsUsed > 1) {
3883
+ rowGapTotal = ((to_double (rowsUsed - 1)) * rowGapPx)
3884
+ }
3885
+
3886
+ ; grid-template-rows: subgrid — adopt the enclosing grid's row tracks
3887
+ ; for the rows this element occupies, so the pieces of a card line up
3888
+ ; with every other card's instead of each one dividing its own height.
3889
+ ;
3890
+ ; The rows are handed down at final placement, by which point the
3891
+ ; parent has sized them either from its own template or from its
3892
+ ; content pass. That is why this could wait until the rest of grid was
3893
+ ; in place: it needs the parent's rows to be settled, and they are.
3894
+ def usingRowSubgrid:boolean false
3895
+ if (rowSpec == "subgrid") {
3896
+ if ((array_length parent.subgridRowSizes) > 0) {
3897
+ usingRowSubgrid = true
3898
+ def sgrSpec:string ""
3899
+ def sgr:int 0
3900
+ while (sgr < (array_length parent.subgridRowSizes)) {
3901
+ if (sgr > 0) {
3902
+ sgrSpec = sgrSpec + " "
3903
+ }
3904
+ sgrSpec = sgrSpec + (to_string (itemAt parent.subgridRowSizes sgr)) + "px"
3905
+ sgr = (sgr + 1)
3906
+ }
3907
+ rowSpec = sgrSpec
3908
+ } {
3909
+ if (parent.subgridPending == false) {
3910
+ this.warn("grid-template-rows: subgrid has no enclosing grid to inherit tracks from; falling back to content-sized rows")
3911
+ }
3912
+ rowSpec = ""
3913
+ }
3914
+ }
3915
+
3916
+ def haveTemplateRows:boolean false
3917
+ if ((strlen rowSpec) > 0) {
3918
+ ; Inherited rows are already absolute pixels, so they do not need
3919
+ ; the container's own height to resolve against.
3920
+ if (parent.hasDefiniteHeight || usingRowSubgrid) {
3921
+ haveTemplateRows = true
3922
+ }
3923
+ }
3924
+
3925
+ if haveTemplateRows {
3926
+ ; Explicit row tracks against a definite container height.
3927
+ def rows:EVGGridTemplate (EVGGridTemplate.parse(rowSpec))
3928
+ if (rows.hadError) {
3929
+ this.warn(("grid-template-rows: " + rows.errorText))
3930
+ }
3931
+ rows.resolve((innerHeight - rowGapTotal))
3932
+ def r2:int 0
3933
+ while (r2 < rowsUsed) {
3934
+ if (r2 < (rows.count())) {
3935
+ def tk:EVGGridTrack (rows.trackAt(r2))
3936
+ push rowSizes tk.sizePx
3937
+ } {
3938
+ ; More rows than tracks: implicit rows repeat the last track.
3939
+ if ((rows.count()) > 0) {
3940
+ def lastTk:EVGGridTrack (rows.trackAt((rows.count()) - 1))
3941
+ push rowSizes lastTk.sizePx
3942
+ } {
3943
+ push rowSizes 0.0
3944
+ }
3945
+ }
3946
+ r2 = (r2 + 1)
3947
+ }
3948
+ } {
3949
+ ; Content-sized rows: measure each single-row item at its cell width
3950
+ ; and take the tallest per row. Items spanning rows are left out of
3951
+ ; this max — they get whatever their spanned rows add up to.
3952
+ def r3:int 0
3953
+ while (r3 < rowsUsed) {
3954
+ push rowSizes 0.0
3955
+ r3 = (r3 + 1)
3956
+ }
3957
+ def m:int 0
3958
+ while (m < itemCount) {
3959
+ def it:EVGElement (itemAt items m)
3960
+ def rspanM:int (itemAt rowSpans m)
3961
+ def ri:int (itemAt placedRow m)
3962
+ ; A row subgrid always spans several rows, so the span-1 rule
3963
+ ; below would leave it out — and with only subgrid cards in the
3964
+ ; grid every row would measure zero and the cards collapse.
3965
+ ; With subgrid it is the grandchildren that size these rows, so
3966
+ ; the card is measured on its own terms and the rows it worked
3967
+ ; out are adopted one for one.
3968
+ def isRowSubgrid:boolean (it.gridTemplateRows == "subgrid")
3969
+ if ((rspanM == 1) || isRowSubgrid) {
3970
+ def cw:double (cols.extentOf((itemAt placedCol m) (itemAt colSpans m) colGapPx))
3971
+ def avail:double (cw - it.box.marginLeftPx - it.box.marginRightPx)
3972
+ ; Measure with auto height: clear any inherited flex height.
3973
+ it.calculatedFlexHeight = 0.0
3974
+ this.layoutElement(it 0.0 0.0 avail 0.0)
3975
+ if isRowSubgrid {
3976
+ def sub:int 0
3977
+ while (sub < rspanM) {
3978
+ if (sub < (array_length it.computedRowSizes)) {
3979
+ def sh:double (itemAt it.computedRowSizes sub)
3980
+ if (sh > (itemAt rowSizes (ri + sub))) {
3981
+ set rowSizes (ri + sub) sh
3982
+ }
3983
+ }
3984
+ sub = (sub + 1)
3985
+ }
3986
+ } {
3987
+ def h:double (it.calculatedHeight + it.box.marginTopPx + it.box.marginBottomPx)
3988
+ if (h > (itemAt rowSizes ri)) {
3989
+ set rowSizes ri h
3990
+ }
3991
+ }
3992
+ }
3993
+ m = (m + 1)
3994
+ }
3995
+
3996
+ ; An explicit row template on a container with no definite height
3997
+ ; used to be dropped whole, so `grid-template-rows: 170px auto` on
3998
+ ; an auto-height deck sized every row from content and the fixed
3999
+ ; band vanished. A track that needs no container height to resolve
4000
+ ; is applied here; a `%` or `fr` row genuinely does need one and is
4001
+ ; left content-sized.
4002
+ if ((strlen rowSpec) > 0) {
4003
+ def declared:EVGGridTemplate (EVGGridTemplate.parse(rowSpec))
4004
+ if (declared.hadError) {
4005
+ this.warn(("grid-template-rows: " + declared.errorText))
4006
+ }
4007
+ def dr:int 0
4008
+ while (dr < rowsUsed) {
4009
+ if (dr < (declared.count())) {
4010
+ def dt:EVGGridTrack (declared.trackAt(dr))
4011
+ if (dt.kind == 0) {
4012
+ set rowSizes dr dt.value
4013
+ }
4014
+ }
4015
+ dr = (dr + 1)
4016
+ }
4017
+ }
4018
+ }
4019
+
4020
+ ; --- final placement -------------------------------------------------
4021
+ def p:int 0
4022
+ while (p < itemCount) {
4023
+ def el:EVGElement (itemAt items p)
4024
+ def cIdx:int (itemAt placedCol p)
4025
+ def rIdx:int (itemAt placedRow p)
4026
+ def cSpan:int (itemAt colSpans p)
4027
+ def rSpan:int (itemAt rowSpans p)
4028
+
4029
+ def cellW:double (cols.extentOf(cIdx cSpan colGapPx))
4030
+ def cellH:double (this.gridRowExtent(rowSizes rIdx rSpan rowGapPx))
4031
+ def offX:double (cols.offsetOf(cIdx colGapPx))
4032
+ def offY:double (this.gridRowOffset(rowSizes rIdx rowGapPx))
4033
+
4034
+ el.calculatedX = (startX + offX + el.box.marginLeftPx)
4035
+ el.calculatedY = (startY + offY + el.box.marginTopPx)
4036
+
4037
+ def boxW:double (cellW - el.box.marginLeftPx - el.box.marginRightPx)
4038
+ def boxH:double (cellH - el.box.marginTopPx - el.box.marginBottomPx)
4039
+ if (boxW < 0.0) {
4040
+ boxW = 0.0
4041
+ }
4042
+ if (boxH < 0.0) {
4043
+ boxH = 0.0
4044
+ }
4045
+
4046
+ ; Grid items stretch to their cell by default (§7.1), which is what
4047
+ ; photo cells want. An explicit height still wins: layoutElement only
4048
+ ; consults calculatedFlexHeight when height.isSet is false.
4049
+ if (boxH > 0.0) {
4050
+ el.calculatedFlexHeight = boxH
4051
+ }
4052
+
4053
+ ; Hand this cell's tracks down to a subgrid child before it lays
4054
+ ; itself out. Both axes work the same way; the rows are simply
4055
+ ; known later, which is the only reason columns landed first.
4056
+ if (el.gridTemplateColumns == "subgrid") {
4057
+ clear el.subgridColumnSizes
4058
+ def sgi:int 0
4059
+ while (sgi < cSpan) {
4060
+ def tk:EVGGridTrack (cols.trackAt(cIdx + sgi))
4061
+ push el.subgridColumnSizes tk.sizePx
4062
+ sgi = (sgi + 1)
4063
+ }
4064
+ }
4065
+ if (el.gridTemplateRows == "subgrid") {
4066
+ clear el.subgridRowSizes
4067
+ def sgj:int 0
4068
+ while (sgj < rSpan) {
4069
+ def rIdx2:int (rIdx + sgj)
4070
+ if (rIdx2 < (array_length rowSizes)) {
4071
+ push el.subgridRowSizes (itemAt rowSizes rIdx2)
4072
+ }
4073
+ sgj = (sgj + 1)
4074
+ }
4075
+ }
4076
+
4077
+ ; A grid item's containing block is its CELL, not the grid's
4078
+ ; content box. Its units were resolved against the container before
4079
+ ; the tracks were sized, so `width: 100%` on a cell came out as the
4080
+ ; whole grid's width — every item in the album spread was laid out
4081
+ ; at full size and simply overlapped. Resolve them again now that
4082
+ ; the cell is known.
4083
+ el.unitsResolved = false
4084
+ el.resolveUnits(boxW boxH)
4085
+
4086
+ this.layoutElement(el el.calculatedX el.calculatedY boxW boxH)
4087
+ p = (p + 1)
4088
+ }
4089
+
4090
+ ; Leave the rows behind for an enclosing grid that has to size its own
4091
+ ; rows around this one.
4092
+ clear parent.computedRowSizes
4093
+ def cr:int 0
4094
+ while (cr < rowsUsed) {
4095
+ push parent.computedRowSizes (itemAt rowSizes cr)
4096
+ cr = (cr + 1)
4097
+ }
4098
+
4099
+ def total:double 0.0
4100
+ def s:int 0
4101
+ while (s < rowsUsed) {
4102
+ total = (total + (itemAt rowSizes s))
4103
+ s = (s + 1)
4104
+ }
4105
+ total = (total + rowGapTotal)
4106
+
4107
+ def absH:double innerHeight
4108
+ if (absH <= 0.0) {
4109
+ absH = total
4110
+ }
4111
+ this.layoutGridOutOfFlow(parent innerWidth absH startX startY)
4112
+
4113
+ return total
4114
+ }
4115
+
4116
+ ; True if any cell of the rspan x span block at (row, col) is already taken.
4117
+ ; Rows beyond the current map are by definition free.
4118
+ fn gridOccupied:boolean (occupied:[boolean] colCount:int row:int col:int span:int rspan:int) {
4119
+ def total:int (array_length occupied)
4120
+ def rr:int 0
4121
+ while (rr < rspan) {
4122
+ def cc:int 0
4123
+ while (cc < span) {
4124
+ def idx:int ((((row + rr) * colCount) + col) + cc)
4125
+ if (idx < total) {
4126
+ if (itemAt occupied idx) {
4127
+ return true
4128
+ }
4129
+ }
4130
+ cc = (cc + 1)
4131
+ }
4132
+ rr = (rr + 1)
4133
+ }
4134
+ return false
4135
+ }
4136
+
4137
+ fn gridRowExtent:double (rowSizes:[double] from:int span:int gap:double) {
4138
+ def total:double 0.0
4139
+ def n:int (array_length rowSizes)
4140
+ def i:int from
4141
+ def placed:int 0
4142
+ while ((i < n) && (placed < span)) {
4143
+ total = (total + (itemAt rowSizes i))
4144
+ placed = (placed + 1)
4145
+ i = (i + 1)
4146
+ }
4147
+ if (placed > 1) {
4148
+ total = (total + ((to_double (placed - 1)) * gap))
4149
+ }
4150
+ return total
4151
+ }
4152
+
4153
+ fn gridRowOffset:double (rowSizes:[double] index:int gap:double) {
4154
+ def total:double 0.0
4155
+ def i:int 0
4156
+ while (i < index) {
4157
+ total = (total + (itemAt rowSizes i))
4158
+ total = (total + gap)
4159
+ i = (i + 1)
4160
+ }
4161
+ return total
4162
+ }
4163
+
4164
+ ; `alongRow` is the same question `crossAxisFits` asks of the element being
4165
+ ; laid out, asked here of one being estimated: does `width: auto` mean
4166
+ ; fit-content, or stretch? It has to be passed rather than read off the
4167
+ ; ambient flag, because this runs BEFORE the child is laid out and the flag
4168
+ ; still describes whoever was laid out last.
4169
+ ;
4170
+ ; Getting it wrong is not subtle. Shrink-wrapping unconditionally shrank
4171
+ ; the profile card's header — a column child that should fill — to the
4172
+ ; width of the two things inside it.
4173
+ fn estimateChildWidth:double (child:EVGElement maxInnerWidth:double alongRow:boolean) {
4174
+ ; Width used for row flow / flex pre-calc — matches layoutElement shrink-wrap
4175
+ if child.width.isSet {
4176
+ return child.width.pixels
4177
+ }
4178
+ if (child.hasFlexWidth) {
4179
+ return child.calculatedFlexWidth
4180
+ }
4181
+ def textContent:string child.textContent
4182
+ if ((strlen textContent) > 0) {
4183
+ if ((child.getChildCount()) == 0) {
4184
+ def fontSize:double child.inheritedFontSize
4185
+ if child.fontSize.isSet {
4186
+ fontSize = child.fontSize.pixels
4187
+ }
4188
+ if (fontSize <= 0.0) {
4189
+ fontSize = 14.0
4190
+ }
4191
+ def contentW:double (textEngine.maxLineWidthSpaced(textContent (child.effectiveFontFamily()) fontSize child.letterSpacing))
4192
+ def measuredW:double (contentW + child.box.paddingLeftPx + child.box.paddingRightPx + (child.box.borderWidthPx * 2.0))
4193
+ if (measuredW < maxInnerWidth) {
4194
+ return measuredW
4195
+ }
4196
+ }
4197
+ }
4198
+ ; A width-less flex CONTAINER is worth as much as its contents, the
4199
+ ; same as it is in `layoutElement`. This used to fall straight through
4200
+ ; to `maxInnerWidth` — the whole row — so a row holding a container and
4201
+ ; a `flex: 1` spacer computed no free space at all and the spacer got
4202
+ ; nothing. The header of the profile demo is exactly that shape: a
4203
+ ; title block, a spacer, and a badge that was supposed to sit at the
4204
+ ; far end and sat against the title instead.
4205
+ ;
4206
+ ; It is the same question `layoutElement` asks two hundred lines up,
4207
+ ; and asking it two different ways is what let the flex basis and the
4208
+ ; finished width disagree.
4209
+ if ((child.display == "flex") && alongRow) {
4210
+ def intrinsic:double (this.intrinsicWidth(child false))
4211
+ if (intrinsic > 0.0) {
4212
+ if (intrinsic < maxInnerWidth) {
4213
+ return intrinsic
4214
+ }
4215
+ }
4216
+ }
4217
+ return maxInnerWidth
4218
+ }
4219
+
4220
+ }