diff --git a/engine/src/flutter/lib/web_ui/dev/generate_scene_test.dart b/engine/src/flutter/lib/web_ui/dev/generate_scene_test.dart new file mode 100644 index 0000000000..4da020825e --- /dev/null +++ b/engine/src/flutter/lib/web_ui/dev/generate_scene_test.dart @@ -0,0 +1,259 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This script takes a json file that comes from the `debugJsonOutput` of an +// `EngineScene` object and generates a function that builds a scene with the +// same dimensions and layering. This makes it easier to take a complex scene +// rendered by an app and whittle it down to a minimal test case. + +import 'dart:convert'; +import 'dart:io'; + +// A bunch of visually unique colors to cycle through. +const List colorStrings = [ + '0xFF7090da', + '0xFF9cb835', + '0xFF7c64d4', + '0xFF5fc250', + '0xFFc853be', + '0xFF4e8d2b', + '0xFFdb3f80', + '0xFF54bd7b', + '0xFFd2404e', + '0xFF53c4ad', + '0xFFd34a2a', + '0xFF46aed7', + '0xFFdc7a2d', + '0xFF6561a8', + '0xFFc0ab39', + '0xFFa44c8c', + '0xFF408147', + '0xFFd18dce', + '0xFF707822', + '0xFFae4462', + '0xFF308870', + '0xFFd99a37', + '0xFF935168', + '0xFF9eb46b', + '0xFFe1808a', + '0xFF627037', + '0xFFda815c', + '0xFF8e6c2b', + '0xFF9f4f30', + '0xFFd4a66f', +]; +int colorIndex = 0; + +String getNextColor() { + final colorString = colorStrings[colorIndex]; + colorIndex++; + colorIndex %= colorStrings.length; + return colorString; +} + +String getColorAsString() { + return 'const ui.Color(${getNextColor()})'; +} + +String offsetAsString(Object? offset) { + offset as Map?; + offset!; + final num x = offset['x']! as num; + final num y = offset['y']! as num; + if (x == 0 && y == 0) { + return 'ui.Offset.zero'; + } + return 'const ui.Offset($x, $y)'; +} + +String rRectAsString(Object? rRect) { + rRect as Map?; + rRect!; + return 'ui.RRect.fromLTRBAndCorners(' + '${rRect['left']}, ' + '${rRect['top']}, ' + '${rRect['right']}, ' + '${rRect['bottom']}, ' + 'topLeft: const ui.Radius.elliptical(${rRect['tlRadiusX']}, ${rRect['tlRadiusY']}), ' + 'topRight: const ui.Radius.elliptical(${rRect['trRadiusX']}, ${rRect['trRadiusY']}), ' + 'bottomRight: const ui.Radius.elliptical(${rRect['brRadiusX']}, ${rRect['brRadiusY']}), ' + 'bottomLeft: const ui.Radius.elliptical(${rRect['blRadiusX']}, ${rRect['blRadiusY']}))'; +} + +String rectAsString(Object? rect) { + rect as Map?; + rect!; + final num left = rect['left']! as num; + final num top = rect['top']! as num; + final num right = rect['right']! as num; + final num bottom = rect['bottom']! as num; + + if (left == 0 && top == 0 && right == 0 && bottom == 0) { + return 'ui.Rect.zero'; + } + return 'const ui.Rect.fromLTRB($left, $top, $right, $bottom)'; +} + +void emitShaderMaskOperation(Map operation, String indent) { + // TODO(jacksongardner): implement + throw UnimplementedError(); +} + +void emitTransformOperation(Map operation, String indent) { + final matrixValues = operation['matrix']! as List; + print('${indent}builder.pushTransform(Float64List.fromList([${matrixValues.join(', ')}]));'); +} + +void emitOpacityOperation(Map operation, String indent) { + final offset = operation['offset']! as Map; + print('${indent}builder.pushOpacity(${operation['alpha']}, offset: ${offsetAsString(offset)});'); +} + +void emitOffsetOperation(Map operation, String indent) { + final offset = operation['offset']! as Map; + print('${indent}builder.pushOffset(${offset['x']}, ${offset['y']});'); +} + +void emitImageFilterOperation(Map operation, String indent) { + // TODO(jacksongardner): implement + throw UnimplementedError(); +} + +void emitColorFilterOperation(Map operation, String indent) { + // TODO(jacksongardner): implement + throw UnimplementedError(); +} + +void emitClipRRectOperation(Map operation, String indent) { + print( + '${indent}builder.pushClipRRect(${rRectAsString(operation['rrect'])}, clipBehavior: ui.Clip.${operation['clip']});', + ); +} + +void emitClipRectOperation(Map operation, String indent) { + print( + '${indent}builder.pushClipRect(${rectAsString(operation['rect'])}, clipBehavior: ui.Clip.${operation['clip']});', + ); +} + +void emitClipPathOperation(Map operation, String indent) { + print( + '${indent}builder.pushClipPath(ui.Path()..addRect(${rectAsString(operation['pathBounds'])}), clipBehavior: ui.Clip.${operation['clip']});', + ); +} + +void emitBackdropFilterOperation(Map operation, String indent) { + // TODO(jacksongardner): implement + throw UnimplementedError(); +} + +void emitOperation(Object? operation, String indent) { + operation as Map?; + operation!; + switch (operation['type']) { + case 'backdropFilter': + emitBackdropFilterOperation(operation, indent); + case 'clipPath': + emitClipPathOperation(operation, indent); + case 'clipRect': + emitClipRectOperation(operation, indent); + case 'clipRRect': + emitClipRRectOperation(operation, indent); + case 'colorFilter': + emitColorFilterOperation(operation, indent); + case 'imageFilter': + emitImageFilterOperation(operation, indent); + case 'offset': + emitOffsetOperation(operation, indent); + case 'opacity': + emitOpacityOperation(operation, indent); + case 'transform': + emitTransformOperation(operation, indent); + case 'shaderMask': + emitShaderMaskOperation(operation, indent); + default: + throw ArgumentError('invalid operation type: ${operation['type']}'); + } +} + +void emitLayer(Map command, String indent) { + final layer = command['layer']! as Map; + print('$indent{'); + final String innerIndent = ' $indent'; + emitOperation(layer['operation'], innerIndent); + emitCommands(layer['commands'], innerIndent); + print('${innerIndent}builder.pop();'); + print('$indent}'); +} + +void emitPlatformView(Map command, String indent) { + final localBounds = command['localBounds']! as Map; + final left = localBounds['left']! as double; + final top = localBounds['top']! as double; + final right = localBounds['right']! as double; + final bottom = localBounds['bottom']! as double; + print( + '${indent}builder.addPlatformView(1, offset: const ui.Offset($left, $top), width: ${right - left}, height: ${bottom - top});', + ); +} + +void emitPicture(Map command, String indent) { + print( + '${indent}builder.addPicture(${offsetAsString(command['offset'])}, drawPicture(${rectAsString(command['localBounds'])}, ${getColorAsString()}));', + ); +} + +void emitCommands(Object? commands, String indent) { + commands as List?; + commands!; + for (final Object? command in commands) { + command as Map?; + command!; + switch (command['type']) { + case 'picture': + emitPicture(command, indent); + case 'platformView': + emitPlatformView(command, indent); + case 'layer': + emitLayer(command, indent); + } + } +} + +int main(List args) { + if (args.length != 1) { + stderr.write('Usage: dart generate_scene_test.dart .\n'); + return 1; + } + + final jsonFile = File(args[0]); + if (!jsonFile.existsSync()) { + stderr.write('Json file at path ${jsonFile.path} not found.\n'); + } + + final fileString = jsonFile.readAsStringSync(); + final sceneJson = jsonDecode(fileString) as Map; + final rootLayer = sceneJson['rootLayer']! as Map; + print(''' +// This file was generated from a JSON file using the `web_ui/dev/generate_scene_test.dart`. + +import 'dart:typed_data'; +import 'package:ui/ui.dart' as ui; + +ui.Picture drawPicture(ui.Rect bounds, ui.Color color) { + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder); + canvas.drawRect(bounds, ui.Paint()..color = color); + return recorder.endRecording(); +} + +ui.Scene buildScene() { + final ui.SceneBuilder builder = ui.SceneBuilder();'''); + emitCommands(rootLayer['commands'], ' '); + print(''' + + return builder.build(); +}'''); + return 0; +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index 6b0e16c5cc..1ca5c6f578 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -573,4 +573,7 @@ class CanvasKitRenderer implements Renderer { baseline: baseline, lineNumber: lineNumber, ); + + @override + void dumpDebugInfo() {} } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart index 627918cd5d..8e4ffc1e49 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -9,11 +9,12 @@ import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; import 'package:web_locale_keymap/web_locale_keymap.dart' as locale_keymap; -import '../engine.dart' show registerHotRestartListener; import 'dom.dart'; +import 'initialization.dart'; import 'key_map.g.dart'; import 'platform_dispatcher.dart'; import 'raw_keyboard.dart'; +import 'renderer.dart'; import 'semantics.dart'; typedef _VoidCallback = void Function(); @@ -600,6 +601,14 @@ class KeyboardConverter { return; } + if (kDebugMode && + event.key == 'F10' && + event.altKey && + event.type == 'keydown' && + !(event.repeat ?? false)) { + renderer.dumpDebugInfo(); + } + assert(_dispatchKeyData == null); bool sentAnyEvents = false; _dispatchKeyData = (ui.KeyData data) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart index 6c7f1773e5..c91fd8e75b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart @@ -41,6 +41,11 @@ class NoopOperation implements LayerOperation { @override String toString() => 'NoopOperation()'; + + @override + Map get debugJsonDescription { + return {'type': 'noop'}; + } } class BackdropFilterLayer with PictureEngineLayer implements ui.BackdropFilterEngineLayer { @@ -80,6 +85,15 @@ class BackdropFilterOperation implements LayerOperation { @override String toString() => 'BackdropFilterOperation(filter: $filter, mode: $mode)'; + + @override + Map get debugJsonDescription { + return { + 'type': 'backdropFilter', + 'filter': filter.toString(), + 'mode': mode.toString(), + }; + } } class ClipPathLayer with PictureEngineLayer implements ui.ClipPathEngineLayer { @@ -128,6 +142,21 @@ class ClipPathOperation implements LayerOperation { @override String toString() => 'ClipPathOperation(path: $path, clip: $clip)'; + + @override + Map get debugJsonDescription { + final ui.Rect bounds = path.getBounds(); + return { + 'type': 'clipPath', + 'pathBounds': { + 'left': bounds.left, + 'top': bounds.top, + 'right': bounds.right, + 'bottom': bounds.bottom, + }, + 'clip': clip.name, + }; + } } class ClipRectLayer with PictureEngineLayer implements ui.ClipRectEngineLayer { @@ -176,6 +205,15 @@ class ClipRectOperation implements LayerOperation { @override String toString() => 'ClipRectOperation(rect: $rect, clip: $clip)'; + + @override + Map get debugJsonDescription { + return { + 'type': 'clipRect', + 'rect': {'left': rect.left, 'top': rect.top, 'right': rect.right, 'bottom': rect.bottom}, + 'clip': clip.name, + }; + } } class ClipRRectLayer with PictureEngineLayer implements ui.ClipRRectEngineLayer { @@ -224,6 +262,28 @@ class ClipRRectOperation implements LayerOperation { @override String toString() => 'ClipRRectOperation(rrect: $rrect, clip: $clip)'; + + @override + Map get debugJsonDescription { + return { + 'type': 'clipRRect', + 'rrect': { + 'left': rrect.left, + 'top': rrect.top, + 'right': rrect.right, + 'bottom': rrect.bottom, + 'tlRadiusX': rrect.tlRadiusX, + 'tlRadiusY': rrect.tlRadiusY, + 'trRadiusX': rrect.trRadiusX, + 'trRadiusY': rrect.trRadiusY, + 'brRadiusX': rrect.brRadiusX, + 'brRadiusY': rrect.brRadiusY, + 'blRadiusX': rrect.blRadiusX, + 'blRadiusY': rrect.blRadiusY, + }, + 'clip': clip.name, + }; + } } class ClipRSuperellipseLayer with PictureEngineLayer implements ui.ClipRSuperellipseEngineLayer { @@ -274,6 +334,28 @@ class ClipRSuperellipseOperation implements LayerOperation { @override String toString() => 'ClipRSuperellipseOperation(rse: $rse, clip: $clip)'; + + @override + Map get debugJsonDescription { + return { + 'type': 'clipRSuperEllipse', + 'rse': { + 'left': rse.left, + 'top': rse.top, + 'right': rse.right, + 'bottom': rse.bottom, + 'tlRadiusX': rse.tlRadiusX, + 'tlRadiusY': rse.tlRadiusY, + 'trRadiusX': rse.trRadiusX, + 'trRadiusY': rse.trRadiusY, + 'brRadiusX': rse.brRadiusX, + 'brRadiusY': rse.brRadiusY, + 'blRadiusX': rse.blRadiusX, + 'blRadiusY': rse.blRadiusY, + }, + 'clip': clip.name, + }; + } } class ColorFilterLayer with PictureEngineLayer implements ui.ColorFilterEngineLayer { @@ -312,6 +394,11 @@ class ColorFilterOperation implements LayerOperation { @override String toString() => 'ColorFilterOperation(filter: $filter)'; + + @override + Map get debugJsonDescription { + return {'type': 'colorFilter', 'filter': filter.toString()}; + } } class ImageFilterLayer with PictureEngineLayer implements ui.ImageFilterEngineLayer { @@ -371,6 +458,15 @@ class ImageFilterOperation implements LayerOperation { @override String toString() => 'ImageFilterOperation(filter: $filter)'; + + @override + Map get debugJsonDescription { + return { + 'type': 'imageFilter', + 'filter': filter.toString(), + 'offset': {'x': offset.dx, 'y': offset.dy}, + }; + } } class OffsetLayer with PictureEngineLayer implements ui.OffsetEngineLayer { @@ -412,6 +508,14 @@ class OffsetOperation implements LayerOperation { @override String toString() => 'OffsetOperation(dx: $dx, dy: $dy)'; + + @override + Map get debugJsonDescription { + return { + 'type': 'offset', + 'offset': {'x': dx, 'y': dy}, + }; + } } class OpacityLayer with PictureEngineLayer implements ui.OpacityEngineLayer { @@ -464,6 +568,15 @@ class OpacityOperation implements LayerOperation { @override String toString() => 'OpacityOperation(offset: $offset, alpha: $alpha)'; + + @override + Map get debugJsonDescription { + return { + 'type': 'opacity', + 'alpha': alpha, + 'offset': {'x': offset.dx, 'y': offset.dy}, + }; + } } class TransformLayer with PictureEngineLayer implements ui.TransformEngineLayer { @@ -508,6 +621,11 @@ class TransformOperation implements LayerOperation { @override String toString() => 'TransformOperation(matrix: $matrix)'; + + @override + Map get debugJsonDescription { + return {'type': 'transform', 'matrix': transform.toList()}; + } } class ShaderMaskLayer with PictureEngineLayer implements ui.ShaderMaskEngineLayer { @@ -558,6 +676,20 @@ class ShaderMaskOperation implements LayerOperation { @override String toString() => 'ShaderMaskOperation(shader: $shader, maskRect: $maskRect, blendMode: $blendMode)'; + + @override + Map get debugJsonDescription { + return { + 'type': 'shaderMask', + 'shader': shader.toString(), + 'maskRect': { + 'left': maskRect.left, + 'top': maskRect.top, + 'right': maskRect.right, + 'bottom': maskRect.bottom, + }, + }; + } } class PlatformView { @@ -614,6 +746,13 @@ mixin PictureEngineLayer implements ui.EngineLayer { return 'PictureEngineLayer($operation)'; } + Map get debugJsonDescription { + return { + 'operation': operation.debugJsonDescription, + 'commands': drawCommands.map((c) => c.debugJsonDescription).toList(), + }; + } + bool get isSimple { if (slices.length > 1) { return false; @@ -643,9 +782,13 @@ abstract class LayerOperation { /// invoked even if it contains no pictures. (Most operations don't need to /// actually be performed at all if they don't contain any pictures.) bool get affectsBackdrop; + + Map get debugJsonDescription; } -sealed class LayerDrawCommand {} +sealed class LayerDrawCommand { + Map get debugJsonDescription; +} class PictureDrawCommand extends LayerDrawCommand { PictureDrawCommand(this.offset, this.picture, this.sliceIndex); @@ -653,6 +796,22 @@ class PictureDrawCommand extends LayerDrawCommand { final int sliceIndex; final ui.Offset offset; final ScenePicture picture; + + @override + Map get debugJsonDescription { + final bounds = picture.cullRect; + return { + 'type': 'picture', + 'sliceIndex': sliceIndex, + 'offset': {'x': offset.dx, 'y': offset.dy}, + 'localBounds': { + 'left': bounds.left, + 'top': bounds.top, + 'right': bounds.right, + 'bottom': bounds.bottom, + }, + }; + } } class PlatformViewDrawCommand extends LayerDrawCommand { @@ -661,12 +820,32 @@ class PlatformViewDrawCommand extends LayerDrawCommand { final int sliceIndex; final int viewId; final ui.Rect bounds; + + @override + Map get debugJsonDescription { + return { + 'type': 'platformView', + 'sliceIndex': sliceIndex, + 'viewId': viewId, + 'localBounds': { + 'left': bounds.left, + 'top': bounds.top, + 'right': bounds.right, + 'bottom': bounds.bottom, + }, + }; + } } class RetainedLayerDrawCommand extends LayerDrawCommand { RetainedLayerDrawCommand(this.layer); final PictureEngineLayer layer; + + @override + Map get debugJsonDescription { + return {'type': 'layer', 'layer': layer.debugJsonDescription}; + } } // Represents how a platform view should be positioned in the scene. @@ -883,6 +1062,11 @@ class PlatformViewNoClip implements PlatformViewClip { @override ui.Rect get outerRect => ui.Rect.largest; + + @override + String toString() { + return 'PlatformViewClip(none)'; + } } class PlatformViewRectClip implements PlatformViewClip { @@ -922,10 +1106,15 @@ class PlatformViewRectClip implements PlatformViewClip { @override ui.Rect get outerRect => rect; + + @override + String toString() { + return 'PlatformViewRectClip($rect)'; + } } class PlatformViewRRectClip implements PlatformViewClip { - PlatformViewRRectClip(this.rrect); + const PlatformViewRRectClip(this.rrect); final ui.RRect rrect; @@ -961,6 +1150,11 @@ class PlatformViewRRectClip implements PlatformViewClip { @override ui.Rect get outerRect => rrect.outerRect; + + @override + String toString() { + return 'PlatformViewRRectClip($rrect)'; + } } class PlatformViewPathClip implements PlatformViewClip { @@ -1001,6 +1195,11 @@ class PlatformViewPathClip implements PlatformViewClip { @override ui.Rect get outerRect => path.getBounds(); + + @override + String toString() { + return 'PlatformViewPathClip($path)'; + } } class LayerSliceBuilder { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart index db88d12f8a..df01560ea6 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart @@ -227,4 +227,6 @@ abstract class Renderer { ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style); Future renderScene(ui.Scene scene, EngineFlutterView view); + + void dumpDebugInfo(); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart index 1f5edb478e..45a782cd2d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart @@ -70,6 +70,10 @@ class EngineScene implements ui.Scene { } return recorder.endRecording().toImageSync(width, height); } + + Map get debugJsonDescription { + return {'rootLayer': rootLayer.debugJsonDescription}; + } } sealed class OcclusionMapNode { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart index cc2ea75835..6f9b71bc92 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; @@ -53,6 +54,9 @@ class EngineSceneView { _SceneRender? _currentRender; _SceneRender? _nextRender; + // Only populated in debug mode. + _SceneRender? _previousRender; + Future renderScene(EngineScene scene, FrameTimingRecorder? recorder) { if (_currentRender != null) { // If a scene is already queued up, drop it and queue this one up instead @@ -71,7 +75,7 @@ class EngineSceneView { Future _kickRenderLoop() async { final _SceneRender current = _currentRender!; await _renderScene(current.scene, current.recorder); - current.done(); + _renderComplete(current); _currentRender = _nextRender; _nextRender = null; if (_currentRender == null) { @@ -194,6 +198,37 @@ class EngineSceneView { } } } + + void _renderComplete(_SceneRender render) { + render.done(); + if (kDebugMode) { + _previousRender = render; + } + } + + String _generateDebugFilename() { + final now = DateTime.now(); + final String y = now.year.toString().padLeft(4, '0'); + final String mo = now.month.toString().padLeft(2, '0'); + final String d = now.day.toString().padLeft(2, '0'); + final String h = now.hour.toString().padLeft(2, '0'); + final String mi = now.minute.toString().padLeft(2, '0'); + final String s = now.second.toString().padLeft(2, '0'); + return 'flutter-scene-$y-$mo-$d-$h-$mi-$s.json'; + } + + void dumpDebugInfo() { + if (kDebugMode && _previousRender != null) { + final Map debugJson = _previousRender!.scene.debugJsonDescription; + final String jsonString = jsonEncode(debugJson); + final blob = createDomBlob([jsonString], {'type': 'application/json'}); + final url = domWindow.URL.createObjectURL(blob); + final element = domDocument.createElement('a'); + element.setAttribute('href', url); + element.setAttribute('download', _generateDebugFilename()); + element.click(); + } + } } sealed class SliceContainer { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index e60e9f7238..45b706e0a9 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -463,6 +463,13 @@ class SkwasmRenderer implements Renderer { imageCreateFromTextureSource(textureSource as JSObject, width, height, surface.handle), ); } + + @override + void dumpDebugInfo() { + for (final view in _sceneViews.values) { + view.dumpDebugInfo(); + } + } } class SkwasmPictureRenderer implements PictureRenderer { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart index 14e3c2743c..78958e14c5 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart @@ -324,6 +324,11 @@ class SkwasmRenderer implements Renderer { required int height, required bool transferOwnership, }) { - throw Exception('Skwasm not implemented on this platform.'); + throw UnimplementedError('Skwasm not implemented on this platform.'); + } + + @override + void dumpDebugInfo() { + throw UnimplementedError('Skwasm not implemented on this platform.'); } }