Add `Radius.clamp` and `Radius.clampValues` (#36106) (#36302)
* Add `Radius.clamp` and `Radius.clampValues` (#36106)
* Build CanvasKit in the Flutter Engine (#32510)
* Clamp `RRect` radii when deflating, assert on negative radii (#36062)
Co-authored-by: Greg Spencer <gspencergoog@users.noreply.github.com>
Co-authored-by: Harry Terkelsen <hterkelsen@users.noreply.github.com>
diff --git a/DEPS b/DEPS
index fa4e448..5eb34d1 100644
--- a/DEPS
+++ b/DEPS
@@ -109,7 +109,7 @@
]
deps = {
- 'src': 'https://github.com/flutter/buildroot.git' + '@' + '337fdd987f500ca48902aef9abbcde98be2803c7',
+ 'src': 'https://github.com/flutter/buildroot.git' + '@' + 'af893d511e89f93194f86dae8a4ef39e3b3fe59b',
# Fuchsia compatibility
#
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 643fd39..b8c5efd 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1005,6 +1005,7 @@
FILE: ../../../flutter/lib/ui/isolate_name_server/isolate_name_server_natives.h
FILE: ../../../flutter/lib/ui/key.dart
FILE: ../../../flutter/lib/ui/lerp.dart
+FILE: ../../../flutter/lib/ui/math.dart
FILE: ../../../flutter/lib/ui/natives.dart
FILE: ../../../flutter/lib/ui/painting.dart
FILE: ../../../flutter/lib/ui/painting/canvas.cc
@@ -1149,6 +1150,7 @@
FILE: ../../../flutter/lib/web_ui/lib/initialization.dart
FILE: ../../../flutter/lib/web_ui/lib/key.dart
FILE: ../../../flutter/lib/web_ui/lib/lerp.dart
+FILE: ../../../flutter/lib/web_ui/lib/math.dart
FILE: ../../../flutter/lib/web_ui/lib/natives.dart
FILE: ../../../flutter/lib/web_ui/lib/painting.dart
FILE: ../../../flutter/lib/web_ui/lib/path.dart
diff --git a/lib/ui/dart_ui.gni b/lib/ui/dart_ui.gni
index 96a7b06..7059f7a 100644
--- a/lib/ui/dart_ui.gni
+++ b/lib/ui/dart_ui.gni
@@ -12,6 +12,7 @@
"//flutter/lib/ui/isolate_name_server.dart",
"//flutter/lib/ui/key.dart",
"//flutter/lib/ui/lerp.dart",
+ "//flutter/lib/ui/math.dart",
"//flutter/lib/ui/natives.dart",
"//flutter/lib/ui/painting.dart",
"//flutter/lib/ui/platform_dispatcher.dart",
diff --git a/lib/ui/geometry.dart b/lib/ui/geometry.dart
index 8a1fe6f..7a39525 100644
--- a/lib/ui/geometry.dart
+++ b/lib/ui/geometry.dart
@@ -939,6 +939,37 @@
/// You can use [Radius.zero] with [RRect] to have right-angle corners.
static const Radius zero = Radius.circular(0.0);
+ /// Returns this [Radius], with values clamped to the given min and max
+ /// [Radius] values.
+ ///
+ /// The `min` value defaults to `Radius.circular(-double.infinity)`, and
+ /// the `max` value defaults to `Radius.circular(double.infinity)`.
+ Radius clamp({Radius? minimum, Radius? maximum}) {
+ minimum ??= const Radius.circular(-double.infinity);
+ maximum ??= const Radius.circular(double.infinity);
+ return Radius.elliptical(
+ clampDouble(x, minimum.x, maximum.x),
+ clampDouble(y, minimum.y, maximum.y),
+ );
+ }
+
+ /// Returns this [Radius], with values clamped to the given min and max
+ /// values in each dimension
+ ///
+ /// The `minimumX` and `minimumY` values default to `-double.infinity`, and
+ /// the `maximumX` and `maximumY` values default to `double.infinity`.
+ Radius clampValues({
+ double? minimumX,
+ double? minimumY,
+ double? maximumX,
+ double? maximumY,
+ }) {
+ return Radius.elliptical(
+ clampDouble(x, minimumX ?? -double.infinity, maximumX ?? double.infinity),
+ clampDouble(y, minimumY ?? -double.infinity, maximumY ?? double.infinity),
+ );
+ }
+
/// Unary negation operator.
///
/// Returns a Radius with the distances negated.
@@ -1056,8 +1087,16 @@
class RRect {
/// Construct a rounded rectangle from its left, top, right, and bottom edges,
/// and the same radii along its horizontal axis and its vertical axis.
- const RRect.fromLTRBXY(double left, double top, double right, double bottom,
- double radiusX, double radiusY) : this._raw(
+ ///
+ /// Will assert in debug mode if `radiusX` or `radiusY` are negative.
+ const RRect.fromLTRBXY(
+ double left,
+ double top,
+ double right,
+ double bottom,
+ double radiusX,
+ double radiusY,
+ ) : this._raw(
top: top,
left: left,
right: right,
@@ -1074,8 +1113,15 @@
/// Construct a rounded rectangle from its left, top, right, and bottom edges,
/// and the same radius in each corner.
- RRect.fromLTRBR(double left, double top, double right, double bottom,
- Radius radius)
+ ///
+ /// Will assert in debug mode if the `radius` is negative in either x or y.
+ RRect.fromLTRBR(
+ double left,
+ double top,
+ double right,
+ double bottom,
+ Radius radius,
+ )
: this._raw(
top: top,
left: left,
@@ -1093,6 +1139,8 @@
/// Construct a rounded rectangle from its bounding box and the same radii
/// along its horizontal axis and its vertical axis.
+ ///
+ /// Will assert in debug mode if `radiusX` or `radiusY` are negative.
RRect.fromRectXY(Rect rect, double radiusX, double radiusY)
: this._raw(
top: rect.top,
@@ -1111,6 +1159,8 @@
/// Construct a rounded rectangle from its bounding box and a radius that is
/// the same in each corner.
+ ///
+ /// Will assert in debug mode if the `radius` is negative in either x or y.
RRect.fromRectAndRadius(Rect rect, Radius radius)
: this._raw(
top: rect.top,
@@ -1130,7 +1180,8 @@
/// Construct a rounded rectangle from its left, top, right, and bottom edges,
/// and topLeft, topRight, bottomRight, and bottomLeft radii.
///
- /// The corner radii default to [Radius.zero], i.e. right-angled corners.
+ /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will
+ /// assert in debug mode if any of the radii are negative in either x or y.
RRect.fromLTRBAndCorners(
double left,
double top,
@@ -1158,7 +1209,8 @@
/// Construct a rounded rectangle from its bounding box and and topLeft,
/// topRight, bottomRight, and bottomLeft radii.
///
- /// The corner radii default to [Radius.zero], i.e. right-angled corners
+ /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will
+ /// assert in debug mode if any of the radii are negative in either x or y.
RRect.fromRectAndCorners(
Rect rect,
{
@@ -1206,7 +1258,15 @@
assert(brRadiusX != null),
assert(brRadiusY != null),
assert(blRadiusX != null),
- assert(blRadiusY != null);
+ assert(blRadiusY != null),
+ assert(tlRadiusX >= 0),
+ assert(tlRadiusY >= 0),
+ assert(trRadiusX >= 0),
+ assert(trRadiusY >= 0),
+ assert(brRadiusX >= 0),
+ assert(brRadiusY >= 0),
+ assert(blRadiusX >= 0),
+ assert(blRadiusY >= 0);
Float32List _getValue32() {
final Float32List result = Float32List(12);
@@ -1302,14 +1362,14 @@
top: top - delta,
right: right + delta,
bottom: bottom + delta,
- tlRadiusX: tlRadiusX + delta,
- tlRadiusY: tlRadiusY + delta,
- trRadiusX: trRadiusX + delta,
- trRadiusY: trRadiusY + delta,
- blRadiusX: blRadiusX + delta,
- blRadiusY: blRadiusY + delta,
- brRadiusX: brRadiusX + delta,
- brRadiusY: brRadiusY + delta,
+ tlRadiusX: math.max(0, tlRadiusX + delta),
+ tlRadiusY: math.max(0, tlRadiusY + delta),
+ trRadiusX: math.max(0, trRadiusX + delta),
+ trRadiusY: math.max(0, trRadiusY + delta),
+ blRadiusX: math.max(0, blRadiusX + delta),
+ blRadiusY: math.max(0, blRadiusY + delta),
+ brRadiusX: math.max(0, brRadiusX + delta),
+ brRadiusY: math.max(0, brRadiusY + delta),
);
}
@@ -1472,6 +1532,7 @@
scale = _getMin(scale, tlRadiusX, trRadiusX, width);
scale = _getMin(scale, trRadiusY, brRadiusY, height);
scale = _getMin(scale, brRadiusX, blRadiusX, width);
+ assert(scale >= 0);
if (scale < 1.0) {
return RRect._raw(
@@ -1590,14 +1651,14 @@
top: a.top * k,
right: a.right * k,
bottom: a.bottom * k,
- tlRadiusX: a.tlRadiusX * k,
- tlRadiusY: a.tlRadiusY * k,
- trRadiusX: a.trRadiusX * k,
- trRadiusY: a.trRadiusY * k,
- brRadiusX: a.brRadiusX * k,
- brRadiusY: a.brRadiusY * k,
- blRadiusX: a.blRadiusX * k,
- blRadiusY: a.blRadiusY * k,
+ tlRadiusX: math.max(0, a.tlRadiusX * k),
+ tlRadiusY: math.max(0, a.tlRadiusY * k),
+ trRadiusX: math.max(0, a.trRadiusX * k),
+ trRadiusY: math.max(0, a.trRadiusY * k),
+ brRadiusX: math.max(0, a.brRadiusX * k),
+ brRadiusY: math.max(0, a.brRadiusY * k),
+ blRadiusX: math.max(0, a.blRadiusX * k),
+ blRadiusY: math.max(0, a.blRadiusY * k),
);
}
} else {
@@ -1607,14 +1668,14 @@
top: b.top * t,
right: b.right * t,
bottom: b.bottom * t,
- tlRadiusX: b.tlRadiusX * t,
- tlRadiusY: b.tlRadiusY * t,
- trRadiusX: b.trRadiusX * t,
- trRadiusY: b.trRadiusY * t,
- brRadiusX: b.brRadiusX * t,
- brRadiusY: b.brRadiusY * t,
- blRadiusX: b.blRadiusX * t,
- blRadiusY: b.blRadiusY * t,
+ tlRadiusX: math.max(0, b.tlRadiusX * t),
+ tlRadiusY: math.max(0, b.tlRadiusY * t),
+ trRadiusX: math.max(0, b.trRadiusX * t),
+ trRadiusY: math.max(0, b.trRadiusY * t),
+ brRadiusX: math.max(0, b.brRadiusX * t),
+ brRadiusY: math.max(0, b.brRadiusY * t),
+ blRadiusX: math.max(0, b.blRadiusX * t),
+ blRadiusY: math.max(0, b.blRadiusY * t),
);
} else {
return RRect._raw(
@@ -1622,14 +1683,14 @@
top: _lerpDouble(a.top, b.top, t),
right: _lerpDouble(a.right, b.right, t),
bottom: _lerpDouble(a.bottom, b.bottom, t),
- tlRadiusX: _lerpDouble(a.tlRadiusX, b.tlRadiusX, t),
- tlRadiusY: _lerpDouble(a.tlRadiusY, b.tlRadiusY, t),
- trRadiusX: _lerpDouble(a.trRadiusX, b.trRadiusX, t),
- trRadiusY: _lerpDouble(a.trRadiusY, b.trRadiusY, t),
- brRadiusX: _lerpDouble(a.brRadiusX, b.brRadiusX, t),
- brRadiusY: _lerpDouble(a.brRadiusY, b.brRadiusY, t),
- blRadiusX: _lerpDouble(a.blRadiusX, b.blRadiusX, t),
- blRadiusY: _lerpDouble(a.blRadiusY, b.blRadiusY, t),
+ tlRadiusX: math.max(0, _lerpDouble(a.tlRadiusX, b.tlRadiusX, t)),
+ tlRadiusY: math.max(0, _lerpDouble(a.tlRadiusY, b.tlRadiusY, t)),
+ trRadiusX: math.max(0, _lerpDouble(a.trRadiusX, b.trRadiusX, t)),
+ trRadiusY: math.max(0, _lerpDouble(a.trRadiusY, b.trRadiusY, t)),
+ brRadiusX: math.max(0, _lerpDouble(a.brRadiusX, b.brRadiusX, t)),
+ brRadiusY: math.max(0, _lerpDouble(a.brRadiusY, b.brRadiusY, t)),
+ blRadiusX: math.max(0, _lerpDouble(a.blRadiusX, b.blRadiusX, t)),
+ blRadiusY: math.max(0, _lerpDouble(a.blRadiusY, b.blRadiusY, t)),
);
}
}
diff --git a/lib/ui/math.dart b/lib/ui/math.dart
new file mode 100644
index 0000000..aeb835a
--- /dev/null
+++ b/lib/ui/math.dart
@@ -0,0 +1,25 @@
+// 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.
+
+part of dart.ui;
+
+/// Same as [num.clamp] but optimized for a non-null [double].
+///
+/// This is faster because it avoids polymorphism, boxing, and special cases for
+/// floating point numbers.
+//
+// See also: //dev/benchmarks/microbenchmarks/lib/foundation/clamp.dart
+double clampDouble(double x, double min, double max) {
+ assert(min <= max && !max.isNaN && !min.isNaN);
+ if (x < min) {
+ return min;
+ }
+ if (x > max) {
+ return max;
+ }
+ if (x.isNaN) {
+ return max;
+ }
+ return x;
+}
diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart
index 3c0a7ad..316de56 100644
--- a/lib/ui/painting.dart
+++ b/lib/ui/painting.dart
@@ -320,7 +320,7 @@
/// The [opacity] value may not be null.
static int getAlphaFromOpacity(double opacity) {
assert(opacity != null);
- return (opacity.clamp(0.0, 1.0) * 255).round();
+ return (clampDouble(opacity, 0.0, 1.0) * 255).round();
}
@override
diff --git a/lib/ui/ui.dart b/lib/ui/ui.dart
index 2695a80..36818d3 100644
--- a/lib/ui/ui.dart
+++ b/lib/ui/ui.dart
@@ -31,6 +31,7 @@
part 'isolate_name_server.dart';
part 'key.dart';
part 'lerp.dart';
+part 'math.dart';
part 'natives.dart';
part 'painting.dart';
part 'platform_dispatcher.dart';
diff --git a/lib/web_ui/dev/build.dart b/lib/web_ui/dev/build.dart
index bf5b55d..087e4fa 100644
--- a/lib/web_ui/dev/build.dart
+++ b/lib/web_ui/dev/build.dart
@@ -48,7 +48,7 @@
if (buildCanvasKit) {
steps.addAll(<PipelineStep>[
GnPipelineStep(target: 'canvaskit'),
- NinjaPipelineStep(target: environment.canvasKitOutDir),
+ NinjaPipelineStep(target: environment.wasmReleaseOutDir),
]);
}
final Pipeline buildPipeline = Pipeline(steps: steps);
@@ -74,7 +74,7 @@
/// state. GN is pretty quick though, so it's OK to not support interruption.
class GnPipelineStep extends ProcessStep {
GnPipelineStep({this.target = 'engine'})
- : assert(target == 'engine' || target == 'sdk');
+ : assert(target == 'engine' || target == 'canvaskit');
@override
String get description => 'gn';
@@ -89,7 +89,7 @@
@override
Future<ProcessManager> createProcess() {
- print('Running gn...');
+ print('Running gn for $target...');
final List<String> gnArgs = <String>[];
if (target == 'engine') {
gnArgs.addAll(<String>[
@@ -98,7 +98,10 @@
'--full-dart-sdk',
]);
} else if (target == 'canvaskit') {
- gnArgs.add('--wasm');
+ gnArgs.addAll(<String>[
+ '--wasm',
+ '--runtime-mode=release',
+ ]);
} else {
throw StateError('Target was not engine or canvaskit: $target');
}
diff --git a/lib/web_ui/dev/environment.dart b/lib/web_ui/dev/environment.dart
index 1c5530b..35df6c0 100644
--- a/lib/web_ui/dev/environment.dart
+++ b/lib/web_ui/dev/environment.dart
@@ -25,8 +25,8 @@
io.Directory(pathlib.join(engineSrcDir.path, 'out'));
final io.Directory hostDebugUnoptDir =
io.Directory(pathlib.join(outDir.path, 'host_debug_unopt'));
- final io.Directory canvasKitOutDir =
- io.Directory(pathlib.join(outDir.path, 'wasm_debug'));
+ final io.Directory wasmReleaseOutDir =
+ io.Directory(pathlib.join(outDir.path, 'wasm_release'));
final io.Directory dartSdkDir =
io.Directory(pathlib.join(hostDebugUnoptDir.path, 'dart-sdk'));
final io.Directory webUiRootDir = io.Directory(
@@ -44,6 +44,7 @@
}
}
+
return Environment._(
self: self,
webUiRootDir: webUiRootDir,
@@ -51,7 +52,7 @@
engineToolsDir: engineToolsDir,
outDir: outDir,
hostDebugUnoptDir: hostDebugUnoptDir,
- canvasKitOutDir: canvasKitOutDir,
+ wasmReleaseOutDir: wasmReleaseOutDir,
dartSdkDir: dartSdkDir,
);
}
@@ -63,7 +64,7 @@
required this.engineToolsDir,
required this.outDir,
required this.hostDebugUnoptDir,
- required this.canvasKitOutDir,
+ required this.wasmReleaseOutDir,
required this.dartSdkDir,
});
@@ -84,11 +85,13 @@
/// This is where you'll find the ninja output, such as the Dart SDK.
final io.Directory outDir;
- /// The "host_debug_unopt" build of the Dart SDK.
+ /// The output directory for the host_debug_unopt build.
final io.Directory hostDebugUnoptDir;
- /// The output directory for the build of CanvasKit.
- final io.Directory canvasKitOutDir;
+ /// The output directory for the wasm_release build.
+ ///
+ /// We build CanvasKit in release mode to reduce code size.
+ final io.Directory wasmReleaseOutDir;
/// The root of the Dart SDK.
final io.Directory dartSdkDir;
diff --git a/lib/web_ui/dev/steps/compile_tests_step.dart b/lib/web_ui/dev/steps/compile_tests_step.dart
index e9931b7..2cc2d22 100644
--- a/lib/web_ui/dev/steps/compile_tests_step.dart
+++ b/lib/web_ui/dev/steps/compile_tests_step.dart
@@ -23,10 +23,12 @@
/// * test/ - compiled test code
/// * test_images/ - test images copied from Skis sources.
class CompileTestsStep implements PipelineStep {
- CompileTestsStep({this.testFiles});
+ CompileTestsStep({this.testFiles, this.useLocalCanvasKit = false});
final List<FilePath>? testFiles;
+ final bool useLocalCanvasKit;
+
@override
String get description => 'compile_tests';
@@ -41,7 +43,7 @@
@override
Future<void> run() async {
await environment.webUiBuildDir.create();
- await copyCanvasKitFiles();
+ await copyCanvasKitFiles(useLocalCanvasKit: useLocalCanvasKit);
await buildHostPage();
await copyTestFonts();
await copySkiaTestImages();
@@ -122,11 +124,13 @@
}
}
-Future<void> copyCanvasKitFiles() async {
+Future<void> copyCanvasKitFiles({bool useLocalCanvasKit = false}) async {
// If CanvasKit has been built locally, use that instead of the CIPD version.
- final io.File localCanvasKitWasm =
- io.File(pathlib.join(environment.canvasKitOutDir.path, 'canvaskit.wasm'));
- final bool builtLocalCanvasKit = localCanvasKitWasm.existsSync();
+ final io.File localCanvasKitWasm = io.File(pathlib.join(
+ environment.wasmReleaseOutDir.path,
+ 'canvaskit.wasm',
+ ));
+ final bool builtLocalCanvasKit = localCanvasKitWasm.existsSync() && useLocalCanvasKit;
final io.Directory targetDir = io.Directory(pathlib.join(
environment.webUiBuildDir.path,
@@ -136,7 +140,10 @@
if (builtLocalCanvasKit) {
final List<io.File> canvasKitFiles = <io.File>[
localCanvasKitWasm,
- io.File(pathlib.join(environment.canvasKitOutDir.path, 'canvaskit.js')),
+ io.File(pathlib.join(
+ environment.wasmReleaseOutDir.path,
+ 'canvaskit.js',
+ )),
];
for (final io.File file in canvasKitFiles) {
final io.File normalTargetFile = io.File(pathlib.join(
diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart
index cb4628f..8984d2f 100644
--- a/lib/web_ui/dev/test_runner.dart
+++ b/lib/web_ui/dev/test_runner.dart
@@ -73,6 +73,11 @@
help: 'Optional. The path to a local build of CanvasKit to use in '
'tests. If omitted, the test runner uses the default CanvasKit '
'build.',
+ )
+ ..addFlag(
+ 'use-local-canvaskit',
+ help: 'Optional. Whether or not to use the locally built version of '
+ 'CanvasKit in the tests.',
);
}
@@ -115,6 +120,9 @@
/// Path to a CanvasKit build. Overrides the default CanvasKit.
String? get overridePathToCanvasKit => argResults!['canvaskit-path'] as String?;
+ /// Whether or not to use the locally built version of CanvasKit.
+ bool get useLocalCanvasKit => boolArg('use-local-canvaskit');
+
@override
Future<bool> run() async {
final List<FilePath> testFiles = runAllTests
@@ -123,7 +131,7 @@
final Pipeline testPipeline = Pipeline(steps: <PipelineStep>[
if (isWatchMode) ClearTerminalScreenStep(),
- CompileTestsStep(testFiles: testFiles),
+ CompileTestsStep(testFiles: testFiles, useLocalCanvasKit: useLocalCanvasKit),
RunTestsStep(
browserName: browserName,
testFiles: testFiles,
diff --git a/lib/web_ui/lib/geometry.dart b/lib/web_ui/lib/geometry.dart
index 3d60b13..7d17c73 100644
--- a/lib/web_ui/lib/geometry.dart
+++ b/lib/web_ui/lib/geometry.dart
@@ -341,6 +341,25 @@
final double x;
final double y;
static const Radius zero = Radius.circular(0.0);
+ Radius clamp({Radius? minimum, Radius? maximum}) {
+ minimum ??= const Radius.circular(-double.infinity);
+ maximum ??= const Radius.circular(double.infinity);
+ return Radius.elliptical(
+ clampDouble(x, minimum.x, maximum.x),
+ clampDouble(y, minimum.y, maximum.y),
+ );
+ }
+ Radius clampValues({
+ double? minimumX,
+ double? minimumY,
+ double? maximumX,
+ double? maximumY,
+ }) {
+ return Radius.elliptical(
+ clampDouble(x, minimumX ?? -double.infinity, maximumX ?? double.infinity),
+ clampDouble(y, minimumY ?? -double.infinity, maximumY ?? double.infinity),
+ );
+ }
Radius operator -() => Radius.elliptical(-x, -y);
Radius operator -(Radius other) => Radius.elliptical(x - other.x, y - other.y);
Radius operator +(Radius other) => Radius.elliptical(x + other.x, y + other.y);
@@ -559,6 +578,14 @@
assert(brRadiusY != null),
assert(blRadiusX != null),
assert(blRadiusY != null),
+ assert(tlRadiusX >= 0),
+ assert(tlRadiusY >= 0),
+ assert(trRadiusX >= 0),
+ assert(trRadiusY >= 0),
+ assert(brRadiusX >= 0),
+ assert(brRadiusY >= 0),
+ assert(blRadiusX >= 0),
+ assert(blRadiusY >= 0),
webOnlyUniformRadii = uniformRadii;
final double left;
@@ -604,14 +631,14 @@
top: top - delta,
right: right + delta,
bottom: bottom + delta,
- tlRadiusX: tlRadiusX + delta,
- tlRadiusY: tlRadiusY + delta,
- trRadiusX: trRadiusX + delta,
- trRadiusY: trRadiusY + delta,
- blRadiusX: blRadiusX + delta,
- blRadiusY: blRadiusY + delta,
- brRadiusX: brRadiusX + delta,
- brRadiusY: brRadiusY + delta,
+ tlRadiusX: math.max(0, tlRadiusX + delta),
+ tlRadiusY: math.max(0, tlRadiusY + delta),
+ trRadiusX: math.max(0, trRadiusX + delta),
+ trRadiusY: math.max(0, trRadiusY + delta),
+ blRadiusX: math.max(0, blRadiusX + delta),
+ blRadiusY: math.max(0, blRadiusY + delta),
+ brRadiusX: math.max(0, brRadiusX + delta),
+ brRadiusY: math.max(0, brRadiusY + delta),
);
}
@@ -816,14 +843,14 @@
top: a.top * k,
right: a.right * k,
bottom: a.bottom * k,
- tlRadiusX: a.tlRadiusX * k,
- tlRadiusY: a.tlRadiusY * k,
- trRadiusX: a.trRadiusX * k,
- trRadiusY: a.trRadiusY * k,
- brRadiusX: a.brRadiusX * k,
- brRadiusY: a.brRadiusY * k,
- blRadiusX: a.blRadiusX * k,
- blRadiusY: a.blRadiusY * k,
+ tlRadiusX: math.max(0, a.tlRadiusX * k),
+ tlRadiusY: math.max(0, a.tlRadiusY * k),
+ trRadiusX: math.max(0, a.trRadiusX * k),
+ trRadiusY: math.max(0, a.trRadiusY * k),
+ brRadiusX: math.max(0, a.brRadiusX * k),
+ brRadiusY: math.max(0, a.brRadiusY * k),
+ blRadiusX: math.max(0, a.blRadiusX * k),
+ blRadiusY: math.max(0, a.blRadiusY * k),
);
}
} else {
@@ -833,14 +860,14 @@
top: b.top * t,
right: b.right * t,
bottom: b.bottom * t,
- tlRadiusX: b.tlRadiusX * t,
- tlRadiusY: b.tlRadiusY * t,
- trRadiusX: b.trRadiusX * t,
- trRadiusY: b.trRadiusY * t,
- brRadiusX: b.brRadiusX * t,
- brRadiusY: b.brRadiusY * t,
- blRadiusX: b.blRadiusX * t,
- blRadiusY: b.blRadiusY * t,
+ tlRadiusX: math.max(0, b.tlRadiusX * t),
+ tlRadiusY: math.max(0, b.tlRadiusY * t),
+ trRadiusX: math.max(0, b.trRadiusX * t),
+ trRadiusY: math.max(0, b.trRadiusY * t),
+ brRadiusX: math.max(0, b.brRadiusX * t),
+ brRadiusY: math.max(0, b.brRadiusY * t),
+ blRadiusX: math.max(0, b.blRadiusX * t),
+ blRadiusY: math.max(0, b.blRadiusY * t),
);
} else {
return RRect._raw(
@@ -848,14 +875,14 @@
top: _lerpDouble(a.top, b.top, t),
right: _lerpDouble(a.right, b.right, t),
bottom: _lerpDouble(a.bottom, b.bottom, t),
- tlRadiusX: _lerpDouble(a.tlRadiusX, b.tlRadiusX, t),
- tlRadiusY: _lerpDouble(a.tlRadiusY, b.tlRadiusY, t),
- trRadiusX: _lerpDouble(a.trRadiusX, b.trRadiusX, t),
- trRadiusY: _lerpDouble(a.trRadiusY, b.trRadiusY, t),
- brRadiusX: _lerpDouble(a.brRadiusX, b.brRadiusX, t),
- brRadiusY: _lerpDouble(a.brRadiusY, b.brRadiusY, t),
- blRadiusX: _lerpDouble(a.blRadiusX, b.blRadiusX, t),
- blRadiusY: _lerpDouble(a.blRadiusY, b.blRadiusY, t),
+ tlRadiusX: math.max(0, _lerpDouble(a.tlRadiusX, b.tlRadiusX, t)),
+ tlRadiusY: math.max(0, _lerpDouble(a.tlRadiusY, b.tlRadiusY, t)),
+ trRadiusX: math.max(0, _lerpDouble(a.trRadiusX, b.trRadiusX, t)),
+ trRadiusY: math.max(0, _lerpDouble(a.trRadiusY, b.trRadiusY, t)),
+ brRadiusX: math.max(0, _lerpDouble(a.brRadiusX, b.brRadiusX, t)),
+ brRadiusY: math.max(0, _lerpDouble(a.brRadiusY, b.brRadiusY, t)),
+ blRadiusX: math.max(0, _lerpDouble(a.blRadiusX, b.blRadiusX, t)),
+ blRadiusY: math.max(0, _lerpDouble(a.blRadiusY, b.blRadiusY, t)),
);
}
}
diff --git a/lib/web_ui/lib/math.dart b/lib/web_ui/lib/math.dart
new file mode 100644
index 0000000..adea127
--- /dev/null
+++ b/lib/web_ui/lib/math.dart
@@ -0,0 +1,19 @@
+// 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.
+
+part of ui;
+
+double clampDouble(double x, double min, double max) {
+ assert(min <= max && !max.isNaN && !min.isNaN);
+ if (x < min) {
+ return min;
+ }
+ if (x > max) {
+ return max;
+ }
+ if (x.isNaN) {
+ return max;
+ }
+ return x;
+}
diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart
index 3ab2ba0..03e44ec 100644
--- a/lib/web_ui/lib/painting.dart
+++ b/lib/web_ui/lib/painting.dart
@@ -146,7 +146,7 @@
static int getAlphaFromOpacity(double opacity) {
assert(opacity != null);
- return (opacity.clamp(0.0, 1.0) * 255).round();
+ return (clampDouble(opacity, 0.0, 1.0) * 255).round();
}
@override
diff --git a/lib/web_ui/lib/src/engine/html/recording_canvas.dart b/lib/web_ui/lib/src/engine/html/recording_canvas.dart
index b1f86da..a7638d1 100644
--- a/lib/web_ui/lib/src/engine/html/recording_canvas.dart
+++ b/lib/web_ui/lib/src/engine/html/recording_canvas.dart
@@ -23,16 +23,8 @@
/// Enable this to print every command applied by a canvas.
const bool _debugDumpPaintCommands = false;
-// Returns the squared length of the x, y (of a border radius)
-// It normalizes x, y values before working with them, by
-// assuming anything < 0 to be 0, because flutter may pass
-// negative radii (which Skia assumes to be 0), see:
-// https://skia.org/user/api/SkRRect_Reference#SkRRect_inset
-double _measureBorderRadius(double x, double y) {
- final double clampedX = x < 0 ? 0 : x;
- final double clampedY = y < 0 ? 0 : y;
- return clampedX * clampedX + clampedY * clampedY;
-}
+// Returns the squared length of the x, y (of a border radius).
+double _measureBorderRadius(double x, double y) => x*x + y*y;
/// Records canvas commands to be applied to a [EngineCanvas].
///
diff --git a/lib/web_ui/lib/ui.dart b/lib/web_ui/lib/ui.dart
index a01d539..1878d4f 100644
--- a/lib/web_ui/lib/ui.dart
+++ b/lib/web_ui/lib/ui.dart
@@ -24,6 +24,7 @@
part 'initialization.dart';
part 'key.dart';
part 'lerp.dart';
+part 'math.dart';
part 'natives.dart';
part 'painting.dart';
part 'path.dart';
diff --git a/lib/web_ui/test/engine/recording_canvas_test.dart b/lib/web_ui/test/engine/recording_canvas_test.dart
index b3f80d1..1e4666e 100644
--- a/lib/web_ui/test/engine/recording_canvas_test.dart
+++ b/lib/web_ui/test/engine/recording_canvas_test.dart
@@ -121,7 +121,7 @@
expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint');
});
- test('negative corners in inner RRect get passed through to draw', () {
+ test('deflated corners in inner RRect get passed through to draw', () {
// This comes from github issue #40728
final RRect outer = RRect.fromRectAndCorners(
const Rect.fromLTWH(0, 0, 88, 48),
@@ -129,9 +129,8 @@
bottomLeft: const Radius.circular(6));
final RRect inner = outer.deflate(1);
- // If these assertions fail, check [_measureBorderRadius] in recording_canvas.dart
- expect(inner.brRadius, equals(const Radius.circular(-1)));
- expect(inner.trRadius, equals(const Radius.circular(-1)));
+ expect(inner.brRadius, equals(Radius.zero));
+ expect(inner.trRadius, equals(Radius.zero));
underTest.drawDRRect(outer, inner, somePaint);
underTest.endRecording();
diff --git a/lib/web_ui/test/geometry_test.dart b/lib/web_ui/test/geometry_test.dart
index 62b7f52..24150d8 100644
--- a/lib/web_ui/test/geometry_test.dart
+++ b/lib/web_ui/test/geometry_test.dart
@@ -75,4 +75,220 @@
expect(const Size(-1.0, -1.0).aspectRatio, 1.0);
expect(const Size(3.0, 4.0).aspectRatio, 3.0 / 4.0);
});
+ test('Radius.clamp() operates as expected', () {
+ final RRect rrectMin = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.circular(-100).clamp(minimum: Radius.zero));
+
+ expect(rrectMin.left, 1);
+ expect(rrectMin.top, 3);
+ expect(rrectMin.right, 5);
+ expect(rrectMin.bottom, 7);
+ expect(rrectMin.trRadius, equals(const Radius.circular(0)));
+ expect(rrectMin.blRadius, equals(const Radius.circular(0)));
+
+ final RRect rrectMax = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.circular(100).clamp(maximum: const Radius.circular(10)));
+
+ expect(rrectMax.left, 1);
+ expect(rrectMax.top, 3);
+ expect(rrectMax.right, 5);
+ expect(rrectMax.bottom, 7);
+ expect(rrectMax.trRadius, equals(const Radius.circular(10)));
+ expect(rrectMax.blRadius, equals(const Radius.circular(10)));
+
+ final RRect rrectMix = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.elliptical(-100, 100).clamp(minimum: Radius.zero, maximum: const Radius.circular(10)));
+
+ expect(rrectMix.left, 1);
+ expect(rrectMix.top, 3);
+ expect(rrectMix.right, 5);
+ expect(rrectMix.bottom, 7);
+ expect(rrectMix.trRadius, equals(const Radius.elliptical(0, 10)));
+ expect(rrectMix.blRadius, equals(const Radius.elliptical(0, 10)));
+
+ final RRect rrectMix1 = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.elliptical(100, -100).clamp(minimum: Radius.zero, maximum: const Radius.circular(10)));
+
+ expect(rrectMix1.left, 1);
+ expect(rrectMix1.top, 3);
+ expect(rrectMix1.right, 5);
+ expect(rrectMix1.bottom, 7);
+ expect(rrectMix1.trRadius, equals(const Radius.elliptical(10, 0)));
+ expect(rrectMix1.blRadius, equals(const Radius.elliptical(10, 0)));
+ });
+ test('Radius.clampValues() operates as expected', () {
+ final RRect rrectMin = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.circular(-100).clampValues(minimumX: 0, minimumY: 0));
+
+ expect(rrectMin.left, 1);
+ expect(rrectMin.top, 3);
+ expect(rrectMin.right, 5);
+ expect(rrectMin.bottom, 7);
+ expect(rrectMin.trRadius, equals(const Radius.circular(0)));
+ expect(rrectMin.blRadius, equals(const Radius.circular(0)));
+
+ final RRect rrectMax = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.circular(100).clampValues(maximumX: 10, maximumY: 20));
+
+ expect(rrectMax.left, 1);
+ expect(rrectMax.top, 3);
+ expect(rrectMax.right, 5);
+ expect(rrectMax.bottom, 7);
+ expect(rrectMax.trRadius, equals(const Radius.elliptical(10, 20)));
+ expect(rrectMax.blRadius, equals(const Radius.elliptical(10, 20)));
+
+ final RRect rrectMix = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.elliptical(-100, 100).clampValues(minimumX: 5, minimumY: 6, maximumX: 10, maximumY: 20));
+
+ expect(rrectMix.left, 1);
+ expect(rrectMix.top, 3);
+ expect(rrectMix.right, 5);
+ expect(rrectMix.bottom, 7);
+ expect(rrectMix.trRadius, equals(const Radius.elliptical(5, 20)));
+ expect(rrectMix.blRadius, equals(const Radius.elliptical(5, 20)));
+
+ final RRect rrectMix2 = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.elliptical(100, -100).clampValues(minimumX: 5, minimumY: 6, maximumX: 10, maximumY: 20));
+
+ expect(rrectMix2.left, 1);
+ expect(rrectMix2.top, 3);
+ expect(rrectMix2.right, 5);
+ expect(rrectMix2.bottom, 7);
+ expect(rrectMix2.trRadius, equals(const Radius.elliptical(10, 6)));
+ expect(rrectMix2.blRadius, equals(const Radius.elliptical(10, 6)));
+ });
+ test('RRect asserts when corner radii are negative', () {
+ bool assertsEnabled = false;
+ assert(() {
+ assertsEnabled = true;
+ return true;
+ }());
+ if (!assertsEnabled) {
+ return;
+ }
+
+ expect(() {
+ RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ topLeft: const Radius.circular(-1),
+ );
+ }, throwsA(isA<AssertionError>()));
+
+ expect(() {
+ RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ topRight: const Radius.circular(-2),
+ );
+ }, throwsA(isA<AssertionError>()));
+
+ expect(() {
+ RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ bottomLeft: const Radius.circular(-3),
+ );
+ }, throwsA(isA<AssertionError>()));
+
+ expect(() {
+ RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ bottomRight: const Radius.circular(-4),
+ );
+ }, throwsA(isA<AssertionError>()));
+ });
+ test('RRect.inflate clamps when deflating past zero', () {
+ RRect rrect = RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ topLeft: const Radius.circular(1),
+ topRight: const Radius.circular(2),
+ bottomLeft: const Radius.circular(3),
+ bottomRight: const Radius.circular(4),
+ ).inflate(-1);
+
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 1);
+ expect(rrect.trRadiusY, 1);
+ expect(rrect.blRadiusX, 2);
+ expect(rrect.blRadiusY, 2);
+ expect(rrect.brRadiusX, 3);
+ expect(rrect.brRadiusY, 3);
+
+ rrect = rrect.inflate(-1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 1);
+ expect(rrect.blRadiusY, 1);
+ expect(rrect.brRadiusX, 2);
+ expect(rrect.brRadiusY, 2);
+
+ rrect = rrect.inflate(-1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 0);
+ expect(rrect.blRadiusY, 0);
+ expect(rrect.brRadiusX, 1);
+ expect(rrect.brRadiusY, 1);
+
+ rrect = rrect.inflate(-1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 0);
+ expect(rrect.blRadiusY, 0);
+ expect(rrect.brRadiusX, 0);
+ expect(rrect.brRadiusY, 0);
+ });
+ test('RRect.deflate clamps when deflating past zero', () {
+ RRect rrect = RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ topLeft: const Radius.circular(1),
+ topRight: const Radius.circular(2),
+ bottomLeft: const Radius.circular(3),
+ bottomRight: const Radius.circular(4),
+ ).deflate(1);
+
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 1);
+ expect(rrect.trRadiusY, 1);
+ expect(rrect.blRadiusX, 2);
+ expect(rrect.blRadiusY, 2);
+ expect(rrect.brRadiusX, 3);
+ expect(rrect.brRadiusY, 3);
+
+ rrect = rrect.deflate(1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 1);
+ expect(rrect.blRadiusY, 1);
+ expect(rrect.brRadiusX, 2);
+ expect(rrect.brRadiusY, 2);
+
+ rrect = rrect.deflate(1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 0);
+ expect(rrect.blRadiusY, 0);
+ expect(rrect.brRadiusX, 1);
+ expect(rrect.brRadiusY, 1);
+
+ rrect = rrect.deflate(1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 0);
+ expect(rrect.blRadiusY, 0);
+ expect(rrect.brRadiusX, 0);
+ expect(rrect.brRadiusY, 0);
+ });
}
diff --git a/testing/dart/geometry_test.dart b/testing/dart/geometry_test.dart
index 8e0101d..8dc7f37 100644
--- a/testing/dart/geometry_test.dart
+++ b/testing/dart/geometry_test.dart
@@ -292,4 +292,225 @@
expect(rrect.brRadiusX, 0.25);
expect(rrect.brRadiusY, 0.75);
});
+
+ test('Radius.clamp() operates as expected', () {
+ final RRect rrectMin = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.circular(-100).clamp(minimum: Radius.zero));
+
+ expect(rrectMin.left, 1);
+ expect(rrectMin.top, 3);
+ expect(rrectMin.right, 5);
+ expect(rrectMin.bottom, 7);
+ expect(rrectMin.trRadius, equals(const Radius.circular(0)));
+ expect(rrectMin.blRadius, equals(const Radius.circular(0)));
+
+ final RRect rrectMax = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.circular(100).clamp(maximum: const Radius.circular(10)));
+
+ expect(rrectMax.left, 1);
+ expect(rrectMax.top, 3);
+ expect(rrectMax.right, 5);
+ expect(rrectMax.bottom, 7);
+ expect(rrectMax.trRadius, equals(const Radius.circular(10)));
+ expect(rrectMax.blRadius, equals(const Radius.circular(10)));
+
+ final RRect rrectMix = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.elliptical(-100, 100).clamp(minimum: Radius.zero, maximum: const Radius.circular(10)));
+
+ expect(rrectMix.left, 1);
+ expect(rrectMix.top, 3);
+ expect(rrectMix.right, 5);
+ expect(rrectMix.bottom, 7);
+ expect(rrectMix.trRadius, equals(const Radius.elliptical(0, 10)));
+ expect(rrectMix.blRadius, equals(const Radius.elliptical(0, 10)));
+
+ final RRect rrectMix1 = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.elliptical(100, -100).clamp(minimum: Radius.zero, maximum: const Radius.circular(10)));
+
+ expect(rrectMix1.left, 1);
+ expect(rrectMix1.top, 3);
+ expect(rrectMix1.right, 5);
+ expect(rrectMix1.bottom, 7);
+ expect(rrectMix1.trRadius, equals(const Radius.elliptical(10, 0)));
+ expect(rrectMix1.blRadius, equals(const Radius.elliptical(10, 0)));
+ });
+
+ test('Radius.clampValues() operates as expected', () {
+ final RRect rrectMin = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.circular(-100).clampValues(minimumX: 0, minimumY: 0));
+
+ expect(rrectMin.left, 1);
+ expect(rrectMin.top, 3);
+ expect(rrectMin.right, 5);
+ expect(rrectMin.bottom, 7);
+ expect(rrectMin.trRadius, equals(const Radius.circular(0)));
+ expect(rrectMin.blRadius, equals(const Radius.circular(0)));
+
+ final RRect rrectMax = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.circular(100).clampValues(maximumX: 10, maximumY: 20));
+
+ expect(rrectMax.left, 1);
+ expect(rrectMax.top, 3);
+ expect(rrectMax.right, 5);
+ expect(rrectMax.bottom, 7);
+ expect(rrectMax.trRadius, equals(const Radius.elliptical(10, 20)));
+ expect(rrectMax.blRadius, equals(const Radius.elliptical(10, 20)));
+
+ final RRect rrectMix = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.elliptical(-100, 100).clampValues(minimumX: 5, minimumY: 6, maximumX: 10, maximumY: 20));
+
+ expect(rrectMix.left, 1);
+ expect(rrectMix.top, 3);
+ expect(rrectMix.right, 5);
+ expect(rrectMix.bottom, 7);
+ expect(rrectMix.trRadius, equals(const Radius.elliptical(5, 20)));
+ expect(rrectMix.blRadius, equals(const Radius.elliptical(5, 20)));
+
+ final RRect rrectMix2 = RRect.fromLTRBR(1, 3, 5, 7,
+ const Radius.elliptical(100, -100).clampValues(minimumX: 5, minimumY: 6, maximumX: 10, maximumY: 20));
+
+ expect(rrectMix2.left, 1);
+ expect(rrectMix2.top, 3);
+ expect(rrectMix2.right, 5);
+ expect(rrectMix2.bottom, 7);
+ expect(rrectMix2.trRadius, equals(const Radius.elliptical(10, 6)));
+ expect(rrectMix2.blRadius, equals(const Radius.elliptical(10, 6)));
+ });
+
+ test('RRect asserts when corner radii are negative', () {
+ bool assertsEnabled = false;
+ assert(() {
+ assertsEnabled = true;
+ return true;
+ }());
+ if (!assertsEnabled) {
+ return;
+ }
+
+ expect(() {
+ RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ topLeft: const Radius.circular(-1),
+ );
+ }, throwsA(isInstanceOf<AssertionError>()));
+
+ expect(() {
+ RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ topRight: const Radius.circular(-2),
+ );
+ }, throwsA(isInstanceOf<AssertionError>()));
+
+ expect(() {
+ RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ bottomLeft: const Radius.circular(-3),
+ );
+ }, throwsA(isInstanceOf<AssertionError>()));
+
+ expect(() {
+ RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ bottomRight: const Radius.circular(-4),
+ );
+ }, throwsA(isInstanceOf<AssertionError>()));
+ });
+
+ test('RRect.inflate clamps when deflating past zero', () {
+ RRect rrect = RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ topLeft: const Radius.circular(1),
+ topRight: const Radius.circular(2),
+ bottomLeft: const Radius.circular(3),
+ bottomRight: const Radius.circular(4),
+ ).inflate(-1);
+
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 1);
+ expect(rrect.trRadiusY, 1);
+ expect(rrect.blRadiusX, 2);
+ expect(rrect.blRadiusY, 2);
+ expect(rrect.brRadiusX, 3);
+ expect(rrect.brRadiusY, 3);
+
+ rrect = rrect.inflate(-1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 1);
+ expect(rrect.blRadiusY, 1);
+ expect(rrect.brRadiusX, 2);
+ expect(rrect.brRadiusY, 2);
+
+ rrect = rrect.inflate(-1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 0);
+ expect(rrect.blRadiusY, 0);
+ expect(rrect.brRadiusX, 1);
+ expect(rrect.brRadiusY, 1);
+
+ rrect = rrect.inflate(-1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 0);
+ expect(rrect.blRadiusY, 0);
+ expect(rrect.brRadiusX, 0);
+ expect(rrect.brRadiusY, 0);
+ });
+
+ test('RRect.deflate clamps when deflating past zero', () {
+ RRect rrect = RRect.fromRectAndCorners(
+ const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+ topLeft: const Radius.circular(1),
+ topRight: const Radius.circular(2),
+ bottomLeft: const Radius.circular(3),
+ bottomRight: const Radius.circular(4),
+ ).deflate(1);
+
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 1);
+ expect(rrect.trRadiusY, 1);
+ expect(rrect.blRadiusX, 2);
+ expect(rrect.blRadiusY, 2);
+ expect(rrect.brRadiusX, 3);
+ expect(rrect.brRadiusY, 3);
+
+ rrect = rrect.deflate(1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 1);
+ expect(rrect.blRadiusY, 1);
+ expect(rrect.brRadiusX, 2);
+ expect(rrect.brRadiusY, 2);
+
+ rrect = rrect.deflate(1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 0);
+ expect(rrect.blRadiusY, 0);
+ expect(rrect.brRadiusX, 1);
+ expect(rrect.brRadiusY, 1);
+
+ rrect = rrect.deflate(1);
+ expect(rrect.tlRadiusX, 0);
+ expect(rrect.tlRadiusY, 0);
+ expect(rrect.trRadiusX, 0);
+ expect(rrect.trRadiusY, 0);
+ expect(rrect.blRadiusX, 0);
+ expect(rrect.blRadiusY, 0);
+ expect(rrect.brRadiusX, 0);
+ expect(rrect.brRadiusY, 0);
+ });
}
diff --git a/tools/gn b/tools/gn
index 7b218ff..f49a3c2 100755
--- a/tools/gn
+++ b/tools/gn
@@ -211,6 +211,14 @@
gn_args = {}
+ gn_args['is_debug'] = args.unoptimized
+
+ # If building for WASM, set the GN args using 'to_gn_wasm_args' as most
+ # of the Flutter SDK specific arguments are unused.
+ if args.target_os == 'wasm':
+ to_gn_wasm_args(args, gn_args)
+ return gn_args
+
if args.bitcode:
if args.target_os != 'ios':
raise Exception('Bitcode is only supported for iOS')
@@ -237,13 +245,8 @@
if args.enable_skshaper:
gn_args['skia_use_icu'] = True
- if args.target_os != 'wasm':
- gn_args['flutter_always_use_skshaper'] = args.always_use_skshaper
- else:
- gn_args['skia_use_harfbuzz'] = True
- gn_args['icu_use_data_file'] = False
+ gn_args['flutter_always_use_skshaper'] = args.always_use_skshaper
gn_args['is_official_build'] = True # Disable Skia test utilities.
- gn_args['is_debug'] = args.unoptimized
gn_args['android_full_debug'
] = args.target_os == 'android' and args.unoptimized
if args.clang is None:
@@ -253,8 +256,6 @@
if args.target_os == 'android' or args.target_os == 'ios':
gn_args['skia_gl_standard'] = 'gles'
- elif args.target_os == 'wasm':
- gn_args['skia_gl_standard'] = 'webgl'
else:
# We explicitly don't want to pick GL because we run GLES tests using SwiftShader.
gn_args['skia_gl_standard'] = ''
@@ -318,36 +319,34 @@
gn_args['dart_debug'] = True
gn_args['dart_debug_optimization_level'] = '0'
- # Flutter-specific arguments which don't apply for a CanvasKit build.
- if args.target_os != 'wasm':
- gn_args['flutter_use_fontconfig'] = args.enable_fontconfig
- gn_args['flutter_enable_skshaper'] = args.enable_skshaper
- gn_args['dart_component_kind'
- ] = 'static_library' # Always link Dart in statically.
- gn_args['embedder_for_target'] = args.embedder_for_target
- gn_args['dart_lib_export_symbols'] = False
- gn_args['flutter_runtime_mode'] = runtime_mode
- gn_args['full_dart_sdk'] = args.full_dart_sdk
- gn_args['dart_version_git_info'] = not args.no_dart_version_git_info
+ gn_args['flutter_use_fontconfig'] = args.enable_fontconfig
+ gn_args['flutter_enable_skshaper'] = args.enable_skshaper
+ gn_args['dart_component_kind'
+ ] = 'static_library' # Always link Dart in statically.
+ gn_args['embedder_for_target'] = args.embedder_for_target
+ gn_args['dart_lib_export_symbols'] = False
+ gn_args['flutter_runtime_mode'] = runtime_mode
+ gn_args['full_dart_sdk'] = args.full_dart_sdk
+ gn_args['dart_version_git_info'] = not args.no_dart_version_git_info
- gn_args['dart_lib_export_symbols'] = False
- if runtime_mode == 'debug':
- gn_args['dart_runtime_mode'] = 'develop'
- elif runtime_mode == 'jit_release':
- gn_args['dart_runtime_mode'] = 'release'
- else:
- gn_args['dart_runtime_mode'] = runtime_mode
+ gn_args['dart_lib_export_symbols'] = False
+ if runtime_mode == 'debug':
+ gn_args['dart_runtime_mode'] = 'develop'
+ elif runtime_mode == 'jit_release':
+ gn_args['dart_runtime_mode'] = 'release'
+ else:
+ gn_args['dart_runtime_mode'] = runtime_mode
- # Desktop embeddings can have more dependencies than the engine library,
- # which can be problematic in some build environments (e.g., building on
- # Linux will bring in pkg-config dependencies at generation time). These
- # flags allow preventing those those targets from being part of the build
- # tree.
- gn_args['enable_desktop_embeddings'] = not args.disable_desktop_embeddings
+ # Desktop embeddings can have more dependencies than the engine library,
+ # which can be problematic in some build environments (e.g., building on
+ # Linux will bring in pkg-config dependencies at generation time). These
+ # flags allow preventing those those targets from being part of the build
+ # tree.
+ gn_args['enable_desktop_embeddings'] = not args.disable_desktop_embeddings
- # Overrides whether Boring SSL is compiled with system as. Only meaningful
- # on Android.
- gn_args['bssl_use_clang_integrated_as'] = True
+ # Overrides whether Boring SSL is compiled with system as. Only meaningful
+ # on Android.
+ gn_args['bssl_use_clang_integrated_as'] = True
if args.allow_deprecated_api_calls:
gn_args['allow_deprecated_api_calls'] = args.allow_deprecated_api_calls
@@ -417,8 +416,7 @@
else:
gn_args['skia_use_gl'] = args.target_os != 'fuchsia'
- if sys.platform == 'darwin' and args.target_os not in ['android', 'fuchsia',
- 'wasm']:
+ if sys.platform == 'darwin' and args.target_os not in ['android', 'fuchsia']:
# OpenGL is deprecated on macOS > 10.11.
# This is not necessarily needed but enabling this until we have a way to
# build a macOS metal only shell and a gl only shell.
@@ -429,7 +427,7 @@
# Enable Vulkan on all platforms except for Android and iOS. This is just
# to save on mobile binary size, as there's no reason the Vulkan embedder
# features can't work on these platforms.
- if args.target_os not in ['android', 'ios', 'wasm']:
+ if args.target_os not in ['android', 'ios']:
gn_args['skia_use_vulkan'] = True
gn_args['skia_vulkan_memory_allocator_dir'
] = '//third_party/vulkan_memory_allocator'
@@ -569,6 +567,67 @@
return gn_args
+# When building for WASM, almost all GN args used in the Flutter SDK
+# build are unused. This method is used instead.
+def to_gn_wasm_args(args, gn_args):
+ gn_args['is_official_build'] = True
+ gn_args['skia_enable_flutter_defines'] = True
+ gn_args['is_component_build'] = False
+ gn_args['use_clang_static_analyzer'] = False
+ gn_args['is_clang'] = True
+ gn_args['target_os'] = 'wasm'
+ gn_args['target_cpu'] = 'wasm'
+ gn_args['skia_use_angle'] = False
+ gn_args['skia_use_dng_sdk'] = False
+ gn_args['skia_use_expat'] = False
+ gn_args['skia_use_vulkan'] = False
+ gn_args['skia_use_webgpu'] = False
+ gn_args['skia_use_libheif'] = False
+ gn_args['skia_use_libjpeg_turbo_decode'] = True
+ gn_args['skia_use_libjpeg_turbo_encode'] = False
+ gn_args['skia_use_libpng_decode'] = True
+ gn_args['skia_use_libpng_encode'] = True
+ gn_args['skia_use_libwebp_decode'] = True
+ gn_args['skia_use_libwebp_encode'] = False
+ gn_args['skia_use_lua'] = False
+ gn_args['skia_use_wuffs'] = True
+ gn_args['skia_use_zlib'] = True
+ gn_args['skia_gl_standard'] = 'webgl'
+ gn_args['skia_enable_gpu'] = True
+ gn_args['skia_enable_sksl_tracing'] = False
+ gn_args['skia_use_icu'] = True
+ gn_args['icu_use_data_file'] = False
+ gn_args['skia_use_freetype'] = True
+ gn_args['skia_use_harfbuzz'] = True
+ gn_args['skia_use_fontconfig'] = False
+ gn_args['skia_use_libheif'] = False
+ gn_args['skia_enable_fontmgr_custom_directory'] = False
+ gn_args['skia_enable_fontmgr_custom_embedded'] = True
+ gn_args['skia_enable_fontmgr_custom_empty'] = False
+ gn_args['skia_enable_skshaper'] = True
+ gn_args['skia_enable_skparagraph'] = True
+ gn_args['skia_canvaskit_force_tracing'] = False
+ gn_args['skia_canvaskit_enable_skp_serialization'] = True
+ gn_args['skia_canvaskit_enable_effects_deserialization'] = False
+ gn_args['skia_canvaskit_enable_skottie'] = False
+ gn_args['skia_canvaskit_include_viewer'] = False
+ gn_args['skia_canvaskit_enable_particles'] = False
+ gn_args['skia_canvaskit_enable_pathops'] = True
+ gn_args['skia_canvaskit_enable_rt_shader'] = True
+ gn_args['skia_canvaskit_enable_matrix_helper'] = False
+ gn_args['skia_canvaskit_enable_canvas_bindings'] = False
+ gn_args['skia_canvaskit_enable_font'] = True
+ gn_args['skia_canvaskit_enable_embedded_font'] = True
+ gn_args['skia_canvaskit_enable_alias_font'] = True
+ gn_args['skia_canvaskit_legacy_draw_vertices_blend_mode'] = False
+ gn_args['skia_canvaskit_enable_debugger'] = False
+ gn_args['skia_canvaskit_enable_paragraph'] = True
+ gn_args['skia_canvaskit_enable_webgl'] = True
+ gn_args['skia_canvaskit_enable_webgpu'] = False
+ is_profile_build = args.runtime_mode == 'profile' or args.runtime_mode == 'debug'
+ gn_args['skia_canvaskit_profile_build'] = is_profile_build
+
+
def parse_args(args):
args = args[1:]
parser = argparse.ArgumentParser(description='A script to run `gn gen`.')
diff --git a/wasm/BUILD.gn b/wasm/BUILD.gn
new file mode 100644
index 0000000..7d799fe
--- /dev/null
+++ b/wasm/BUILD.gn
@@ -0,0 +1,12 @@
+# Copyright 2022 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 BUILD.gn is kept separate from //flutter/BUILD.gn because
+# //flutter/BUILD.gn pulls in Flutter SDK dependencies which will crash
+# when the target CPU is WASM.
+
+# This is the default target when building when the target CPU is WASM.
+group("wasm") {
+ deps = [ "//third_party/skia/modules/canvaskit" ]
+}