DevTools v2: timeline, selection, live CSS variables, snapshot export + stylesheet var() fixes - #18
Conversation
- HyperRenderDebugHooks.onFrameTiming(id, phase, micros): each RenderHyperBox reports performLayout and paint duration. Paint is canvas recording time, not raster. Only timed in debug mode while the hook is set; release and hook-less debug builds take the direct path. - HyperRenderDebugHooks.onSelectionChanged(id, start, end): pushed from paint when the selection differs from the last report. Every selection change repaints, so this catches them all without touching each selection setter. - debugFragments() adds globalOffset, charLength, rubyText, rubyHeight so the inspector can show fragment boundaries in selection coordinates. Both hooks are part of isActive. New tests fail if the layout report or the change-only selection guard is removed.
New panel tabs and service extensions (devtools v2.x roadmap): - Timeline (ext.hyperRender.getTimeline): last 120 layout/paint samples per renderer (= per virtualized chunk) in a ring buffer, slowest first, paint sparkline, 1 s live polling. - Selection (ext.hyperRender.getSelection): selection range plus every text/ruby fragment's global [start, end) with the selected part highlighted and ruby text/height/position. - CSS Vars (ext.hyperRender.getCssVariables): read-only definition sites of custom properties. Live editing is NOT done: var() is substituted when HyperViewer resolves styles at parse time, so it needs a re-resolve trigger in the root package. - Export (ext.hyperRender.exportSnapshot): one self-describing JSON object; shown in a selectable dialog with a copy button, since clipboard access can be denied inside the DevTools iframe. Data assembly lives in plain-Dart files (lib/src/inspector_data.dart, devtools_ui/lib/src/inspector_logic.dart) with unit tests; a Chrome widget test drives every tab in demo mode. devtools_ui/test was gitignored as create-boilerplate; un-ignored for these tests. Requires hyper_render_core ^1.10.0 (the new hooks). Extension rebuilt and validated. ROADMAP: tick 3 of 4, mark CSS vars half done, and remove the never-implemented 'Float region visualizer' claim.
- RenderHyperBox.attach(): DevTools forgets a renderer's selection on detach, so a box re-attached with its selection intact (GlobalKey reparent, keep-alive) must re-report it. Regression test fails without the reset. - demo_mode_test is @teston('browser'): the panel pulls in dart:js_interop, so a plain VM 'flutter test' skips it instead of failing to load. - README: drop the 'float visualizer' claim (it never existed).
csslib parses var() as a VarUsage node whose span is only "--name)" /
"--name, fallback)" (the same quirk the FunctionTerm branch already
works around). _convertRuleSet kept that raw span, so the resolver saw
the value "--c)" and every var() in <style> / customCss produced
nothing, fallback included. Only inline style="" (a separate parser)
worked, and that is all the existing tests covered, while the CSS
matrix marked var() as supported.
Rebuild it as "var(" + span. Covers :root inheritance, fallback,
lengths in multi-term shorthands and nested var() fallbacks; all 4
new tests fail without the fix.
Override a --var from the DevTools CSS Vars tab and every HyperViewer
re-resolves its styles.
- core: StyleResolver.customPropertyOverrides replaces a custom
property's value at its single cascade write site, so every var()
read afterwards sees it, including one later in the same rule. A
trailing '* { --x: v !important }' cannot do that: var() is
substituted in the normal pass, before !important runs.
- core: HyperRenderDebugHooks.cssVariableOverrides (ValueNotifier).
- root: in debug builds HyperViewer listens and re-parses, passing the
overrides to all three resolver sites, including the compute()
isolate via the args record (statics are not shared across
isolates). No-op in release.
- devtools: ext.hyperRender.setCssVariable (empty value removes,
'*' clears all, non --names -> invalidParams); getCssVariables also
returns the active overrides. Panel: edit / undo per row, 'Reset N
overrides', override badge. The edit dialog owns its controller:
disposing it right after showDialog returned crashed the closing
TextField (caught by the demo widget test).
Verified live: a HyperViewer with :root { --brand: red } goes
red -> blue -> red through setCssVariable over vm_service. Root tests
cover sync and virtualized (async) modes and fail if the listener
does not re-parse. Extension rebuilt and validated.
var() was substituted while declarations were applied, so a --name
defined by a later, higher-specificity, inline or !important
declaration on the same element was missed, e.g.
'p { color: var(--c) } .x { --c: blue }' read the parent's --c. This
was hidden while stylesheet var() did not resolve at all (157a46c).
Compute the element's final custom properties first (rules by
specificity, inline, !important, with overrides applied), seed the
style with them, and make every --name write during the cascade store
that final value, so no var() observes an intermediate one. Skipped
entirely unless a rule or the inline style declares a custom property.
Four new tests fail without the pre-pass; the 8 golden failures are
the same set as on main (macOS fonts).
Also:
- root requires hyper_render_core ^1.10.0: HyperViewer now uses
StyleResolver.customPropertyOverrides and
HyperRenderDebugHooks.cssVariableOverrides, so a 1.9.x core cannot
compile it (the v1.7.1 lower-bound defect).
- test the compute() isolate parse path (useMicrotaskParsing: false,
runAsync): fails if the isolate resolver drops the overrides.
✅ Layout Regression — All fixtures within 60 FPS budget
One or more fixtures exceeded the 16 ms budget.
No action required. |
❌ Visual Regression Detected27 golden test(s) failed on Flutter 3.41.5 / ubuntu-22.04. The rendered output no longer matches the reference images. If the change is intentional, regenerate the goldens on the docker run --rm \
-v $(pwd):/workspace -w /workspace \
ghcr.io/cirruslabs/flutter:3.41.5 \
bash -c "apt-get update -qq && \
apt-get install -y fonts-noto fonts-noto-cjk fonts-roboto && \
flutter pub get && \
flutter test test/golden/ --update-goldens"
git add test/golden/goldens/
git commit -m "chore: update golden references (Flutter 3.41.5)"
|
…hing Making stylesheet var() work (157a46c) made untrusted <style> CSS (extracted before sanitization) live, so this audits that surface: - var() expansion is bounded: checked per substitution at 16 KB. A 10-refs x 10-levels --l chain would otherwise expand to ~10^10 chars from untrusted HTML. Over-long or empty results are invalid and the declaration is ignored, not applied empty. - url() and calc()/min()/max()/clamp() had the same csslib span quirk as var() (UriTerm/CalcTerm drop the function name), so stylesheet background-image: url() and width: calc() never worked either. A sweep of 50 value shapes found no other affected term types. - background/background-image URLs go through UrlSafety.isSafe (the <img src> policy). Nothing paints backgroundImage today, but it must not hold javascript:/file:/svg URLs once something does. - ':root' was unknown to the pseudo-class matcher and fell through to 'match', so it applied to EVERY element: :root { font-size: 125% } compounded per level, and :root { --c } reset descendant overrides. - Perf: var() lookup no longer merges local+inherited maps per declaration, rules cache whether they declare custom properties, and elements without their own custom properties share the parent's map. 300 :root vars x 2000 elements: 457 ms -> 37 ms. Tests: core stylesheet_var_security_test (bomb, cycles, 6 blocked schemes x sheet/var/inline), stylesheet_var_performance_test (ratio with vs without vars; ~15x before, ~1.4x now), :root and url/calc regressions; root HyperViewer-level <style> bomb + scheme tests and var/url/calc fuzz seeds. Each new test fails with its fix reverted.
- Pre-flight: root 'flutter analyze' reached into html/markdown/ highlight/math/epub, which only resolve against their own pubspec (200 uri_does_not_exist errors in a clean checkout). Exclude every sub-package except core from the root analysis and analyze each one in place after its own pub get. - Deploy Live Web Demo: Pages serves the gh-pages branch, but the job used actions/deploy-pages, whose github-pages environment does not allow main, so every run failed and the demo stayed on v1.7.1. Build and push to gh-pages instead (no repo settings change). - Core Validation: sub-package steps ran 'flutter test --no-pub' in directories that were never resolved; pub get first. Add the missing math, epub, devtools and devtools_ui (--platform chrome) test steps and their path filters.
✅ Layout Regression — All fixtures within 60 FPS budget
One or more fixtures exceeded the 16 ms budget.
No action required. |
❌ Visual Regression Detected27 golden test(s) failed on Flutter 3.41.5 / ubuntu-22.04. The rendered output no longer matches the reference images. If the change is intentional, regenerate the goldens on the docker run --rm \
-v $(pwd):/workspace -w /workspace \
ghcr.io/cirruslabs/flutter:3.41.5 \
bash -c "apt-get update -qq && \
apt-get install -y fonts-noto fonts-noto-cjk fonts-roboto && \
flutter pub get && \
flutter test test/golden/ --update-goldens"
git add test/golden/goldens/
git commit -m "chore: update golden references (Flutter 3.41.5)"
|
…s they found
- HtmlToSpanConverter (0% covered): text/inline/block/break, links +
recognizer disposal, display:none, image/media/formula/placeholder
atoms, ruby, and all three build helpers. Found: whitespace
normalization used Dart's \s, collapsing (U+00A0) — now the
CSS whitespace set (cssWhitespaceRun), like every other call site.
- FormulaWidget (0% covered): Greek, prefix-collision cases (\in vs
\infty/\int, \le vs \leq), frac/sqrt, scripts, customBuilder,
FormulaParser. Found: x^{2} / a_{i} (the usual LaTeX spelling) were
left as x^2 — braces were stripped only after the script tables ran.
- Paint paths (render_hyper_box_paint was 48%): rasterized pixel
checks for solid/dashed/dotted/double borders (+ dashed < solid),
block background + radius, inline background/border, debug bounds
(absent without the flag), selection highlight (before/after) and a
float placeholder. Notes in the file on why the Ahem test font needs
padding / transparent text, and why counts use straight RGBA.
…s sync Versions (only packages with changes since their last release move): - hyper_render_core 1.9.0 -> 1.10.0 (new debug hooks, stylesheet var()/url()/calc() fixes, :root, perf) - hyper_render 1.9.1 -> 1.10.0 (requires core ^1.10.0) - hyper_render_devtools 1.7.0 -> 1.8.0 (requires core ^1.10.0) - html/markdown/highlight/math/clipboard/epub: no code change, stay. Docs: LIMITATIONS claimed background-image url() as supported (matrix and code say nothing paints it); CSS matrix var()/calc() rows now say stylesheet resolution works as of 1.10.0 and min/max/clamp are not evaluated; MIGRATION_GUIDE gets an 'Upgrading to v1.10.0' section on the rendering changes; root ROADMAP milestone table was stale (v1.9.0 listed as diagramming, v1.7-1.8 as 'Current/Q3'); install snippets and CHANGELOG headings carry the new versions. Demo: integration_test/all_demos_test.dart drives the real macOS app through all 27 home demos and their hub sub-pages, failing on any FlutterError (including RenderFlex overflow, which the widget-test smoke net ignores). macOS deployment target raised 10.15 -> 12.0 (Podfile + Runner) — Xcode 26 rejects lower.
✅ Layout Regression — All fixtures within 60 FPS budget
One or more fixtures exceeded the 16 ms budget.
No action required. |
It ran 'flutter test test/integration/' with no device target, i.e. on the host VM: the same tests test-pr already runs on Linux. Its Android leg could not boot an x86 emulator on Apple-Silicon macos-latest (HVF error: HV_UNSUPPORTED) and both legs bill at the macOS rate (~10x Linux). Real on-device coverage is example/integration_test/ all_demos_test.dart, run locally with -d macos.
✅ Layout Regression — All fixtures within 60 FPS budget
One or more fixtures exceeded the 16 ms budget.
No action required. |
var()now resolvesvar()inside<style>/customCssrules never worked before this PR. csslib parsesvar()as aVarUsagenode whose span lacks thevar(prefix, so the resolver received"--c)"and produced nothing (fallback included). Only inlinestyle=""worked, and that is all the existing tests covered, while the CSS matrix markedvar()✅.157a46cfixes the parsing.32e3a68cascades custom properties before substitutingvar(), as CSS does. Until now a--namedefined by a later, higher-specificity, inline or!importantdeclaration on the same element was missed (e.g.p { color: var(--c) } .x { --c: blue }).Pages that use stylesheet
var()will render differently after upgrading (correctly, but visibly).DevTools v2.x (roadmap: all 4 items)
getTimelinegetSelection[start, end), selected part highlighted, ruby text/height/positiongetCssVariables,setCssVariable--varand everyHyperViewerre-resolves (debug only). A bareHyperRenderWidgetwith a prebuilt document is not re-resolved.exportSnapshotNew core API:
HyperRenderDebugHooks.onFrameTiming,onSelectionChangedandcssVariableOverrides;StyleResolver.customPropertyOverrides; extradebugFragments()fields. The timing/selection hooks only run in debug mode while a listener is set.Also: the never-implemented "Float region visualizer" claim has been removed from ROADMAP and README.
Release order
hyper_render_core1.10.0 →hyper_render1.10.0 (now requires core^1.10.0) →hyper_render_devtools(requires core^1.10.0). Core's pubspec version is not bumped in this PR.Verification
vm_serviceon a running macOS app; live edit went red → blue → red.hyper_render_html73,hyper_render_devtools19,devtools_ui7 (the widget test is@TestOn('browser'):flutter test --platform chrome).compute()isolate path.main, nothing new.extension/devtools/buildwas rebuilt by hand (devtools_extensions0.2.2build_and_copypasses the removed--web-rendererflag) and passesdevtools_extensions validate.changed_devtoolspath filter, but no job runs the devtools tests.