Created
September 8, 2026 16:38
-
-
Save eseidel/0d662de7c2c4801e36aafd34e9f542f5 to your computer and use it in GitHub Desktop.
preview_main.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Generated entry point. The build compiles this file, not the app's own | |
| // main.dart, and removes it again when the build finishes. | |
| // | |
| // The point of it is ordering: the safe-area binding has to exist before the | |
| // app calls runApp, and the font has to be registered before the first frame. | |
| // Nothing about the app's source can be assumed, so the app is imported as a | |
| // library and its main() is called last. | |
| // | |
| // In the real toolchain this is three files -- a generated entry point and two | |
| // shims copied in beside it. Here they are one file so it stands alone. | |
| import 'dart:js_interop'; | |
| import 'dart:typed_data'; | |
| import 'package:flutter/services.dart'; | |
| import 'package:flutter/widgets.dart'; | |
| // The app's own entrypoint, imported as a library. The generator writes this | |
| // path relative to lib/: `lib/main.dart` becomes 'main.dart', and | |
| // `lib/src/app.dart` becomes 'src/app.dart'. | |
| import 'main.dart' as app; | |
| void main() async { | |
| installViewInsets(); | |
| // Before runApp, so the first frame is already in the right typeface rather | |
| // than repainting out of Roboto a moment later. Never throws, and the host | |
| // serves the file from loopback, so this is not a network wait. | |
| await loadSystemFont(); | |
| app.main(); | |
| } | |
| // ---------------------------------------------------------------- safe area | |
| /// Installs the binding that applies the insets. | |
| /// | |
| /// Must run before the app's `runApp`, and before anything else touches a | |
| /// binding: `runApp` calls `WidgetsFlutterBinding.ensureInitialized`, which | |
| /// returns whichever binding already exists, so getting in first is all it | |
| /// takes to wrap an app whose source we do not touch. | |
| void installViewInsets() { | |
| // Constructed rather than checked for. `WidgetsBinding.instance` throws | |
| // when no binding exists yet, which is exactly the state this runs in. | |
| _PreviewBinding(); | |
| } | |
| // Gives a payload the safe area a Flutter app gets everywhere else. | |
| // | |
| // `EngineFlutterView` holds | |
| // `final ViewConfiguration _viewConfiguration = const ViewConfiguration()` | |
| // and never reassigns it, and `padding`, `viewPadding` and | |
| // `systemGestureInsets` all read from that const. So they are permanently | |
| // zero, `SafeArea` does nothing, and content sits under the status bar and the | |
| // home indicator. This puts real numbers back into `MediaQuery`. | |
| // | |
| // The keyboard is not part of this. `viewInsets` is computed separately, in | |
| // `window.dart` via `DimensionsProvider.computeKeyboardInsets`, which in full | |
| // page mode measures `visualViewport` against the physical height. That is the | |
| // calculation this file used to duplicate, and duplicating it was worse than | |
| // leaving it alone: on iOS the engine reads `documentElement.clientHeight` | |
| // while not editing and the visual viewport while editing, and a plain | |
| // `innerHeight - visualViewport.height` is not the same number. | |
| // | |
| // The view stays full screen, so this costs nothing: an app can still paint | |
| // behind the status bar and inset only its content, which is what insetting | |
| // the web view natively would have made impossible. | |
| // | |
| // The proper fix is in `web_ui`, which would do this for every Flutter web app | |
| // and delete this file. | |
| class _PreviewBinding extends WidgetsFlutterBinding { | |
| // `wrapWithDefaultView`, not `attachRootWidget`. `runApp` hands | |
| // `attachRootWidget` a tree that is already inside the `View`, and above the | |
| // `View` there is no `MediaQuery` to read or replace. This runs first, so | |
| // the override lands under `MediaQuery.fromView` where it can see the real | |
| // values and pass on corrected ones. | |
| @override | |
| Widget wrapWithDefaultView(Widget rootWidget) => | |
| super.wrapWithDefaultView(_ViewInsets(child: rootWidget)); | |
| } | |
| class _ViewInsets extends StatefulWidget { | |
| const _ViewInsets({required this.child}); | |
| final Widget child; | |
| @override | |
| State<_ViewInsets> createState() => _ViewInsetsState(); | |
| } | |
| class _ViewInsetsState extends State<_ViewInsets> { | |
| EdgeInsets _padding = EdgeInsets.zero; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| // Nothing here may take the app down with it. An app that will not boot is | |
| // far worse than one drawn under the status bar, so every failure ends in | |
| // zero insets and a running app. | |
| try { | |
| _addProbe(); | |
| _read(); | |
| // Rotation changes the insets. `previewinsets` is the host telling us | |
| // they moved, for hosts that supply them. | |
| _window.addEventListener('resize', _onChange.toJS); | |
| _window.addEventListener('orientationchange', _onChange.toJS); | |
| _window.addEventListener('previewinsets', _onChange.toJS); | |
| } on Object { | |
| // Zero insets, which is where Flutter Web starts anyway. | |
| } | |
| } | |
| void _onChange(JSObject _) { | |
| try { | |
| final before = _padding; | |
| _read(); | |
| if (before != _padding && mounted) setState(() {}); | |
| } on Object { | |
| // Keep the last good numbers. | |
| } | |
| } | |
| /// A zero-sized fixed element whose padding is the insets. | |
| /// | |
| /// Padding rather than custom properties: `getComputedStyle` resolves a real | |
| /// property to pixels, but hands back a custom property's text unevaluated, | |
| /// so `--x:env(safe-area-inset-top)` reads as the string | |
| /// "env(safe-area-inset-top)" and measures as nothing. | |
| _Element? _probe; | |
| void _addProbe() { | |
| final probe = _document.createElement('div'); | |
| probe.setAttribute( | |
| 'style', | |
| 'position:fixed;top:0;left:0;width:0;height:0;visibility:hidden;' | |
| 'pointer-events:none;' | |
| 'padding-top:env(safe-area-inset-top,0px);' | |
| 'padding-right:env(safe-area-inset-right,0px);' | |
| 'padding-bottom:env(safe-area-inset-bottom,0px);' | |
| 'padding-left:env(safe-area-inset-left,0px);', | |
| ); | |
| _document.body.appendChild(probe); | |
| _probe = probe; | |
| } | |
| /// Reads the browser's numbers into the fields. Never calls `setState`, so | |
| /// it is also usable from `initState`. | |
| void _read() { | |
| // A host that knows the insets is believed over the page's own reading of | |
| // them. `env(safe-area-inset-*)` reports zero inside a WKWebView even with | |
| // `viewport-fit=cover`, so on a phone the CSS route measures nothing and | |
| // the host's numbers are the only real ones. `env()` still answers in an | |
| // ordinary browser, which is what the probe is for. | |
| var padding = _hostInsets() ?? EdgeInsets.zero; | |
| if (padding == EdgeInsets.zero) { | |
| final probe = _probe; | |
| final style = probe == null ? null : _computedStyle(probe); | |
| padding = style == null | |
| ? EdgeInsets.zero | |
| : EdgeInsets.fromLTRB( | |
| _px(style.getPropertyValue('padding-left')), | |
| _px(style.getPropertyValue('padding-top')), | |
| _px(style.getPropertyValue('padding-right')), | |
| _px(style.getPropertyValue('padding-bottom')), | |
| ); | |
| } | |
| _padding = padding; | |
| } | |
| /// The insets the host published, if it published any. | |
| /// | |
| /// Read only. Nothing is called and nothing is sent back; this is a value | |
| /// the container leaves on the window. | |
| static EdgeInsets? _hostInsets() { | |
| final insets = _window.previewInsets; | |
| if (insets == null) return null; | |
| return EdgeInsets.fromLTRB( | |
| insets.left, | |
| insets.top, | |
| insets.right, | |
| insets.bottom, | |
| ); | |
| } | |
| static double _px(String value) => | |
| double.tryParse(value.trim().replaceAll('px', '')) ?? 0; | |
| @override | |
| Widget build(BuildContext context) { | |
| final media = MediaQuery.maybeOf(context); | |
| // Nothing to correct if there is nothing to correct: leave the tree alone | |
| // rather than inventing a MediaQuery the app did not have. | |
| if (media == null) return widget.child; | |
| // The keyboard covers the bottom inset rather than adding to it, the same | |
| // way it does on iOS. Whether it is up comes from the engine's own | |
| // `viewInsets`, so this reads that rather than measuring the viewport a | |
| // second time. `viewInsets` itself is passed through untouched. | |
| final bottom = media.viewInsets.bottom > 0 ? 0.0 : _padding.bottom; | |
| return MediaQuery( | |
| data: media.copyWith( | |
| padding: _padding.copyWith(bottom: bottom), | |
| viewPadding: _padding, | |
| ), | |
| child: widget.child, | |
| ); | |
| } | |
| } | |
| // -------------------------------------------------------------- system font | |
| // Draws a payload in the phone's own font instead of a downloaded stand-in. | |
| // | |
| // Material's iOS typography asks for `CupertinoSystemText` and | |
| // `CupertinoSystemDisplay` (`typography.dart`), not `.SF UI Text`. CanvasKit | |
| // can resolve neither, so it falls back to fetching one Roboto face from | |
| // fonts.gstatic.com: weight 400 only, which makes w100..w500 identical and | |
| // everything above it synthetically bold. That is the largest visible | |
| // difference between a payload and the same app built natively, measured at | |
| // 9.05% differing pixels on a typography screen against 1.37% when the faces | |
| // are actually present. | |
| // | |
| // WebKit will not hand a page the system font: `queryLocalFonts` is undefined | |
| // and `FontFace local("SF Pro Text")` rejects with a NetworkError. The | |
| // container is under no such rule. It reads /System/Library/Fonts/Core/SFUI.ttf | |
| // (whose path it asks CoreText for, rather than hardcoding) and serves it over | |
| // the loopback server the payload is already being served from. The bytes never | |
| // leave the phone they came from. | |
| // | |
| // Gaps only, never an override. These two families are ones no payload | |
| // declares, so an app shipping its own font is untouched. | |
| /// The families Material asks for on iOS and CanvasKit cannot resolve. | |
| const _families = ['CupertinoSystemText', 'CupertinoSystemDisplay']; | |
| /// What happened, for a payload that wants to say so on screen. | |
| /// | |
| /// There is no way to ask CanvasKit whether a family resolved, and "the text | |
| /// looks slightly different" is not something anyone can judge from memory. So | |
| /// the one component that knows records it. | |
| String systemFontStatus = 'not attempted'; | |
| /// How long the font cost, split into fetching it and registering it. | |
| /// | |
| /// This runs before `runApp`, so it is time the first frame waits for. The | |
| /// fetch is loopback, so it should be a disk read; the registration is | |
| /// CanvasKit parsing 7.9MB of variable font, which is the part nobody has a | |
| /// prior for. If the total is large the answer is to stop awaiting this and | |
| /// accept one frame in the wrong typeface. | |
| Duration systemFontFetch = Duration.zero; | |
| Duration systemFontRegister = Duration.zero; | |
| /// Loads the system font, if the host is serving one. | |
| /// | |
| /// The caller awaits this before `runApp`, so whatever it does the app does not | |
| /// start until it returns. That makes an unbounded wait here a black screen | |
| /// forever, caused by a feature whose worst honest outcome is the wrong | |
| /// typeface, so the wait is bounded and the work is not: on timeout the app | |
| /// starts in Roboto and the font lands a moment later, costing a reflow. | |
| Future<void> loadSystemFont({ | |
| Duration budget = const Duration(seconds: 2), | |
| }) { | |
| final loading = _load(); | |
| return loading.timeout( | |
| budget, | |
| onTimeout: () { | |
| systemFontStatus = 'still loading after ${budget.inMilliseconds}ms; ' | |
| 'started without it'; | |
| }, | |
| ); | |
| } | |
| Future<void> _load() async { | |
| try { | |
| final clock = Stopwatch()..start(); | |
| final bytes = await _fetch('_preview/font/regular.ttf'); | |
| systemFontFetch = clock.elapsed; | |
| if (bytes == null) { | |
| systemFontStatus = 'host served no font'; | |
| return; | |
| } | |
| clock.reset(); | |
| for (final family in _families) { | |
| final loader = FontLoader(family)..addFont(Future.value(bytes)); | |
| await loader.load(); | |
| } | |
| systemFontRegister = clock.elapsed; | |
| systemFontStatus = 'loaded ${bytes.lengthInBytes} bytes into ' | |
| '${_families.join(", ")}'; | |
| } on Object catch (e) { | |
| systemFontStatus = 'failed: $e'; | |
| } | |
| } | |
| /// The bytes at [path], relative to the page, or null if it is not served. | |
| /// | |
| /// Relative because the host serves the payload from an unguessable path | |
| /// segment it picks at runtime, and rewrites `<base href>` to match. Resolving | |
| /// against that base is what keeps this from having to know the port or the | |
| /// secret. | |
| Future<ByteData?> _fetch(String path) async { | |
| final response = await _jsFetch(path).toDart; | |
| if (!response.ok) return null; | |
| final buffer = await response.arrayBuffer().toDart; | |
| return ByteData.view(buffer.toDart); | |
| } | |
| // ------------------------------------------------------------ JS interop | |
| @JS('document') | |
| external _Document get _document; | |
| @JS('window') | |
| external _Window get _window; | |
| @JS('getComputedStyle') | |
| external _Style _computedStyle(_Element element); | |
| @JS('fetch') | |
| external JSPromise<_Response> _jsFetch(String url); | |
| extension type _Document._(JSObject _) implements JSObject { | |
| external _Element get body; | |
| external _Element createElement(String tag); | |
| } | |
| extension type _Element._(JSObject _) implements JSObject { | |
| external void setAttribute(String name, String value); | |
| external void appendChild(_Element child); | |
| } | |
| extension type _Style._(JSObject _) implements JSObject { | |
| external String getPropertyValue(String property); | |
| } | |
| extension type _Window._(JSObject _) implements JSObject { | |
| external void addEventListener(String type, JSFunction listener); | |
| external _HostInsets? get previewInsets; | |
| } | |
| extension type _HostInsets._(JSObject _) implements JSObject { | |
| external double get top; | |
| external double get right; | |
| external double get bottom; | |
| external double get left; | |
| } | |
| extension type _Response._(JSObject _) implements JSObject { | |
| external bool get ok; | |
| external JSPromise<JSArrayBuffer> arrayBuffer(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment