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