From 7561caa71f73a82aaa7748e2673b803185dac352 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Mon, 24 Mar 2025 20:07:16 -0500 Subject: [PATCH] Add factory constructors for shorthands that would collide (#165597) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EdgeInsets and EdgeInsetsDirectional are subtypes of EdgeInsetsGeometry. The both have only, all, and symmetric constructors, which creates collisions in a world where Dart support shorthand notation such as: ```dart Padding( padding: .only(bottom: 10.0), ), ``` The same goes for all these classes: - AlignmentGeometry - Alignment - AlignmentDirectional - BorderRadiusGeometry - BorderRadius - BorderRadiusDirectional - BoxBorder - Border - BorderDirectional - EdgeInsetsGeometry - EdgeInsets - EdgeInsetsDirectional The shorthands feature is not planning to exhaustively check subtypes. However, these classes were identified as the most impactful for the shorthands feature (❗ ). Adding these factories in the super classes enables future shorthand support. Prior in https://github.com/flutter/flutter/pull/159703 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/lib/src/painting/alignment.dart | 6 ++ .../lib/src/painting/border_radius.dart | 62 ++++++++++++++++++ .../flutter/lib/src/painting/box_border.dart | 42 ++++++++++++ .../flutter/lib/src/painting/edge_insets.dart | 40 ++++++++++++ .../flutter/test/painting/alignment_test.dart | 5 ++ .../test/painting/border_radius_test.dart | 64 +++++++++++++++++++ .../flutter/test/painting/border_test.dart | 21 ++++++ .../test/painting/edge_insets_test.dart | 31 +++++++++ 8 files changed, 271 insertions(+) diff --git a/packages/flutter/lib/src/painting/alignment.dart b/packages/flutter/lib/src/painting/alignment.dart index f4389fb2e18..6d77171bcb0 100644 --- a/packages/flutter/lib/src/painting/alignment.dart +++ b/packages/flutter/lib/src/painting/alignment.dart @@ -26,6 +26,12 @@ abstract class AlignmentGeometry { /// const constructors so that they can be used in const expressions. const AlignmentGeometry(); + /// Creates an [Alignment]. + const factory AlignmentGeometry.xy(double x, double y) = Alignment; + + /// Creates a directional alignment, or [AlignmentDirectional]. + const factory AlignmentGeometry.directional(double start, double y) = AlignmentDirectional; + double get _x; double get _start; diff --git a/packages/flutter/lib/src/painting/border_radius.dart b/packages/flutter/lib/src/painting/border_radius.dart index 6a6dbf09154..f6bd3f646d2 100644 --- a/packages/flutter/lib/src/painting/border_radius.dart +++ b/packages/flutter/lib/src/painting/border_radius.dart @@ -26,6 +26,68 @@ abstract class BorderRadiusGeometry { /// const constructors so that they can be used in const expressions. const BorderRadiusGeometry(); + /// Creates a [BorderRadius] where all radii are `radius`. + // The radius applies equally on all sides, so BorderRadiusDirectional is + // irrelevant in this case. + const factory BorderRadiusGeometry.all(Radius radius) = BorderRadius.all; + + /// Creates a [BorderRadius] where all radii are [Radius.circular(radius)]. + // The radius applies equally on all sides, so BorderRadiusDirectional is + // irrelevant in this case. + factory BorderRadiusGeometry.circular(double radius) = BorderRadius.circular; + + /// Creates a horizontally symmetrical border radius. + /// + /// Utilizing the `left` and `right` properties will return a [BorderRadius], + /// while `start` and `end` will yield a [BorderRadiusDirectional]. These + /// properties cannot be used interchangeably. + factory BorderRadiusGeometry.horizontal({ + Radius? left, // BorderRadius + Radius? right, // BorderRadius + Radius? start, // BorderRadiusDirectional + Radius? end, // BorderRadiusDirectional + }) { + assert( + (left == null && right == null) || (start == null && end == null), + 'The left and right values cannot be used in conjunction with start and end.', + ); + + if (start != null || end != null) { + return BorderRadiusDirectional.horizontal( + start: start ?? Radius.zero, + end: end ?? Radius.zero, + ); + } + return BorderRadius.horizontal(left: left ?? Radius.zero, right: right ?? Radius.zero); + } + + /// Creates a [BorderRadius] with only the given non-zero values. + /// + /// The other corners will be right angles. + const factory BorderRadiusGeometry.only({ + Radius topLeft, + Radius topRight, + Radius bottomLeft, + Radius bottomRight, + }) = BorderRadius.only; + + /// Creates a [BorderRadiusDirectional] with only the given non-zero values. + /// + /// The other corners will be right angles. + const factory BorderRadiusGeometry.directional({ + Radius topStart, + Radius topEnd, + Radius bottomStart, + Radius bottomEnd, + }) = BorderRadiusDirectional.only; + + /// Creates a vertically symmetric [BorderRadius] where the top and bottom + /// sides of the rectangle have the same radii. + const factory BorderRadiusGeometry.vertical({Radius top, Radius bottom}) = BorderRadius.vertical; + + /// A [BorderRadius] with all zero radii. + static const BorderRadiusGeometry zero = BorderRadius.zero; + Radius get _topLeft; Radius get _topRight; Radius get _bottomLeft; diff --git a/packages/flutter/lib/src/painting/box_border.dart b/packages/flutter/lib/src/painting/box_border.dart index 2f9f47791c0..f5045f592d5 100644 --- a/packages/flutter/lib/src/painting/box_border.dart +++ b/packages/flutter/lib/src/painting/box_border.dart @@ -67,6 +67,48 @@ abstract class BoxBorder extends ShapeBorder { /// const constructors so that they can be used in const expressions. const BoxBorder(); + /// Creates a [Border]. + /// + /// All the sides of the border default to [BorderSide.none]. + factory BoxBorder.fromLTRB({ + BorderSide top = BorderSide.none, + BorderSide right = BorderSide.none, + BorderSide bottom = BorderSide.none, + BorderSide left = BorderSide.none, + }) => Border(top: top, right: right, bottom: bottom, left: left); + + /// A uniform [Border] with all sides the same color and width. + /// + /// The sides default to black solid borders, one logical pixel wide. + factory BoxBorder.all({Color color, double width, BorderStyle style, double strokeAlign}) = + Border.all; + + /// Creates a [Border] whose sides are all the same. + const factory BoxBorder.fromBorderSide(BorderSide side) = Border.fromBorderSide; + + /// Creates a [Border] with symmetrical vertical and horizontal sides. + /// + /// The `vertical` argument applies to the [left] and [right] sides, and the + /// `horizontal` argument applies to the [top] and [bottom] sides. + /// + /// All arguments default to [BorderSide.none]. + const factory BoxBorder.symmetric({BorderSide vertical, BorderSide horizontal}) = + Border.symmetric; + + /// Creates a [BorderDirectional]. + /// + /// The [start] and [end] sides represent the horizontal sides; the start side + /// is on the leading edge given the reading direction, and the end side is on + /// the trailing edge. They are resolved during [paint]. + /// + /// All the sides of the border default to [BorderSide.none]. + factory BoxBorder.fromSTEB({ + BorderSide top = BorderSide.none, + BorderSide start = BorderSide.none, + BorderSide end = BorderSide.none, + BorderSide bottom = BorderSide.none, + }) => BorderDirectional(top: top, start: start, end: end, bottom: bottom); + /// The top side of this border. /// /// This getter is available on both [Border] and [BorderDirectional]. If diff --git a/packages/flutter/lib/src/painting/edge_insets.dart b/packages/flutter/lib/src/painting/edge_insets.dart index ba1d11c81cd..993d7807282 100644 --- a/packages/flutter/lib/src/painting/edge_insets.dart +++ b/packages/flutter/lib/src/painting/edge_insets.dart @@ -32,6 +32,46 @@ abstract class EdgeInsetsGeometry { /// const constructors so that they can be used in const expressions. const EdgeInsetsGeometry(); + /// Creates insets where all the offsets are `value`. + const factory EdgeInsetsGeometry.all(double value) = EdgeInsets.all; + + /// Creates [EdgeInsets] with only the given values non-zero. + const factory EdgeInsetsGeometry.only({double left, double right, double top, double bottom}) = + EdgeInsets.only; + + /// Creates [EdgeInsetsDirectional] with only the given values non-zero. + const factory EdgeInsetsGeometry.directional({ + double start, + double end, + double top, + double bottom, + }) = EdgeInsetsDirectional.only; + + /// Creates [EdgeInsets] with symmetrical vertical and horizontal offsets. + const factory EdgeInsetsGeometry.symmetric({double vertical, double horizontal}) = + EdgeInsets.symmetric; + + /// Creates [EdgeInsets] from offsets from the left, top, right, and bottom. + const factory EdgeInsetsGeometry.fromLTRB(double left, double top, double right, double bottom) = + EdgeInsets.fromLTRB; + + /// Creates [EdgeInsets] that match the given view padding. + /// + /// If you need the current system padding or view insets in the context of a + /// widget, consider using [MediaQuery.paddingOf] to obtain these values + /// rather than using the value from a [FlutterView] directly, so that you get + /// notified of changes. + factory EdgeInsetsGeometry.fromViewPadding(ui.ViewPadding padding, double devicePixelRatio) = + EdgeInsets.fromViewPadding; + + /// Creates [EdgeInsetsDirectional] from offsets from the start, top, end, and + /// bottom. + const factory EdgeInsetsGeometry.fromSTEB(double start, double top, double end, double bottom) = + EdgeInsetsDirectional.fromSTEB; + + /// An [EdgeInsets] with zero offsets in each direction. + static const EdgeInsetsGeometry zero = EdgeInsets.zero; + double get _bottom; double get _end; double get _left; diff --git a/packages/flutter/test/painting/alignment_test.dart b/packages/flutter/test/painting/alignment_test.dart index 5a65722eeab..fa6612087d8 100644 --- a/packages/flutter/test/painting/alignment_test.dart +++ b/packages/flutter/test/painting/alignment_test.dart @@ -333,4 +333,9 @@ void main() { 'Alignment(1.0, 2.0) + AlignmentDirectional.centerEnd', ); }); + + test('AlignmentGeometry factories', () { + expect(const AlignmentGeometry.xy(4, 5), const Alignment(4, 5)); + expect(const AlignmentGeometry.directional(4, 5), const AlignmentDirectional(4, 5)); + }); } diff --git a/packages/flutter/test/painting/border_radius_test.dart b/packages/flutter/test/painting/border_radius_test.dart index a516035282b..7b61124ac3a 100644 --- a/packages/flutter/test/painting/border_radius_test.dart +++ b/packages/flutter/test/painting/border_radius_test.dart @@ -621,4 +621,68 @@ void main() { borderRadius, ); }); + + test('BorderRadiusGeometry factories', () { + const Radius radius5 = Radius.circular(5); + const Radius radius10 = Radius.circular(10); + const Radius radius15 = Radius.circular(15); + const Radius radius20 = Radius.circular(20); + expect(const BorderRadiusGeometry.all(radius10), const BorderRadius.all(radius10)); + expect(BorderRadiusGeometry.circular(10), BorderRadius.circular(10)); + expect( + BorderRadiusGeometry.horizontal(left: radius5, right: radius10), + const BorderRadius.horizontal(left: radius5, right: radius10), + ); + expect( + BorderRadiusGeometry.horizontal(start: radius5, end: radius10), + const BorderRadiusDirectional.horizontal(start: radius5, end: radius10), + ); + expect(() { + BorderRadiusGeometry.horizontal(start: radius5, left: radius10); + }, throwsAssertionError); + expect(() { + BorderRadiusGeometry.horizontal(end: radius5, right: radius10); + }, throwsAssertionError); + expect(() { + BorderRadiusGeometry.horizontal( + end: radius5, + right: radius10, + start: radius5, + left: radius10, + ); + }, throwsAssertionError); + expect( + const BorderRadiusGeometry.only( + topLeft: radius5, + topRight: radius10, + bottomLeft: radius15, + bottomRight: radius20, + ), + const BorderRadius.only( + topLeft: radius5, + topRight: radius10, + bottomLeft: radius15, + bottomRight: radius20, + ), + ); + expect( + const BorderRadiusGeometry.directional( + topStart: radius5, + topEnd: radius10, + bottomStart: radius15, + bottomEnd: radius20, + ), + const BorderRadiusDirectional.only( + topStart: radius5, + topEnd: radius10, + bottomStart: radius15, + bottomEnd: radius20, + ), + ); + expect( + const BorderRadiusGeometry.vertical(top: radius5, bottom: radius10), + const BorderRadius.vertical(top: radius5, bottom: radius10), + ); + expect(BorderRadiusGeometry.zero, BorderRadius.zero); + }); } diff --git a/packages/flutter/test/painting/border_test.dart b/packages/flutter/test/painting/border_test.dart index 902cd38b327..ca550d182bb 100644 --- a/packages/flutter/test/painting/border_test.dart +++ b/packages/flutter/test/painting/border_test.dart @@ -482,6 +482,27 @@ void main() { expect((ShapeWithoutInterior() + ShapeWithInterior()).preferPaintInterior, isFalse); expect((ShapeWithoutInterior() + ShapeWithoutInterior()).preferPaintInterior, isFalse); }); + + test('BoxBorder factories', () { + const BorderSide side1 = BorderSide(); + const BorderSide side2 = BorderSide(width: 2); + const BorderSide side3 = BorderSide(width: 3); + const BorderSide side4 = BorderSide(width: 4); + expect( + BoxBorder.fromLTRB(left: side1, top: side2, right: side3, bottom: side4), + const Border(left: side1, top: side2, right: side3, bottom: side4), + ); + expect(BoxBorder.all(width: 4), Border.all(width: 4)); + expect(const BoxBorder.fromBorderSide(side3), const Border.fromBorderSide(side3)); + expect( + const BoxBorder.symmetric(horizontal: side2, vertical: side3), + const Border.symmetric(horizontal: side2, vertical: side3), + ); + expect( + BoxBorder.fromSTEB(start: side1, top: side2, end: side3, bottom: side4), + const BorderDirectional(start: side1, top: side2, end: side3, bottom: side4), + ); + }); } class ShapeWithInterior extends ShapeBorder { diff --git a/packages/flutter/test/painting/edge_insets_test.dart b/packages/flutter/test/painting/edge_insets_test.dart index 0142a536239..01e3f7de10f 100644 --- a/packages/flutter/test/painting/edge_insets_test.dart +++ b/packages/flutter/test/painting/edge_insets_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' as ui; + import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -442,4 +444,33 @@ void main() { final EdgeInsetsDirectional copy = sourceEdgeInsets.copyWith(start: 5.0, top: 6.0); expect(copy, const EdgeInsetsDirectional.only(start: 5.0, top: 6.0, bottom: 3.0, end: 4.0)); }); + + test('EdgeInsetsGeometry factories', () { + expect(const EdgeInsetsGeometry.all(10), const EdgeInsets.all(10)); + expect( + const EdgeInsetsGeometry.only(left: 10, top: 20, right: 30, bottom: 40), + const EdgeInsets.only(left: 10, top: 20, right: 30, bottom: 40), + ); + expect( + const EdgeInsetsGeometry.directional(start: 10, top: 20, end: 30, bottom: 40), + const EdgeInsetsDirectional.only(start: 10, top: 20, end: 30, bottom: 40), + ); + expect( + const EdgeInsetsGeometry.symmetric(horizontal: 10, vertical: 20), + const EdgeInsets.symmetric(horizontal: 10, vertical: 20), + ); + expect( + const EdgeInsetsGeometry.fromLTRB(10, 20, 30, 40), + const EdgeInsets.fromLTRB(10, 20, 30, 40), + ); + expect( + EdgeInsetsGeometry.fromViewPadding(ui.ViewPadding.zero, 10), + EdgeInsets.fromViewPadding(ui.ViewPadding.zero, 10), + ); + expect( + const EdgeInsetsGeometry.fromSTEB(10, 20, 20, 40), + const EdgeInsetsDirectional.fromSTEB(10, 20, 20, 40), + ); + expect(EdgeInsetsGeometry.zero, EdgeInsets.zero); + }); }