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