From 3a181e495a403a6c8c88a5990cab9ed17444a5d7 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Fri, 13 Jan 2023 08:04:22 -0800 Subject: [PATCH] Added LinearBorder, an OutlinedBorder like BoxBorder (#116940) --- .../linear_border/linear_border.0.dart | 303 ++++++++++++++ .../test/painting/linear_border.0_test.dart | 40 ++ packages/flutter/lib/painting.dart | 1 + .../lib/src/painting/linear_border.dart | 389 ++++++++++++++++++ .../test/painting/linear_border_test.dart | 168 ++++++++ 5 files changed, 901 insertions(+) create mode 100644 examples/api/lib/painting/linear_border/linear_border.0.dart create mode 100644 examples/api/test/painting/linear_border.0_test.dart create mode 100644 packages/flutter/lib/src/painting/linear_border.dart create mode 100644 packages/flutter/test/painting/linear_border_test.dart diff --git a/examples/api/lib/painting/linear_border/linear_border.0.dart b/examples/api/lib/painting/linear_border/linear_border.0.dart new file mode 100644 index 000000000000..c011cedb7d3a --- /dev/null +++ b/examples/api/lib/painting/linear_border/linear_border.0.dart @@ -0,0 +1,303 @@ +// Copyright 2014 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. + +// Examples of LinearBorder and LinearBorderEdge. + + +import 'package:flutter/material.dart'; + +void main() { + runApp(const ExampleApp()); +} + +class ExampleApp extends StatelessWidget { + const ExampleApp({ super.key }); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData.light(useMaterial3: true), + home: const Directionality( + textDirection: TextDirection.ltr, // Or try rtl. + child: Home(), + ), + ); + } +} + +class SampleCard extends StatelessWidget { + const SampleCard({ super.key, required this.title, required this.subtitle, required this.children }); + + final String title; + final String subtitle; + final List children; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextTheme textTheme = theme.textTheme; + final ColorScheme colorScheme = theme.colorScheme; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(title, style: textTheme.titleMedium), + Text(subtitle, style: textTheme.bodyMedium!.copyWith(color: colorScheme.secondary)), + const SizedBox(height: 16), + Row( + children: List.generate(children.length * 2 - 1, (int index) { + return index.isEven ? children[index ~/2] : const SizedBox(width: 16); + }), + ), + ], + ), + ), + ); + } +} + +class Home extends StatefulWidget { + const Home({ super.key }); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State { + final LinearBorder shape0 = LinearBorder.top(); + final LinearBorder shape1 = LinearBorder.top(size: 0); + late LinearBorder shape = shape0; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final BorderSide primarySide0 = BorderSide(width: 0, color: colorScheme.inversePrimary); // hairline + final BorderSide primarySide2 = BorderSide(width: 2, color: colorScheme.onPrimaryContainer); + final BorderSide primarySide3 = BorderSide(width: 3, color: colorScheme.inversePrimary); + + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Demonstrates using LinearBorder.bottom() to define + // an underline border for the standard button types. + // The underline's color and width is defined by the ButtonStyle's + // side parameter. The side can also be specified as a + // LinearBorder parameter and if both are specified then the + // ButtonStyle's side is used. This set up makes it possible + // for a button theme to specify the shape and for indidividual + // buttons to specify the shape border's color and width. + SampleCard( + title: 'LinearBorder.bottom()', + subtitle: 'Standard button widgets', + children: [ + TextButton( + style: TextButton.styleFrom( + side: primarySide3, + shape: LinearBorder.bottom(), + ), + onPressed: () { }, + child: const Text('Text'), + ), + OutlinedButton( + style: OutlinedButton.styleFrom( + side: primarySide3, + shape: LinearBorder.bottom(), + ), + onPressed: () { }, + child: const Text('Outlined'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + side: primarySide3, + shape: LinearBorder.bottom(), + ), + onPressed: () { }, + child: const Text('Elevated'), + ), + ], + ), + const SizedBox(height: 32), + // Demonstrates creating LinearBorders with a single edge + // by using the convenience constructors like LinearBorder.start(). + // The edges are drawn with a BorderSide with width:0, which + // means that a "hairline" line is stroked. Wider borders are + // drawn with filled rectangles. + SampleCard( + title: 'LinearBorder', + subtitle: 'Convenience constructors', + children: [ + TextButton( + style: TextButton.styleFrom( + side: primarySide0, + shape: LinearBorder.start(), + ), + onPressed: () { }, + child: const Text('Start()'), + ), + TextButton( + style: TextButton.styleFrom( + side: primarySide0, + shape: LinearBorder.end(), + ), + onPressed: () { }, + child: const Text('End()'), + ), + TextButton( + style: TextButton.styleFrom( + side: primarySide0, + shape: LinearBorder.top(), + ), + onPressed: () { }, + child: const Text('Top()'), + ), + TextButton( + style: TextButton.styleFrom( + side: primarySide0, + shape: LinearBorder.bottom(), + ), + onPressed: () { }, + child: const Text('Bottom()'), + ), + ], + ), + const SizedBox(height: 32), + // Demonstrates creating LinearBorders with a single edge + // that's smaller than the button's bounding box. The size + // parameter specifies a percentage of the available space + // and alignment is -1 for start-alignment, 0 for centered, + // and 1 for end-alignment. + SampleCard( + title: 'LinearBorder', + subtitle: 'Size and alignment parameters', + children: [ + TextButton( + style: TextButton.styleFrom( + side: primarySide2, + shape: LinearBorder.bottom( + size: 0.5, + ), + ), + onPressed: () { }, + child: const Text('Center'), + ), + TextButton( + style: TextButton.styleFrom( + side: primarySide2, + shape: LinearBorder.bottom( + size: 0.75, + alignment: -1, + ), + ), + onPressed: () { }, + child: const Text('Start'), + ), + TextButton( + style: TextButton.styleFrom( + side: primarySide2, + shape: LinearBorder.bottom( + size: 0.75, + alignment: 1, + ), + ), + onPressed: () { }, + child: const Text('End'), + ), + ], + ), + const SizedBox(height: 32), + // Demonstrates creating LinearBorders with more than one edge. + // In these cases the default constructor is used and each edge + // is defined with one LinearBorderEdge object. + SampleCard( + title: 'LinearBorder', + subtitle: 'LinearBorderEdge parameters', + children: [ + TextButton( + style: TextButton.styleFrom( + side: primarySide0, + shape: const LinearBorder( + top: LinearBorderEdge(), + bottom: LinearBorderEdge(), + ), + ), + onPressed: () { }, + child: const Text('Horizontal'), + ), + TextButton( + style: TextButton.styleFrom( + side: primarySide0, + shape: const LinearBorder( + start: LinearBorderEdge(), + end: LinearBorderEdge(), + ), + ), + onPressed: () { }, + child: const Text('Vertical'), + ), + TextButton( + style: TextButton.styleFrom( + side: primarySide0, + shape: const LinearBorder( + start: LinearBorderEdge(), + bottom: LinearBorderEdge(), + ), + ), + onPressed: () { }, + child: const Text('Corner'), + ), + ], + ), + const SizedBox(height: 32), + // Demonstrates that changing properties of LinearBorders + // causes them to animate to their new configuration. + SampleCard( + title: 'Interpolation', + subtitle: 'LinearBorder.top() => LinearBorder.top(size: 0)', + children: [ + IconButton( + icon: const Icon(Icons.play_arrow), + onPressed: () { + setState(() { + shape = shape == shape0 ? shape1 : shape0; + }); + }, + ), + TextButton( + style: TextButton.styleFrom( + side: primarySide3, + shape: shape, + ), + onPressed: () { }, + child: const Text('Press Play'), + ), + TextButton( + style: ButtonStyle( + side: MaterialStateProperty.resolveWith((Set states) { + return states.contains(MaterialState.hovered) ? primarySide3 : null; + }), + shape: MaterialStateProperty.resolveWith((Set states) { + return states.contains(MaterialState.hovered) ? shape0 : shape1; + }), + + ), + onPressed: () { }, + child: const Text('Hover'), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/examples/api/test/painting/linear_border.0_test.dart b/examples/api/test/painting/linear_border.0_test.dart new file mode 100644 index 000000000000..7359ee394463 --- /dev/null +++ b/examples/api/test/painting/linear_border.0_test.dart @@ -0,0 +1,40 @@ +// Copyright 2014 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 'package:flutter/material.dart'; + +import 'package:flutter_api_samples/painting/linear_border/linear_border.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Smoke Test', (WidgetTester tester) async { + await tester.pumpWidget( + const example.ExampleApp(), + ); + expect(find.byType(example.Home), findsOneWidget); + + // Scroll the interpolation example into view + + await tester.scrollUntilVisible( + find.byIcon(Icons.play_arrow), + 500.0, + scrollable: find.byType(Scrollable), + ); + expect(find.byIcon(Icons.play_arrow), findsOneWidget); + + // Run the interpolation example + + await tester.tap(find.byIcon(Icons.play_arrow)); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.play_arrow)); + await tester.pumpAndSettle(); + + final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Interpolation'))); + await gesture.moveTo(tester.getCenter(find.text('Hover'))); + await tester.pumpAndSettle(); + await gesture.moveTo(tester.getCenter(find.text('Interpolation'))); + await tester.pumpAndSettle(); + }); +} diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart index ce86d8157dee..8a78b6bf4a18 100644 --- a/packages/flutter/lib/painting.dart +++ b/packages/flutter/lib/painting.dart @@ -47,6 +47,7 @@ export 'src/painting/image_provider.dart'; export 'src/painting/image_resolution.dart'; export 'src/painting/image_stream.dart'; export 'src/painting/inline_span.dart'; +export 'src/painting/linear_border.dart'; export 'src/painting/matrix_utils.dart'; export 'src/painting/notched_shapes.dart'; export 'src/painting/oval_border.dart'; diff --git a/packages/flutter/lib/src/painting/linear_border.dart b/packages/flutter/lib/src/painting/linear_border.dart new file mode 100644 index 000000000000..bc2cf814c4ad --- /dev/null +++ b/packages/flutter/lib/src/painting/linear_border.dart @@ -0,0 +1,389 @@ +// Copyright 2014 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:ui' show lerpDouble; + +import 'package:flutter/foundation.dart'; + +import 'basic_types.dart'; +import 'borders.dart'; +import 'edge_insets.dart'; + +/// Defines the relative size and alignment of one edge. +/// +/// A [LinearBorder] defines a box outline as zero to four edges, each +/// of which is rendered as a single line. The width and color of the +/// lines is defined by [LinearBorder.side]. +/// +/// Each line's length is defined by [size], a value between 0.0 and 1.0 +/// (the default) which defines the length as a percentage of the +/// length of a box edge. +/// +/// When [size] is less than 1.0, the line is aligned within the +/// available space according to [alignment], a value between -1.0 and +/// 1.0. The default is 0.0, which means centered, -1.0 means align on the +/// "start" side, and 1.0 means align on the "end" side. The meaning of +/// start and end depend on the current [TextDirection], see +/// [Directionality]. +@immutable +class LinearBorderEdge { + /// Defines one side of a [LinearBorder]. + /// + /// The values of [size] and [alignment] must be between + /// 0.0 and 1.0, and -1.0 and 1.0 respectively. + const LinearBorderEdge({ + this.size = 1.0, + this.alignment = 0.0, + }) : assert(size >= 0.0 && size <= 1.0); + + /// A value between 0.0 and 1.0 that defines the length of the edge as a + /// percentage of the length of the corresponding box + /// edge. Default is 1.0. + final double size; + + /// A value between -1.0 and 1.0 that defines how edges for which [size] + /// is less than 1.0 are aligned relative to the corresponding box edge. + /// + /// * -1.0, aligned in the "start" direction. That's left + /// for [TextDirection.ltr] and right for [TextDirection.rtl]. + /// * 0.0, centered. + /// * 1.0, aligned in the "end" direction. That's right + /// for [TextDirection.ltr] and left for [TextDirection.rtl]. + final double alignment; + + /// Linearly interpolates between two [LinearBorder]s. + /// + /// If both `a` and `b` are null then null is returned. If `a` is null + /// then we interpolate to `b` varying [size] from 0.0 to `b.size`. If `b` + /// is null then we interpolate from `a` varying size from `a.size` to zero. + /// Otherwise both values are interpolated. + static LinearBorderEdge? lerp(LinearBorderEdge? a, LinearBorderEdge? b, double t) { + if (a == null && b == null) { + return null; + } + + a ??= LinearBorderEdge(alignment: b!.alignment, size: 0); + b ??= LinearBorderEdge(alignment: a.alignment, size: 0); + + return LinearBorderEdge( + size: lerpDouble(a.size, b.size, t)!, + alignment: lerpDouble(a.alignment, b.alignment, t)!, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is LinearBorderEdge + && other.size == size + && other.alignment == alignment; + } + + @override + int get hashCode => Object.hash(size, alignment); + + @override + String toString() { + final StringBuffer s = StringBuffer('${objectRuntimeType(this, 'LinearBorderEdge')}('); + if (size != 1.0 ) { + s.write('size: $size'); + } + if (alignment != 0) { + final String comma = size != 1.0 ? ', ' : ''; + s.write('${comma}alignment: $alignment'); + } + s.write(')'); + return s.toString(); + } +} + +/// An [OutlinedBorder] like [BoxBorder] that allows one to define a rectangular (box) border +/// in terms of zero to four [LinearBorderEdge]s, each of which is rendered as a single line. +/// +/// The color and width of each line are defined by [side]. When [LinearBorder] is used +/// with a class whose border sides and shape are defined by a [ButtonStyle], then a non-null +/// [ButtonStyle.side] will override the one specified here. For example the [LinearBorder] +/// in the [TextButton] example below adds a red underline to the button. This is because +/// TextButton's `side` parameter overrides the `side` property of its [ButtonStyle.shape]. +/// +/// ```dart +/// TextButton( +/// style: TextButton.styleFrom( +/// side: const BorderSide(color: Colors.red), +/// shape: const LinearBorder( +/// side: BorderSide(color: Colors.blue), +/// bottom: LinearBorderEdge(), +/// ), +/// ), +/// onPressed: () { }, +/// child: const Text('Red LinearBorder'), +/// ) +///``` +/// +/// This class resolves itself against the current [TextDirection] (see [Directionality]). +/// Start and end values resolve to left and right for [TextDirection.ltr] and to +/// right and left for [TextDirection.rtl]. +/// +/// Convenience constructors are included for the common case where just one edge is specified: +/// [LinearBorder.start], [LinearBorder.end], [LinearBorder.top], [LinearBorder.bottom]. +class LinearBorder extends OutlinedBorder { + /// Creates a rectangular box border that's rendered as zero to four lines. + const LinearBorder({ + super.side, + this.start, + this.end, + this.top, + this.bottom, + }); + + /// Creates a rectangular box border with an edge on the left for [TextDirection.ltr] + /// or on the right for [TextDirection.rtl]. + LinearBorder.start({ + super.side, + double alignment = 0.0, + double size = 1.0 + }) : start = LinearBorderEdge(alignment: alignment, size: size), + end = null, + top = null, + bottom = null; + + /// Creates a rectangular box border with an edge on the right for [TextDirection.ltr] + /// or on the left for [TextDirection.rtl]. + LinearBorder.end({ + super.side, + double alignment = 0.0, + double size = 1.0 + }) : start = null, + end = LinearBorderEdge(alignment: alignment, size: size), + top = null, + bottom = null; + + /// Creates a rectangular box border with an edge on the top. + LinearBorder.top({ + super.side, + double alignment = 0.0, + double size = 1.0 + }) : start = null, + end = null, + top = LinearBorderEdge(alignment: alignment, size: size), + bottom = null; + + /// Creates a rectangular box border with an edge on the bottom. + LinearBorder.bottom({ + super.side, + double alignment = 0.0, + double size = 1.0 + }) : start = null, + end = null, + top = null, + bottom = LinearBorderEdge(alignment: alignment, size: size); + + /// No border. + static const LinearBorder none = LinearBorder(); + + /// Defines the left edge for [TextDirection.ltr] or the right + /// for [TextDirection.rtl]. + final LinearBorderEdge? start; + + /// Defines the right edge for [TextDirection.ltr] or the left + /// for [TextDirection.rtl]. + final LinearBorderEdge? end; + + /// Defines the top edge. + final LinearBorderEdge? top; + + /// Defines the bottom edge. + final LinearBorderEdge? bottom; + + @override + LinearBorder scale(double t) { + return LinearBorder( + side: side.scale(t), + ); + } + + @override + EdgeInsetsGeometry get dimensions { + final double width = side.width; + return EdgeInsetsDirectional.fromSTEB( + start == null ? 0.0 : width, + top == null ? 0.0 : width, + end == null ? 0.0 : width, + bottom == null ? 0.0 : width, + ); + } + + @override + ShapeBorder? lerpFrom(ShapeBorder? a, double t) { + if (a is LinearBorder) { + return LinearBorder( + side: BorderSide.lerp(a.side, side, t), + start: LinearBorderEdge.lerp(a.start, start, t), + end: LinearBorderEdge.lerp(a.end, end, t), + top: LinearBorderEdge.lerp(a.top, top, t), + bottom: LinearBorderEdge.lerp(a.bottom, bottom, t), + ); + } + return super.lerpFrom(a, t); + } + + @override + ShapeBorder? lerpTo(ShapeBorder? b, double t) { + if (b is LinearBorder) { + return LinearBorder( + side: BorderSide.lerp(side, b.side, t), + start: LinearBorderEdge.lerp(start, b.start, t), + end: LinearBorderEdge.lerp(end, b.end, t), + top: LinearBorderEdge.lerp(top, b.top, t), + bottom: LinearBorderEdge.lerp(bottom, b.bottom, t), + ); + } + return super.lerpTo(b, t); + } + + /// Returns a copy of this LinearBorder with the given fields replaced with + /// the new values. + @override + LinearBorder copyWith({ + BorderSide? side, + LinearBorderEdge? start, + LinearBorderEdge? end, + LinearBorderEdge? top, + LinearBorderEdge? bottom, + }) { + return LinearBorder( + side: side ?? this.side, + start: start ?? this.start, + end: end ?? this.end, + top: top ?? this.top, + bottom: bottom ?? this.bottom, + ); + } + + @override + Path getInnerPath(Rect rect, { TextDirection? textDirection }) { + final Rect adjustedRect = dimensions.resolve(textDirection).deflateRect(rect); + return Path() + ..addRect(adjustedRect); + } + + @override + Path getOuterPath(Rect rect, { TextDirection? textDirection }) { + return Path() + ..addRect(rect); + } + + @override + void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) { + final EdgeInsets insets = dimensions.resolve(textDirection); + final bool rtl = textDirection == TextDirection.rtl; + + final Path path = Path(); + final Paint paint = Paint() + ..strokeWidth = 0.0; + + void drawEdge(Rect rect, Color color) { + paint.color = color; + path.reset(); + path.moveTo(rect.left, rect.top); + if (rect.width == 0.0) { + paint.style = PaintingStyle.stroke; + path.lineTo(rect.left, rect.bottom); + } else if (rect.height == 0.0) { + paint.style = PaintingStyle.stroke; + path.lineTo(rect.right, rect.top); + } else { + paint.style = PaintingStyle.fill; + path.lineTo(rect.right, rect.top); + path.lineTo(rect.right, rect.bottom); + path.lineTo(rect.left, rect.bottom); + } + canvas.drawPath(path, paint); + } + + if (start != null && start!.size != 0.0 && side.style != BorderStyle.none) { + final Rect insetRect = Rect.fromLTWH(rect.left, rect.top + insets.top, rect.width, rect.height - insets.vertical); + final double x = rtl ? rect.right - insets.right : rect.left; + final double width = rtl ? insets.right : insets.left; + final double height = insetRect.height * start!.size; + final double y = (insetRect.height - height) * ((start!.alignment + 1.0) / 2.0); + final Rect r = Rect.fromLTWH(x, y, width, height); + drawEdge(r, side.color); + } + + if (end != null && end!.size != 0.0 && side.style != BorderStyle.none) { + final Rect insetRect = Rect.fromLTWH(rect.left, rect.top + insets.top, rect.width, rect.height - insets.vertical); + final double x = rtl ? rect.left : rect.right - insets.right; + final double width = rtl ? insets.left : insets.right; + final double height = insetRect.height * end!.size; + final double y = (insetRect.height - height) * ((end!.alignment + 1.0) / 2.0); + final Rect r = Rect.fromLTWH(x, y, width, height); + drawEdge(r, side.color); + } + + if (top != null && top!.size != 0.0 && side.style != BorderStyle.none) { + final double width = rect.width * top!.size; + final double startX = (rect.width - width) * ((top!.alignment + 1.0) / 2.0); + final double x = rtl ? rect.width - startX - width : startX; + final Rect r = Rect.fromLTWH(x, rect.top, width, insets.top); + drawEdge(r, side.color); + } + + if (bottom != null && bottom!.size != 0.0 && side.style != BorderStyle.none) { + final double width = rect.width * bottom!.size; + final double startX = (rect.width - width) * ((bottom!.alignment + 1.0) / 2.0); + final double x = rtl ? rect.width - startX - width: startX; + final Rect r = Rect.fromLTWH(x, rect.bottom - insets.bottom, width, side.width); + drawEdge(r, side.color); + } + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is LinearBorder + && other.side == side + && other.start == start + && other.end == end + && other.top == top + && other.bottom == bottom; + } + + @override + int get hashCode => Object.hash(side, start, end, top, bottom); + + @override + String toString() { + if (this == LinearBorder.none) { + return 'LinearBorder.none'; + } + + final StringBuffer s = StringBuffer('${objectRuntimeType(this, 'LinearBorder')}(side: $side'); + + if (start != null ) { + s.write(', start: $start'); + } + if (end != null ) { + s.write(', end: $end'); + } + if (top != null ) { + s.write(', top: $top'); + } + if (bottom != null ) { + s.write(', bottom: $bottom'); + } + s.write(')'); + return s.toString(); + } +} diff --git a/packages/flutter/test/painting/linear_border_test.dart b/packages/flutter/test/painting/linear_border_test.dart new file mode 100644 index 000000000000..fb70fabb0e6f --- /dev/null +++ b/packages/flutter/test/painting/linear_border_test.dart @@ -0,0 +1,168 @@ +// Copyright 2014 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 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + + +const Rect canvasRect = Rect.fromLTWH(0, 0, 100, 100); +const BorderSide borderSide = BorderSide(width: 4, color: Color(0x0f00ff00)); + +// Test points for rectangular filled paths based on a BorderSide with width 4 and +// a 100x100 bounding rectangle (canvasRect). +List rectIncludes(Rect r) { + return [r.topLeft, r.topRight, r.bottomLeft, r.bottomRight, r.center]; +} +final List leftRectIncludes = rectIncludes(const Rect.fromLTWH(0, 0, 4, 100)); +final List rightRectIncludes = rectIncludes(const Rect.fromLTWH(96, 0, 4, 100)); +final List topRectIncludes = rectIncludes(const Rect.fromLTWH(0, 0, 100, 4)); +final List bottomRectIncludes = rectIncludes(const Rect.fromLTWH(0, 96, 100, 4)); + + +void main() { + test('LinearBorderEdge defaults', () { + expect(const LinearBorderEdge().size, 1); + expect(const LinearBorderEdge().alignment, 0); + }); + + test('LinearBorder defaults', () { + void expectEmptyBorder(LinearBorder border) { + expect(border.side, BorderSide.none); + expect(border.dimensions, EdgeInsets.zero); + expect(border.preferPaintInterior, false); + expect(border.start, null); + expect(border.end, null); + expect(border.top, null); + expect(border.bottom, null); + } + expectEmptyBorder(LinearBorder.none); + + expect(LinearBorder.start().side, BorderSide.none); + expect(LinearBorder.start().start, const LinearBorderEdge()); + expect(LinearBorder.start().end, null); + expect(LinearBorder.start().top, null); + expect(LinearBorder.start().bottom, null); + + expect(LinearBorder.end().side, BorderSide.none); + expect(LinearBorder.end().start, null); + expect(LinearBorder.end().end, const LinearBorderEdge()); + expect(LinearBorder.end().top, null); + expect(LinearBorder.end().bottom, null); + + expect(LinearBorder.top().side, BorderSide.none); + expect(LinearBorder.top().start, null); + expect(LinearBorder.top().end, null); + expect(LinearBorder.top().top, const LinearBorderEdge()); + expect(LinearBorder.top().bottom, null); + + expect(LinearBorder.bottom().side, BorderSide.none); + expect(LinearBorder.bottom().start, null); + expect(LinearBorder.bottom().end, null); + expect(LinearBorder.bottom().top, null); + expect(LinearBorder.bottom().bottom, const LinearBorderEdge()); + }); + + test('LinearBorder copyWith, ==, hashCode', () { + expect(LinearBorder.none, LinearBorder.none.copyWith()); + expect(LinearBorder.none.hashCode, LinearBorder.none.copyWith().hashCode); + const BorderSide side = BorderSide(width: 10.0, color: Color(0xff123456)); + expect(LinearBorder.none.copyWith(side: side), const LinearBorder(side: side)); + }); + + test('LinearBorderEdge, LinearBorder toString()', () { + expect(const LinearBorderEdge(size: 0.5, alignment: -0.5).toString(), 'LinearBorderEdge(size: 0.5, alignment: -0.5)'); + expect(LinearBorder.none.toString(), 'LinearBorder.none'); + const BorderSide side = BorderSide(width: 10.0, color: Color(0xff123456)); + expect(const LinearBorder(side: side).toString(), 'LinearBorder(side: BorderSide(color: Color(0xff123456), width: 10.0))'); + expect( + const LinearBorder( + side: side, + start: LinearBorderEdge(size: 0, alignment: -0.75), + end: LinearBorderEdge(size: 0.25, alignment: -0.5), + top: LinearBorderEdge(size: 0.5, alignment: 0.5), + bottom: LinearBorderEdge(size: 0.75, alignment: 0.75), + ).toString(), + 'LinearBorder(' + 'side: BorderSide(color: Color(0xff123456), width: 10.0), ' + 'start: LinearBorderEdge(size: 0.0, alignment: -0.75), ' + 'end: LinearBorderEdge(size: 0.25, alignment: -0.5), ' + 'top: LinearBorderEdge(size: 0.5, alignment: 0.5), ' + 'bottom: LinearBorderEdge(size: 0.75, alignment: 0.75))', + ); + }, + skip: isBrowser, // [intended] see https://github.com/flutter/flutter/issues/118207 + ); + + test('LinearBorder.start()', () { + final LinearBorder border = LinearBorder.start(side: borderSide); + expect( + (Canvas canvas) => border.paint(canvas, canvasRect, textDirection: TextDirection.ltr), + paints + ..path( + includes: leftRectIncludes, + excludes: rightRectIncludes, + color: borderSide.color, + ), + ); + expect( + (Canvas canvas) => border.paint(canvas, canvasRect, textDirection: TextDirection.rtl), + paints + ..path( + includes: rightRectIncludes, + excludes: leftRectIncludes, + color: borderSide.color, + ), + ); + }); + + test('LinearBorder.end()', () { + final LinearBorder border = LinearBorder.end(side: borderSide); + expect( + (Canvas canvas) => border.paint(canvas, canvasRect, textDirection: TextDirection.ltr), + paints + ..path( + includes: rightRectIncludes, + excludes: leftRectIncludes, + color: borderSide.color, + ), + ); + expect( + (Canvas canvas) => border.paint(canvas, canvasRect, textDirection: TextDirection.rtl), + paints + ..path( + includes: leftRectIncludes, + excludes: rightRectIncludes, + color: borderSide.color, + ), + ); + }); + + test('LinearBorder.top()', () { + final LinearBorder border = LinearBorder.top(side: borderSide); + expect( + (Canvas canvas) => border.paint(canvas, canvasRect, textDirection: TextDirection.ltr), + paints + ..path( + includes: topRectIncludes, + excludes: bottomRectIncludes, + color: borderSide.color, + ), + ); + }); + + test('LinearBorder.bottom()', () { + final LinearBorder border = LinearBorder.bottom(side: borderSide); + expect( + (Canvas canvas) => border.paint(canvas, canvasRect, textDirection: TextDirection.ltr), + paints + ..path( + includes: bottomRectIncludes, + excludes: topRectIncludes, + color: borderSide.color, + ), + ); + }); +}