From 0fce942de77dd5a81ea4248fb0a4ea84993d17c6 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 12 Sep 2022 16:09:05 -0700 Subject: [PATCH] Add `Radius.clamp` and `Radius.clampValues` (#36106) --- ci/licenses_golden/licenses_flutter | 2 + lib/ui/geometry.dart | 31 +++++++++++ lib/ui/math.dart | 25 +++++++++ lib/ui/painting.dart | 2 +- lib/ui/ui.dart | 1 + lib/web_ui/lib/geometry.dart | 19 +++++++ lib/web_ui/lib/math.dart | 19 +++++++ lib/web_ui/lib/painting.dart | 2 +- lib/web_ui/lib/ui.dart | 1 + lib/web_ui/test/geometry_test.dart | 82 ++++++++++++++++++++++++++++ testing/dart/geometry_test.dart | 84 +++++++++++++++++++++++++++++ 11 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 lib/ui/math.dart create mode 100644 lib/web_ui/lib/math.dart diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 643fd39f80bf1..b8c5efd95cf05 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.cc 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/hash_codes.dart 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/geometry.dart b/lib/ui/geometry.dart index 8a1fe6f00f9ca..2b7d61a47651a 100644 --- a/lib/ui/geometry.dart +++ b/lib/ui/geometry.dart @@ -939,6 +939,37 @@ class Radius { /// 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. diff --git a/lib/ui/math.dart b/lib/ui/math.dart new file mode 100644 index 0000000000000..aeb835a2fc287 --- /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 3c0a7ad4ae07c..316de56944caf 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -320,7 +320,7 @@ class Color { /// 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 2695a80916ee4..36818d3c74480 100644 --- a/lib/ui/ui.dart +++ b/lib/ui/ui.dart @@ -31,6 +31,7 @@ part 'hooks.dart'; 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/lib/geometry.dart b/lib/web_ui/lib/geometry.dart index 3d60b13e92601..412f19137f2f9 100644 --- a/lib/web_ui/lib/geometry.dart +++ b/lib/web_ui/lib/geometry.dart @@ -341,6 +341,25 @@ class Radius { 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); diff --git a/lib/web_ui/lib/math.dart b/lib/web_ui/lib/math.dart new file mode 100644 index 0000000000000..adea127ccec1f --- /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 3ab2ba0d3e69b..03e44ecce0b0d 100644 --- a/lib/web_ui/lib/painting.dart +++ b/lib/web_ui/lib/painting.dart @@ -146,7 +146,7 @@ class Color { 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/ui.dart b/lib/web_ui/lib/ui.dart index a01d53936b6e0..1878d4faa7b39 100644 --- a/lib/web_ui/lib/ui.dart +++ b/lib/web_ui/lib/ui.dart @@ -24,6 +24,7 @@ part 'hash_codes.dart'; 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/geometry_test.dart b/lib/web_ui/test/geometry_test.dart index 62b7f528cb7af..40b2ffd4c9d9c 100644 --- a/lib/web_ui/test/geometry_test.dart +++ b/lib/web_ui/test/geometry_test.dart @@ -75,4 +75,86 @@ void testMain() { 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))); + }); } diff --git a/testing/dart/geometry_test.dart b/testing/dart/geometry_test.dart index 8e0101dd59b9c..27c22ff7607d1 100644 --- a/testing/dart/geometry_test.dart +++ b/testing/dart/geometry_test.dart @@ -292,4 +292,88 @@ void main() { 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))); + }); }