Skip to content

Instantly share code, notes, and snippets.

@eseidel
Created September 8, 2026 16:38
Show Gist options
  • Select an option

  • Save eseidel/0d662de7c2c4801e36aafd34e9f542f5 to your computer and use it in GitHub Desktop.

Select an option

Save eseidel/0d662de7c2c4801e36aafd34e9f542f5 to your computer and use it in GitHub Desktop.
preview_main.dart
// 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