diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index e6fc889d9e2..e2eca45b00f 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -52156,6 +52156,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/semantics.dart + ../../../flutter/LICENS ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/app_bootstrap.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/arena.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart + ../../../flutter/LICENSE @@ -52217,6 +52218,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/lazy_path.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/context_menu.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/prevent_default.dart + ../../../flutter/LICENSE @@ -55182,6 +55184,7 @@ FILE: ../../../flutter/lib/web_ui/lib/semantics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/app_bootstrap.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/arena.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -55243,6 +55246,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/lazy_path.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/context_menu.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/prevent_default.dart diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index cf5b9920a01..4b3b9269eaa 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -17,6 +17,7 @@ library engine; export 'engine/alarm_clock.dart'; export 'engine/app_bootstrap.dart'; +export 'engine/arena.dart'; export 'engine/browser_detection.dart'; export 'engine/canvaskit/canvas.dart'; export 'engine/canvaskit/canvaskit_api.dart'; @@ -77,6 +78,7 @@ export 'engine/js_interop/js_typed_data.dart'; export 'engine/key_map.g.dart'; export 'engine/keyboard_binding.dart'; export 'engine/layers.dart'; +export 'engine/lazy_path.dart'; export 'engine/mouse/context_menu.dart'; export 'engine/mouse/cursor.dart'; export 'engine/mouse/prevent_default.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/arena.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/arena.dart new file mode 100644 index 00000000000..50080a62c2e --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/arena.dart @@ -0,0 +1,22 @@ +// 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. + +abstract class Collectable { + void collect(); +} + +class Arena { + List _collectables = []; + void add(Collectable collectable) { + _collectables.add(collectable); + } + + void collect() { + final List collectables = _collectables; + _collectables = []; + for (final collect in collectables) { + collect.collect(); + } + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart index a2af8cc4c81..b4b1c9e879a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart @@ -4,20 +4,9 @@ import 'dart:typed_data'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import '../validators.dart'; -import '../vector_math.dart'; -import 'canvas.dart'; -import 'canvaskit_api.dart'; -import 'image.dart'; -import 'painting.dart'; -import 'path.dart'; -import 'picture.dart'; -import 'picture_recorder.dart'; -import 'text.dart'; -import 'vertices.dart'; - /// An implementation of [ui.Canvas] that is backed by a CanvasKit canvas. class CanvasKitCanvas implements ui.Canvas { factory CanvasKitCanvas(ui.PictureRecorder recorder, [ui.Rect? cullRect]) { @@ -140,7 +129,7 @@ class CanvasKitCanvas implements ui.Canvas { @override void clipPath(ui.Path path, {bool doAntiAlias = true}) { - _canvas.clipPath(path as CkPath, doAntiAlias); + _canvas.clipPath((path as LazyPath).builtPath as CkPath, doAntiAlias); } @override @@ -264,7 +253,7 @@ class CanvasKitCanvas implements ui.Canvas { @override void drawPath(ui.Path path, ui.Paint paint) { - _canvas.drawPath(path as CkPath, paint as CkPaint); + _canvas.drawPath((path as LazyPath).builtPath as CkPath, paint as CkPaint); } @override @@ -436,6 +425,11 @@ class CanvasKitCanvas implements ui.Canvas { } void _drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) { - _canvas.drawShadow(path as CkPath, color, elevation, transparentOccluder); + _canvas.drawShadow( + (path as LazyPath).builtPath as CkPath, + color, + elevation, + transparentOccluder, + ); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart index 998dd94ccfa..9614dfdbdef 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart @@ -4,14 +4,9 @@ import 'dart:typed_data'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import '../vector_math.dart'; -import 'layer.dart'; -import 'layer_tree.dart'; -import 'path.dart'; -import 'picture.dart'; - class LayerScene implements ui.Scene { LayerScene(RootLayer rootLayer) : layerTree = LayerTree(rootLayer); @@ -114,7 +109,9 @@ class LayerSceneBuilder implements ui.SceneBuilder { ui.Clip clipBehavior = ui.Clip.antiAlias, ui.EngineLayer? oldLayer, }) { - return pushLayer(ClipPathEngineLayer(path as CkPath, clipBehavior)); + return pushLayer( + ClipPathEngineLayer((path as LazyPath).builtPath as CkPath, clipBehavior), + ); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart index 16b8be4af94..014126a60b3 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart @@ -7,7 +7,7 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; -import '../scene_painting.dart'; +import '../lazy_path.dart'; import '../vector_math.dart'; import 'canvaskit_api.dart'; import 'native_memory.dart'; @@ -16,7 +16,7 @@ import 'path_metrics.dart'; /// An implementation of [ui.Path] which is backed by an `SkPath`. /// /// The `SkPath` is required for `CkCanvas` methods which take a path. -class CkPath implements ScenePath { +class CkPath implements DisposablePath { factory CkPath() { final SkPath skPath = SkPath(); skPath.setFillType(toSkFillType(ui.PathFillType.nonZero)); @@ -44,6 +44,11 @@ class CkPath implements ScenePath { ui.PathFillType _fillType; + @override + void dispose() { + _ref.dispose(); + } + @override ui.PathFillType get fillType => _fillType; @@ -155,7 +160,7 @@ class CkPath implements ScenePath { } @override - ui.PathMetrics computeMetrics({bool forceClosed = false}) { + CkPathMetrics computeMetrics({bool forceClosed = false}) { return CkPathMetrics(this, forceClosed); } @@ -310,3 +315,13 @@ class CkPath implements ScenePath { return skiaObject.isEmpty(); } } + +class CkPathConstructors implements DisposablePathConstructors { + @override + CkPath createNew() => CkPath(); + + @override + CkPath combinePaths(ui.PathOperation operation, DisposablePath path1, DisposablePath path2) { + return CkPath.combine(operation, path1 as CkPath, path2 as CkPath); + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart index 72da6df96e0..76242d95037 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart @@ -5,13 +5,10 @@ import 'dart:collection'; import 'dart:typed_data'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'canvaskit_api.dart'; -import 'native_memory.dart'; -import 'path.dart'; - -class CkPathMetrics extends IterableBase implements ui.PathMetrics { +class CkPathMetrics extends IterableBase implements DisposablePathMetrics { CkPathMetrics(this._path, this._forceClosed); final CkPath _path; @@ -19,11 +16,11 @@ class CkPathMetrics extends IterableBase implements ui.PathMetric /// The [CkPath.isEmpty] case is special-cased to avoid booting the WASM machinery just to find out there are no contours. @override - late final Iterator iterator = + late final DisposablePathMetricIterator iterator = _path.isEmpty ? const CkPathMetricIteratorEmpty._() : CkContourMeasureIter(this); } -class CkContourMeasureIter implements Iterator { +class CkContourMeasureIter implements DisposablePathMetricIterator { CkContourMeasureIter(this._metrics) { _ref = UniqueRef( this, @@ -32,6 +29,11 @@ class CkContourMeasureIter implements Iterator { ); } + @override + void dispose() { + _ref.dispose(); + } + final CkPathMetrics _metrics; late final UniqueRef _ref; @@ -43,8 +45,8 @@ class CkContourMeasureIter implements Iterator { int _contourIndexCounter = 0; @override - ui.PathMetric get current { - final ui.PathMetric? currentMetric = _current; + CkContourMeasure get current { + final CkContourMeasure? currentMetric = _current; if (currentMetric == null) { throw RangeError( 'PathMetricIterator is not pointing to a PathMetric. This can happen in two situations:\n' @@ -71,7 +73,7 @@ class CkContourMeasureIter implements Iterator { } } -class CkContourMeasure implements ui.PathMetric { +class CkContourMeasure implements DisposablePathMetric { CkContourMeasure(this._metrics, SkContourMeasure skiaObject, this.contourIndex) { _ref = UniqueRef(this, skiaObject, 'PathMetric'); } @@ -85,11 +87,16 @@ class CkContourMeasure implements ui.PathMetric { SkContourMeasure get skiaObject => _ref.nativeObject; + @override + void dispose() { + _ref.dispose(); + } + @override final int contourIndex; @override - ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) { + CkPath extractPath(double start, double end, {bool startWithMoveTo = true}) { final SkPath skPath = skiaObject.getSegment(start, end, startWithMoveTo); return CkPath.fromSkPath(skPath, _metrics._path.fillType); } @@ -111,11 +118,11 @@ class CkContourMeasure implements ui.PathMetric { } } -class CkPathMetricIteratorEmpty implements Iterator { +class CkPathMetricIteratorEmpty implements DisposablePathMetricIterator { const CkPathMetricIteratorEmpty._(); @override - ui.PathMetric get current { + CkContourMeasure get current { throw RangeError('PathMetric iterator is empty.'); } @@ -123,4 +130,7 @@ class CkPathMetricIteratorEmpty implements Iterator { bool moveNext() { return false; } + + @override + void dispose() {} } 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 7c2686cd7ba..49f7368969c 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 @@ -328,15 +328,17 @@ class CanvasKitRenderer implements Renderer { ui.FilterQuality? filterQuality, ) => CkImageShader(image, tmx, tmy, matrix4, filterQuality); - @override - ui.Path createPath() => CkPath(); + CkPathConstructors pathConstructors = CkPathConstructors(); @override - ui.Path copyPath(ui.Path src) => CkPath.from(src as CkPath); + ui.Path createPath() => LazyPath(pathConstructors); + + @override + ui.Path copyPath(ui.Path src) => LazyPath.fromLazyPath(src as LazyPath); @override ui.Path combinePaths(ui.PathOperation op, ui.Path path1, ui.Path path2) => - CkPath.combine(op, path1, path2); + LazyPath.combined(op, path1 as LazyPath, path2 as LazyPath); @override ui.TextStyle createTextStyle({ diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart new file mode 100644 index 00000000000..f4ef312b054 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart @@ -0,0 +1,738 @@ +// 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. + +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +abstract class DisposablePath implements ScenePath { + @override + DisposablePathMetrics computeMetrics({bool forceClosed = false}); + + void dispose(); +} + +abstract class DisposablePathMetricIterator implements Iterator { + @override + DisposablePathMetric get current; + + void dispose(); +} + +abstract class DisposablePathMetrics implements ui.PathMetrics { + @override + DisposablePathMetricIterator get iterator; +} + +abstract class DisposablePathMetric implements ui.PathMetric { + @override + DisposablePath extractPath(double start, double end, {bool startWithMoveTo = true}); + + void dispose(); +} + +sealed class PathCommand { + void apply(DisposablePath path); +} + +final class MoveToCommand implements PathCommand { + MoveToCommand(this.x, this.y); + + final double x; + final double y; + + @override + void apply(DisposablePath path) { + path.moveTo(x, y); + } +} + +final class RelativeMoveToCommand implements PathCommand { + RelativeMoveToCommand(this.dx, this.dy); + + final double dx; + final double dy; + + @override + void apply(DisposablePath path) { + path.relativeMoveTo(dx, dy); + } +} + +final class LineToCommand implements PathCommand { + LineToCommand(this.x, this.y); + + final double x; + final double y; + + @override + void apply(DisposablePath path) { + path.lineTo(x, y); + } +} + +final class RelativeLineToCommand implements PathCommand { + RelativeLineToCommand(this.dx, this.dy); + + final double dx; + final double dy; + + @override + void apply(DisposablePath path) { + path.relativeLineTo(dx, dy); + } +} + +final class QuadraticBezierToCommand implements PathCommand { + QuadraticBezierToCommand(this.x1, this.y1, this.x2, this.y2); + + final double x1; + final double y1; + final double x2; + final double y2; + + @override + void apply(DisposablePath path) { + path.quadraticBezierTo(x1, y1, x2, y2); + } +} + +final class RelativeQuadraticBezierToCommand implements PathCommand { + RelativeQuadraticBezierToCommand(this.x1, this.y1, this.x2, this.y2); + + final double x1; + final double y1; + final double x2; + final double y2; + + @override + void apply(DisposablePath path) { + path.relativeQuadraticBezierTo(x1, y1, x2, y2); + } +} + +final class CubicToCommand implements PathCommand { + CubicToCommand(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); + + final double x1; + final double y1; + final double x2; + final double y2; + final double x3; + final double y3; + + @override + void apply(DisposablePath path) { + path.cubicTo(x1, y1, x2, y2, x3, y3); + } +} + +final class RelativeCubicToCommand implements PathCommand { + RelativeCubicToCommand(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); + + final double x1; + final double y1; + final double x2; + final double y2; + final double x3; + final double y3; + + @override + void apply(DisposablePath path) { + path.relativeCubicTo(x1, y1, x2, y2, x3, y3); + } +} + +final class ConicToCommand implements PathCommand { + ConicToCommand(this.x1, this.y1, this.x2, this.y2, this.w); + + final double x1; + final double y1; + final double x2; + final double y2; + final double w; + + @override + void apply(DisposablePath path) { + path.conicTo(x1, y1, x2, y2, w); + } +} + +final class RelativeConicToCommand implements PathCommand { + RelativeConicToCommand(this.x1, this.y1, this.x2, this.y2, this.w); + + final double x1; + final double y1; + final double x2; + final double y2; + final double w; + + @override + void apply(DisposablePath path) { + path.relativeConicTo(x1, y1, x2, y2, w); + } +} + +final class ArcToCommand implements PathCommand { + ArcToCommand(this.rect, this.startAngle, this.sweepAngle, this.forceMoveTo); + + final ui.Rect rect; + final double startAngle; + final double sweepAngle; + bool forceMoveTo; + + @override + void apply(DisposablePath path) { + path.arcTo(rect, startAngle, sweepAngle, forceMoveTo); + } +} + +final class ArcToPointCommand implements PathCommand { + ArcToPointCommand( + this.arcEnd, { + required this.radius, + required this.rotation, + required this.largeArc, + required this.clockwise, + }); + + ui.Offset arcEnd; + ui.Radius radius; + double rotation; + bool largeArc; + bool clockwise; + + @override + void apply(DisposablePath path) { + path.arcToPoint( + arcEnd, + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise, + ); + } +} + +final class RelativeArcToPointCommand implements PathCommand { + RelativeArcToPointCommand( + this.arcEndDelta, { + required this.radius, + required this.rotation, + required this.largeArc, + required this.clockwise, + }); + + ui.Offset arcEndDelta; + ui.Radius radius; + double rotation; + bool largeArc; + bool clockwise; + + @override + void apply(DisposablePath path) { + path.relativeArcToPoint( + arcEndDelta, + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise, + ); + } +} + +final class AddRectCommand implements PathCommand { + AddRectCommand(this.rect); + + final ui.Rect rect; + + @override + void apply(DisposablePath path) { + path.addRect(rect); + } +} + +final class AddOvalCommand implements PathCommand { + AddOvalCommand(this.oval); + + final ui.Rect oval; + + @override + void apply(DisposablePath path) { + path.addOval(oval); + } +} + +final class AddArcCommand implements PathCommand { + AddArcCommand(this.oval, this.startAngle, this.sweepAngle); + + final ui.Rect oval; + final double startAngle; + final double sweepAngle; + + @override + void apply(DisposablePath path) { + path.addArc(oval, startAngle, sweepAngle); + } +} + +final class AddPolygonCommand implements PathCommand { + AddPolygonCommand(this.points, this.close); + + final List points; + final bool close; + + @override + void apply(DisposablePath path) { + path.addPolygon(points, close); + } +} + +final class AddRRectCommand implements PathCommand { + AddRRectCommand(this.rrect); + + final ui.RRect rrect; + + @override + void apply(DisposablePath path) { + path.addRRect(rrect); + } +} + +final class AddRSuperellipseCommand implements PathCommand { + AddRSuperellipseCommand(this.rSuperellipse); + + final ui.RSuperellipse rSuperellipse; + + @override + void apply(DisposablePath path) { + path.addRSuperellipse(rSuperellipse); + } +} + +final class AddPathCommand implements PathCommand { + AddPathCommand(this.path, this.offset, {this.matrix4}); + + final LazyPath path; + final ui.Offset offset; + final Float64List? matrix4; + + @override + void apply(DisposablePath p) { + p.addPath(path.builtPath, offset, matrix4: matrix4); + } +} + +final class ExtendWithPathCommand implements PathCommand { + ExtendWithPathCommand(this.path, this.offset, {this.matrix4}); + + final LazyPath path; + final ui.Offset offset; + final Float64List? matrix4; + + @override + void apply(DisposablePath p) { + p.extendWithPath(path.builtPath, offset, matrix4: matrix4); + } +} + +final class ClosePathCommand implements PathCommand { + @override + void apply(DisposablePath path) { + path.close(); + } +} + +abstract class DisposablePathConstructors { + DisposablePath createNew(); + DisposablePath combinePaths( + ui.PathOperation operation, + DisposablePath path1, + DisposablePath path2, + ); +} + +class LazyPath implements ScenePath, Collectable { + factory LazyPath(DisposablePathConstructors constructors) => + LazyPath._(constructors, ui.PathFillType.nonZero, () => constructors.createNew()); + LazyPath._(this.constructors, this._fillType, this.initializer) : _commands = []; + LazyPath.fromLazyPath(LazyPath other) + : _fillType = other._fillType, + constructors = other.constructors, + initializer = other.initializer, + _commands = List.from(other._commands); + factory LazyPath.shifted(LazyPath basePath, ui.Offset offset) { + final pathCopy = LazyPath.fromLazyPath(basePath); + return LazyPath._( + pathCopy.constructors, + pathCopy._fillType, + () => pathCopy.builtPath.shift(offset) as DisposablePath, + ); + } + factory LazyPath.transformed(LazyPath basePath, Float64List matrix4) { + final pathCopy = LazyPath.fromLazyPath(basePath); + return LazyPath._( + pathCopy.constructors, + pathCopy._fillType, + () => pathCopy.builtPath.transform(matrix4) as DisposablePath, + ); + } + factory LazyPath.combined(ui.PathOperation operation, LazyPath path1, LazyPath path2) { + final pathCopy1 = LazyPath.fromLazyPath(path1); + final pathCopy2 = LazyPath.fromLazyPath(path2); + return LazyPath._( + pathCopy1.constructors, + pathCopy1._fillType, + () => + pathCopy1.constructors.combinePaths(operation, pathCopy1.builtPath, pathCopy2.builtPath), + ); + } + LazyPath extracted( + LazyPathMetric pathMetric, + double start, + double end, { + bool startWithMoveTo = true, + }) { + return LazyPath._( + constructors, + pathMetric.iterator.path._fillType, + () => pathMetric.buildExtractedPath(start, end, startWithMoveTo: startWithMoveTo), + ); + } + + DisposablePathConstructors constructors; + DisposablePath Function() initializer; + + ui.PathFillType _fillType; + + @override + ui.PathFillType get fillType => _fillType; + + @override + set fillType(ui.PathFillType fillType) { + _fillType = fillType; + _cachedPath?.fillType = fillType; + } + + DisposablePath? _cachedPath; + final List _commands; + + DisposablePath get builtPath { + if (_cachedPath != null) { + return _cachedPath!; + } + final DisposablePath path = initializer(); + path.fillType = _fillType; + for (final command in _commands) { + command.apply(path); + } + + _cachedPath = path; + EnginePlatformDispatcher.instance.frameArena.add(this); + return path; + } + + void _addCommand(PathCommand command) { + _commands.add(command); + if (_cachedPath != null) { + command.apply(_cachedPath!); + } + } + + @override + void moveTo(double x, double y) { + _addCommand(MoveToCommand(x, y)); + } + + @override + void relativeMoveTo(double dx, double dy) { + _addCommand(RelativeMoveToCommand(dx, dy)); + } + + @override + void lineTo(double x, double y) { + _addCommand(LineToCommand(x, y)); + } + + @override + void relativeLineTo(double dx, double dy) { + _addCommand(RelativeLineToCommand(dx, dy)); + } + + @override + void quadraticBezierTo(double x1, double y1, double x2, double y2) { + _addCommand(QuadraticBezierToCommand(x1, y1, x2, y2)); + } + + @override + void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { + _addCommand(RelativeQuadraticBezierToCommand(x1, y1, x2, y2)); + } + + @override + void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + _addCommand(CubicToCommand(x1, y1, x2, y2, x3, y3)); + } + + @override + void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + _addCommand(RelativeCubicToCommand(x1, y1, x2, y2, x3, y3)); + } + + @override + void conicTo(double x1, double y1, double x2, double y2, double w) { + _addCommand(ConicToCommand(x1, y1, x2, y2, w)); + } + + @override + void relativeConicTo(double x1, double y1, double x2, double y2, double w) { + _addCommand(RelativeConicToCommand(x1, y1, x2, y2, w)); + } + + @override + void arcTo(ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { + _addCommand(ArcToCommand(rect, startAngle, sweepAngle, forceMoveTo)); + } + + @override + void arcToPoint( + ui.Offset arcEnd, { + ui.Radius radius = ui.Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + _addCommand( + ArcToPointCommand( + arcEnd, + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise, + ), + ); + } + + @override + void relativeArcToPoint( + ui.Offset arcEndDelta, { + ui.Radius radius = ui.Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + _addCommand( + RelativeArcToPointCommand( + arcEndDelta, + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise, + ), + ); + } + + @override + void addRect(ui.Rect rect) { + _addCommand(AddRectCommand(rect)); + } + + @override + void addOval(ui.Rect oval) { + _addCommand(AddOvalCommand(oval)); + } + + @override + void addArc(ui.Rect oval, double startAngle, double sweepAngle) { + _addCommand(AddArcCommand(oval, startAngle, sweepAngle)); + } + + @override + void addPolygon(List points, bool close) { + _addCommand(AddPolygonCommand(points, close)); + } + + @override + void addRRect(ui.RRect rrect) { + _addCommand(AddRRectCommand(rrect)); + } + + @override + void addRSuperellipse(ui.RSuperellipse rSuperellipse) { + _addCommand(AddRSuperellipseCommand(rSuperellipse)); + } + + @override + void addPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) { + _addCommand(AddPathCommand(path as LazyPath, offset, matrix4: matrix4)); + } + + @override + void extendWithPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) { + _addCommand(ExtendWithPathCommand(path as LazyPath, offset, matrix4: matrix4)); + } + + @override + void close() { + _addCommand(ClosePathCommand()); + } + + @override + void reset() { + _commands.clear(); + _fillType = ui.PathFillType.nonZero; + _cachedPath?.dispose(); + _cachedPath = null; + initializer = constructors.createNew; + } + + @override + bool contains(ui.Offset point) { + return builtPath.contains(point); + } + + @override + ui.Path shift(ui.Offset offset) { + return LazyPath.shifted(this, offset); + } + + @override + ui.Path transform(Float64List matrix4) { + return LazyPath.transformed(this, matrix4); + } + + @override + ui.Rect getBounds() { + return builtPath.getBounds(); + } + + @override + LazyPathMetrics computeMetrics({bool forceClosed = false}) { + return LazyPathMetrics(path: this, forceClosed: forceClosed); + } + + @override + void collect() { + _cachedPath?.dispose(); + _cachedPath = null; + } + + @override + String toSvgString() { + return builtPath.toSvgString(); + } +} + +class LazyPathMetrics extends IterableBase implements ui.PathMetrics { + LazyPathMetrics({required LazyPath path, required bool forceClosed}) + : iterator = LazyPathMetricIterator(path, forceClosed); + + @override + final LazyPathMetricIterator iterator; +} + +class LazyPathMetricIterator implements Iterator, Collectable { + LazyPathMetricIterator(this.path, this.forceClosed); + + final LazyPath path; + final bool forceClosed; + int _nextIndex = 0; + DisposablePathMetricIterator? _cachedIterator; + final List _metrics = []; + bool _isAtEnd = false; + + @override + ui.PathMetric get current { + if (_nextIndex == 0 || _isAtEnd) { + throw RangeError( + 'PathMetricIterator is not pointing to a PathMetric. This can happen in two situations:\n' + '- The iteration has not started yet. If so, call "moveNext" to start iteration.\n' + '- The iterator ran out of elements. If so, check that "moveNext" returns true prior to calling "current".', + ); + } + return LazyPathMetric(this, _nextIndex - 1); + } + + @override + bool moveNext() { + if (_isAtEnd) { + return false; + } + buildIterator(); + assert(_cachedIterator != null); + assert(_nextIndex == _metrics.length); + _nextIndex++; + if (_cachedIterator!.moveNext()) { + _metrics.add(_cachedIterator!.current); + return true; + } else { + _isAtEnd = true; + return false; + } + } + + @override + void collect() { + _cachedIterator?.dispose(); + _cachedIterator = null; + + for (final metric in _metrics) { + metric.dispose(); + } + _metrics.clear(); + } + + void buildIterator() { + if (_cachedIterator != null) { + return; + } + _cachedIterator = path.builtPath.computeMetrics(forceClosed: forceClosed).iterator; + for (int i = 0; i < _nextIndex; i++) { + if (_cachedIterator!.moveNext()) { + _metrics.add(_cachedIterator!.current); + } else { + break; + } + } + EnginePlatformDispatcher.instance.frameArena.add(this); + } + + DisposablePathMetric builtMetricAtIndex(int index) { + buildIterator(); + return _metrics[index]; + } +} + +class LazyPathMetric implements ui.PathMetric { + LazyPathMetric(this.iterator, this.contourIndex); + + final LazyPathMetricIterator iterator; + + @override + final int contourIndex; + + DisposablePathMetric get builtMetric => iterator.builtMetricAtIndex(contourIndex); + + @override + ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) { + return iterator.path.extracted(this, start, end, startWithMoveTo: startWithMoveTo); + } + + DisposablePath buildExtractedPath(double start, double end, {required bool startWithMoveTo}) { + return builtMetric.extractPath(start, end, startWithMoveTo: startWithMoveTo); + } + + @override + ui.Tangent? getTangentForOffset(double distance) { + return builtMetric.getTangentForOffset(distance); + } + + @override + bool get isClosed => builtMetric.isClosed; + + @override + double get length => builtMetric.length; +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 5bf16d1ec8c..bb42f9c8736 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -44,6 +44,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { late StreamSubscription _onViewDisposedListener; + final Arena frameArena = Arena(); + /// The [EnginePlatformDispatcher] singleton. static EnginePlatformDispatcher get instance => _instance; static final EnginePlatformDispatcher _instance = EnginePlatformDispatcher(); @@ -260,6 +262,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _viewsRenderedInCurrentFrame = {}; invoke(_onDrawFrame, _onDrawFrameZone); _viewsRenderedInCurrentFrame = null; + frameArena.collect(); } /// A callback that is invoked when pointer data is available. @@ -740,7 +743,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { // order to perform golden tests in Flutter framework because on the HTML // renderer, golden tests render to DOM and then take a browser screenshot, // https://github.com/flutter/flutter/issues/137073. - if (shouldRender || renderer.rendererTag == 'html') { + if (shouldRender) { await renderer.renderScene(scene, target); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart index fee2a4eaf52..1049dbd35ab 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart @@ -143,8 +143,7 @@ class SkwasmCanvas implements SceneCanvas { @override void clipPath(ui.Path path, {bool doAntiAlias = true}) { - path as SkwasmPath; - canvasClipPath(_handle, path.handle, doAntiAlias); + canvasClipPath(_handle, ((path as LazyPath).builtPath as SkwasmPath).handle, doAntiAlias); } @override @@ -238,9 +237,8 @@ class SkwasmCanvas implements SceneCanvas { @override void drawPath(ui.Path path, ui.Paint paint) { - path as SkwasmPath; final paintHandle = (paint as SkwasmPaint).toRawPaint(); - canvasDrawPath(_handle, path.handle, paintHandle); + canvasDrawPath(_handle, ((path as LazyPath).builtPath as SkwasmPath).handle, paintHandle); paintDispose(paintHandle); } @@ -391,10 +389,9 @@ class SkwasmCanvas implements SceneCanvas { @override void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) { - path as SkwasmPath; canvasDrawShadow( _handle, - path.handle, + ((path as LazyPath).builtPath as SkwasmPath).handle, elevation, EngineFlutterDisplay.instance.devicePixelRatio, color.value, diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart index b1753ddb7e8..f778f9cee57 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart @@ -14,7 +14,7 @@ enum PathDirection { clockwise, counterClockwise } enum PathArcSize { small, large } -class SkwasmPath extends SkwasmObjectWrapper implements ScenePath { +class SkwasmPath extends SkwasmObjectWrapper implements ScenePath, DisposablePath { factory SkwasmPath() { return SkwasmPath.fromHandle(pathCreate()); } @@ -234,7 +234,7 @@ class SkwasmPath extends SkwasmObjectWrapper implements ScenePath { SkwasmPath.fromHandle(pathCombine(operation.index, path1.handle, path2.handle)); @override - ui.PathMetrics computeMetrics({bool forceClosed = false}) { + SkwasmPathMetrics computeMetrics({bool forceClosed = false}) { return SkwasmPathMetrics(path: this, forceClosed: forceClosed); } @@ -249,3 +249,13 @@ class SkwasmPath extends SkwasmObjectWrapper implements ScenePath { return svgString; } } + +class SkwasmPathConstructors implements DisposablePathConstructors { + @override + SkwasmPath createNew() => SkwasmPath(); + + @override + SkwasmPath combinePaths(ui.PathOperation operation, DisposablePath path1, DisposablePath path2) { + return SkwasmPath.combine(operation, path1 as SkwasmPath, path2 as SkwasmPath); + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path_metrics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path_metrics.dart index bb443109890..1035a0bd1ee 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path_metrics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path_metrics.dart @@ -5,21 +5,22 @@ import 'dart:collection'; import 'dart:ffi'; +import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; -class SkwasmPathMetrics extends IterableBase implements ui.PathMetrics { +class SkwasmPathMetrics extends IterableBase implements DisposablePathMetrics { SkwasmPathMetrics({required this.path, required this.forceClosed}); SkwasmPath path; bool forceClosed; @override - late Iterator iterator = SkwasmPathMetricIterator(path, forceClosed); + late DisposablePathMetricIterator iterator = SkwasmPathMetricIterator(path, forceClosed); } class SkwasmPathMetricIterator extends SkwasmObjectWrapper - implements Iterator { + implements DisposablePathMetricIterator { SkwasmPathMetricIterator(SkwasmPath path, bool forceClosed) : super(contourMeasureIterCreate(path.handle, forceClosed, 1.0), _registry); @@ -32,7 +33,7 @@ class SkwasmPathMetricIterator extends SkwasmObjectWrapper implements ui.PathMetric { +class SkwasmPathMetric extends SkwasmObjectWrapper + implements DisposablePathMetric { SkwasmPathMetric(ContourMeasureHandle handle, this.contourIndex) : super(handle, _registry); static final SkwasmFinalizationRegistry _registry = @@ -69,7 +71,7 @@ class SkwasmPathMetric extends SkwasmObjectWrapper implements final int contourIndex; @override - ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) { + DisposablePath extractPath(double start, double end, {bool startWithMoveTo = true}) { return SkwasmPath.fromHandle(contourMeasureGetSegment(handle, start, end, startWithMoveTo)); } 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 ea07ea716a0..eee758c0e64 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 @@ -21,17 +21,19 @@ class SkwasmRenderer implements Renderer { bool get isMultiThreaded => skwasmIsMultiThreaded(); + SkwasmPathConstructors pathConstructors = SkwasmPathConstructors(); + @override final SkwasmFontCollection fontCollection = SkwasmFontCollection(); @override ui.Path combinePaths(ui.PathOperation op, ui.Path path1, ui.Path path2) { - return SkwasmPath.combine(op, path1 as SkwasmPath, path2 as SkwasmPath); + return LazyPath.combined(op, path1 as LazyPath, path2 as LazyPath); } @override ui.Path copyPath(ui.Path src) { - return SkwasmPath.from(src as SkwasmPath); + return LazyPath.fromLazyPath(src as LazyPath); } @override @@ -153,7 +155,7 @@ class SkwasmRenderer implements Renderer { ); @override - ui.Path createPath() => SkwasmPath(); + ui.Path createPath() => LazyPath(pathConstructors); @override ui.PictureRecorder createPictureRecorder() => SkwasmPictureRecorder(); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 806dcf2e31a..f121cf16dbb 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -206,23 +206,21 @@ void _pathOpTests() { }); test('Path.combine test', () { - final ui.Path path1 = ui.Path(); - expect(path1, isA()); + final CkPath path1 = CkPath(); path1.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); path1.addOval(const ui.Rect.fromLTRB(10, 10, 100, 100)); - final ui.Path path2 = ui.Path(); - expect(path2, isA()); + final CkPath path2 = CkPath(); path2.addRect(const ui.Rect.fromLTRB(5, 5, 15, 15)); path2.addOval(const ui.Rect.fromLTRB(15, 15, 105, 105)); - final ui.Path union = ui.Path.combine(ui.PathOperation.union, path1, path2); + final ui.Path union = CkPath.combine(ui.PathOperation.union, path1, path2); expect(union, isA()); expect(union.getBounds(), const ui.Rect.fromLTRB(0, 0, 105, 105)); // Smoke-test other operations. for (final ui.PathOperation operation in ui.PathOperation.values) { - final ui.Path combined = ui.Path.combine(operation, path1, path2); + final ui.Path combined = CkPath.combine(operation, path1, path2); expect(combined, isA()); } }); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart index ef884a2df54..8bdb7b7eeef 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart @@ -23,8 +23,7 @@ void testMain() { }); test(CkPathMetrics, () { - final ui.Path path = ui.Path(); - expect(path, isA()); + final CkPath path = CkPath(); expect(path.computeMetrics().length, 0); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); @@ -65,8 +64,7 @@ void testMain() { }); test('CkPath.reset', () { - final ui.Path path = ui.Path(); - expect(path, isA()); + final CkPath path = CkPath(); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); expect(path.contains(const ui.Offset(5, 5)), isTrue); @@ -95,8 +93,7 @@ void testMain() { }); test('CkContourMeasure iteration', () { - final ui.Path path = ui.Path(); - expect(path, isA()); + final CkPath path = CkPath(); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); path.addRect(const ui.Rect.fromLTRB(20, 20, 30, 30)); path.addRect(const ui.Rect.fromLTRB(40, 40, 50, 50)); @@ -111,22 +108,21 @@ void testMain() { }); test('CkContourMeasure index', () { - final ui.Path path = ui.Path(); - expect(path, isA()); + final CkPath path = CkPath(); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); path.addRect(const ui.Rect.fromLTRB(20, 20, 30, 30)); path.addRect(const ui.Rect.fromLTRB(40, 40, 50, 50)); - final ui.PathMetrics metrics = path.computeMetrics(); + final CkPathMetrics metrics = path.computeMetrics(); final CkContourMeasureIter iterator = metrics.iterator as CkContourMeasureIter; expect(iterator.moveNext(), isTrue); - final CkContourMeasure measure0 = iterator.current as CkContourMeasure; + final CkContourMeasure measure0 = iterator.current; expect(measure0.contourIndex, 0); expect(measure0.extractPath(0, 15).getBounds(), const ui.Rect.fromLTRB(0, 0, 10, 5)); expect(iterator.moveNext(), isTrue); - final CkContourMeasure measure1 = iterator.current as CkContourMeasure; + final CkContourMeasure measure1 = iterator.current; expect(measure1.contourIndex, 1); expect(measure1.extractPath(0, 15).getBounds(), const ui.Rect.fromLTRB(20, 20, 30, 25)); }); @@ -135,12 +131,11 @@ void testMain() { const ui.Rect rect1 = ui.Rect.fromLTRB(0, 0, 10, 10); const ui.Rect rect2 = ui.Rect.fromLTRB(10, 10, 20, 20); - final ui.Path original = ui.Path(); + final CkPath original = CkPath(); original.addRect(rect1); - expect(original, isA()); expect(original.getBounds(), rect1); - final ui.Path copy = ui.Path.from(original); + final ui.Path copy = CkPath.from(original); expect(copy, isA()); expect(copy.getBounds(), rect1); diff --git a/engine/src/flutter/lib/web_ui/test/engine/lazy_path_test.dart b/engine/src/flutter/lib/web_ui/test/engine/lazy_path_test.dart new file mode 100644 index 00000000000..5289c2d15f7 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/engine/lazy_path_test.dart @@ -0,0 +1,297 @@ +// 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. + +import 'dart:collection'; +import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; + +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +class FakePath implements DisposablePath { + int _apiCallCount = 0; + + int get apiCallCount => _apiCallCount; + + bool isDisposed = false; + + final List computedMetrics = []; + + @override + void dispose() { + isDisposed = true; + } + + @override + PathFillType fillType = PathFillType.nonZero; + + @override + void moveTo(double x, double y) { + _apiCallCount++; + } + + @override + void relativeMoveTo(double dx, double dy) { + _apiCallCount++; + } + + @override + void lineTo(double x, double y) { + _apiCallCount++; + } + + @override + void relativeLineTo(double dx, double dy) { + _apiCallCount++; + } + + @override + void quadraticBezierTo(double x1, double y1, double x2, double y2) { + _apiCallCount++; + } + + @override + void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { + _apiCallCount++; + } + + @override + void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + _apiCallCount++; + } + + @override + void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + _apiCallCount++; + } + + @override + void conicTo(double x1, double y1, double x2, double y2, double w) { + _apiCallCount++; + } + + @override + void relativeConicTo(double x1, double y1, double x2, double y2, double w) { + _apiCallCount++; + } + + @override + void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { + _apiCallCount++; + } + + @override + void arcToPoint( + Offset arcEnd, { + Radius radius = Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + _apiCallCount++; + } + + @override + void relativeArcToPoint( + Offset arcEndDelta, { + Radius radius = Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + _apiCallCount++; + } + + @override + void addRect(Rect rect) { + _apiCallCount++; + } + + @override + void addOval(Rect oval) { + _apiCallCount++; + } + + @override + void addArc(Rect oval, double startAngle, double sweepAngle) { + _apiCallCount++; + } + + @override + void addPolygon(List points, bool close) { + _apiCallCount++; + } + + @override + void addRRect(RRect rrect) { + _apiCallCount++; + } + + @override + void addRSuperellipse(RSuperellipse rsuperellipse) { + _apiCallCount++; + } + + @override + void addPath(Path path, Offset offset, {Float64List? matrix4}) { + _apiCallCount++; + } + + @override + void extendWithPath(Path path, Offset offset, {Float64List? matrix4}) { + _apiCallCount++; + } + + @override + void close() { + _apiCallCount++; + } + + @override + void reset() { + _apiCallCount++; + } + + @override + bool contains(Offset point) { + return false; + } + + @override + Path shift(Offset offset) { + return this; + } + + @override + Path transform(Float64List matrix4) { + return this; + } + + @override + Rect getBounds() { + return Rect.zero; + } + + @override + FakePathMetrics computeMetrics({bool forceClosed = false}) { + final metrics = FakePathMetrics(); + computedMetrics.add(metrics); + return metrics; + } + + @override + String toSvgString() { + return ''; + } +} + +class FakePathMetrics extends IterableBase implements DisposablePathMetrics { + @override + final FakePathMetricsIterator iterator = FakePathMetricsIterator(); +} + +class FakePathMetricsIterator implements DisposablePathMetricIterator { + bool isDisposed = false; + + @override + void dispose() { + isDisposed = true; + } + + @override + bool moveNext() { + return false; + } + + @override + Never get current { + throw RangeError('nope'); + } +} + +class FakePathConstructors implements DisposablePathConstructors { + final List createdPaths = []; + + @override + FakePath createNew() { + final path = FakePath(); + createdPaths.add(path); + return path; + } + + @override + FakePath combinePaths(PathOperation operation, DisposablePath path1, DisposablePath path2) { + final path = FakePath(); + createdPaths.add(path); + return path; + } +} + +void testMain() { + test('LazyPath lifecycle', () { + final constructors = FakePathConstructors(); + final LazyPath path = LazyPath(constructors); + expect(constructors.createdPaths, isEmpty); + + path.moveTo(0, 0); + path.relativeMoveTo(0, 0); + path.lineTo(0, 0); + path.relativeLineTo(0, 0); + path.quadraticBezierTo(0, 0, 0, 0); + path.relativeQuadraticBezierTo(0, 0, 0, 0); + path.cubicTo(0, 0, 0, 0, 0, 0); + path.relativeCubicTo(0, 0, 0, 0, 0, 0); + path.conicTo(0, 0, 0, 0, 0); + path.relativeConicTo(0, 0, 0, 0, 0); + path.arcTo(Rect.zero, 0, 0, false); + path.arcToPoint(Offset.zero); + path.relativeArcToPoint(Offset.zero); + path.addRect(Rect.zero); + path.addOval(Rect.zero); + path.addArc(Rect.zero, 0, 0); + path.addPolygon([], false); + path.addRRect(RRect.zero); + path.addRSuperellipse(RSuperellipse.zero); + path.close(); + + expect(constructors.createdPaths, isEmpty); + + path.getBounds(); + + expect(constructors.createdPaths.length, 1); + final disposablePath = constructors.createdPaths.first; + + expect(disposablePath.isDisposed, false); + expect(disposablePath.apiCallCount, 20); + + final metrics = path.computeMetrics(); + expect(metrics.iterator.moveNext(), false); + expect(disposablePath.computedMetrics.length, 1); + final disposableMetrics = disposablePath.computedMetrics.first; + expect(disposableMetrics.iterator.isDisposed, false); + + EnginePlatformDispatcher.instance.frameArena.collect(); + + expect(constructors.createdPaths.length, 1); + expect(disposablePath.isDisposed, true); + expect(disposablePath.computedMetrics.length, 1); + expect(disposableMetrics.iterator.isDisposed, true); + + path.getBounds(); + + expect(constructors.createdPaths.length, 2); + final resurrectedPath = constructors.createdPaths.last; + + expect(resurrectedPath.isDisposed, false); + expect(resurrectedPath.apiCallCount, 20); + expect(metrics.iterator.moveNext(), false); + + EnginePlatformDispatcher.instance.frameArena.collect(); + }); +}