Skip to content

Apply view metrics overrides below the framework - #194311

Merged
auto-submit[bot] merged 27 commits into
flutter:masterfrom
victorsanni:view-metrics-overrides-apply
Oct 30, 2026
Merged

auto-submit[bot] merged 27 commits into
flutter:masterfrom
victorsanni:view-metrics-overrides-apply

Conversation

@victorsanni

@victorsanni victorsanni commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Part of flutter/devtools#9893 (the DevTools accessibility page). Design: flutter.dev/go/view-metrics-overrides.

#192764 added the debugViewMetricsOverrides registry and the ext.flutter.viewMetricsOverride service extension. This PR makes those overrides take effect, which the DevTools override controls depend on. Next up: flutter_test support for test views with their own ids, and moving ext.flutter.brightnessOverride onto the registry.

What this does

In debug mode, BindingBase.platformDispatcher wraps PlatformDispatcher.instance. The wrapper and its views report overridden metrics, so everything that reads them through the binding (MediaQuery, RenderView, gestures, semantics, image resolution, accessibility evaluations) sees the overrides without any override-specific code. Changing an override fires the same dart:ui callbacks a real platform change would.

RenderView, View, RawView and MediaQueryData.fromView map a raw engine view to the binding's wrapped one, so overrides apply to it too. Widget tests see overrides through TestWidgetsFlutterBinding, and explicit test values still take precedence. Profile and release builds are unchanged.

New API: debugApplyViewMetricsOverrides wraps a dispatcher, for bindings that supply their own, and debugViewWithMetricsOverrides maps a raw engine view to its wrapper.

Behavior changes and caveats

  • Debug only: View.of(context) and the binding's views are no longer identical to the ones PlatformDispatcher.instance reports. A view's platformDispatcher is a wrapper for that view, not PlatformDispatcher.instance or an app binding's dispatcher. Compare views by viewId.
  • Debug only: a FlutterView test double passed to View, RawView, RenderView or MediaQueryData.fromView must implement platformDispatcher and viewId.
  • TestPlatformDispatcher.scaleFontSize asks the wrapped dispatcher unless textScaleFactorTestValue is set, so non-linear text scaling applies in tests. A fake dispatcher without scaleFontSize still gets linear scaling.
  • An overridden physicalSize lays out at that size but still renders into the real surface, anchored top-left.
  • An ancestor MediaQuery still wins over platform-wide overrides, as it does over real platform settings. debugBrightnessOverride still wins over an overridden brightness.
  • ui.window, direct PlatformDispatcher.instance reads and engine-drawn content (platform views, system UI, keyboard) don't see overrides.
  • Text scalers compare by textScaleFactor, so a new textScaling table with the same first-point ratio only reaches already laid-out text when that text lays out again.

Tests

  • foundation/debug_view_metrics_override_test.dart: the wrappers, per metric and per view, plus view identity, normalization, change notifications and validation.
  • widgets/debug_view_metrics_override_test.dart: end to end through MediaQuery, views, layout, pointer events, semantics, images and accessibility evaluations, and precedence of explicit test values.
  • service_extensions_test.dart covers an override set through the extension. platform_dispatcher_test.dart covers scaleFontSize delegation and its fallback for fakes.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

In debug mode, BindingBase.platformDispatcher is now
ui.PlatformDispatcher.instance wrapped so that the entries of
debugViewMetricsOverrides apply to the view metrics it and its
FlutterViews report. Everything the framework reads through the binding
- MediaQueryData.fromView, the RenderView's ViewConfiguration, pointer
coordinate conversion, SemanticsBinding.accessibilityFeatures, image
resolution, accessibility evaluations - sees the overridden values, with
no override-aware code in any of those places.

- debugApplyViewMetricsOverrides installs the wrapper; BindingBase
  applies it to the engine singleton, TestWidgetsFlutterBinding to the
  dispatcher its TestPlatformDispatcher wraps. Explicit test values win.
- debugViewWithMetricsOverrides normalizes a raw dart:ui view to the one
  the binding vends; RenderView, View, RawView and MediaQueryData.fromView
  use it.
- debugSetViewMetricsOverride and debugClearViewMetricsOverrides diff
  the change by group and invoke exactly the dart:ui callbacks the
  platform would, in dart:ui's order, in the zones they were registered
  in, continuing past a callback that throws.
- Platform-wide metrics resolve per view through view.platformDispatcher
  and from the implicit view's entry on the binding's dispatcher.
- A physicalSize override makes the view fixed size and is the size an
  unsized scene renders at; textScaling tables are interpolated;
  displayFeatures move with an overridden device pixel ratio.

The wrappers implement the complete dart:ui interfaces with no
noSuchMethod, so an engine roll that adds a member breaks this file at
compile time rather than returning null to the framework. Nothing is
constructed outside of debug mode.

Part of flutter/devtools#9893
@github-actions github-actions Bot added a: tests "flutter test", flutter_test, or one of our tests framework flutter/packages/flutter repository. See also f: labels. labels Oct 13, 2026
@victorsanni victorsanni added the CICD Run CI/CD label Oct 13, 2026
Moves the wrappers from _view_metrics.dart into debug.dart, next to the
registry, and simplifies them: one module-level cache of view wrappers,
per-view dispatchers that need no back-reference to their root, and one
function that diffs an override change and announces it on every wrapped
dispatcher, continuing past a failing callback. The callbacks run in the
caller's zone, as TestPlatformDispatcher's own synthetic notifications do.

Stops passing an overridden physical size to FlutterView.render: the
engine discards frames that do not satisfy the real view constraints, so
the scene is rendered at the real size instead.

flutter_test wraps super.platformDispatcher instead of
PlatformDispatcher.instance, and the platform's own AccessibilityFeatures
object is reported unless a flag is actually overridden.
@victorsanni
victorsanni marked this pull request as ready for review October 14, 2026 01:32

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the application and propagation of debugViewMetricsOverrides in debug mode by wrapping the platform dispatcher and normalizing views across RenderView, MediaQueryData, View, and RawView. The review feedback identifies a potential memory leak of WeakReference objects in _debugWrappedDispatchers and potential StateError exceptions when textScaling is an empty list in textScaleFactor and scaleFontSize.

Review follow-ups:

- The calls to debugApplyViewMetricsOverrides and
  debugViewWithMetricsOverrides in BindingBase, RenderView,
  MediaQueryData.fromView and View/RawView are behind kDebugMode, so the
  mode gating is visible where the helpers are used. The helpers keep
  their own assert gating for any other caller.
- debug.dart imports dart:ui with an explicit show list again. The
  wrappers' implements clauses are what catch a member added to dart:ui;
  the import never did.
- The per-group change detection moves onto DebugViewMetricsOverride as
  _changesFrom, which == and the replay of dart:ui callbacks on an
  override change now share.
- The guarded reads of a view's dispatcher and id are explained at the
  call site, and the helper returns T? instead of a one-element record:
  both members are non-nullable in dart:ui.
- _debugWrapView is idempotent, like _debugWrapDispatcher.
- The comment on the dispatcher registry says when there is more than
  one wrapped dispatcher and why every one is notified.
@victorsanni
victorsanni requested a review from chunhtai October 22, 2026 01:06
binding.dart imports dart:ui as ui with a show list that has no FlutterView, so [ui.FlutterView.viewId] does not resolve now that the comment_references lint is enabled (flutter#194728). The unprefixed reference resolves through the file's @DocImport 'dart:ui', like the other FlutterView references in it.
The tests in service_extensions_test.dart share one binding, test each extension in one test, and run in a random order on CI. The separate end-to-end test for viewMetricsOverride left a frame scheduled and two extension events behind, so whichever tests the day's seed put after it failed: the ones that start by expecting no scheduled frame, and the viewMetricsOverride test, which counts that extension's events from zero.

The end-to-end check is now part of the viewMetricsOverride test, and that test runs any frame it leaves scheduled from a tear-down, so that a failing expectation cannot leave one behind either.
debugViewWithMetricsOverrides wrapped the engine's dispatcher as a side effect, only so that the lookup on the next line would succeed for a View constructed before the binding. The view wrapper is keyed by the engine view itself, so such a View already ends up with the view the binding vends later; the engine's dispatcher now just widens the condition.

Also corrects the BindingBase.platformDispatcher docs, which said TestWidgetsFlutterBinding applies debugApplyViewMetricsOverrides (it builds on super.platformDispatcher), and says on View.view and RawView.view that the normalization is debug-only, as RenderView's docs do.
debugViewWithMetricsOverrides read platformDispatcher and viewId through a
helper that treated a NoSuchMethodError or UnimplementedError as "not an
engine view". A test double handed to RenderView, View, RawView or
MediaQueryData.fromView now has to implement platformDispatcher, which the
one in the repository that did not now does.
The same kDebugMode-gated expression RenderView and MediaQueryData.fromView
use, so that nothing override-related is on the release path. The check
that a deprecated RenderView renders into the normalized view is an
initializer list assert again.
debugClearViewMetricsOverrides replayed the dart:ui callbacks once per
removed entry. The callbacks are not per view, so the groups the entries
changed are now combined and announced once.
TestPlatformDispatcher.scaleFontSize multiplied by textScaleFactor even
without a test value, so a DebugViewMetricsOverride.textScaling table of
more than one point reached widget tests as a linear factor. Without a
test value it now asks the dispatcher it wraps, so that dispatcher's own
curve applies. An explicit textScaleFactorTestValue still scales
linearly. Existing tests see the same values, because the engine returns
sizes unchanged at a factor of 1 and the web engine is linear.

The fake dispatcher in platform_dispatcher_test now implements
scaleFontSize instead of relying on the old behavior.
A textScaling table reports the ratio of its first point as
textScaleFactor. When that ratio is 1, SystemTextScaler equals
TextScaler.noScaling and TextStyle never asks it to scale, so a table
like [(8, 8), (24, 36)] scaled no text at all. dart:ui never reports
that state, because its scaleFontSize returns its input whenever
textScaleFactor is 1. Both fromJson and the direct gate now reject such
a table, as they reject the other tables they cannot apply.

The textScaling documentation also states that text already laid out
picks up a new table with the same first-point ratio only when it lays
out again, because SystemTextScalers compare by their factor.
debugViewWithMetricsOverrides wrapped any view that reported the
engine's dispatcher or a wrapped one. That included decorators around
engine views and test doubles, so in debug mode only, View.of returned
an object of a different type than the one passed to View, even with no
override installed. Now only the view the dispatcher itself reports for
the id is normalized, along with the deprecated window, which stands for
the implicit view. The window now normalizes to the same wrapper as the
implicit view instead of a separate one. Every other view is returned
unchanged.
@victorsanni
victorsanni requested a review from chunhtai October 29, 2026 19:02
TestPlatformDispatcher.scaleFontSize used to need only textScaleFactor.
Since it started asking the wrapped dispatcher, a fake dispatcher that
implements textScaleFactor but not scaleFontSize threw as soon as text
laid out. Such a fake now gets the linear scaling it got before, the
way TestFlutterView.devicePixelRatio already handles fakes. Dispatchers
that implement scaleFontSize, including the debug wrapper, still decide
the scaling themselves, so text scaling tables keep reaching text.

The platform_dispatcher_test fake no longer implements scaleFontSize, so
its existing test covers the fallback.
In debug mode, the view these return for an engine view is a wrapper
that applies debugViewMetricsOverrides. Neither the view nor its
platformDispatcher is identical to what PlatformDispatcher.instance
reports. State that where people look when they compare them.
chunhtai
chunhtai previously approved these changes Oct 30, 2026

@chunhtai chunhtai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

The widgets test spelled out Object? Function(MediaQueryData) six times
and the foundation test spelled out DebugViewMetricsOverride
Function(DebugViewMetricsOverride) six times. Name them _MetricReader
and _MetricChange. Most of the diff is dart format re-indenting the two
map literals around their shorter types.

@chunhtai chunhtai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@victorsanni victorsanni added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 30, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into flutter:master with commit 0c2e34f Oct 30, 2026
23 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 30, 2026
@victorsanni
victorsanni deleted the view-metrics-overrides-apply branch October 30, 2026 21:21
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Oct 31, 2026
flutter/flutter@5e805bf...ee8b482

2026-10-31 [email protected] Roll Skia from 9bab765df357 to 607de4b035dc (1 revision) (flutter/flutter#195728)
2026-10-31 [email protected] Roll Skia from 284054bec98a to 9bab765df357 (2 revisions) (flutter/flutter#195726)
2026-10-31 [email protected] Roll Skia from 3a3f56f7ddd4 to 284054bec98a (1 revision) (flutter/flutter#195724)
2026-10-31 [email protected] Roll Skia from 466994d27b1e to 3a3f56f7ddd4 (1 revision) (flutter/flutter#195719)
2026-10-30 [email protected] Fix XcodeProjectInterpreter dependency cycle in flutter_tools (flutter/flutter#195694)
2026-10-30 [email protected] Document that Positioned also works inside an Overlay (flutter/flutter#190607)
2026-10-30 [email protected] Roll Skia from ce0b85b46db6 to 466994d27b1e (3 revisions) (flutter/flutter#195711)
2026-10-30 [email protected] Adds Vulkan sync validation test run (flutter/flutter#195596)
2026-10-30 [email protected] [Android] Let the raster thread use every core of a chip without little cores (flutter/flutter#195071)
2026-10-30 [email protected] Roll Skia from 121d04c01e2d to ce0b85b46db6 (6 revisions) (flutter/flutter#195699)
2026-10-30 [email protected] [web] Anchor platform view slots to top-left to fix RTL document layout (flutter/flutter#194601)
2026-10-30 [email protected] Update golden file testing docs (flutter/flutter#195449)
2026-10-30 [email protected] [tool] Migrate Cache.flutterRoot static state to instance configuration (flutter/flutter#190795)
2026-10-30 [email protected] Apply view metrics overrides below the framework (flutter/flutter#194311)
2026-10-30 [email protected] Roll Skia from c0c46ee9f938 to 121d04c01e2d (9 revisions) (flutter/flutter#195687)
2026-10-30 [email protected] Use uniforms in the Porter Duff blend shader instead of specialization constants (flutter/flutter#195458)
2026-10-30 [email protected] [flutter_tools] Inject dependencies into iOS, macOS, and Windows application packages (flutter/flutter#194047)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
gmackall pushed a commit to gmackall/flutter that referenced this pull request Nov 3, 2026
Part of flutter/devtools#9893 (the DevTools accessibility page). Design:
[flutter.dev/go/view-metrics-overrides](https://flutter.dev/go/view-metrics-overrides).

flutter#192764 added the `debugViewMetricsOverrides` registry and the
`ext.flutter.viewMetricsOverride` service extension. This PR makes those
overrides take effect, which the DevTools override controls depend on.
Next up: `flutter_test` support for test views with their own ids, and
moving `ext.flutter.brightnessOverride` onto the registry.

## What this does

In debug mode, `BindingBase.platformDispatcher` wraps
`PlatformDispatcher.instance`. The wrapper and its views report
overridden metrics, so everything that reads them through the binding
(`MediaQuery`, `RenderView`, gestures, semantics, image resolution,
accessibility evaluations) sees the overrides without any
override-specific code. Changing an override fires the same `dart:ui`
callbacks a real platform change would.

`RenderView`, `View`, `RawView` and `MediaQueryData.fromView` map a raw
engine view to the binding's wrapped one, so overrides apply to it too.
Widget tests see overrides through `TestWidgetsFlutterBinding`, and
explicit test values still take precedence. Profile and release builds
are unchanged.

New API: `debugApplyViewMetricsOverrides` wraps a dispatcher, for
bindings that supply their own, and `debugViewWithMetricsOverrides` maps
a raw engine view to its wrapper.

## Behavior changes and caveats

- Debug only: `View.of(context)` and the binding's views are no longer
`identical` to the ones `PlatformDispatcher.instance` reports. A view's
`platformDispatcher` is a wrapper for that view, not
`PlatformDispatcher.instance` or an app binding's dispatcher. Compare
views by `viewId`.
- Debug only: a `FlutterView` test double passed to `View`, `RawView`,
`RenderView` or `MediaQueryData.fromView` must implement
`platformDispatcher` and `viewId`.
- `TestPlatformDispatcher.scaleFontSize` asks the wrapped dispatcher
unless `textScaleFactorTestValue` is set, so non-linear text scaling
applies in tests. A fake dispatcher without `scaleFontSize` still gets
linear scaling.
- An overridden `physicalSize` lays out at that size but still renders
into the real surface, anchored top-left.
- An ancestor `MediaQuery` still wins over platform-wide overrides, as
it does over real platform settings. `debugBrightnessOverride` still
wins over an overridden brightness.
- `ui.window`, direct `PlatformDispatcher.instance` reads and
engine-drawn content (platform views, system UI, keyboard) don't see
overrides.
- Text scalers compare by `textScaleFactor`, so a new `textScaling`
table with the same first-point ratio only reaches already laid-out text
when that text lays out again.

## Tests

- `foundation/debug_view_metrics_override_test.dart`: the wrappers, per
metric and per view, plus view identity, normalization, change
notifications and validation.
- `widgets/debug_view_metrics_override_test.dart`: end to end through
`MediaQuery`, views, layout, pointer events, semantics, images and
accessibility evaluations, and precedence of explicit test values.
- `service_extensions_test.dart` covers an override set through the
extension. `platform_dispatcher_test.dart` covers `scaleFontSize`
delegation and its fallback for fakes.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/CONTRIBUTING.ai.md
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: tests "flutter test", flutter_test, or one of our tests CICD Run CI/CD framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants